原文連接:https://zhuanlan.zhihu.com/p/82130600java
Android的視頻相關的開發,大概一直是整個Android生態,以及Android API中,最爲分裂以及兼容性問題最爲突出的一部分。攝像頭,以及視頻編碼相關的API,Google一直對這方面的控制力很是差,致使不一樣廠商對這兩個API的實現有很多差別,並且從API的設計來看,一直以來優化也至關有限,甚至有人認爲這是「Android上最難用的API之一」web
以微信爲例,咱們錄製一個540p的mp4文件,對於Android來講,大致上是遵循這麼一個流程:數組
大致上就是從攝像頭輸出的YUV幀通過預處理以後,送入編碼器,得到編碼好的h264視頻流。微信
上面只是針對視頻流的編碼,另外還須要對音頻流單獨錄製,最後再將視頻流和音頻流進行合成出最終視頻。多線程
這篇文章主要將會對視頻流的編碼中兩個常見問題進行分析:app
1.視頻編碼器的選擇(硬編 or 軟編)?
2.如何對攝像頭輸出的YUV幀進行快速預處理(鏡像,縮放,旋轉)?異步
對於錄製視頻的需求,很多app都須要對每一幀數據進行單獨處理,所以不多會直接用到MediaRecorder來直接錄取視頻,通常來講,會有這麼兩個選擇:
1.MediaCodec
2.FFMpeg+x264/openh264ide
咱們來逐個解析一下優化
MediaCodec是API 16以後Google推出的用於音視頻編解碼的一套偏底層的API,能夠直接利用硬件加速進行視頻的編解碼。調用的時候須要先初始化MediaCodec做爲視頻的編碼器,而後只須要不停傳入原始的YUV數據進入編碼器就能夠直接輸出編碼好的h264流,整個API設計模型來看,就是同時包含了輸入端和輸出端的兩條隊列:編碼
所以,做爲編碼器,輸入端隊列存放的就是原始YUV數據,輸出端隊列輸出的就是編碼好的h264流,做爲解碼器則對應相反。在調用的時候,MediaCodec提供了同步和異步兩種調用方式,可是異步使用Callback的方式是在API 21以後才加入的,以同步調用爲例,通常來講調用方式大概是這樣(摘自官方例子):
MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B 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 } } codec.stop(); codec.release();
簡單解釋一下,經過getInputBuffers
獲取輸入隊列,而後調用dequeueInputBuffer
獲取輸入隊列空閒數組下標,注意dequeueOutputBuffer
會有幾個特殊的返回值表示當前編解碼狀態的變化,而後再經過queueInputBuffer
把原始YUV數據送入編碼器,而在輸出隊列端一樣經過getOutputBuffers
和dequeueOutputBuffer
獲取輸出的h264流,處理完輸出數據以後,須要經過releaseOutputBuffer
把輸出buffer還給系統,從新放到輸出隊列中。
從上面例子來看的確是很是原始的API,因爲MediaCodec底層是直接調用了手機平臺硬件的編解碼能力,因此速度很是快,可是由於Google對整個Android硬件生態的掌控力很是弱,因此這個API有不少問題:
MediaCodec在初始化的時候,在configure
的時候,須要傳入一個MediaFormat對象,看成爲編碼器使用的時候,咱們通常須要在MediaFormat中指定視頻的寬高,幀率,碼率,I幀間隔等基本信息,除此以外,還有一個重要的信息就是,指定編碼器接受的YUV幀的顏色格式。這個是由於因爲YUV根據其採樣比例,UV份量的排列順序有不少種不一樣的顏色格式,而對於Android的攝像頭在onPreviewFrame
輸出的YUV幀格式,若是沒有配置任何參數的狀況下,基本上都是NV21格式,但Google對MediaCodec的API在設計和規範的時候,顯得很不厚道,過於貼近Android的HAL層了,致使了NV21格式並非全部機器的MediaCodec都支持這種格式做爲編碼器的輸入格式! 所以,在初始化MediaCodec的時候,咱們須要經過codecInfo.getCapabilitiesForType
來查詢機器上的MediaCodec實現具體支持哪些YUV格式做爲輸入格式,通常來講,起碼在4.4+的系統上,這兩種格式在大部分機器都有支持:
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
兩種格式分別是YUV420P和NV21,若是機器上只支持YUV420P格式的狀況下,則須要先將攝像頭輸出的NV21格式先轉換成YUV420P,才能送入編碼器進行編碼,不然最終出來的視頻就會花屏,或者顏色出現錯亂
這個算是一個不大不小的坑,基本上用上了MediaCodec進行視頻編碼都會趕上這個問題
若是使用MediaCodec來編碼H264視頻流,對於H264格式來講,會有一些針對壓縮率以及碼率相關的視頻質量設置,典型的諸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置這些參數可讓咱們在同等的碼率下,得到更高的壓縮率,從而提高視頻的質量,Android也提供了對應的API進行設置,能夠設置到MediaFormat中這些設置項:
MediaFormat.KEY_BITRATE_MODE MediaFormat.KEY_PROFILE MediaFormat.KEY_LEVEL
但問題是,對於Profile,Level, Bitrate mode這些設置,在大部分手機上都是不支持的,即便是設置了最終也不會生效,例如設置了Profile爲high,最後出來的視頻依然還會是Baseline,Shit....
這個問題,在7.0如下的機器幾乎是必現的,其中一個可能的緣由是,Android在源碼層級hardcode了profile的的設置:
// XXX if (h264type.eProfile != OMX_VIDEO_AVCProfileBaseline) { ALOGW("Use baseline profile instead of %d for AVC recording", h264type.eProfile); h264type.eProfile = OMX_VIDEO_AVCProfileBaseline; }
Android直到7.0以後才取消了這段地方的Hardcode
if (h264type.eProfile == OMX_VIDEO_AVCProfileBaseline) { .... } else if (h264type.eProfile == OMX_VIDEO_AVCProfileMain || h264type.eProfile == OMX_VIDEO_AVCProfileHigh) { ..... }
這個問題能夠說間接致使了MediaCodec編碼出來的視頻質量偏低,同等碼率下,難以得到跟軟編碼甚至iOS那樣的視頻質量。
前面說到,MediaCodec這個API在設計的時候,過於貼近HAL層,這在不少Soc的實現上,是直接把傳入MediaCodec的buffer,在不通過任何前置處理的狀況下就直接送入了Soc中。而在編碼h264視頻流的時候,因爲h264的編碼塊大小通常是16x16,因而乎在一開始設置視頻的寬高的時候,若是設置了一個沒有對齊16的大小,例如960x540,在某些cpu上,最終編碼出來的視頻就會直接花屏!
很明顯這仍是由於廠商在實現這個API的時候,對傳入的數據缺乏校驗以及前置處理致使的,目前來看,華爲,三星的Soc出現這個問題會比較頻繁,其餘廠商的一些早期Soc也有這種問題,通常來講解決方法仍是在設置視頻寬高的時候,統一設置成對齊16位以後的大小就行了。
除了使用MediaCodec進行編碼以外,另一種比較流行的方案就是使用ffmpeg+x264/openh264進行軟編碼,ffmpeg是用於一些視頻幀的預處理。這裏主要是使用x264/openh264做爲視頻的編碼器。
x264基本上被認爲是當今市面上最快的商用視頻編碼器,並且基本上全部h264的特性都支持,經過合理配置各類參數仍是可以獲得較好的壓縮率和編碼速度的,限於篇幅,這裏再也不闡述h264的參數配置
openh264則是由思科開源的另一個h264編碼器,項目在2013年開源,對比起x264來講略顯年輕,不過因爲思科支付滿了h264的年度專利費,因此對於外部用戶來講,至關於能夠直接無償使用了,另外,firefox直接內置了openh264,做爲其在webRTC中的視頻的編解碼器使用。
但對比起x264,openh264在h264高級特性的支持比較差:
從編碼效率上來看,openh264的速度也並不會比x264快,不過其最大的好處,仍是可以直接無償使用吧。
從上面的分析來看,
1.硬編的好處主要在於速度快,並且系統自帶不須要引入外部的庫,可是特性支持有限,並且硬編的壓縮率通常偏低,2.而對於軟編碼來講,雖然速度較慢,可是壓縮率比較高,並且支持的H264特性也會比硬編碼多不少,相對來講比較可控。就可用性而言,3.在4.4+的系統上,MediaCodec的可用性是可以基本保證的,可是不一樣等級的機器的編碼器能力會有很多差異,建議能夠根據機器的配置,選擇不一樣的編碼器配置。視頻流合流而後包裝到mp4文件,這部分咱們能夠經過