標籤:能力 獲得 解釋 表示 neon view 旋轉 NPU nta
Android的視頻相關的開發,大概一直是整個Android生態,以及Android API中,最為分裂以及相容性問題最為突出的一部分。網路攝影機,以及視頻編碼相關的API,Google一直對這方面的控制力非常差,導致不同廠商對這兩個API的實現有不少差異,而且從API的設計來看,一直以來最佳化也相當有限,甚至有人認為這是“Android上最難用的API之一”
以為例,我們錄製一個540p的mp4檔案,對於Android來說,大體上是遵循這麼一個流程:
大體上就是從網路攝影機輸出的YUV幀經過預先處理之後,送入編碼器,獲得編碼好的h264視頻流。
上面只是針對視頻流的編碼,另外還需要對音頻流單獨錄製,最後再將視頻流和音頻流進行合成出最終視頻。
這篇文章主要將會對視頻流的編碼中兩個常見問題進行分析:
1. 視頻編碼器的選擇(硬編 or 軟編)?
2. 如何對網路攝影機輸出的YUV幀進行快速預先處理(鏡像,縮放,旋轉)?
視頻編碼器的選擇
對於錄製視頻的需求,不少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編碼參數的調優:
2. openh264(https://github.com/cisco/openh264)則是由思科開源的另外一個h264編碼器,項目在2013年開源,對比起x264來說略顯年輕,不過由于思科支付滿了h264的年度專利費,所以對於外部使用者來說,相當於可以直接免費使用了,另外,firefox直接內建了openh264,作為其在webRTC中的視頻的轉碼器使用。
但對比起x264,openh264在h264進階特性的支援比較差:
從編碼效率上來看,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
android平台yuv縮放相關<轉>