談談關於Android視頻編碼的那些坑

本文講的是談論關於Android視頻編碼的那些坑,Android的視頻相關的開發,大概一直是整個Android生態,以及Android API中,最爲分裂以及兼容性問題最爲突出的一部分。攝像頭,以及視頻編碼相關的API,谷歌一直對這方面的控制力很是差,致使不一樣廠商對這兩個API的實現有很多差別,並且從API的設計來看,一直以來優化也至關有限,甚至有人認爲這是「安卓上最難用的API之一」java

以微信爲例,咱們錄製一個540P的MP4文件,對於安卓來講,大致上是遵循這麼一個流程:android

談談關於Android的視頻編碼的那些坑

大致上就是從攝像頭輸出的YUV幀通過預處理以後,送入編碼器,得到編碼好的H264視頻流。算法

上面只是針對視頻流的編碼,另外還須要對音頻流單獨錄製,最後再將視頻流和音頻流進行合成出最終視頻。微信

這篇文章主要將會對視頻流的編碼中兩個常見問題進行分析:多線程

  • 視頻編碼器的選擇(硬編或軟編)?
  • 如何對攝像頭輸出的YUV幀進行快速預處理(鏡像,縮放,旋轉)?

視頻編碼器的選擇異步

對於錄製視頻的需求,很多應用都須要對每一幀數據進行單獨處理,所以不多會直接用到MediaRecorder來直接錄製視頻,通常來講,會有這麼兩個選擇ide

  • MediaCodec
  • FFmpeg的+ X264 / openh264

咱們來逐個解析一下函數

MediaCodec性能

MediaCodec是API 16以後Google推出的用於音視頻編解碼的一套偏底層的API,能夠直接利用硬件加速進行視頻的編解碼。調用的時候須要先初始MediaCodec做爲視頻的編碼器,而後只須要不停傳入原始的YUV數據進入編碼器就能夠直接輸出編碼好的H264流,整個API設計模型來看,就是同時包含了輸入端和輸出端的兩條隊列:測試

談談關於Android的視頻編碼的那些坑

所以,做爲編碼器,輸入端隊列存放的就是原始YUV數據,輸出端隊列輸出的就是編碼好的H264流,做爲解碼器則對應相反。在調用的時候,MediaCodec提供了同步和異步兩種調用方式,可是異步使用Callback的方式是在API 21以後才加入的,以同步調用爲例,通常來講調用方式大概是這樣(摘自官方例子):

 
  1. MediaCodec codec = MediaCodec.createByCodecName(name ); 
  2. codec.configure(format,...); 
  3. MediaFormat outputFormat = codec.getOutputFormat(); //  選項 B 
  4. codec.start(); 
  5. for  (;;){ 
  6.   int  inputBufferId = codec.dequeueInputBuffer(timeoutUs); 
  7.   if(inputBufferId> = 0){ 
  8.     ByteBuffer inputBuffer = codec.getInputBuffer(...); 
  9.     // 用 有效的數據 填充inputBuffer 
  10.     ... 
  11.     codec.queueInputBuffer(inputBufferId,...); 
  12.   } 
  13.   int  outputBufferId = codec.dequeueOutputBuffer(...); 
  14.   if(outputBufferId> = 0){ 
  15.     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); 
  16.     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); //  選項 A 
  17.     // bufferFormat  是 相同  於 OUTPUTFORMAT 
  18.     // OutputBuffer中  是 準備  要 被處理  或 渲染。 
  19.     ... 
  20.     codec.releaseOutputBuffer(outputBufferId,...); 
  21.   }  else  if(outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){ 
  22.     //隨後的數據將符合  到 新的格式。 
  23.     //能夠  忽略 使用getOutputFormat(outputBufferId) 
  24.     outputFormat = codec.getOutputFormat(); //  選項 B 
  25.   } 
  26. codec.stop(); 
  27. codec.release(); 

簡單解釋一下,經過getInputBuffers獲取輸入隊列,而後調用dequeueInputBuffer獲取輸入隊列空閒數據下標,注意dequeueOutputBuffer會有幾個特殊的返回值表示當前編碼狀態的變化,而後再經過queueInputBuffer把原始數據送入編碼器,而在輸出隊列端一樣經過getOutputBuffers和dequeueOutputBuffer獲取輸出的h264流,處理完輸出數據以後,須要經過releaseOutputBuffer把輸出緩衝器還給系統,從新放到輸出隊列中。

關於MediaCodec更復雜的使用例子,能夠參考下CTS測試裏面的使用方式:EncodeDecodeTest.java

從上面例子來看的確是很是原始的API,因爲MediaCodec底層是直接調用了手機平臺硬件的編解碼能力,因此速度很是快,可是由於谷歌對整個Android的硬件生態的掌控力很是弱,因此這個API有不少問題:

1,顏色格式問題

MediaCodec在初始化的時候,在配置的時候,須要傳入一個MediaFormat對象,看成爲編碼器使用的時候,咱們通常須要在MediaFormat中指定視頻的寬高,幀率,碼率,I幀間隔等基本信息,除此以外,還有一個重要的信息就是,指定編碼器接受的YUV幀的顏色格式。這個是由於因爲YUV根據其採樣比例,UV份量的排列順序有不少種不一樣的顏色格式,而對於Android的攝像頭在onPreviewFrame輸出的YUV幀格式,若是沒有配置任何參數的狀況下,基本都是NV21格式,但Google對MediaCodec的API在設計和規範的時候,顯得很不厚道,過於貼近Android的HAL層了,致使了NV21格式並非全部機器的MediaCodec都支持這種格式做爲編碼器的輸入格式!所以,在初始化MediaCodec的時候,咱們須要經過codecInfo.getCapabilitiesForType來實現具體支持哪些媒體代碼實現具體支持哪些YUV格式做爲輸入格式,通常來講,起碼在4.4+的系統上,這兩種格式在大部分機器都有支持:

 
  1. MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar 
  2. MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar 

兩種格式分別是YUV420P和NV21,若是機器上只支持YUV420P格式的狀況下,則須要先將攝像頭輸出的NV21格式先轉換成YUV420P,才能送入編碼器進行編碼,不然最終出來的視頻就會花屏,或者顏色出現錯亂

這個算是一個不大不小的坑,基本上用上了MediaCodec進行視頻編碼都會趕上這個問題

2,編碼器支持特性至關有限

若是使用MediaCodec來編碼H264視頻流,對於H264格式來講,會有一些針對壓縮率以及碼率相關的視頻質量設置,典型的如Profile(baseline,main,high),Profile Level,Bitrate mode(CBR, CQ,VBR),合理配置這些參數可讓咱們在同等的碼率下,得到更高的壓縮率,從而提高視頻的質量,Android也提供了對應的API進行設置,能夠設置到MediaFormat中這些設置項:

 
  1. MediaFormat.KEY_BITRATE_MODE 
  2. MediaFormat.KEY_PROFILE 
  3. MediaFormat.KEY_LEVEL 

但問題是,對於Profile,Level,Bitrate mode這些設置,在大部分手機上都是不支持的,即便是設置了最終也不會生效,例如設置了Profile爲high,最後出來的視頻依然還會是基線,媽....

這個問題,在7.0如下的機器幾乎是必須的,其中一個可能的緣由是,Android在源碼層級hardcode了profile的的設置:

 
  1. // XXX 
  2. if(h264type.eProfile!= OMX_VIDEO_AVCProfileBaseline){ 
  3.     ALOGW(「使用基線配置文件代替AVC錄製的%d」 , 
  4.             h264type.eProfile); 
  5.     h264type.eProfile = OMX_VIDEO_AVCProfileBaseline; 

Android直到7.0以後才取消了這段地方的Hardcode

 
  1. if(h264type.eProfile == OMX_VIDEO_AVCProfileBaseline){ 
  2.     .... 
  3. }  不然 若是(h264type.eProfile == OMX_VIDEO_AVCProfileMain || 
  4.             h264type.eProfile == OMX_VIDEO_AVCProfileHigh){ 
  5.     ..... 

這個問題能夠說間接致使了MediaCodec編碼出來的視頻質量偏低,同等碼率下,難以得到跟軟編碼甚至iOS版那樣的視頻質量。

3,16位對齊要求

前面說到,MediaCodec這個API在設計的時候,過於貼近HAL層,這在不少志的實現上,是直接把傳入MediaCodec的緩衝液中,在不通過任何前置處理的狀況下就直接送入了志中。而在編碼H264視頻流的時候,因爲H264的編碼塊大小通常是16×16,因而乎在一開始設置視頻的寬高的時候,若是設置了一個沒有對齊16的大小,例如960×540,在某些cpu上,最終編碼出來的視頻就會直接花屏!

很明顯這仍是由於廠商在實現這個API的時候,對傳入的數據缺乏校驗以及前置處理致使的,目前來看,華爲,三星的志出現這個問題會比較頻繁,其餘廠商的一些早期志也有這種問題,通常來講解決方法仍是在設置視頻寬高的時候,統一設置成對齊16位以後的大小就行了。

FFmpeg的+ X264 / openh264

除了使用MediaCodec進行編碼以外,另一種比較流行的方案就是使用的ffmpeg + X264 / openh264進行軟編碼,FFMPEG是用於一些視頻幀的預處理。這裏主要是使用X264 / openh264做爲視頻的編碼器。

X264基本上被認爲是當今市面上最快的商用視頻編碼器,並且基本上全部的H264的特性都支持,經過合理配置各類參數仍是可以獲得較好的壓縮率和編碼速度的,限於篇幅,這裏再也不闡述h264的參數配置,有興趣能夠看下這裏和這裏對x264編碼參數的調優。

openh264則是由思科開源的另外一個h264編碼器,項目在2013年開源,對比起x264來講略顯年輕,不過因爲思科支付滿了,因此對於外部用戶來講,至關於能夠直接無償使用了,另外,火狐直接內置了openh264,做爲其在的WebRTC中的視頻的編解碼器使用。

但對比起X264,openh264在H264高級特性的支持比較差:

  • 簡介只支持到基準,等級5.2
  • 多線程編碼只支持片基,不支持基於幀的多線程編碼

從編碼效率上來看,openh264的速度也並不會比X264快,不過其最大的好處,仍是可以直接無償使用吧。

軟硬編對比

從上面的分析來看,硬編的好處主要在於速度快,並且系統自帶不須要引入外部的庫,可是特性支持有限,並且硬編的壓縮率通常偏低,而對於軟編碼來講,雖然速度較慢,但​​是壓縮率比較高,並且支持的H264特性也會比硬編碼多不少,相對來講比較可控。就可用性而言,在4.4+的系統上,MediaCodec的可用性是可以基本保證的,可是不一樣等級的機器的編碼器能力會有很多差異,建議能夠根據機器的配置,選擇不一樣的編碼器配置。

YUV幀的預處理

根據最開始給出的流程,在送入編碼器以前,咱們須要先對攝像頭輸出的YUV幀進行一些前置處理

1.縮放

若是設置了相機的預覽大小爲1080p的狀況下,在onPreviewFrame中輸出的YUV幀直接就是1920x1080的大小,若是須要編碼跟這個大小不同的視頻,咱們就須要在錄製的過程當中,實時的對YUV幀進行縮放。

以微信爲例,攝像頭預覽1080的數據,須要編碼960×540大小的視頻。

最爲常見的作法是使用ffmpeg的這種的sws_scale函數進行直接縮放,效果/性能比較好的通常是選擇SWS_FAST_BILINEAR算法:

 
  1. mScaleYuvCtxPtr = sws_getContext( 
  2.                    srcWidth, 
  3.                    srcHeight, 
  4.                    AV_PIX_FMT_NV21, 
  5.                    dstWidth, 
  6.                    dstHeight, 
  7.                    AV_PIX_FMT_NV21, 
  8.                    SWS_FAST_BILINEAR,  NULL ,  NULL ,  NULL ); 
  9. sws_scale(mScaleYuvCtxPtr, 
  10.                     (const uint8_t * const *)srcAvPicture-> data, 
  11.                     srcAvPicture-> linesize,0,srcHeight, 
  12.                     dstAvPicture-> data,dstAvPicture-> linesize); 

在nexus 6p上,直接使用ffmpeg來進行縮放的時間基本上都須要40ms +,對於咱們須要記錄30fps的來講,每幀處理時間最多就30ms左右,若是光是縮放就消耗瞭如此多的時間,上錄製出來的視頻只能在15fps的上下了。

很明顯,直接使用的ffmpeg進行縮放是在是太慢了,不得不說swsscale簡直就是ffmpeg的裏面的渣渣,在對比了幾種業界經常使用的算以後,咱們最後考慮實現使用這種快速縮放的算法:

談談關於Android的視頻編碼的那些坑

咱們選擇一種叫作的局部均值算法,先後兩行四個臨近點算出最終圖片的四個像素點,對於源圖片的每行像素,咱們可使用Neon直接實現,以縮放Y份量爲例:

 
  1. const uint8 * src_next = src_ptr + src_stride; 
  2.   asm揮發性( 
  3.     「1:\ n」     
  4.       「vld4.8 {d0,d1,d2,d3},[%0]!\ n」 
  5.       「vld4.8 {d4,d5,d6,d7},[%1]!\ n」 
  6.       「subs%3,%3,#16 \ n」   //每一個循環處理16次 
  7.  
  8.       「vrhadd.u8 d0,d0,d1 \ n」 
  9.       「vrhadd.u8 d4,d4,d5 \ n」 
  10.       「vrhadd.u8 d0,d0,d4 \ n」 
  11.  
  12.       「vrhadd.u8 d2,d2,d3 \ n」 
  13.       「vrhadd.u8 d6,d6,d7 \ n」 
  14.       「vrhadd.u8 d2,d2,d6 \ n」 
  15.  
  16.       「vst2.8 {d0,d2},[%2]!\ n」   //存儲奇數像素 
  17.  
  18.       「bgt 1b \ n」 
  19.     :  「+ r」 (src_ptr),//%0 
  20.       「+ r」 (src_next),//%1 
  21.       「+ r」 (dst),//%2 
  22.       「+ r」 (dst_width)//%3 
  23.     : 
  24.     :  「q0」 ,  「q1」 ,  「q2」 ,  「q3」               // Clobber List 
  25.   ); 

上面使用的霓虹燈指令每次只能讀取和存儲8或者16位的數據,對於多出來的數據,只須要用一樣的算法改爲用Ç語言實現便可。

在使用上述的算法優化以後,進行每幀縮放,在Nexus 6p上,只須要不到5ms就能完成了,而對於縮小質量來講,ffmpeg的SWS_FAST_BILINEAR算法和上述算法縮放出來的圖片進行對比,峯值信噪比(psnr)在大部分場景下大概在38-40左右,質量也足夠好了。

2.旋轉

在android機器上,因爲攝像頭安裝角度不一樣,onPreviewFrame出來的YUV幀通常都是旋轉了90或者270度,若是最終視頻是要豎拍的,那通常來講須要把YUV幀進行旋轉。

對於旋轉的算法,若是是純C實現的代碼,通常來講是個O(n ^ 2)複雜度的算法,若是是旋轉960x540的yuv幀數據,在nexus 6p上,每幀旋轉也須要30ms +,這顯然也是不能接受的。

在這裏咱們換個思路,能不能不對YUV幀進行旋轉?(固然是能夠的6666)

事實上在mp4文件格式的頭部,咱們能夠指定一個旋轉矩陣,具體來講是在moov.trak.tkhd盒裏面指定,視頻播放器在播放視頻的時候,會在讀取這裏矩陣信息,從而決定視頻自己的旋轉角度,位移,縮放等,具體可參考下蘋果的文檔

經過ffmpeg的,咱們能夠很輕鬆的給合成以後的MP4文件打上這個旋轉角度:

 
  1. char  rotateStr [1024]; 
  2. sprintf(rotateStr,  「%d」 ,rotate); 
  3. av_dict_set(&out_stream-> metadata,  「rotate」 ,rotateStr,0); 

因而能夠在錄製的時候省下一大筆旋轉的開銷了,興奮!

3.鏡像

在使用前置攝像頭拍攝的時候,若是不對YUV幀進行處理,那麼直接拍出來的視頻是會鏡像翻轉的,這裏原理就跟照鏡子同樣,從前置攝像頭方向拿出來的YUV幀恰好是反的,但有些時候拍出來的鏡像視頻可能不合咱們的需求,所以這個時候咱們就須要對YUV幀進行鏡像翻轉。

但因爲攝像頭安裝角度通常是90或者270度,因此實際上原生的YUV幀是水平翻轉過來的,所以作鏡像翻轉的時候,只須要恰好以中間爲中軸,分別上下交換每行數據便可,注意Ÿ跟UV要分開處理,這種算法用霓虹燈實現至關簡單:

 
  1. asm揮發性( 
  2.       「1:\ n」 
  3.         「vld4.8 {d0,d1,d2,d3},[%2]!\ n」   //  從 src  載入 32 
  4.         「vld4.8 {d4,d5,d6,d7},[%3]!\ n」   //  從 dst  載入 32 
  5.         「subs%4,%4,#32 \ n」   // 32每一個循環處理 
  6.         「vst4.8 {d0,d1,d2,d3},[%1]!\ n」   //存儲32  到 dst 
  7.         「vst4.8 {d4,d5,d6,d7},[%0]!\ n」   //存儲32  到 src 
  8.         「bgt 1b \ n」 
  9.       :  「+ r」 (src),//%0 
  10.         「+ r」 (dst),//%1 
  11.         「+ r」 (srcdata),//%2 
  12.         「+ r」 (dstdata),//%3 
  13.         「+ r」 (count )//%4 //  輸出 寄存器 
  14.       ://輸入寄存器 
  15.       :  「cc」 ,  「memory」 ,  「q0」 ,  「q1」 ,  「q2」 ,  「q3」   // Clobber List 
  16.     ); 

一樣,剩餘的數據用純C代碼實現就行了,在nexus6p上,這種鏡像翻轉一幀1080x1920 YUV數據大概只要不到5ms

在編碼好h264視頻流以後,最終處理就是把音頻流跟視頻流合然而後包裝到mp4文件,這部分咱們能夠經過系統的MediaMuxer,mp4v2,或者ffmpeg來實現,這部分比較簡單,在這裏就再也不闡述了。

 

 

 

閱讀原文

相關文章
相關標籤/搜索