android平臺yuv縮放相關<轉>

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

以微信爲例,咱們錄製一個540p的mp4文件,對於Android來講,大致上是遵循這麼一個流程:html

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

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

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

1. 視頻編碼器的選擇(硬編 or 軟編)?github

2. 如何對攝像頭輸出的YUV幀進行快速預處理(鏡像,縮放,旋轉)?web

視頻編碼器的選擇算法

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

  •  MediaCodec微信

  •  FFMpeg+x264/openh264

咱們來逐個解析一下

MediaCodec

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

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

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

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

EncodeDecodeTest.java:

https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/EncodeDecodeTest.java

 

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

 

1. 顏色格式問題

 

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+的系統上,這兩種格式在大部分機器都有支持:

 

 

 

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

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

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

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

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

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

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

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

3. 16位對齊要求

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

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


FFMpeg+x264/openh264

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

 

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

  • https://www.nmm-hd.org/d/index.php?title=X264%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D&variant=zh-cn

  • http://www.cnblogs.com/wainiwann/p/5647521.html

     

 

2. openh264(https://github.com/cisco/openh264)則是由思科開源的另一個h264編碼器,項目在2013年開源,對比起x264來講略顯年輕,不過因爲思科支付滿了h264的年度專利費,因此對於外部用戶來講,至關於能夠直接無償使用了,另外,firefox直接內置了openh264,做爲其在webRTC中的視頻的編解碼器使用。

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

  • Profile只支持到baseline, level 5.2

  • 多線程編碼只支持slice based,不支持frame based的多線程編碼

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


軟硬編對比

 

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

 

 


 

YUV幀的預處理

 

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

 

1.縮放

 

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

 

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

 

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

 

 

 

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

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

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

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

在使用上述的算法優化以後,進行每幀縮放,在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幀進行旋轉?

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

https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-18737

 

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

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

3.鏡像

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

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

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


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

原貼地址:https://juejin.im/entry/599d73976fb9a02487552efa

相關文章
相關標籤/搜索