Camera2錄製視頻(二):MediaCodeC+OpenGL視頻編碼

原文地址
原創文章,未經做者容許不得轉載java

秋風清,秋月明
葉葉梧桐檻外聲
難教歸夢成git

MediaLearn

歡迎你們關注個人項目MediaLearn,這是一個以學習分享音視頻知識爲目的創建的項目,目前僅侷限於Android平臺,後續會逐漸擴展。
對音視頻領域知識感興趣的朋友,歡迎一塊兒來學習!!!github

在上一篇文章Camera2錄製視頻(一):音頻的錄製及編碼,主要分享了使用Camera2搭配MediaCodeC和MediaMuxer進行視頻錄製中的音頻錄製部分。那麼在這篇文章中呢,就着手分析使用MediaCodeC完成視頻的錄製編碼和MediaMuxer完成Mux視頻合成模塊。有關使用MediaCodeC硬編碼對視頻編解碼的相關視頻,我以前也有分享,想看的朋友們能夠點擊如下傳送門回顧。數組

MediaCodeC硬編碼將圖片集編碼爲視頻Mp4文件MediaCodeC編碼視頻
MediaCodeC將視頻完整解碼,並存儲爲圖片文件。使用兩種不一樣的方式,硬編碼解碼視頻
MediaCodeC解碼視頻指定幀硬編碼解碼指定幀緩存

概述

項目中使用的攝像頭API爲Camera2bash

在文章開始以前,依然是老規矩,咱們從結果導向,梳理流程。看看在視頻錄製這個階段,流程是如何運做的,數據在這其中發生了什麼變化。session

流程梳理

當設備的攝像頭在運轉時,sensor【傳感器】會將光信號轉爲電信號,再轉爲數字信號。sensor會輸出四種格式的圖片格式:YUV、RGB、RAW RGB DATA、JPEG。YUV是最經常使用的一種格式,YUV輸出的數據中亮度信號是無損的,RGB會有必定的損耗會丟掉一些原始信息。而RAW DATA是最原始的信息,可是存儲空間會變大,並且須要一些特定軟件才能打開。
在廢棄的攝像頭API——Camera中,默認的預覽回調數據格式就是NV21。而在Camera2中,函數只提供了Surface做爲橋接對象。如果想獲取YUV、或者JPEG和RAW_SENSOR的話,可使用ImageReader提供的Surface,再經過監聽獲取圖像信息。
不論是Camera仍是Camera2,都是支持設置Surface的。經過Surface,咱們能夠將Camera拿到的數據直接輸送到GPU經過OpenGL來渲染處理。這樣能夠不用再CPU中處理Camera幀數據,從而節省大量時間。好了,接下來我用一張流程圖,來展現MediaCodeC如何編碼Camera幀數據的。 app

  • 一、由於咱們須要的是H264數據,因此須要給MediaCodeC配置Mime爲video/avc
  • 二、MediaCodeC配置好以後,經過createInputSurface建立出一個做輸入的Input—Surface
  • 三、將Input-surface做爲參數,配置Android平臺EGL環境的windowSurface
  • 四、建立OpenGL的program程序,獲得一個可用的紋理ID,從而構建出一個SurfaceTexture。這個對象能夠提供給已經廢棄的CameraAPI,也能夠構建出一個Surface提供給Camera2API。

經過以上操做,咱們就能夠把Camera採集的數據直接傳入到GPU,不用在CPU中費力的處理一番。ide

代碼實現

在上一篇文章我提到過,會將整個視頻錄製中涉及到的各個功能模塊化,以供後續複用。那麼在視頻的錄製編碼這塊,我將它分裝爲了一個Runnable——VideoRecorderVideoRecorder的內部職責爲,封裝了MediaCodeC+OpenGL編碼的流程。對外提供OpenGL紋理的Surface,和硬編碼編碼後的ByteBuffer、以及BufferInfo和其餘視頻幀相關的信息。模塊化

// MediaCodeC配置
codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

val s = codec.createInputSurface()
val surfaceTexture = encodeCore.buildEGLSurface(s)
inputSurface = Surface(surfaceTexture)
// 構建一個搭載了OpenGL紋理的Surface,而後回調出去
readySurface.invoke(inputSurface)
// 開始編碼
codec.start()

// 計時
val startTime = System.nanoTime()

// 使用數組來保持視頻錄製線程和音頻錄製線程以及Mux線程的同步
while (isRecording.isNotEmpty()) {
    // 編碼數據
    drainEncoder(false)
    frameCount++
    // OpenGL 繪製
    encodeCore.draw()
    val curFrameTime = System.nanoTime() - startTime
    encodeCore.swapData(curFrameTime)
}
// 發送編碼結束信號
drainEncoder(true)
複製代碼

以上僞代碼表明瞭視頻編碼的所有流程,首先咱們須要配置一個合適的MediaCodeC,經過MediaCodeC的codec.createInputSurface函數獲得一個Surface對象,前文我稱之爲InputSurface。而後配置EGL環境,構建OpenGLProgram。【有關OpenGL相關的代碼,我都封裝到了SurfaceEncodeCore這個類裏面。SurfaceEncodeCore的內部職責主要是:構建EGL環境,配置OpenGL程序,繪製紋理】。
OpenGL自己是不負責窗口管理和上下文環境管理的,這個功能由各自平臺提供。Android裏負責爲OpenGL提供窗口管理和上下文環境管理的就是EGL。在EGL裏,是使用EGLSurface將輸出渲染到設備屏幕。而建立EGLSurface有兩種方式,一種是建立一個可實際顯示的Surface,經過eglCreateWindowSurface函數,而這個函數須要一個Surface做爲參數。另外一個是經過eglCreatePbufferSurface建立一個離屏Surface。至此,MediaCodeC的輸入渠道就搭建完畢,這個渠道會在錄製期間,不停接受Camera回調的數據,並經過OpenGLProgram處理。咱們只須要從MediaCodeC源源不斷地提取出已經編碼好的H264碼流,對外回調視頻幀數據便可。
函數drainEncoder的實現爲:

fun MediaCodec.handleOutputBuffer(bufferInfo: MediaCodec.BufferInfo, defTimeOut: Long,
                                  formatChanged: () -> Unit = {},
                                  render: (bufferId: Int) -> Unit,
                                  needEnd: Boolean = true) {
    loopOut@ while (true) {
        //  獲取可用的輸出緩存隊列
        val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)
        Log.d("handleOutputBuffer", "output buffer id : $outputBufferId ")
        if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
            if (needEnd) {
                break@loopOut
            }
        } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            formatChanged.invoke()
        } else if (outputBufferId >= 0) {
            render.invoke(outputBufferId)
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                break@loopOut
            }
        }
    }
}

private fun drainEncoder(isEnd: Boolean = false) {
        if (isEnd) {
            codec.signalEndOfInputStream()
        }
        codec.handleOutputBuffer(bufferInfo, 2500, {
            if (!isFormatChanged) {
                outputFormatChanged.invoke(codec.outputFormat)
                isFormatChanged = true
            }
        }, {
            val encodedData = codec.getOutputBuffer(it)
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
                bufferInfo.size = 0
            }
            if (bufferInfo.size != 0) {
                Log.d(TAG, "buffer info offset ${bufferInfo.offset} time is ${bufferInfo.presentationTimeUs} ")
                encodedData.position(bufferInfo.offset)
                encodedData.limit(bufferInfo.offset + bufferInfo.size)
                Log.d(TAG, "sent " + bufferInfo.size + " bytes to muxer")
                dataCallback.invoke(frameCount, bufferInfo.presentationTimeUs, bufferInfo, encodedData)
            }
            codec.releaseOutputBuffer(it, false)
        }, !isEnd)
    }

複製代碼

這是MediaCodeC處理輸出數據的老一套代碼了,根據dequeueOutputBuffer返回的ID,確認目前編碼器處於何種狀態。再分別加以處理,將獲得的原始數據對外回調。

混合器Mux模塊

至此爲止,整個視頻錄製功能中,視頻錄製編碼模塊完成、音頻錄製編碼模塊完成,只須要一個Mux模塊。將其他兩個模塊提供的數據,串聯起來輸出Mp4文件便可。
在Mux模塊中,已經沒有什麼技術含量了,具體工做就是,維護了兩個數據隊列。一個是視頻幀隊列,另外一個是音頻幀隊列。Mux模塊無限循環地從兩個隊列中提取隊列首端數據。而後比較視頻幀數據和音頻幀數據中的時間戳大小,將時間小的先行封裝便可。具體代碼可參考Muxer

mediaMuxer = MediaMuxer(p, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 添加視頻軌
val videoTrackId = mediaMuxer!!.addTrack(videoTrack)
// 添加音頻軌
val audioTrackId = mediaMuxer!!.addTrack(audioTrack)
mediaMuxer!!.start()

while (isRecording.isNotEmpty()) {
    // 從隊列中提取首端數據
    val videoFrame = videoQueue.firstSafe
    val audioFrame = audioQueue.firstSafe

    val videoTime = videoFrame?.bufferInfo?.presentationTimeUs ?: -1L
    val audioTime = audioFrame?.bufferInfo?.presentationTimeUs ?: -1L
    
    // 比較音頻幀和視頻幀的時間戳
    if (videoTime == -1L && audioTime != -1L) {
        writeAudio(audioTrackId)
        } else if (audioTime == -1L && videoTime != -1L) {
            writeVideo(videoTrackId)
        } else if (audioTime != -1L && videoTime != -1L) {
            // 先寫小一點的時間戳的數據
            if (audioTime < videoTime) {
                // 封裝音頻幀數據
                writeAudio(audioTrackId)
            } else {
                // 封裝視頻幀數據
                writeVideo(videoTrackId)
            }
        } else {
            // do nothing
        }
}
複製代碼

Camera2視頻尺寸選擇

好了。整個視頻錄製所有功能已所有整理完畢。接下來咱們分析一個視頻的尺寸選擇問題,以及Camera2中使我迷惑的點————如何選擇視頻尺寸?
在Android官方文檔中,要想獲取Camera2攝像頭數據,必須依靠Surface。To capture or stream images from a camera device, the application must first create a camera capture session with a set of output Surfaces for use with the camera device, with createCaptureSession(SessionConfiguration).。Camera2會根據你配置的Surface來匹配相應的尺寸,Each Surface has to be pre-configured with an appropriate size and format (if applicable) to match the sizes and formats available from the camera device,每個Surface都必須提早配置好相應的尺寸,以便去匹配Camera2合適的Size。
Camera2在配置的時候,會返回一個可供選擇的尺寸集合,表示當前設備攝像頭所支持的全部尺寸。我在測試視頻錄製時,測試設備返回的尺寸列表以下:

那麼OK。既然知道了支持的尺寸,那麼我在配置MediaCodeC的時候,設置的寬高從這裏面選擇就ok了,就不用再進一步的圖像處理了。但是實際我試驗的結果卻不理想,在MediaCodeC設置第一個尺寸的時候,錄製的視頻畫面毫無變形。可選擇其餘尺寸譬如720 X 960、720 X 1280卻變形嚴重。可當我選擇1088 X 1088 或者 960 X 960,這種等寬高尺寸時,錄製的畫面卻毫無變形。對此我也是毫無頭緒,由於摸不清楚Surface匹配的尺寸機制問題,在OpenGL繪製的時候,就不知道該如何裁剪,這纔是大問題。若是有解決這個問題的朋友,但願能給我提一些建議。
作視頻圖像處理的時候,我走了一些彎路【手動狗頭】。以前我錯誤的斷定了Surface尺寸的來源,致使在視頻尺寸這裏進行了不正確的邏輯判斷。事實上使用SurfaceTexture的setDefaultBufferSize函數能夠達到尺寸匹配。但相應的,Camera2返回的size是寬高相反的,因此這裏的setDefaultBufferSize的寬高也是相反的才能匹配。

以上

相關文章
相關標籤/搜索