MediaCodec編碼OpenGL速度和清晰度均衡


 
## 概述

 

在安卓平台為了實現h264視頻編碼,我們通常可以使用libx264, ffmpeg等第三方視頻編碼庫,但是如果對編碼的速度有一定的要求,要實現實時甚至超實時的高速視頻編碼,我們並沒有太多選項,只能使用Android提供的MediaCodec硬編碼模塊。
MediaCodec模塊在實際使用中會遇到很多問題,本文主要討論使用MediaCodec來對OpenGL渲染的畫面進行編碼視頻時,如何達到速度快和畫面清晰的均衡。

 

**注意,本文將默認你已經熟悉使用MediaCodec,配合SurfaceTexture進行OpenGL畫面編碼的基本流程**



## 分析

 

### 影響編碼速度的因素
出去設備硬件的因素,影響MediaCodec對視頻畫面進行編碼的速度的其他因素並不多,我們實踐探索下來主要發現以下幾點:

 

1. 畫面尺寸
畫面尺寸就是要編碼的視頻畫面的寬高了,它直接定義了每一幀要編碼的畫面數據的大小,所以它也主要決定了Mediacodec在進行每幀編碼時的任務量。這個很容易理解。

 

2. 幀速率
幀速率指的是每一秒時長的視頻所包含的靜止畫面的幀數。由於Mediacodec在編碼時是以幀為單位,對每一個靜止幀畫面進行編碼,所以,幀速率直接決定了MediaCodec在編碼固定時長的視頻時需要編碼的畫面數量。
比如,同樣編碼一段10秒鍾的視頻,在不考慮畫面內容和其他因素的影響下,如果幀速率是30幀每秒,那么MediaCodec的編碼工作量大致就是幀速率15幀每秒的情況的2倍,所以其耗時也大致是其2倍。

 

3. 同步模式下的Time Out設置
MediaCodec有兩種工作模式,同步模式和異步模式,其中異步模式只有在Android 5.0以上的系統才支持。如果要兼容5.0以下的系統,那么就必須用到同步模式,而網上流傳的眾多對MediaCodec的編碼流程講解(比如經典的bigflake)也都是基於同步模式的。
那么在這些文檔和案例指導下,大家在使用MediaCodec進行編碼時,也都使用着大致相同的設置。
其中,同步模式下,在編碼流程中,我們需要調用MediaCodec的`dequeueOutputBuffer`接口來從隊列里獲取編碼輸出緩沖區的內容。 `dequeueOutputBuffer`接口的第二個參數,`timeoutUs`是一個以微妙為單位的時間值,它定義了從MediaCodec獲取輸出緩沖區內容時的超時期限。
目前流傳的文檔教程和案例中,大多將該值設置為10000微秒,也就是10毫秒。但是當我們把這個值降低到1毫秒甚至更低時,會發現,MediaCodec整個編碼流程的速度會得到大幅度的提升,而基於我們的測試,編碼結果也並未因此受到太多可視的影響。
當然,Android官方文檔中也並未對`timeoutUs`的合適取值范圍給出具體的建議,我這里得出的結論是在我們項目實際開發中測試得出,並且其結果滿足我們的需求。
如果你的項目不需要保持對5.0以下系統的支持,我還是建議你使用異步模式來進行編碼,這樣可以直接規避這種同步等待的問題。

 

4. Encoder Profile
Profile是對視頻壓縮特性的描述,它主要是定義了編碼工具的集合。Profile越高,就說明其采用了越高級的壓縮特性。而通常,更高級的壓縮方式通常需要耗費更多的壓縮時間,也就是說,當profile設置的越高時,其編碼耗費的時間往往更多。
例如,當我們在使用ffmpeg進行編碼時,如果我們把編碼配置設置為`ultra-fast`時,profile會被強制設置為baseline。

 

### 影響畫面清晰度的因素

 

1. Encoder Profile Level
Profile是對視頻壓縮特性的描述,它主要是定義了編碼工具的集合。Profile越高,就說明其采用了越高級的壓縮特性,相應的,同樣配置下所編碼得到的視頻文件的清晰度就越高,並且碼流越小。
安卓系統支持的H264編碼profile一共有3種
* Baseline Profile
* Main Profile
* High Profile

 

其中Baseline是從Android 3.0之后開始支持,據我們測試,Main Profile及更高的Profile的自定義設置時是從Android 7.0之后開始支持,之前版本Android系統調用相應接口對其設置均不起作用。而就算Android7.0以后的系統,也存在部分機型(比如部分OPPO和華為機型)不支持的情況。所以如果你的產品需要保持對主流機型的支持,Profile的可靠選擇也只有Baseline了。

 

2. Biterate
Biterate, 即視頻碼率,是視頻數據(視頻色彩量、亮度量、像素量)每秒輸出的位數。一般用的單位是kbps。
通常情況下,碼率越高則視頻清晰度越高。但是由於人肉眼分辨的范圍有限,碼率設置過高所帶來的畫面清晰度替身也很難被人眼辨別,所以碼率設置過高也沒有意義。
通常情況下,我推薦一個公式來計算如何對編碼器設置對應的碼率:
Biterate = Width * Height * FrameRate * Factor
其中,Width、Height和FrameRate分別代表視頻的寬度,高度和幀速率,而Factor則是一個系數,用來控制碼率。
通常情況下,在網絡流媒體使用場景中,可以將Factor設置為0.1~0.2,這樣能在保證畫面損失不嚴重的情況下生成出來的視頻文件大小較小;在普通本地瀏覽的使用場景中,可以將Factor設置為0.25~0.5,這樣可以保證畫面清晰度不會因為編碼而造成過多肉眼可見的損失,這樣生成出來的視頻文件也相對較大;在高清視頻處理的使用場景中,可以將Factor設置為0.5以上。
不過,當視頻畫面顏色越豐富、畫面變化越快時,視頻編碼需要的碼率就更高,如果遇到這種視頻畫面場景,需要適當提高碼率來保證清晰度。
需要注意的是,大多數安卓機型在對Bitrate的支持都有一個上限,如果設置的Bitrate值超過了上限,可能導致編碼器拋出異常,進而編碼流程失敗。所以在設計Bitrate的值時,需要通過`MediaCodecInfo.CodecCapabilities`提供的相關接口來檢查系統支持的Bitrate上限。

 

3. Biterate Mode
雖然我們可以通過接口為編碼器設置相關的碼率,但是編碼器在編碼過程中如果處理我們設置的碼率也是有幾種不同的模式的。
* BITRATE_MODE_CQ
忽略用戶設置的碼率,由編碼器自己控制碼率,並盡可能保證畫面清晰度和碼率的均衡。
* BITRATE_MODE_CBR
無論視頻的畫面內容如果,盡可能遵守用戶設置的碼率
* BITRATE_MODE_VBR
盡可能遵守用戶設置的碼率,但是會根據幀畫面之間運動矢量(通俗理解就是幀與幀之間的畫面變化程度)來動態調整碼率,如果運動矢量較大,則在該時間段將碼率調高,如果畫面變換很小,則碼率降低。
 
所以,我們在設置碼率的同時,也要注意對Bitrate Mode的設置。不同的設置對於生成出來的視頻文件的大小和清晰度的影響都是不同的。

 

4. 時間戳和幀速率
我們知道,在使用MediaCodec對OpenGL渲染內容進行編碼的過程中,可以通過設置`MediaFormat`的`MediaFormat.KEY_FRAME_RATE`字段來設置編碼器的幀速率;也可以通過設置`MediaCodec.BufferInfo`對象的`presentationTimeUs`值來設置每一幀畫面的時間戳。但是通常會忽略,我們在運行完OpenGL相關繪制命令,在調用`swapbuffer`之前需要調用`eglPresentationTimeANDROID`接口來設置當前幀的時間戳。
如果沒有調用`eglPresentationTimeANDROID`來設置當前幀的時間戳,只設置了`MediaCodec.BufferInfo`對象的`presentationTimeUs`,編碼產生的視頻在播放時不會有任何時間上的異常,所以很多開發者往往忽略了`eglPresentationTimeANDROID`接口的調用。但實際上,如果不調用`eglPresentationTimeANDROID`,編碼出的視頻在清晰度上會有額外的損失。
由於MediaCodec的設計是面向實時視頻畫面流編碼的使用場景,所以MediaCodec會根據用戶向其輸入畫面的速度來對編碼的速度進行調節。如果我們不通過`eglPresentationTimeANDROID`來在編碼之前對畫面的時間戳進行設置,那么MediaCodec往往會將我們向其輸入畫面的速度默認為實時速度,來對編碼速度進行調節。這種調節會造成碼率降低,視頻畫面清晰度降低。
當我們通過`eglPresentationTimeANDROID`在編碼之前對畫面的時間戳進行了正確的設置,MediaCodec會以實際每幀的時間戳為依據來對編碼素材進行調節。這樣可以保證其不會對碼率進行額外的降低,保證畫面清晰度設置生效。

 

## 解決方案

 

結合以上的分析,為了當我們在使用MediaCodec進行視頻編碼時,為了達到編碼速度和畫面清晰度的平衡,我們需要在通過幾個方面進行綜合的優化。

 

### 1. Profile方法
綜合上文的介紹,為了保證app在各個Android機型上的完美適配,其實我們在Profile方面能做的選擇不多,最安全的情況是將Profile設置為Baseline。

 

### 2. Bitrate方法
Bitrate的設置我推薦使用Biterate = Width * Height * FrameRate * Factor的公式結合產品的使用場景進行設置。

 

### 3. Biterate Mode方法
Biterate Mode的默認設置是BITRATE_MODE_VBR,我推薦在系統和機型支持的情況下盡量將Biterate Mode設置為BITRATE_MODE_CQ。
在BITRATE_MODE_CQ情況下,編碼器自身對碼率和編碼速度的調節往往能達到理想的效果,生成的視頻文件不至於過多,但是畫面清晰度優秀。
當然也要注意做好系統和機型的適配,進行異常處理,因為在某些機型上可能出現不支持BITRATE_MODE_CQ而導致MediaCodec在`configure`方法時失敗,那么此時,我們需要回退到系統默認的Biterate Mode了。

 

### 4. 時間戳正確設置
我們在完成每幀OpenGL畫面渲染,調用`swapbuffer`前一定要先調用`eglPresentationTimeANDROID`方法來正確設置該幀的時間戳。
`eglPresentationTimeANDROID`默認並不直接暴露,我們在包含相應頭文件前先定義拓展宏來載入該函數。下面是載入`eglPresentationTimeANDROID`函數的相關代碼:
``` c++
#define EGL_EGLEXT_PROTOTYPES
#include <EGL/eglext.h>

 

eglPresentationTimeANDROID(mDisplay, mSurface, (EGLnsecsANDROID) time); // 在調用eglSwapBuffers先正確設置當前時間戳
eglSwapBuffers(mDisplay, mSurface);
```






免責聲明!

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



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