MediaCodec在Android視頻硬解碼組件的應用


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

  1. 雲棲社區>
  2. 博客列表>
  3. 正文

MediaCodec在Android視頻硬解碼組件的應用

 
cheenc  2018-09-03 11:21:35 瀏覽433 評論0

摘要: 本文大致介紹了一下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 大致結構

如上圖所示,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM