banner
jzman

jzman

Coding、思考、自觉。
github

Camera2、MediaCodec錄製mp4

PS:做想做的事,每天至少做一点,不求做多少,慢慢改变。

了解了音視頻的相關知識,可以先閱讀下面兩篇文章:

本文的主要內容是通過 Android 原生的硬編解碼框架 MediaCodec 和復用器 MediaMuxer 實現 mp4 視頻文件的錄製,視頻數據源由 Camera2 來提供,這裡重點是編碼、復用的這個過程而不是 mp4 的錄製,如果僅僅是視頻錄製,可以選擇更方便的 MediaRecorder,按照慣例還是以案例的形式學習 MediaCodec,其更多用法將在後續的文章中介紹,本文主要內容如下:

  1. Camera2 的使用
  2. MediaCodec 輸入方式
  3. MediaCodec 編碼 Camera2 數據
  4. 錄製流程
  5. 錄製效果

Camera2 的使用#

Camera2 是從 Android 5.0 開始推出的新的相機 API,最新的是 CameraX,CameraX 基於 Camera2,相較 Camera2 提供了更好用的 API,後文中涉及到的相關的 API 可以直接參考下面這張示意圖,這也是 Camera2 的使用示意圖,如下:

image

MediaCodec 的輸入方式#

為了能夠使用 MediaCodec 進行編碼操作,就需要將相機的數據輸入到編碼器 MediaCodec 中,可以通過兩種方式將數據寫入 MediaCodec,具體如下:

  • Surface:使用 Surface 作為編碼器 MediaCodec 的輸入,即將 MediaCodec 創建的 Surface 作為其輸入,這個 Surface 由 MediaCodec 的 createInputSurface 方法創建,當相機將會將有效數據渲染到該 Surface 中,MediaCodec 就可以直接輸出編碼後的數據了。
  • InputBuffer:使用輸入緩衝區作為編碼器 MediaCodec 的輸入,這裡需要填充的數據就是原始幀數據,對應 Camera2 來說可直接通過 ImageReader 來進行幀數據的獲取,獲取到的 Image 包含了寬度、高度、格式、時間戳及 YUV 數據分量等信息,可控程度更高。

MediaCodec 編碼 Camera2 數據#

簡單說一下 MediaCodec 的數據處理方式,Android 5.0 之前只支持 ByteBuffer [] 的同步方式,之後推薦使用 ByteBuffer 的同步、異步方式,這裡使用 ByteBuffer 的同步方式,涉及的流程主要是視頻數據編碼和復用,因為前面提到 MediaCodec 的輸入是通過 Surface 完成的,所以這裡只需要獲取的已經編碼好的數據和使用復用器 MediaMuxer 來實現 Mp4 文件的生成,關鍵代碼如下:

// 返回已成功編碼的輸出緩衝區的索引
var outputBufferId: Int = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0)
if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    // 添加視頻軌道
    mTrackIndex = mMediaMuxer.addTrack(mMediaCodec.outputFormat)
    mMediaMuxer.start()
    mStartMuxer = true
} else {
    while (outputBufferId >= 0) {
        if (!mStartMuxer) {
            Log.i(TAG, "MediaMuxer not start")
            continue
        }
        // 獲取有效數據
        val outputBuffer = mMediaCodec.getOutputBuffer(outputBufferId) ?: continue
        outputBuffer.position(bufferInfo.offset)
        outputBuffer.limit(bufferInfo.offset + bufferInfo.size)
        if (pts == 0L) {
            pts = bufferInfo.presentationTimeUs
        }
        bufferInfo.presentationTimeUs = bufferInfo.presentationTimeUs - pts
        // 將數據寫入復用器以生成文件
        mMediaMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo)
        Log.d(
            TAG,
            "pts = ${bufferInfo.presentationTimeUs / 1000000.0f} s ,${pts / 1000} ms"
        )
        mMediaCodec.releaseOutputBuffer(outputBufferId, false)
        outputBufferId = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0)
    }
}

錄製流程#

這裡使用 Surface 作為編碼器 MediaCodec 的輸入,在 MediaCodec 進入配置狀態才可以創建 Surface,也就是 createInputSurface 只能在 configure 與 start 之間調用,參考如下:

// 配置狀態
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// 創建Surface作為MediaCodec的輸入,createInputSurface只能在configure與start之間調用創建Surface
mSurface = mMediaCodec.createInputSurface()
// start ...

將其添加到 SessionConfiguration 的輸出 Surface 列表中,參考如下:

// 創建CaptureSession
@RequiresApi(Build.VERSION_CODES.P)
private suspend fun createCaptureSession(): CameraCaptureSession = suspendCoroutine { cont ->
    val outputs = mutableListOf<OutputConfiguration>()
    // 預覽Surface                                                                              
    outputs.add(OutputConfiguration(mSurface))
    // 添加MediaCodec用作輸入的Surface                                                                               
    outputs.add(OutputConfiguration(EncodeManager.getSurface()))
    val sessionConfiguration = SessionConfiguration(
        SessionConfiguration.SESSION_REGULAR,
        outputs, mExecutor, ...)
    mCameraDevice.createCaptureSession(sessionConfiguration)
}

然後發起 CaptureRequest 開啟預覽和接收 Surface 輸出,同時開啟編碼,參考如下:

// 添加預覽的Surface和生成Image的Surface
mCaptureRequestBuild = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val sur = EncodeManager.getSurface()
mCaptureRequestBuild.addTarget(sur)
mCaptureRequestBuild.addTarget(mSurface)

// 設置各種參數
mCaptureRequestBuild.set(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE, 1) // 視頻穩定功能是否激活
// 發送CaptureRequest
mCameraCaptureSession.setRepeatingRequest(
    mCaptureRequestBuild.build(),
    null,
    mCameraHandler
)
// 開始編碼
EncodeManager.startEncode()

錄製效果#

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。