CameraX 视频捕获架构

捕获系统通常会录制视频流和音频流,对其进行压缩,对这两个流进行多路复用,然后将生成的流写入磁盘。

视频和音频捕获系统概念图
图 1. 视频和音频捕获系统概念图。

在 CameraX 中,用于视频捕获的解决方案是 VideoCapture 用例:

展示 CameraX 如何处理 VideoCapture 用例的概念图
图 2. 展示 CameraX 如何处理 VideoCapture 用例的概念图。

如图 2 所示,CameraX 视频捕获包括几个高级架构组件:

  • SurfaceProvider,表示视频来源。
  • AudioSource,表示音频来源。
  • 用于对视频/音频进行编码和压缩的两个编码器。
  • 用于对两个流进行多路复用的媒体复用器。
  • 用于写出结果的文件保存器。

VideoCapture API 会对复杂的捕获引擎进行抽象化处理,为应用提供更加简单且直观的 API。

VideoCapture API 概述

VideoCapture 是一种 CameraX 用例,既可以单独使用,也可以与其他用例搭配使用。受支持的具体组合取决于相机硬件功能,不过 Preview 和 VideoCapture 这一用例组合适用于所有设备。

VideoCapture API 包含可与应用通信的以下对象:

  • VideoCapture 是顶级用例类。VideoCapture 通过 CameraSelector 和其他 CameraX 用例绑定到 LifecycleOwner。如需详细了解这些概念和用法,请参阅 CameraX 架构。
  • Recorder 是与 VideoCapture 紧密耦合的 VideoOutput 实现。 Recorder 用于执行视频和音频捕获操作。应用通过 Recorder 创建录制对象。
  • PendingRecording 会配置录制对象,同时提供启用音频和设置事件监听器等选项。您必须使用 Recorder 来创建 PendingRecording。PendingRecording 不会录制任何内容。
  • Recording 会执行实际录制操作。您必须使用 PendingRecording 来创建 Recording。

图 3 展示了这些对象之间的关系:

展示 VideoCapture 用例中发生的交互的示意图
图 3. 展示 VideoCapture 用例中发生的交互的示意图。

图例:

  1. 使用 QualitySelector 创建 Recorder。
  2. 使用其中一个 OutputOptions 配置 Recorder。
  3. 如果需要,使用 withAudioEnabled() 启用音频。
  4. 使用 VideoRecordEvent 监听器调用 start() 以开始录制。
  5. 针对 Recording 使用 pause()/resume()/stop() 来控制录制操作。
  6. 在事件监听器内响应 VideoRecordEvents。

详细的 API 列表位于源代码内的 current-txt 中。

使用 VideoCapture API

如需将 CameraX VideoCapture 用例集成到您的应用中,请执行以下操作:

  1. 绑定 VideoCapture。
  2. 准备和配置录制。
  3. 开始和控制运行时录制。

后面的部分概述了您可以在每个步骤中执行哪些操作,以获取端到端录制会话。

绑定 VideoCapture

如需绑定 VideoCapture 用例,请执行以下操作:

  1. 创建一个 Recorder 对象。
  2. 创建 VideoCapture 对象。
  3. 绑定到 Lifecycle。

CameraX VideoCapture API 遵循构建器设计模式。应用使用 Recorder.Builder 来创建 Recorder。您还可以通过 QualitySelector 对象为 Recorder 配置视频分辨率。

CameraX Recorder 支持以下预定义的视频分辨率 Qualities:

  • Quality.UHD,适用于 4K 超高清视频大小 (2160p)
  • Quality.QHD,适用于 2K 分辨率视频大小 (1440p),在 CameraX 中引入 1.7.0-alpha02
  • Quality.FHD,适用于全高清视频大小 (1080p)
  • Quality.HD,适用于高清视频大小 (720p)
  • Quality.SD,适用于标清视频大小 (480p)

请注意,获得应用授权后,CameraX 还可以选择其他分辨率。

每个选项对应的确切视频大小取决于相机和编码器的功能。如需了解详情,请参阅 CamcorderProfile 的文档。

应用可以通过创建 QualitySelector 来配置分辨率。您可以使用以下方法之一创建 QualitySelector:

  • 使用 fromOrderedList() 提供几个首选分辨率,并包含一个后备策略,以备在不支持任何首选分辨率时使用。

    CameraX 可以根据所选相机的功能确定最佳后备匹配项。如需了解详情,请参阅 QualitySelector 的 FallbackStrategy specification。例如,以下代码会请求支持的最高录制分辨率;如果所有请求分辨率都不受支持,则授权 CameraX 选择最接近 Quality.SD 分辨率的分辨率:

    val qualitySelector = QualitySelector.fromOrderedList(
             listOf(Quality.UHD, Quality.QHD, Quality.FHD, Quality.HD, Quality.SD),
             FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
    
  • 首先查询相机功能,然后使用 QualitySelector::from() 从受支持的分辨率中进行选择:

    val cameraInfo = cameraProvider.availableCameraInfos.filter {
        Camera2CameraInfo
        .from(it)
        .getCameraCharacteristic(CameraCharacteristics.LENS\_FACING) == CameraMetadata.LENS_FACING_BACK
    }
    
    val supportedQualities = QualitySelector.getSupportedQualities(cameraInfo[0])
    val filteredQualities = arrayListOf (Quality.UHD, Quality.QHD, Quality.FHD, Quality.HD, Quality.SD)
                           .filter { supportedQualities.contains(it) }
    
    // Use a simple ListView with the id of simple_quality_list_view
    viewBinding.simpleQualityListView.apply {
        adapter = ArrayAdapter(context,
                               android.R.layout.simple_list_item_1,
                               filteredQualities.map { it.qualityToString() })
    
        // Set up the user interaction to manually show or hide the system UI.
        setOnItemClickListener { _, _, position, _ ->
            // Inside View.OnClickListener,
            // convert Quality.* constant to QualitySelector
            val qualitySelector = QualitySelector.from(filteredQualities[position])
    
            // Create a new Recorder/VideoCapture for the new quality
            // and bind to lifecycle
            val recorder = Recorder.Builder()
                .setQualitySelector(qualitySelector).build()
    
             // ...
        }
    }
    
    // A helper function to translate Quality to a string
    fun Quality.qualityToString() : String {
        return when (this) {
            Quality.UHD -> "UHD"
            Quality.QHD -> "QHD"
            Quality.FHD -> "FHD"
            Quality.HD -> "HD"
            Quality.SD -> "SD"
            else -> throw IllegalArgumentException()
        }
    }
    
    

    请注意,QualitySelector.getSupportedQualities() 返回的功能肯定适用于 VideoCapture 用例或 VideoCapture 和 Preview 用例的组合。与 ImageCapture 或 ImageAnalysis 用例绑定时,如果请求的相机不支持所需的组合,CameraX 仍可能会绑定失败。

具有 QualitySelector 后,应用即可创建 VideoCapture 对象并执行绑定。请注意,此绑定与和其他用例的绑定相同:

val recorder = Recorder.Builder()
    .setExecutor(cameraExecutor).setQualitySelector(qualitySelector)
    .build()
val videoCapture = VideoCapture.withOutput(recorder)

try {
    // Bind use cases to camera
    cameraProvider.bindToLifecycle(
            this, CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture)
} catch(exc: Exception) {
    Log.e(TAG, "Use case binding failed", exc)
}

请注意,bindToLifecycle() 会返回一个 Camera 对象。如需详细了解如何控制相机输出(如变焦和曝光),请参阅此指南

Recorder 会选择最适合系统的格式。最常见的视频编解码器是 H.264 AVC,其容器格式为 MPEG-4。

配置和创建录制对象

应用可以通过 Recorder 创建录制对象来执行视频和音频捕获操作。应用通过执行以下操作来创建录制对象:

  1. 使用 prepareRecording() 配置 OutputOptions。
  2. (可选)启用录音功能。
  3. 使用 start() 注册 VideoRecordEvent 监听器,并开始捕获视频。

当您调用 start() 函数时,Recorder 会返回 Recording 对象。应用可以使用此 Recording 对象完成捕获或执行其他操作,例如暂停或恢复。

Recorder 一次支持一个 Recording 对象。对前面的 Recording 对象调用 Recording.stop() 或 Recording.close() 后,您便可以开始新的录制。

我们来更详细地看看这些步骤。首先,应用使用 Recorder.prepareRecording() 为 Recorder 配置 OutputOptions。Recorder 支持以下类型的 OutputOptions:

  • FileDescriptorOutputOptions,用于捕获到 FileDescriptor 中。
  • FileOutputOptions,用于捕获到 File 中。
  • MediaStoreOutputOptions,用于捕获到 MediaStore 中。

无论使用哪种 OutputOptions 类型,您都能通过 setFileSizeLimit() 来设置文件大小上限。其他选项特定于单个输出类型,例如 ParcelFileDescriptor 特定于 FileDescriptorOutputOptions。

prepareRecording() 会返回 PendingRecording 对象,该对象是一个中间对象,用于创建相应的 Recording 对象。PendingRecording 是一个瞬态类,在大多数情况下应不可见,并且很少被应用缓存。

应用可以进一步配置录制对象,例如:

  • 使用 withAudioEnabled() 启用音频。
  • 使用 start(Executor, Consumer<VideoRecordEvent>) 注册监听器,以接收视频录制事件。
  • 允许在录制所附加到的 VideoCapture 重新绑定到另一个摄像头时继续录制,方法是使用 PendingRecording.asPersistentRecording()。

要开始录制,请调用 PendingRecording.start()。CameraX 会将 PendingRecording 转换为 Recording,将录制请求加入队列,并将新创建的 Recording 对象返回给应用。一旦在相应相机设备上开始录制,CameraX 就会发送 VideoRecordEvent.EVENT_TYPE_START 事件。

以下示例展示了如何将视频和音频录制到 MediaStore 文件中:

// Create MediaStoreOutputOptions for our recorder
val name = "CameraX-recording-" +
        SimpleDateFormat(FILENAME_FORMAT, Locale.US)
                .format(System.currentTimeMillis()) + ".mp4"
val contentValues = ContentValues().apply {
   put(MediaStore.Video.Media.DISPLAY_NAME, name)
}
val mediaStoreOutput = MediaStoreOutputOptions.Builder(this.contentResolver,
                              MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                              .setContentValues(contentValues)
                              .build()

// 2. Configure Recorder and Start recording to the mediaStoreOutput.
val recording = videoCapture.output
                .prepareRecording(context, mediaStoreOutput)
                .withAudioEnabled()
                .start(ContextCompat.getMainExecutor(this), captureListener)

虽然默认情况下前置摄像头上的摄像头预览会镜像,但默认情况下 VideoCapture 录制的视频不会镜像。借助 CameraX 1.3,现在可以镜像视频录制,以便前置摄像头预览和录制的视频保持一致。

有三种 MirrorMode 选项:MIRROR_MODE_OFF、MIRROR_MODE_ON 和 MIRROR_MODE_ON_FRONT_ONLY。为了与相机预览保持一致,Google 建议使用 MIROR_MODE_ON_FRONT_ONLY,这意味着后置摄像头不启用镜像,但前置摄像头启用镜像。如需详细了解 MirrorMode,请参阅 MirrorMode constants。

此代码段展示了如何使用 MIRROR_MODE_ON_FRONT_ONLY 调用 VideoCapture.Builder.setMirrorMode()。如需了解详情,请参阅 setMirrorMode()。

Kotlin

val recorder = Recorder.Builder().build()

val videoCapture = VideoCapture.Builder(recorder)
    .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
    .build()

useCases.add(videoCapture);

Java

Recorder.Builder builder = new Recorder.Builder();
if (mVideoQuality != QUALITY_AUTO) {
    builder.setQualitySelector(
        QualitySelector.from(mVideoQuality));
}
  VideoCapture<Recorder> videoCapture = new VideoCapture.Builder<>(builder.build())
      .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
      .build();
    useCases.add(videoCapture);

动态范围(HDR 视频)

CameraX 支持使用 DynamicRange 录制 10 位高动态范围 (HDR) 视频,例如 HLG10、HDR10、HDR10+ 或杜比视界。

如需使用 HDR 视频录制功能,请完成以下步骤:

  1. 使用 VideoCapabilities.getSupportedDynamicRanges() 查询相机支持的动态范围。
  2. 可选:如果您配置了自定义 QualitySelector,请使用 getSupportedQualities(DynamicRange) 检查该动态范围支持哪些画质。
  3. 使用 setDynamicRange() 配置 VideoCapture.Builder。

以下示例在相机支持 HLG 10 位 HDR 录制时启用该功能:

Kotlin

val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
val supportedHdrRanges = videoCapabilities.supportedDynamicRanges
    .filter { it != DynamicRange.SDR }

val recorder = Recorder.Builder().build()
val videoCapture = VideoCapture.Builder(recorder)
    .apply {
        if (supportedHdrRanges.contains(DynamicRange.HLG_10_BIT)) {
            setDynamicRange(DynamicRange.HLG_10_BIT)
        }
    }
    .build()

Java

VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(cameraInfo);
Set<DynamicRange> supportedRanges = videoCapabilities.getSupportedDynamicRanges();

Recorder recorder = new Recorder.Builder().build();
VideoCapture.Builder<Recorder> builder = new VideoCapture.Builder<>(recorder);
if (supportedRanges.contains(DynamicRange.HLG_10_BIT)) {
    builder.setDynamicRange(DynamicRange.HLG_10_BIT);
}
VideoCapture<Recorder> videoCapture = builder.build();

视频防抖

CameraX 支持视频防抖功能,可减少视频录制期间的手持抖动和颤动。

如需启用视频防抖功能,请完成以下步骤:

  1. 使用 VideoCapabilities.isStabilizationSupported() 检查相机设备是否支持防抖。
  2. 使用 setVideoStabilizationEnabled(true) 在 VideoCapture.Builder 上启用它。

以下示例在相机支持防抖功能时启用该功能:

Kotlin

val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
val videoCapture = VideoCapture.Builder(recorder)
    .setVideoStabilizationEnabled(videoCapabilities.isStabilizationSupported)
    .build()

Java

VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(cameraInfo);
VideoCapture<Recorder> videoCapture = new VideoCapture.Builder<>(recorder)
    .setVideoStabilizationEnabled(videoCapabilities.isStabilizationSupported())
    .build();

控制活跃录制对象

您可以使用以下方法暂停、恢复和停止正在进行的 Recording:

  • pause,用于暂停当前的活跃录制。
  • resume(),用于恢复已暂停的活跃录制。
  • stop(),用于完成录制并清空所有关联的录制对象。
  • mute() 将当前录音静音或取消静音。

请注意,无论录制处于暂停状态还是活跃状态,您都可以调用 stop() 来终止 Recording。

如果您已使用 PendingRecording.start() 注册了 EventListener,Recording 会使用 VideoRecordEvent 进行通信。

  • VideoRecordEvent.EVENT_TYPE_STATUS 用于录制统计信息,例如当前文件的大小和录制的时间跨度。
  • VideoRecordEvent.EVENT_TYPE_FINALIZE 用于录制结果,会包含最终文件的 URI 以及任何相关错误等信息。

在您的应用收到表示录制会话成功的 EVENT_TYPE_FINALIZE 后,您就可以从 OutputOptions 中指定的位置访问捕获的视频。

实时音频振幅和状态

在录制期间,VideoRecordEvent.Status 事件通过 AudioStats 提供实时音频统计信息:

  • AudioStats.getAudioAmplitude() 会返回当前音频峰值振幅,范围为 0.0 到 1.0。您可以使用此值来驱动界面计量器或音频波形可视化工具。
  • AudioStats.getAudioState() 表示音频串流的当前状态,例如 AUDIO_STATE_ACTIVE、AUDIO_STATE_MUTED 或 AUDIO_STATE_SOURCE_SILENCED。

以下示例从状态事件中读取音频振幅:

Kotlin

when (event) {
    is VideoRecordEvent.Status -> {
        val audioStats = event.recordingStats.audioStats
        if (audioStats.hasAudio()) {
            val amplitude = audioStats.audioAmplitude
            updateAudioWaveform(amplitude)
        }
    }
    // ...
}

Java

if (event instanceof VideoRecordEvent.Status) {
    VideoRecordEvent.Status status = (VideoRecordEvent.Status) event;
    AudioStats audioStats = status.getRecordingStats().getAudioStats();
    if (audioStats.hasAudio()) {
        double amplitude = audioStats.getAudioAmplitude();
        updateAudioWaveform(amplitude);
    }
}

高速和慢动作视频

CameraX 支持高速视频录制(例如 120、240 或 480 fps),这可实现两种不同的用例:

  • 高帧速率视频(由播放器处理的慢动作):CameraX 以高帧速率(例如 120 fps)拍摄并保存帧。视频播放器可控制播放速度,让用户能够在正常速度和慢动作之间切换,或执行逐帧分析。
  • 可直接播放的慢动作视频:CameraX 会处理高速视频流,并输出一个调整了帧时间戳的标准 30 fps 视频文件。慢动作效果是“内置”的,因此视频会在任何标准播放器中自动以慢动作播放,无需特殊播放器支持。例如,以 120 fps 拍摄的视频在以 30 fps 播放时,会产生 1/4 倍速的慢动作视频。

检查高速功能

高速视频拍摄需要设备制造商提供硬件支持和相应的 CamcorderProfile 定义,例如 CamcorderProfile.QUALITY_HIGH_SPEED_1080P 或 QUALITY_HIGH_SPEED_720P。

在配置高速录制之前,请使用 Recorder.getHighSpeedVideoCapabilities(CameraInfo) 查询高速录制功能:

Kotlin

val highSpeedCapabilities = Recorder.getHighSpeedVideoCapabilities(cameraInfo)
if (highSpeedCapabilities == null) {
    // Camera does not support high-speed video capture
    return
}
val supportedQualities = highSpeedCapabilities.getSupportedQualities(DynamicRange.SDR)

Java

VideoCapabilities highSpeedCapabilities = Recorder.getHighSpeedVideoCapabilities(cameraInfo);
if (highSpeedCapabilities == null) {
    // Camera does not support high-speed video capture
    return;
}
List<Quality> supportedQualities = highSpeedCapabilities.getSupportedQualities(DynamicRange.SDR);

配置 HighSpeedVideoSessionConfig

如需使用高速拍摄,请在 HighSpeedVideoSessionConfig 中组合使用 VideoCapture 和 Preview 用例。如需配置会话,请完成以下步骤:

  1. 创建一个 VideoCapture 用例和一个 Preview 用例。
  2. 使用 VideoCapture 用例初始化 HighSpeedVideoSessionConfig.Builder 并附加 Preview。
  3. 使用 CameraInfo.getSupportedFrameRateRanges(SessionConfig) 查询支持的帧速率范围,并设置所选范围。
  4. 选择高帧率视频或可直接播放的慢动作视频:
    • 如需创建高帧速率视频,请保留 setSlowMotionEnabled(false)(默认)。
    • 如需创建可播放的慢动作视频,请调用 setSlowMotionEnabled(true)。
  5. 使用 cameraProvider.bindToLifecycle() 绑定会话配置。

以下示例配置并绑定了一个可播放的慢动作会话:

Kotlin

val preview = Preview.Builder().build()
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(supportedQualities.first()))
    .build()
val videoCapture = VideoCapture.withOutput(recorder)

// Build HighSpeedVideoSessionConfig
val sessionConfigBuilder = HighSpeedVideoSessionConfig.Builder(videoCapture)
    .setPreview(preview)
    // Enable automatic slow-motion playback at 30 fps
    .setSlowMotionEnabled(true)

// Query and set a supported high-speed frame rate range (such as 120 or 240 fps).
// The set is non-empty because getHighSpeedVideoCapabilities() returned non-null earlier.
val supportedFpsRanges = cameraInfo.getSupportedFrameRateRanges(sessionConfigBuilder.build())
sessionConfigBuilder.setFrameRateRange(supportedFpsRanges.first())

val sessionConfig = sessionConfigBuilder.build()

// Bind session config to lifecycle
cameraProvider.bindToLifecycle(
    lifecycleOwner,
    CameraSelector.DEFAULT_BACK_CAMERA,
    sessionConfig
)

Java

Preview preview = new Preview.Builder().build();
Recorder recorder = new Recorder.Builder()
    .setQualitySelector(QualitySelector.from(supportedQualities.get(0)))
    .build();
VideoCapture<Recorder> videoCapture = VideoCapture.withOutput(recorder);

HighSpeedVideoSessionConfig.Builder sessionConfigBuilder =
    new HighSpeedVideoSessionConfig.Builder(videoCapture)
        .setPreview(preview)
        .setSlowMotionEnabled(true);

// The set is non-empty because getHighSpeedVideoCapabilities() returned non-null earlier.
Set<Range<Integer>> supportedFpsRanges =
    cameraInfo.getSupportedFrameRateRanges(sessionConfigBuilder.build());
sessionConfigBuilder.setFrameRateRange(supportedFpsRanges.iterator().next());

HighSpeedVideoSessionConfig sessionConfig = sessionConfigBuilder.build();

cameraProvider.bindToLifecycle(
    lifecycleOwner,
    CameraSelector.DEFAULT_BACK_CAMERA,
    sessionConfig
);

您还可以使用 CameraX 1.7.0-alpha02 中引入的 Kotlin DSL 来简洁地配置高速会话:

val sessionConfig = highSpeedVideoSessionConfig(videoCapture) {
    this.preview = preview
    isSlowMotionEnabled = true
    frameRateRange = supportedFpsRanges.first()
}
cameraProvider.bindToLifecycle(
    lifecycleOwner,
    CameraSelector.DEFAULT_BACK_CAMERA,
    sessionConfig
)

录制高速或慢动作视频

绑定 HighSpeedVideoSessionConfig 后,使用 VideoCapture 开始录制,方法与标准视频录制相同:

Kotlin

val recording = videoCapture.output
    .prepareRecording(context, mediaStoreOutput)
    .withAudioEnabled()
    .start(ContextCompat.getMainExecutor(context)) { event ->
        when (event) {
            is VideoRecordEvent.Start -> Log.d(TAG, "Recording started")
            is VideoRecordEvent.Finalize -> {
                if (!event.hasError()) {
                    Log.d(TAG, "Video saved: ${event.outputResults.outputUri}")
                }
            }
        }
    }

Java

Recording recording = videoCapture.getOutput()
    .prepareRecording(context, mediaStoreOutput)
    .withAudioEnabled()
    .start(ContextCompat.getMainExecutor(context), event -> {
        if (event instanceof VideoRecordEvent.Start) {
            Log.d(TAG, "Recording started");
        } else if (event instanceof VideoRecordEvent.Finalize) {
            VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) event;
            if (!finalize.hasError()) {
                Log.d(TAG, "Video saved: " + finalize.getOutputResults().getOutputUri());
            }
        }
    });

其他资源

如需详细了解 CameraX,请参阅下面列出的其他资源: