在 Android 4.1 版本提供了 MediaCodec 接口來訪問設備的編解碼器,不一樣於 FFmpeg 的軟件編解碼,它採用的是硬件編解碼能力,所以在速度上會比軟解更具備優點,可是因爲 Android 的碎片化問題,機型衆多,版本各異,致使 MediaCodec 在機型兼容性上須要花精力去適配,而且編解碼流程不可控,全交由廠商的底層硬件去實現,最終獲得的視頻質量不必定很理想。git
雖然 MediaCodec 仍然存在必定的弊端,可是對於快速實現編解碼需求,仍是很值得參考的。github
以將相機預覽的 YUV 數據編碼成 H264 視頻流爲例來解析 MediaCodec 的使用。面試
下圖展現了 MediaCodec 的工做方式,一個典型的生產者消費者模型,兩邊的 Client 分別表明輸入端和輸出端,輸入端將數據交給 MediaCodec 進行編碼或者解碼,而輸出端就獲得編碼或者解碼後的內容。ide
輸入端和輸出端是經過輸入隊列緩衝區和輸出隊列緩衝區,兩條緩衝區隊列的形式來和 MediaCodec 傳遞數據。編碼
首先從輸入隊列中出隊獲得一個可用的緩衝區,將它填滿數據以後,再將緩衝區入隊,交由 MediaCodec 去處理。spa
MediaCodec 處理完了以後,再從輸出隊列中出隊獲得一個可用的緩衝區,這個緩衝裏面的數據就是編碼或者解碼後的數據了,把這些數據進行相應的處理以後,還須要釋放這個緩衝區,讓它回到隊列中去,可供下一次使用。線程
另外,MediaCodec 也存在相應的 生命週期,以下圖所示:code
當建立了 MediaCodec 以後,是處於未初始化的 Uninitialized
狀態,調用 configure 方法以後就處於 Configured
狀態,調用了 start 方法以後,就處於 Executing
狀態。orm
在 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,此時是 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 對象,它表示媒體數據格式的相關信息,對於視頻主要有如下信息要設置:
其中,碼率就是指單位傳輸時間傳送的數據位數,通常用 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/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
。
其中,時間戳一般是緩衝區渲染的時間,而標識則有多種標識,標識當前緩衝區屬於那種類型:
在編碼的時候能夠計算當前緩衝區的時間戳,也能夠直接傳遞 0 就行了,對於標識也能夠直接傳遞 0 做爲參數。
把數據傳入給 MediaCodec 以後,經過 dequeueOutputBuffer
方法取出編解碼後的數據,除了指定超時時間外,還須要傳入 MediaCodec.BufferInfo
對象,這個對象裏面有着編碼後數據的長度、偏移量以及標識符。
取出 MediaCodec.BufferInfo
內的數據以後,根據不一樣的標識符進行不一樣的操做:
00 00 00 01 67
和 00 00 00 01 68
開頭的數據,這個數據是必需要有的,它裏面有着視頻的寬、高信息。00 00 00 01 65
的數據,對於返回的 flags ,不符合預約義的標識,則能夠直接寫入,那些數據可能表明的是 H264 中的 P 幀 或者 B 幀。
對於編解碼後的數據,進行操做後,經過 releaseOutputBuffer
方法釋放對應的緩衝區,其中第二個參數 render
表明是否要渲染到 surface 上,這裏暫時不須要就爲 false 。
當想要中止編碼時,經過 MediaCodec 的 stop
方法切換到 Uninitialized
狀態,而後再調用 release
方法釋放掉。
這裏並無採用使用 BUFFER_FLAG_END_OF_STREAM
標識符的方式來中止編碼,而是直接切換狀態了,在經過 Surface 方式進行錄製時,再去採用這種方式了。
對於 MediaCodec 硬編碼解析之相機內容編碼成 H264 文件就到這裏了,主要仍是講述了關於 MediaCodec 的使用,一旦熟悉使用了,完成編碼工做也就很簡單了。
阿里P6P7【安卓】進階資料分享+加薪跳槽必備面試題