https://yq.aliyun.com/articles/632892

MediaCodec在Android視頻硬解碼組件的應用
摘要: 本文大致介紹了一下Android MediaCodec 在解碼的接口調用流程和主流業務邏輯。
背景:
隨着多媒體產業的發展,手機端對視頻解碼性能要求越來越高。如果采用cpu進行解碼,則會占用很多cpu資源。現在主流做法是利用手機gpu資源進行視頻解碼。Android系統在Android4.0(API 16)增加了 MediaCodec,可以支持app調用java接口,進而使用底層硬件的音視頻編解碼能力。Android ndk在 Android 5.0(API21) 提供了對應的Native方法。功能大體相同。
MediaCodec 可以處理編碼,也可以處理解碼;可以處理音頻,也可以處理視頻,里面有軟解(cpu),也有硬解(gpu)。具體手機Android 系統一般會寫在 media_codecs.xml 上。不同手機位置不一樣。根據我的經驗,大多數手機上是/system/etc/目錄下。
這里主要是講視頻解碼。
Android MediaCodec內部大致結構

如上圖所示,mediacodec 內部有兩種緩沖,一種是InputBuffer,另一種是OutputBuffer。兩種緩沖的大小一般是底層硬件的代碼決定。解碼過程中,Client需要不斷的查詢InputBuffer和OutputBuffer的情況,如果InputBuffer有空閑,則應放入相應碼流;如果OutputBuffer有輸出,則應該及時去消費視頻幀並且釋放。
codec則內部自啟線程,也是不斷的查詢InputBuffer和OutputBuffer的情況,如果OutputBuffer有空閑並且有未處理的InputBuffer,則去解碼一幀;否則掛起。
Android MediaCodec啟動流程
1.判斷Android Runtime版本
由於ndk接口只有在Android 5.0以上才有,我們先判斷Android版本,如果版本號在5.0以上則使用Ndk接口,否則,使用java反調的方式。
2.創建解碼器
mediaCodec提供創建解碼器的方式有兩種,一種比較簡單的方式是通過MIME直接創建解碼器。MIME是解碼器的類型。例如創建264解碼器的話只需要調用如下函數即可:
AMediaCodec_createDecoderByType("video/avc")
如果手機上264解碼器不止一個(通常手機上會有一個硬解碼器和一個Google的軟解碼器),那么MediaCodec會按照默認的順序選擇一個。當然這個順序可以更改。手機一般情況下,是默認選用硬解的。
如果想精確的選擇創建的解碼器,可以通過名字創建:
AMediaCodec_createCodecByName("OMX.video.decoder.avc")
3.配置解碼器
AMediaCodec_configure(handle,format,surface, crypto, flag)
這個函數有兩個需要注意,一個是mediaFormat,另一個是surface.crypto是加密相關的,我們這里每太用到。flag是編碼應該注意的參數,解碼一般填0.
mediaFormat 是Client需要提前告訴解碼器的一些參數格式,包括width,height, sps, pps等。例如:
AMediaFormat* videoFormat = AMediaFormat_new(); AMediaFormat_setString(videoFormat, "mime", "video/avc"); AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_WIDTH, width); // 視頻寬度 AMediaFormat_setInt32(videoFormat, AMEDIAFORMAT_KEY_HEIGHT, height); // 視頻高度 AMediaFormat_setBuffer(videoFormat, "csd-0", sps, spsSize); // sps AMediaFormat_setBuffer(videoFormat, "csd-1", pps, ppsSize); // pps
我發現如果直接將sps,pps放到第一個I幀之前,format不設置,也能解碼成功。如果提前設置的話,configure函數應該可以提前檢查參數,如果參數不支持,則提前返回失敗。
surface參數直接決定了解碼器 的工作方式。我們如果傳入一個nativeWindow,則解碼器接完之后的AImage將會通過Release方法直接渲染到surface上,然后就有畫面了。這樣省去了圖像從GPU到CPU,CPU再到GPU的拷貝,效率較高;如果我們傳入nullptr,我們則需要通過接口將圖像地址獲取。這樣有個好處就是后面可以接一些CPU的圖像處理,達到我們的要求,然后再進行圖像渲染。
4.啟動解碼器
這個比較簡單。就是Start接口調用一下即可。如果沒有configure,則會失敗。
AMediaCodec_start();
數據流程
啟動之后就開始送數據,取數據進行解碼了。根據之前的大致結構描述,數據流程也基本分為兩步,送數據主要圍繞InputBuffer展開,取數據主要圍繞OutputBuffer展開。為了達到最佳實踐,我們發現最好是用兩個線程分別處理這兩個過程,以免互相影響導致效率降低。
1.送數據
送數據分3步驟,第一步,獲取InputBufferIndex.這步的主要目的是看看InputBuffer是不是滿了。如果InputBuffer滿,則上游應該進行相應的數據緩存操作。
MediaCodec_dequeueInputBuffer(handle, 2000);
第二步,獲取InputBuffer地址,然后填數據:
AMediaCodec_getInputBuffer(handle, idx, &size);
第三步,告訴MediaCodec我們數據填好了:
AMediaCodec_queueInputBuffer(handle, idx, 0, bufferSize, pts, 0);
這里具體一些參數這里不講了,詳情Android Developer上有詳細解釋。我這里是有個疑問,為啥要獲取InputBuffer地址,然后填數據再告訴它填好了,這樣需要兩個函數getInputBuffer和queueInputBuffer。如果直接用一個函數SendDataToInputBuffer代替不更好么?
這里要提一句的是Android 硬解碼只支持AnnexB 格式的碼流,也就是 00 00 00 01 開頭的碼流,如果是avcc 字節長度開頭的碼流,需要提前轉一下。
2.取數據
取數據相對送數據復雜一些,第一步獲取index,這是看看有沒有解碼好的幀:
AMediaCodec_dequeueOutputBuffer(handle, &info, 2000);
如果有,則取幀。若surface填nullptr,則可以通過接口獲取數據地址:
AMediaCodec_getOutputBuffer(handle, idx, &outsize);
如果surface之前填有值,我們可以通過release接口直接將圖像渲染到Surface上:
AMediaCodec_releaseOutputBuffer(handle, idx, bRender);
取數據需要注意一點的就是getOutputBuffer時候可能會獲取到一些負值。並且這些負值都是很有意義的。例如
AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED
就表示了輸出格式發生改變等等。我們需要關注這些信息,及時更新解碼器的輸出格式。
硬解碼業務路線
1.代替軟解的硬解碼
最簡單的方式,就是configure時候Surface填null,然后將解碼后的數據拷貝出來。這樣做的有點很明顯,就是跟之前的軟解邏輯基本一樣,外面並不需要改變太多,之前的VideoProcess 也能接着用,也不需要渲染引擎的配合,封裝性好。缺點是多了一次解碼器內存到自己內存的拷貝。
2.利用解碼器緩存
如果我們針對業務一的拷貝做優化,減少拷貝,這就是第二種業務路線。我們可以利用解碼器的緩存進行輸出存儲。也就是說,我們調用ouputBuffer之后,獲得輸出緩存index,並不着急拷貝出圖像。而是等到渲染時候,調用GetOutputBuffer獲取圖像指針,然后調用Image2D,進行生成gpu紋理。
3.利用GPU Image直接渲染
如果我們configure傳surface,我們可以通過gpu傳遞的方法,直接進行渲染,這樣可以減少GPU <-> CPU之間的內存拷貝。首先configure時候傳surface,我們調用ouputBuffer之后,獲得輸出緩存index,得到渲染時候,直接調用releaseOutputBuffer(handle,idx,true),則解碼器的圖像直接渲染到surface圖像上了。
這樣雖然效率高,但是弊端也很明顯,第一,那就是圖像后處理做不了。第二,這種方案依賴解碼器緩存,這會帶來一些問題。如果解碼器被提前析構,則緩存內容都沒有了。又或者一些播放業務邏輯對解碼器緩存要求較多(比如倒放),這也做不了。
4.利用GPU Image,SurfaceTexture類渲染到OpenGL管線
針對業務路線3,Android系統也考慮到這個問題,提供我們一種方案做折中。我們可以先建立自己的OpenGL環境,然后從建立Texture,通過Texture建立SurfaceTexture,然后取出surface,進行Configure。這樣,MediaCodec的Release就渲染到SurfaceTexture類了。然后我們調用Update方法,就同步到OpenGL的Texture上了。之后可以接各種后處理,然后swapbuffer進行顯示等等。
這樣處理得話,基本所有的業務邏輯都可以滿足。但是有一個小問題就是流暢性不足。具體為:當輸出一個surface,並且OpenGL還沒消費這個surface時候,解碼輸出是被阻塞的。也就是說,outputBuffer和OpenGL cosume 這個surface必須串行執行。如果並行,則會有覆蓋的問題。
因此我們可以采取一步小調整:將OpenGL得到的Texture 拷貝一份(是GPU->GPU復制,紋理復制)。這樣OpenGL就不會阻塞解碼輸出了。但是代價會帶來拷貝性能損耗。
5.多路同步,增大流暢性
Android 6.0 (API23)新增了一個接口 —— setOutputSurface。顧名思義,這個可以動態的設置輸出的Surface。這就完美解決了上面的問題。具體為,我們可以事先建立多個Texture,然后OutputBuffer時候循環輸出到任意一個空閑Texture並標記為帶數據,當OpenGL消費了圖像之后,將Texture回歸空閑。這樣相當於在OutputBuffer和OpenGL消費之間建立了一個紋理緩沖。可以完成多線程並行的需求。
缺點很明顯就是需要Android 6.0才能支持,不過現在通過Android統計面板https://developer.android.com/about/dashboards/
能看到大部分手機都在Android 6.0之上。
最后
Google的官方文檔關於 Android MediaCodec 還是很詳細的。應該還有很多隱藏屬性待我們發覺。我們要多查查官方文檔手冊:
java文檔:https://developer.android.com/reference/android/media/MediaCodec
ndk文檔:https://developer.android.com/ndk/reference/group/media
同時 Android Samples 有Sample Code,可供參考
https://github.com/googlesamples/android-BasicMediaDecoder/
ffmpeg上也有相關封裝,具體文件為:/libavcodec/mediacodecdec.c