在 Android 4.1 版本提供了 MediaCodec 接口來訪問設備的編解碼器,不同於 FFmpeg 的軟件編解碼,它採用的是硬件編解碼能力,因此在速度上會比軟解更具有優勢,但是由於 Android 的碎片化問題,機型眾多,版本各異,導致 MediaCodec 在機型兼容性上需要花精力去適配,並且編解碼流程不可控,全交由廠商的底層硬件去實現,最終得到的視頻質量不一定很理想。
雖然 MediaCodec 仍然存在一定的弊端,但是對於快速實現編解碼需求,還是很值得參考的。
以將相機預覽的 YUV 數據編碼成 H264 視頻流為例來解析 MediaCodec 的使用。
使用解析MediaCodec 工作模型下圖展示了 MediaCodec 的工作方式,一個典型的生產者消費者模型,兩邊的 Client 分別代表輸入端和輸出端,輸入端將數據交給 MediaCodec 進行編碼或者解碼,而輸出端就得到編碼或者解碼後的內容。
輸入端和輸出端是通過輸入隊列緩衝區和輸出隊列緩衝區,兩條緩衝區隊列的形式來和 MediaCodec 傳遞數據。
首先從輸入隊列中出隊得到一個可用的緩衝區,將它填滿數據之後,再將緩衝區入隊,交由 MediaCodec 去處理。
MediaCodec 處理完了之後,再從輸出隊列中出隊得到一個可用的緩衝區,這個緩衝裡面的數據就是編碼或者解碼後的數據了,把這些數據進行相應的處理之後,還需要釋放這個緩衝區,讓它回到隊列中去,可供下一次使用。
MediaCodec 生命周期另外,MediaCodec 也存在相應的 生命周期,如下圖所示:
當創建了 MediaCodec 之後,是處於未初始化的 Uninitialized 狀態,調用 configure 方法之後就處於 Configured 狀態,調用了 start 方法之後,就處於 Executing 狀態。
在 Executing 狀態下開始處理數據,它又有三個子狀態,分別是:
當一調用 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 對象,它表示媒體數據格式的相關信息,對於視頻主要有以下信息要設置:
其中,碼率就是指單位傳輸時間傳送的數據位數,一般用 kbps 即千位每秒來表示。而幀率就是指每秒顯示的幀數。
其實對於碼率有三種模式可以控制:
對於顏色格式,由於是將 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 = 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 。
其中,時間戳通常是緩衝區渲染的時間,而標識則有多種標識,標識當前緩衝區屬於那種類型:
在編碼的時候可以計算當前緩衝區的時間戳,也可以直接傳遞 0 就好了,對於標識也可以直接傳遞 0 作為參數。
把數據傳入給 MediaCodec 之後,通過 dequeueOutputBuffer 方法取出編解碼後的數據,除了指定超時時間外,還需要傳入 MediaCodec.BufferInfo 對象,這個對象裡面有着編碼後數據的長度、偏移量以及標識符。
取出 MediaCodec.BufferInfo 內的數據之後,根據不同的標識符進行不同的操作:
對於返回的 flags ,不符合預定義的標識,則可以直接寫入,那些數據可能代表的是 H264 中的 P 幀 或者 B 幀。
對於編解碼後的數據,進行操作後,通過 releaseOutputBuffer 方法釋放對應的緩衝區,其中第二個參數 render 代表是否要渲染到 surface 上,這裡暫時不需要就為 false 。
停止編碼當想要停止編碼時,通過 MediaCodec 的 stop 方法切換到 Uninitialized 狀態,然後再調用 release 方法釋放掉。
這裡並沒有採用使用 BUFFER_FLAG_END_OF_STREAM 標識符的方式來停止編碼,而是直接切換狀態了,在通過 Surface 方式進行錄製時,再去採用這種方式了。
對於 MediaCodec 硬編碼解析之相機內容編碼成 H264 文件就到這裡了,主要還是講述了關於 MediaCodec 的使用,一旦熟悉使用了,完成編碼工作也就很簡單了。
技術交流,歡迎加我微信:ezglumes ,拉你入技術交流群。
私信領取相關資料
推薦閱讀:
音視頻開發工作經驗分享 || 視頻版
OpenGL ES 學習資源分享
開通專輯 | 細數那些年寫過的技術文章專輯
NDK 學習進階免費視頻來了
你想要的音視頻開發資料庫來了
推薦幾個堪稱教科書級別的 Android 音視頻入門項目
覺得不錯,點個在看唄~
