CameraX 錄影架構

擷取系統通常會錄製影片和音訊串流,將其壓縮並多路複用兩個串流,然後將結果串流寫入磁碟。

影片和音訊擷取系統的概念圖
圖 1.影片和音訊擷取系統的概念圖。

在 CameraX 中,影片擷取解決方案為 VideoCapture 用途:

顯示相機 X 處理影片擷取應用實例的概念圖
圖 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 是 VideoOutput 的實作,與 VideoCapture 緊密結合。Recorder 是用於執行影片和音訊擷取。應用程式會從 Recorder 建立錄製內容。
  • PendingRecording 可設定錄製內容,並提供啟用音訊及設定事件監聽器等選項。您必須使用 Recorder 才能建立 PendingRecording。PendingRecording 不會錄下任何內容。
  • Recording 會執行實際錄製內容。您必須使用 PendingRecording 才能建立 Recording。

圖 3 說明這些物件之間的關係:

顯示影片擷取應用實例中發生的互動圖表
圖 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 影片解析度:

  • 4K UHD 超高畫質影片尺寸 (2160p) 的 Quality.UHD
  • Quad HD 高畫質影片尺寸 (1440p) 的 Quality.QHD,在 CameraX 1.7.0-alpha02 中推出
  • Full HD 高畫質影片尺寸 (1080p) 的 Quality.FHD
  • HD 高畫質影片尺寸 (720p) 的 Quality.HD
  • SD 標準畫質影片尺寸 (480p) 的Quality.SD

請注意,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() 為錄製工具設定 OutputOptions。Recorder 支援下列 OutputOptions 類型:

  • 用來錄製 FileDescriptor 的 FileDescriptorOutputOptions。
  • 用來錄製 File 的 FileOutputOptions。
  • 用來錄製 MediaStore 的 MediaStoreOutputOptions。

所有 OutputOptions 類型都支援透過 setFileSizeLimit() 設定檔案大小上限。其他選項則適用於個別輸出類型,例如 ParcelFileDescriptor 適用於 FileDescriptorOutputOptions。

prepareRecording() 會回傳 PendingRecording 物件,也就是用於建立對應 Recording 物件的中繼物件。PendingRecording 是暫時性類別,在大部分情況下應該不會顯示,而且應用程式很少快取。

應用程式可進一步設定錄製內容,例如:

  • 啟用包含 withAudioEnabled() 的音訊。
  • 透過 start(Executor, Consumer<VideoRecordEvent>) 註冊監聽器,以便接收錄製內容。
  • 使用 PendingRecording.asPersistentRecording(),允許錄製作業在附加的 VideoCapture 重新繫結至其他相機時,繼續錄製。

如要開始錄製,請呼叫 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 支援 10 位元高動態範圍 (HDR) 影片錄製,例如 HLG10、HDR10、HDR10+ 或 Dolby Vision,使用 DynamicRange。

如要使用 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 之間。您可以使用這個值驅動 UI 計量器或音訊波形視覺化工具。
  • 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,請參閱下列其他資源: