Android MediaCodec 硬編碼 H264 文件

在 Android 4.1 版本提供了 MediaCodec 接口來訪問設備的編解碼器,不一樣於 FFmpeg 的軟件編解碼,它採用的是硬件編解碼能力,所以在速度上會比軟解更具備優點,可是因爲 Android 的碎片化問題,機型衆多,版本各異,致使 MediaCodec 在機型兼容性上須要花精力去適配,而且編解碼流程不可控,全交由廠商的底層硬件去實現,最終獲得的視頻質量不必定很理想。git

雖然 MediaCodec 仍然存在必定的弊端,可是對於快速實現編解碼需求,仍是很值得參考的。github

以將相機預覽的 YUV 數據編碼成 H264 視頻流爲例來解析 MediaCodec 的使用。面試

使用解析

MediaCodec 工做模型

下圖展現了 MediaCodec 的工做方式,一個典型的生產者消費者模型,兩邊的 Client 分別表明輸入端和輸出端,輸入端將數據交給 MediaCodec 進行編碼或者解碼,而輸出端就獲得編碼或者解碼後的內容。ide

輸入端和輸出端是經過輸入隊列緩衝區和輸出隊列緩衝區,兩條緩衝區隊列的形式來和 MediaCodec  傳遞數據。編碼

首先從輸入隊列中出隊獲得一個可用的緩衝區,將它填滿數據以後,再將緩衝區入隊,交由 MediaCodec 去處理。spa

MediaCodec 處理完了以後,再從輸出隊列中出隊獲得一個可用的緩衝區,這個緩衝裏面的數據就是編碼或者解碼後的數據了,把這些數據進行相應的處理以後,還須要釋放這個緩衝區,讓它回到隊列中去,可供下一次使用。線程

MediaCodec 生命週期

另外,MediaCodec 也存在相應的 生命週期,以下圖所示:code

當建立了 MediaCodec 以後,是處於未初始化的 Uninitialized 狀態,調用 configure 方法以後就處於 Configured 狀態,調用了 start 方法以後,就處於 Executing 狀態。orm

在 Executing 狀態下開始處理數據,它又有三個子狀態,分別是:視頻

  • Flushed
  • Running
  • End of Stream

當一調用 start 方法以後,就進入了 Flushed 狀態,從輸入緩衝區隊列中取出一個緩衝區就進入了 Running 狀態,當入隊的緩衝區帶有 EOS 標誌時, 就會切換到 End of Stream 狀態, MediaCodec 再也不接受入隊的緩衝區,可是仍然會對已入隊的且沒有進行編解碼操做的緩衝區進行操做、輸出,直到輸出的緩衝區帶有 EOS 標誌,表示編解碼操做完成了。

在 Executing 狀態下能夠調用 flush 方法,使 MediaCodec 切換到 Flushed 狀態。

在 Executing 狀態下能夠調用 stop 方法,使 MediaCodec 切換到 Uninitialized 狀態,而後再次調用 configure 方法進入 Configured 狀態。另外,當調用 reset 方法也會進入到 Uninitialized 狀態。

當再也不須要 MediaCodec 時,調用 release 方法將它釋放掉,進入 Released 狀態。

當 MediaCodec 工做發生異常時,會進入到 Error 狀態,此時仍是能夠經過 reset 方法恢復過來,進入 Uninitialized 狀態。

MediaCodec 調用流程

理解了 MediaCodec 的生命週期和工做流程以後,就能夠上手來進行編碼工做了。

以 MediaCodec 同步調用爲例,使用過程以下:

// 建立 MediaCodec,此時是 Uninitialized 狀態
 MediaCodec codec = MediaCodec.createByCodecName(name);
 // 調用 configure 進入 Configured 狀態
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 // 調用 start 進入 Executing 狀態,開始編解碼工做
 codec.start();
 for (;;) {
   // 從輸入緩衝區隊列中取出可用緩衝區,並填充數據
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   // 從輸出緩衝區隊列中拿到編解碼後的內容,進行相應操做後釋放,供下一次使用
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 // 調用 stop 方法進入 Uninitialized 狀態
 codec.stop();
 // 調用 release 方法釋放,結束操做
 codec.release();

代碼解析

MediaFormat 設置

首先須要建立並設置好 MediaFormat 對象,它表示媒體數據格式的相關信息,對於視頻主要有如下信息要設置:

  • 顏色格式
  • 碼率
  • 碼率控制模式
  • 幀率
  • I 幀間隔

其中,碼率就是指單位傳輸時間傳送的數據位數,通常用 kbps 即千位每秒來表示。而幀率就是指每秒顯示的幀數。

其實對於碼率有三種模式能夠控制:

  • BITRATE_MODE_CQ
  • 表示不控制碼率,盡最大可能保證圖像質量
  • BITRATE_MODE_VBR
  • 表示 MediaCodec 會根據圖像內容的複雜度來動態調整輸出碼率,圖像負責則碼率高,圖像簡單則碼率低
  • BITRATE_MODE_CBR
  • 表示 MediaCodec 會把輸出的碼率控制爲設定的大小

對於顏色格式,因爲是將 YUV 數據編碼成 H264,而 YUV 格式又有不少,這又涉及到機型兼容性問題。在對相機編碼時要作好格式的處理,好比相機使用的是 NV21 格式,MediaFormat 使用的是 COLOR_FormatYUV420SemiPlanar,也就是 NV12 模式,那麼就得作一個轉換,把 NV21 轉換到 NV12 。

對於 I 幀間隔,也就是隔多久出現一個 H264 編碼中的 I 幀。

完整 MediaFormat 設置示例:

MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        // 馬率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
        // 調整碼率的控流模式
        mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
        // 設置幀率
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        // 設置 I 幀間隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

當開始編解碼操做時,開啓編解碼線程,處理相機預覽返回的 YUV 數據。

在這裏用到了相機的一個封裝庫:

https://github.com/glumes/EzC...

編解碼操做

編解碼操做代碼以下:

while (isEncoding) {
    // YUV 顏色格式轉換
    if (!mEncodeDataQueue.isEmpty()) {
        input = mEncodeDataQueue.poll();
        byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
        NV21ToNV12(input, yuv420sp, mWidth, mHeight);
        input = yuv420sp;
    }
    if (input != null) {
        try {
            // 從輸入緩衝區隊列中拿到可用緩衝區,填充數據,再入隊
            ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
            ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
            int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
            if (inputBufferIndex >= 0) {
                // 計算時間戳
                pts = computePresentationTime(generateIndex);
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                inputBuffer.put(input);
                mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
                generateIndex += 1;
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
            // 從輸出緩衝區隊列中拿到編碼好的內容,對內容進行相應處理後在釋放
            while (outputBufferIndex >= 0) {
                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                byte[] outData = new byte[bufferInfo.size];
                outputBuffer.get(outData);
                // flags 利用位操做,定義的 flag 都是 2 的倍數
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // 配置相關的內容,也就是 SPS,PPS
                    mOutputStream.write(outData, 0, outData.length);
                } else if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { // 關鍵幀
                    mOutputStream.write(outData, 0, outData.length);
                } else {
                    // 非關鍵幀和SPS、PPS,直接寫入文件,多是B幀或者P幀
                    mOutputStream.write(outData, 0, outData.length);
                }
                mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
            }
        } catch (IOException e) {
            Log.e(TAG, e.getMessage());
        }
    } else {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Log.e(TAG, e.getMessage());
        }
    }
}

首先,要把要把相機的 NV21 格式轉換成 NV12 格式,而後 經過 dequeueInputBuffer 方法去從可用的輸入緩衝區隊列中出隊取出緩衝區,填充完數據後再經過 queueInputBuffer方法入隊。

dequeueInputBuffer 返回緩衝區索引,若是索引小於 0 ,則表示當前沒有可用的緩衝區。它的參數 timeoutUs 表示超時時間 ,畢竟用的是 MediaCodec 的同步模式,若是沒有可用緩衝區,就會阻塞指定參數時間,若是參數爲負數,則會一直阻塞下去。

queueInputBuffer 方法將數據入隊時,除了要傳遞出隊時的索引值,而後還須要傳入當前緩衝區的時間戳 presentationTimeUs 和當前緩衝區的一個標識 flag 。

其中,時間戳一般是緩衝區渲染的時間,而標識則有多種標識,標識當前緩衝區屬於那種類型:

  • BUFFER_FLAG_CODEC_CONFIG
  • 標識當前緩衝區攜帶的是編解碼器的初始化信息,並非媒體數據
  • BUFFER_FLAG_END_OF_STREAM
  • 結束標識,當前緩衝區是最後一個了,到了流的末尾
  • BUFFER_FLAG_KEY_FRAME
  • 表示當前緩衝區是關鍵幀信息,也就是 I 幀信息

在編碼的時候能夠計算當前緩衝區的時間戳,也能夠直接傳遞 0 就行了,對於標識也能夠直接傳遞 0 做爲參數。

把數據傳入給 MediaCodec 以後,經過 dequeueOutputBuffer 方法取出編解碼後的數據,除了指定超時時間外,還須要傳入 MediaCodec.BufferInfo 對象,這個對象裏面有着編碼後數據的長度、偏移量以及標識符。

取出 MediaCodec.BufferInfo 內的數據以後,根據不一樣的標識符進行不一樣的操做:

  • BUFFER_FLAG_CODEC_CONFIG
  • 表示當前數據是一些配置數據,在 H264 編碼中就是 SPS 和 PPS 數據,也就是 00 00 00 01 67 和 00 00 00 01 68 開頭的數據,這個數據是必需要有的,它裏面有着視頻的寬、高信息。
  • BUFFER_FLAG_KEY_FRAME
  • 關鍵幀數據,對於 I 幀數據,也就是開頭是 00 00 00 01 65 的數據,
  • BUFFER_FLAG_END_OF_STREAM
  • 表示結束,MediaCodec 工做結束

對於返回的 flags ,不符合預約義的標識,則能夠直接寫入,那些數據可能表明的是 H264 中的 P 幀 或者 B 幀。

對於編解碼後的數據,進行操做後,經過 releaseOutputBuffer 方法釋放對應的緩衝區,其中第二個參數 render 表明是否要渲染到 surface 上,這裏暫時不須要就爲 false 。

中止編碼

當想要中止編碼時,經過 MediaCodec 的 stop 方法切換到 Uninitialized 狀態,而後再調用 release 方法釋放掉。

這裏並無採用使用 BUFFER_FLAG_END_OF_STREAM 標識符的方式來中止編碼,而是直接切換狀態了,在經過 Surface 方式進行錄製時,再去採用這種方式了。

對於 MediaCodec 硬編碼解析之相機內容編碼成 H264 文件就到這裏了,主要仍是講述了關於 MediaCodec 的使用,一旦熟悉使用了,完成編碼工做也就很簡單了。
阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題

相關文章
相關標籤/搜索