天天看點

Android MediaCodec 寫死 H264 檔案

在 Android 4.1 版本提供了 MediaCodec 接口來通路裝置的編解碼器,不同于 FFmpeg 的軟體編解碼,它采用的是硬體編解碼能力,是以在速度上會比軟解更具有優勢,但是由于 Android 的碎片化問題,機型衆多,版本各異,導緻 MediaCodec 在機型相容性上需要花精力去适配,并且編解碼流程不可控,全交由廠商的底層硬體去實作,最終得到的視訊品質不一定很理想。

雖然 MediaCodec 仍然存在一定的弊端,但是對于快速實作編解碼需求,還是很值得參考的。

以将相機預覽的 YUV 資料編碼成 H264 視訊流為例來解析 MediaCodec 的使用。

使用解析

MediaCodec 工作模型

下圖展示了 MediaCodec 的工作方式,一個典型的生産者消費者模型,兩邊的 Client 分别代表輸入端和輸出端,輸入端将資料交給 MediaCodec 進行編碼或者解碼,而輸出端就得到編碼或者解碼後的内容。

Android MediaCodec 寫死 H264 檔案

輸入端和輸出端是通過輸入隊列緩沖區和輸出隊列緩沖區,兩條緩沖區隊列的形式來和 MediaCodec  傳遞資料。

首先從輸入隊列中出隊得到一個可用的緩沖區,将它填滿資料之後,再将緩沖區入隊,交由 MediaCodec 去處理。

MediaCodec 處理完了之後,再從輸出隊列中出隊得到一個可用的緩沖區,這個緩沖裡面的資料就是編碼或者解碼後的資料了,把這些資料進行相應的處理之後,還需要釋放這個緩沖區,讓它回到隊列中去,可供下一次使用。

MediaCodec 生命周期

另外,MediaCodec 也存在相應的 生命周期,如下圖所示:

Android MediaCodec 寫死 H264 檔案

當建立了 MediaCodec 之後,是處于未初始化的

Uninitialized

狀态,調用 configure 方法之後就處于

Configured

狀态,調用了 start 方法之後,就處于

Executing

狀态。

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, …);
   } elseif (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/EzCameraKit

編解碼操作

編解碼操作代碼如下:

while (isEncoding) {
    // YUV 顔色格式轉換
    if (!mEncodeDataQueue.isEmpty()) {
        input = mEncodeDataQueue.poll();
        byte[] yuv420sp = newbyte[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 = newbyte[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);
                } elseif ((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

内的資料之後,根據不同的辨別符進行不同的操作:

    • 表示目前資料是一些配置資料,在 H264 編碼中就是 SPS 和 PPS 資料,也就是

      00 00 00 01 67

      00 00 00 01 68

      開頭的資料,這個資料是必須要有的,它裡面有着視訊的寬、高資訊。
    • 關鍵幀資料,對于 I 幀資料,也就是開頭是

      00 00 00 01 65

      的資料,
    • 表示結束,MediaCodec 工作結束

對于傳回的 flags ,不符合預定義的辨別,則可以直接寫入,那些資料可能代表的是 H264 中的 P 幀 或者 B 幀。

對于編解碼後的資料,進行操作後,通過

releaseOutputBuffer

方法釋放對應的緩沖區,其中第二個參數

render

代表是否要渲染到 surface 上,這裡暫時不需要就為 false 。

停止編碼

當想要停止編碼時,通過 MediaCodec 的

stop

方法切換到

Uninitialized

狀态,然後再調用

release

方法釋放掉。

這裡并沒有采用使用

BUFFER_FLAG_END_OF_STREAM

辨別符的方式來停止編碼,而是直接切換狀态了,在通過 Surface 方式進行錄制時,再去采用這種方式了。

對于 MediaCodec 寫死解析之相機内容編碼成 H264 檔案就到這裡了,主要還是講述了關于 MediaCodec 的使用,一旦熟悉使用了,完成編碼工作也就很簡單了。

安卓以及音視訊雜談
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。  
Android MediaCodec 寫死 H264 檔案

繼續閱讀