自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

談?wù)勱P(guān)于Android視頻編碼的那些坑

移動(dòng)開發(fā) Android
Android的視頻相關(guān)的開發(fā),大概一直是整個(gè)Android生態(tài),以及Android API中,最為分裂以及兼容性問題最為突出的一部分。攝像頭,以及視頻編碼相關(guān)的API,Google一直對(duì)這方面的控制力非常差,導(dǎo)致不同廠商對(duì)這兩個(gè)API的實(shí)現(xiàn)有不少差異,而且從API的設(shè)計(jì)來看,一直以來優(yōu)化也相當(dāng)有限,甚至有人認(rèn)為這是“Android上最難用的API之一”

Android的視頻相關(guān)的開發(fā),大概一直是整個(gè)Android生態(tài),以及Android API中,最為分裂以及兼容性問題最為突出的一部分。攝像頭,以及視頻編碼相關(guān)的API,Google一直對(duì)這方面的控制力非常差,導(dǎo)致不同廠商對(duì)這兩個(gè)API的實(shí)現(xiàn)有不少差異,而且從API的設(shè)計(jì)來看,一直以來優(yōu)化也相當(dāng)有限,甚至有人認(rèn)為這是“Android上最難用的API之一”

以微信為例,我們錄制一個(gè)540p的mp4文件,對(duì)于Android來說,大體上是遵循這么一個(gè)流程:

談?wù)勱P(guān)于Android視頻編碼的那些坑

大體上就是從攝像頭輸出的YUV幀經(jīng)過預(yù)處理之后,送入編碼器,獲得編碼好的h264視頻流。

上面只是針對(duì)視頻流的編碼,另外還需要對(duì)音頻流單獨(dú)錄制,最后再將視頻流和音頻流進(jìn)行合成出最終視頻。

這篇文章主要將會(huì)對(duì)視頻流的編碼中兩個(gè)常見問題進(jìn)行分析:

  • 視頻編碼器的選擇(硬編 or 軟編)?
  • 如何對(duì)攝像頭輸出的YUV幀進(jìn)行快速預(yù)處理(鏡像,縮放,旋轉(zhuǎn))?

視頻編碼器的選擇

對(duì)于錄制視頻的需求,不少app都需要對(duì)每一幀數(shù)據(jù)進(jìn)行單獨(dú)處理,因此很少會(huì)直接用到 MediaRecorder 來直接錄取視頻,一般來說,會(huì)有這么兩個(gè)選擇

  • MediaCodec
  • FFMpeg+x264/openh264

我們來逐個(gè)解析一下

MediaCodec

MediaCodec是API 16之后Google推出的用于音視頻編解碼的一套偏底層的API,可以直接利用硬件加速進(jìn)行視頻的編解碼。調(diào)用的時(shí)候需要先初始化MediaCodec作為視頻的編碼器,然后只需要不停傳入原始的YUV數(shù)據(jù)進(jìn)入編碼器就可以直接輸出編碼好的h264流,整個(gè)API設(shè)計(jì)模型來看,就是同時(shí)包含了輸入端和輸出端的兩條隊(duì)列:

談?wù)勱P(guān)于Android視頻編碼的那些坑

因此,作為編碼器,輸入端隊(duì)列存放的就是原始YUV數(shù)據(jù),輸出端隊(duì)列輸出的就是編碼好的h264流,作為解碼器則對(duì)應(yīng)相反。在調(diào)用的時(shí)候,MediaCodec提供了同步和異步兩種調(diào)用方式,但是異步使用Callback的方式是在API 21之后才加入的,以同步調(diào)用為例,一般來說調(diào)用方式大概是這樣(摘自官方例子):

 

  1. MediaCodec codec = MediaCodec.createByCodecName(name); 
  2. codec.configure(format, …); 
  3. MediaFormat outputFormat = codec.getOutputFormat(); // option B 
  4. codec.start(); 
  5. for (;;) { 
  6.   int inputBufferId = codec.dequeueInputBuffer(timeoutUs); 
  7.   if (inputBufferId >= 0) { 
  8.     ByteBuffer inputBuffer = codec.getInputBuffer(…); 
  9.     // fill inputBuffer with valid data 
  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); // option A 
  17.     // bufferFormat is identical to outputFormat 
  18.     // outputBuffer is ready to be processed or rendered. 
  19.     … 
  20.     codec.releaseOutputBuffer(outputBufferId, …); 
  21.   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 
  22.     // Subsequent data will conform to new format. 
  23.     // Can ignore if using getOutputFormat(outputBufferId) 
  24.     outputFormat = codec.getOutputFormat(); // option B 
  25.   } 
  26. codec.stop(); 
  27. codec.release(); 

簡(jiǎn)單解釋一下,通過 getInputBuffers 獲取輸入隊(duì)列,然后調(diào)用 dequeueInputBuffer 獲取輸入隊(duì)列空閑數(shù)組下標(biāo),注意 dequeueOutputBuffer 會(huì)有幾個(gè)特殊的返回值表示當(dāng)前編解碼狀態(tài)的變化,然后再通過 queueInputBuffer 把原始YUV數(shù)據(jù)送入編碼器,而在輸出隊(duì)列端同樣通過 getOutputBuffers 和 dequeueOutputBuffer 獲取輸出的h264流,處理完輸出數(shù)據(jù)之后,需要通過 releaseOutputBuffer 把輸出buffer還給系統(tǒng),重新放到輸出隊(duì)列中。

關(guān)于MediaCodec更復(fù)雜的使用例子,可以參照下CTS測(cè)試?yán)锩娴氖褂梅绞剑?EncodeDecodeTest.java

從上面例子來看的確是非常原始的API,由于MediaCodec底層是直接調(diào)用了手機(jī)平臺(tái)硬件的編解碼能力,所以速度非???,但是因?yàn)镚oogle對(duì)整個(gè)Android硬件生態(tài)的掌控力非常弱,所以這個(gè)API有很多問題:

1、顏色格式問題

MediaCodec在初始化的時(shí)候,在 configure 的時(shí)候,需要傳入一個(gè)MediaFormat對(duì)象,當(dāng)作為編碼器使用的時(shí)候,我們一般需要在MediaFormat中指定視頻的寬高,幀率,碼率,I幀間隔等基本信息,除此之外,還有一個(gè)重要的信息就是,指定編碼器接受的YUV幀的顏色格式。這個(gè)是因?yàn)橛捎赮UV根據(jù)其采樣比例,UV分量的排列順序有很多種不同的顏色格式,而對(duì)于Android的攝像頭在 onPreviewFrame 輸出的YUV幀格式,如果沒有配置任何參數(shù)的情況下,基本上都是NV21格式,但Google對(duì)MediaCodec的API在設(shè)計(jì)和規(guī)范的時(shí)候,顯得很不厚道,過于貼近Android的HAL層了,導(dǎo)致了NV21格式并不是所有機(jī)器的MediaCodec都支持這種格式作為編碼器的輸入格式! 因此,在初始化MediaCodec的時(shí)候,我們需要通過 codecInfo.getCapabilitiesForType 來查詢機(jī)器上的MediaCodec實(shí)現(xiàn)具體支持哪些YUV格式作為輸入格式,一般來說,起碼在4.4+的系統(tǒng)上,這兩種格式在大部分機(jī)器都有支持:

 

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

兩種格式分別是YUV420P和NV21,如果機(jī)器上只支持YUV420P格式的情況下,則需要先將攝像頭輸出的NV21格式先轉(zhuǎn)換成YUV420P,才能送入編碼器進(jìn)行編碼,否則最終出來的視頻就會(huì)花屏,或者顏色出現(xiàn)錯(cuò)亂

這個(gè)算是一個(gè)不大不小的坑,基本上用上了MediaCodec進(jìn)行視頻編碼都會(huì)遇上這個(gè)問題

2、編碼器支持特性相當(dāng)有限

如果使用MediaCodec來編碼H264視頻流,對(duì)于H264格式來說,會(huì)有一些針對(duì)壓縮率以及碼率相關(guān)的視頻質(zhì)量設(shè)置,典型的諸如Profile(baseline, main, high),Profile Level, Bitrate mode(CBR, CQ, VBR),合理配置這些參數(shù)可以讓我們?cè)谕鹊拇a率下,獲得更高的壓縮率,從而提升視頻的質(zhì)量,Android也提供了對(duì)應(yīng)的API進(jìn)行設(shè)置,可以設(shè)置到MediaFormat中這些設(shè)置項(xiàng):

 

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

但問題是,對(duì)于Profile,Level, Bitrate mode這些設(shè)置,在大部分手機(jī)上都是不支持的,即使是設(shè)置了最終也不會(huì)生效,例如設(shè)置了Profile為high,最后出來的視頻依然還會(huì)是Baseline,Shit....

這個(gè)問題,在7.0以下的機(jī)器幾乎是必現(xiàn)的,其中一個(gè)可能的原因是,Android在源碼層級(jí) hardcode 了profile的的設(shè)置:

 

  1. // XXX 
  2. if (h264type.eProfile != OMX_VIDEO_AVCProfileBaseline) { 
  3.     ALOGW("Use baseline profile instead of %d for AVC recording"
  4.             h264type.eProfile); 
  5.     h264type.eProfile = OMX_VIDEO_AVCProfileBaseline; 

Android直到 7.0 之后才取消了這段地方的Hardcode

 

  1. if (h264type.eProfile == OMX_VIDEO_AVCProfileBaseline) { 
  2.     .... 
  3. else if (h264type.eProfile == OMX_VIDEO_AVCProfileMain || 
  4.             h264type.eProfile == OMX_VIDEO_AVCProfileHigh) { 
  5.     ..... 

這個(gè)問題可以說間接導(dǎo)致了MediaCodec編碼出來的視頻質(zhì)量偏低,同等碼率下,難以獲得跟軟編碼甚至iOS那樣的視頻質(zhì)量。

3、16位對(duì)齊要求

前面說到,MediaCodec這個(gè)API在設(shè)計(jì)的時(shí)候,過于貼近HAL層,這在很多Soc的實(shí)現(xiàn)上,是直接把傳入MediaCodec的buffer,在不經(jīng)過任何前置處理的情況下就直接送入了Soc中。而在編碼h264視頻流的時(shí)候,由于h264的編碼塊大小一般是16x16,于是乎在一開始設(shè)置視頻的寬高的時(shí)候,如果設(shè)置了一個(gè)沒有對(duì)齊16的大小,例如960x540,在某些cpu上,最終編碼出來的視頻就會(huì)直接 花屏 !

很明顯這還是因?yàn)閺S商在實(shí)現(xiàn)這個(gè)API的時(shí)候,對(duì)傳入的數(shù)據(jù)缺少校驗(yàn)以及前置處理導(dǎo)致的,目前來看,華為,三星的Soc出現(xiàn)這個(gè)問題會(huì)比較頻繁,其他廠商的一些早期Soc也有這種問題,一般來說解決方法還是在設(shè)置視頻寬高的時(shí)候,統(tǒng)一設(shè)置成對(duì)齊16位之后的大小就好了。

FFMpeg+x264/openh264

除了使用MediaCodec進(jìn)行編碼之外,另外一種比較流行的方案就是使用ffmpeg+x264/openh264進(jìn)行軟編碼,ffmpeg是用于一些視頻幀的預(yù)處理。這里主要是使用x264/openh264作為視頻的編碼器。

x264基本上被認(rèn)為是當(dāng)今市面上最快的商用視頻編碼器,而且基本上所有h264的特性都支持,通過合理配置各種參數(shù)還是能夠得到較好的壓縮率和編碼速度的,限于篇幅,這里不再闡述h264的參數(shù)配置,有興趣可以看下 這里 和 這里 對(duì)x264編碼參數(shù)的調(diào)優(yōu)。

openh264 則是由思科開源的另外一個(gè)h264編碼器,項(xiàng)目在2013年開源,對(duì)比起x264來說略顯年輕,不過由于思科支付滿了h264的年度專利費(fèi),所以對(duì)于外部用戶來說,相當(dāng)于可以直接免費(fèi)使用了,另外,firefox直接內(nèi)置了openh264,作為其在webRTC中的視頻的編解碼器使用。

但對(duì)比起x264,openh264在h264高級(jí)特性的支持比較差:

  • Profile只支持到baseline, level 5.2
  • 多線程編碼只支持slice based,不支持frame based的多線程編碼

從編碼效率上來看,openh264的速度也并不會(huì)比x264快,不過其最大的好處,還是能夠直接免費(fèi)使用吧。

軟硬編對(duì)比

從上面的分析來看,硬編的好處主要在于速度快,而且系統(tǒng)自帶不需要引入外部的庫(kù),但是特性支持有限,而且硬編的壓縮率一般偏低,而對(duì)于軟編碼來說,雖然速度較慢,但是壓縮率比較高,而且支持的H264特性也會(huì)比硬編碼多很多,相對(duì)來說比較可控。就可用性而言,在4.4+的系統(tǒng)上,MediaCodec的可用性是能夠基本保證的,但是不同等級(jí)的機(jī)器的編碼器能力會(huì)有不少差別,建議可以根據(jù)機(jī)器的配置,選擇不同的編碼器配置。

YUV幀的預(yù)處理

根據(jù)最開始給出的流程,在送入編碼器之前,我們需要先對(duì)攝像頭輸出的YUV幀進(jìn)行一些前置處理

1.縮放

如果設(shè)置了camera的預(yù)覽大小為1080p的情況下,在 onPreviewFrame 中輸出的YUV幀直接就是1920x1080的大小,如果需要編碼跟這個(gè)大小不一樣的視頻,我們就需要在錄制的過程中, 實(shí)時(shí) 的對(duì)YUV幀進(jìn)行縮放。

以微信為例,攝像頭預(yù)覽1080p的數(shù)據(jù),需要編碼960x540大小的視頻。

最為常見的做法是使用ffmpeg這種的sws_scale函數(shù)進(jìn)行直接縮放,效果/性能比較好的一般是選擇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, NULLNULLNULL); 
  9. sws_scale(mScaleYuvCtxPtr, 
  10.                     (const uint8_t* const *) srcAvPicture->data, 
  11.                     srcAvPicture->linesize, 0, srcHeight, 
  12.                     dstAvPicture->data, dstAvPicture->linesize); 

在nexus 6p上,直接使用ffmpeg來進(jìn)行縮放的時(shí)間基本上都需要 40ms+ ,對(duì)于我們需要錄制30fps的來說,每幀處理時(shí)間最多就30ms左右,如果光是縮放就消耗了如此多的時(shí)間,基本上錄制出來的視頻只能在15fps上下了。

很明顯,直接使用ffmpeg進(jìn)行縮放是在是太慢了,不得不說swsscale簡(jiǎn)直就是ffmpeg里面的渣渣,在對(duì)比了幾種業(yè)界常用的算之后,我們最后考慮實(shí)現(xiàn)使用這種快速縮放的算法:

談?wù)勱P(guān)于Android視頻編碼的那些坑

我們選擇一種叫做的 局部均值 算法,前后兩行四個(gè)臨近點(diǎn)算出最終圖片的四個(gè)像素點(diǎn),對(duì)于源圖片的每行像素,我們可以使用Neon直接實(shí)現(xiàn),以縮放Y分量為例:

 

  1. const uint8* src_next = src_ptr + src_stride; 
  2.   asm volatile ( 
  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 processed per loop 
  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"  // store odd pixels 
  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.   ); 

上面使用的Neon指令每次只能讀取和存儲(chǔ)8或者16位的數(shù)據(jù),對(duì)于多出來的數(shù)據(jù),只需要用同樣的算法改成用C語(yǔ)言實(shí)現(xiàn)即可。

在使用上述的算法優(yōu)化之后,進(jìn)行每幀縮放,在Nexus 6p上,只需要不到 5ms 就能完成了,而對(duì)于縮放質(zhì)量來說,ffmpeg的SWS_FAST_BILINEAR算法和上述算法縮放出來的圖片進(jìn)行對(duì)比,峰值信噪比(psnr)在大部分場(chǎng)景下大概在 38-40 左右,質(zhì)量也足夠好了。

2.旋轉(zhuǎn)

在android機(jī)器上,由于攝像頭安裝角度不同, onPreviewFrame 出來的YUV幀一般都是旋轉(zhuǎn)了90或者270度,如果最終視頻是要豎拍的,那一般來說需要把YUV幀進(jìn)行旋轉(zhuǎn)。

對(duì)于旋轉(zhuǎn)的算法,如果是純C實(shí)現(xiàn)的代碼,一般來說是個(gè)O(n^2 ) 復(fù)雜度的算法,如果是旋轉(zhuǎn)960x540的yuv幀數(shù)據(jù),在nexus 6p上,每幀旋轉(zhuǎn)也需要 30ms+ ,這顯然也是不能接受的。

在這里我們換個(gè)思路,能不能不對(duì)YUV幀進(jìn)行旋轉(zhuǎn)?(當(dāng)然是可以的6666)

事實(shí)上在mp4文件格式的頭部,我們可以指定一個(gè)旋轉(zhuǎn)矩陣,具體來說是在 moov.trak.tkhd box 里面指定,視頻播放器在播放視頻的時(shí)候,會(huì)在讀取這里矩陣信息,從而決定視頻本身的旋轉(zhuǎn)角度,位移,縮放等,具體可以參考下蘋果的 文檔

通過ffmpeg,我們可以很輕松的給合成之后的mp4文件打上這個(gè)旋轉(zhuǎn)角度:

 

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

于是可以在錄制的時(shí)候省下一大筆旋轉(zhuǎn)的開銷了,excited!

3.鏡像

在使用前置攝像頭拍攝的時(shí)候,如果不對(duì)YUV幀進(jìn)行處理,那么直接拍出來的視頻是會(huì) 鏡像翻轉(zhuǎn) 的,這里原理就跟照鏡子一樣,從前置攝像頭方向拿出來的YUV幀剛好是反的,但有些時(shí)候拍出來的鏡像視頻可能不合我們的需求,因此這個(gè)時(shí)候我們就需要對(duì)YUV幀進(jìn)行鏡像翻轉(zhuǎn)。

但由于攝像頭安裝角度一般是90或者270度,所以實(shí)際上原生的YUV幀是水平翻轉(zhuǎn)過來的,因此做鏡像翻轉(zhuǎn)的時(shí)候,只需要?jiǎng)偤靡灾虚g為中軸,分別上下交換每行數(shù)據(jù)即可,注意Y跟UV要分開處理,這種算法用Neon實(shí)現(xiàn)相當(dāng)簡(jiǎn)單:

 

  1. asm volatile ( 
  2.       "1:                                          \n" 
  3.         "vld4.8     {d0, d1, d2, d3}, [%2]!        \n"  // load 32 from src 
  4.         "vld4.8     {d4, d5, d6, d7}, [%3]!        \n"  // load 32 from dst 
  5.         "subs       %4, %4, #32                    \n"  // 32 processed per loop 
  6.         "vst4.8     {d0, d1, d2, d3}, [%1]!        \n"  // store 32 to dst 
  7.         "vst4.8     {d4, d5, d6, d7}, [%0]!        \n"  // store 32 to 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  // Output registers 
  14.       :                     // Input registers 
  15.       : "cc""memory""q0""q1""q2""q3"  // Clobber List 
  16.     ); 

同樣,剩余的數(shù)據(jù)用純C代碼實(shí)現(xiàn)就好了, 在nexus6p上,這種鏡像翻轉(zhuǎn)一幀1080x1920 YUV數(shù)據(jù)大概只要不到 5ms

在編碼好h264視頻流之后,最終處理就是把音頻流跟視頻流合流然后包裝到mp4文件,這部分我們可以通過系統(tǒng)的 MediaMuxer , mp4v2 ,或者ffmpeg來實(shí)現(xiàn),這部分比較簡(jiǎn)單,在這里就不再闡述了。

責(zé)任編輯:未麗燕 來源: Ragnarok Note
相關(guān)推薦

2018-12-26 13:22:05

NVMeNVMe-oF數(shù)據(jù)

2023-04-13 00:24:00

前端編碼JavaScrip

2014-03-20 09:17:36

2016-12-28 13:19:08

Android開發(fā)坑和小技巧

2017-08-09 08:25:35

DBA數(shù)據(jù)庫(kù)OLAP

2018-01-16 08:57:24

Nginx規(guī)則實(shí)用

2017-07-19 14:26:01

前端JavaScriptDOM

2021-09-07 14:35:48

DevSecOps開源項(xiàng)目

2022-05-15 08:13:50

Mysql數(shù)據(jù)庫(kù)Mycat

2015-03-12 09:51:09

CoreDataiCloud

2020-04-21 15:18:11

財(cái)務(wù)信息化

2015-07-20 10:00:28

Linux內(nèi)核編碼風(fēng)格

2017-03-23 14:30:13

Linux內(nèi)核驅(qū)動(dòng)編碼風(fēng)格

2020-07-30 07:30:17

存儲(chǔ)技術(shù)數(shù)據(jù)

2012-11-09 11:39:11

Windows 8

2017-03-31 10:27:08

推送服務(wù)移動(dòng)

2017-07-06 11:41:48

CIOIT技術(shù)

2020-05-28 16:15:50

HTTP暗坑前端

2022-11-04 07:57:59

編程編碼編譯器

2017-11-28 15:24:14

ETA配送構(gòu)造
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)