成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

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

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

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

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

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

視頻編碼器的選擇

對于錄制視頻的需求,不少app都需要對每一幀數據進行單獨處理,因此很少會直接用到 MediaRecorder 來直接錄取視頻,一般來說,會有這么兩個選擇

  • 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(); // 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(); 

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

關于MediaCodec更復雜的使用例子,可以參照下CTS測試里面的使用方式: EncodeDecodeTest.java

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

1、顏色格式問題

MediaCodec在初始化的時候,在 configure 的時候,需要傳入一個MediaFormat對象,當作為編碼器使用的時候,我們一般需要在MediaFormat中指定視頻的寬高,幀率,碼率,I幀間隔等基本信息,除此之外,還有一個重要的信息就是,指定編碼器接受的YUV幀的顏色格式。這個是因為由于YUV根據其采樣比例,UV分量的排列順序有很多種不同的顏色格式,而對于Android的攝像頭在 onPreviewFrame 輸出的YUV幀格式,如果沒有配置任何參數的情況下,基本上都是NV21格式,但Google對MediaCodec的API在設計和規(guī)范的時候,顯得很不厚道,過于貼近Android的HAL層了,導致了NV21格式并不是所有機器的MediaCodec都支持這種格式作為編碼器的輸入格式! 因此,在初始化MediaCodec的時候,我們需要通過 codecInfo.getCapabilitiesForType 來查詢機器上的MediaCodec實現具體支持哪些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,最后出來的視頻依然還會是Baseline,Shit....

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

 

  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.     ..... 

這個問題可以說間接導致了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作為視頻的編碼器。

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

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算法:

 

  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來進行縮放的時間基本上都需要 40ms+ ,對于我們需要錄制30fps的來說,每幀處理時間最多就30ms左右,如果光是縮放就消耗了如此多的時間,基本上錄制出來的視頻只能在15fps上下了。

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

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

我們選擇一種叫做的 局部均值 算法,前后兩行四個臨近點算出最終圖片的四個像素點,對于源圖片的每行像素,我們可以使用Neon直接實現,以縮放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指令每次只能讀取和存儲8或者16位的數據,對于多出來的數據,只需要用同樣的算法改成用C語言實現即可。

在使用上述的算法優(yōu)化之后,進行每幀縮放,在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 box 里面指定,視頻播放器在播放視頻的時候,會在讀取這里矩陣信息,從而決定視頻本身的旋轉角度,位移,縮放等,具體可以參考下蘋果的 文檔

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

 

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

于是可以在錄制的時候省下一大筆旋轉的開銷了,excited!

3.鏡像

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

但由于攝像頭安裝角度一般是90或者270度,所以實際上原生的YUV幀是水平翻轉過來的,因此做鏡像翻轉的時候,只需要剛好以中間為中軸,分別上下交換每行數據即可,注意Y跟UV要分開處理,這種算法用Neon實現相當簡單:

 

  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.     ); 

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

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

責任編輯:未麗燕 來源: Ragnarok Note
相關推薦

2018-12-26 13:22:05

NVMeNVMe-oF數據

2023-04-13 00:24:00

前端編碼JavaScrip

2014-03-20 09:17:36

2016-12-28 13:19:08

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

2018-01-16 08:57:24

Nginx規(guī)則實用

2017-08-09 08:25:35

DBA數據庫OLAP

2017-07-19 14:26:01

前端JavaScriptDOM

2021-09-07 14:35:48

DevSecOps開源項目

2022-05-15 08:13:50

Mysql數據庫Mycat

2020-04-21 15:18:11

財務信息化

2015-03-12 09:51:09

CoreDataiCloud

2020-07-30 07:30:17

存儲技術數據

2012-11-09 11:39:11

Windows 8

2015-07-20 10:00:28

Linux內核編碼風格

2017-03-23 14:30:13

Linux內核驅動編碼風格

2013-04-12 15:59:33

2020-05-28 16:15:50

HTTP暗坑前端

2017-03-31 10:27:08

推送服務移動

2017-07-06 11:41:48

CIOIT技術

2017-11-28 15:24:14

ETA配送構造
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 色香婷婷 | 男女午夜免费视频 | 成年人免费网站 | 国产美女一区二区 | 美女视频一区二区三区 | 色偷偷人人澡人人爽人人模 | 91精品国产91久久久久游泳池 | 国产成人精品a视频 | 成人国产精品久久 | 国产精品日本一区二区在线播放 | 精品国产乱码久久久久久蜜柚 | 免费欧美| 精品久久久久久亚洲综合网 | 午夜小视频免费观看 | 久久亚洲视频 | 日日夜夜影院 | 午夜精品一区二区三区在线 | 在线亚洲电影 | 亚洲三区在线观看 | 亚洲日韩中文字幕一区 | 日日夜夜天天干 | 日韩一及片 | 亚洲综合在线一区 | 成年人精品视频 | 亚洲欧美国产精品一区二区 | 一级做a爰片性色毛片16美国 | 精品一区电影 | 美国av毛片 | 色屁屁在线观看 | 国产一区不卡在线观看 | 91看国产 | 成人欧美一区二区三区在线播放 | 超碰精品在线 | 亚洲精品一二三 | 欧美国产日韩在线观看成人 | 一区二区三区精品视频 | 国产综合视频 | 久久黄色 | 欧美日韩高清在线一区 | 国产精品免费在线 | 国产视频第一页 |