轉碼對於普通用戶來說不可見的,但卻是短視頻SDK的一個重要過程。 ...
一. 前言
一些涉及的基本概念:
- 轉碼:一般指多媒體文件格式的轉換,比如解析度、碼率、封裝格式等;
- 解復用(demux):從某種封裝中分離出視頻track和音頻track,然後交給後續模塊進行處理;
- 復用(mux):將視頻壓縮數據(例如H.264)和音頻壓縮數據(例如AAC)合併到某種封裝格式的文件中去。常提到的MP4即是一種封裝;
- 編碼(encode):通過專門的演算法(例如H.264或AAC)來對原始音視頻數據進行壓縮;
- 解碼(decode):對壓縮後的數據進行解壓縮。
短視頻APP中錄製完成後,為什麼要做轉碼:
- 原始視頻文件碼率較大,上傳下載都需要很長時間,不利於傳播;
- 編輯時增加特效、轉場效果後,只是在預覽中有效,原始文件並未改變,需要進行一次轉碼來把這些效果合成進最終的文件;
- 多段視頻進行編輯前轉碼拼接為一個文件,方便後續的編輯;
- 目標格式和源文件格式不一致,比如需要從mp4轉成gif。
為什麼不在服務端做轉碼呢?
- 短視頻需要加入濾鏡等效果,在移動端轉碼可以充分利用手機的GPU等資源,實現實時添加濾鏡實時看到效果;
- 原始視頻碼率較大,上傳下載都需要很長時間。
轉碼的主要流程如下:
![](http://upload-images.jianshu.io/upload_images/4469440-a390733bf991acc0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
其中Audio Filter和Video Filter分別是指音頻和視頻的預處理。
- 短視頻轉碼的時機:
- 多段視頻的導入;
- 轉場完的合成;
- 編輯完的合成。
二. Demuxer方案的選擇
Demuxer模塊的實現,主要有以下三種方案:
-
方案一,使用播放器
播放器的主要功能是播放,也就是從原始文件/流中提取出音視頻,按照pts完成音視頻的渲染。轉碼並不需要渲染,要求在保持音視頻同步的情況下,儘快把解碼數據重新按要求編碼成新的音視頻包,重新復用成文件。我們也曾經為了實現儘快這個要求,把播放器強行改造成快速播放的模式,但後來遇到了很多問題:- 音視頻同步時機的問題,視頻的解碼是慢於音頻的解碼,必然需要實現同步邏輯。player中如果改成快速播放模式,player內部加上音視頻同步的邏輯,改動非常大。如果player不管同步,解碼數據直接上拋給調用層,則需要在短視頻上層做音視頻同步,引入了額外的工作量;
- 使用硬解碼時,從
SurfaceTexture
中獲取的timestamp不准。因此最後放棄了這個方案。
-
方案二,使用MediaExtractor
MediaExtractor是Android系統封裝好的用來分離容器中的視頻track和音頻track的Java類。優點是使用簡單,缺點是支持的格式有限。 -
方案三,使用FFmpeg
使用FFmpeg的av_read_frame
API來做解復用,即實現簡易版的播放器邏輯。- 優點:FFmpeg中對視頻格式有大量相容的邏輯,相比MediaExtractor相容性好,增加新的輸入格式的支持會更容易,同時音視頻同步邏輯的控制更簡單;
- 缺點: 需要引用FFmpeg,相對來說SDK體積較大。
方案二的相容性不如方案三。相比方案一,方案三把音視頻的解復用和解碼都放到了同一個線程,av_read_frame
能輸出同步交織的音視頻packet,上層邏輯調用更清晰。
同時短視頻其他功能模塊已經引入了FFmpeg,轉碼模塊引入FFmpeg並不增加包大小,所以選擇了FFmpeg方案。
三. 轉碼的數據傳遞
金山雲多媒體SDK實踐中,Demuxer實際上是在C層做的,但是介面的封裝是在Java層。解碼結構也是一樣。Demuxer和Decoder之間如何高效地在Java和C層之間傳遞待解碼的音視頻包?
3.1 AVPacket的傳遞
FFmpeg的demuxer模塊解復用出來的為音頻或視頻的AVPacket。最開始的時候我們並沒有在Java層對整個AVPacket的地址指針進行封裝,而是把數據封裝在ByteBuffer
和其他的參數中。這樣遇到了很多因為AVPacket中的參數沒有傳遞到解碼模塊導致的問題。
最終我們通過intptr_t
在C層保存AVPacket的指針,同時在Java層以long
類型來保存和傳遞這個指針,解決了這個問題。
3.2 AVFormatContext/AVCodecParams的傳遞
為了實現模塊的復用,我們把Demuxer和Decoder分成了兩個模塊。使用FFmpeg來實現時,Decoder模塊可以和Demuxer模塊共用AVFormatContext
,通過AVFormatContext
來創建AVCodecContext
。
但是這樣會有一個問題,Demuxer的工作速度會快於Decoder,此時AVFormatContext
是由Demuxer來創建的,Demuxer停止的時候會釋放AVFormatContext
。如果交給Decoder模塊來釋放,不利於模塊的復用和解耦。最終我們發現在FFmpeg 3.3的版本中,AVCodecParams
結構圖中有Decoder所需要的全部信息,可以通過傳遞AVCodecParams
來構造AVCodecContext
。
四. 轉碼提速
轉碼的速度是客戶非常關心的一個點,轉碼時間太長,用戶體驗會非常差。我們花了非常多的精力來對短視頻的轉碼時間進行提速。經驗主要有以下這些點:
4.1 調整視頻軟編編碼參數
轉碼的時間大部分都被視頻的編碼占用了,我們把x264編碼做了調整,在保證畫質影響較小的前提下,節省了30%以上的編碼時間。
4.2 優化GPU數據讀取
使用視頻軟編時,如何從GPU中把數據“下載”到CPU上,我們嘗試了很多中方案,具體的我們會在另一篇文章中詳細解釋。之前的方案是使用ImageReader
讀取RGBA數據。優化為用OpenGL ES將RGBA轉換為YUVA。讀取數據後從YUVA再轉為I420,下載和格式轉化總耗時,提速了大約40%。
4.3 開啟硬編
硬編的缺點: 在Android平臺上,硬編的相容性較差,同時視頻硬編的壓縮比差於軟編。
硬編的優點是顯而易見的,編碼器速度快,占用的資源也相對較少。
4.4 開啟硬解
經過大量的測試,硬解的相容性相較於硬編會好很多,使用硬解碼,直接使用MediaCodec渲染到texture上,省去手動上傳YUV的步驟,也節省了軟解碼的時間開銷。
4.4.1 硬編解遇到的坑
關於Android的硬編解網上已經有很多例子,官方文檔也比較完善。不過在實現過程中還是會遇到一些意想不到的問題。
- 圖像質量的問題
在硬編上線後,我們對比畫質發現轉碼圖像質量較差。原因是使用MediaCodec API時,選擇的是MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
,CBR的好處是碼率比較穩定,但是會犧牲畫質,移動直播中選用CBR更合理。短視頻轉碼場景硬編時推薦使用MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
,VBR會獲得更好的圖像質量。對於軟編時,我們也嘗試過ABR(也就是VBR),但實際測試下來效果並不能保證。
- 硬解不相容AVCC/HVCC 碼流格式
H.264碼流主要分Annex-B和AVCC兩種格式,H.265碼流主要分為Annex-B和HVCC格式。AnnexB與AVCC/HVCC的區別在於參數集與幀格式,AnnexB的參數集sps、pps以NAL的形式存在碼流中(帶內傳輸),以startcode分割NAL。
而AVCC/HVCC 的參數集存儲在extradata中(帶外傳輸),使用NALU長度(固定位元組,通常為4位元組,從extradata中解析)分隔NAL,通常MP4、MKV使用AVCC格式來存儲。
Android的硬解只接受Annex-B格式的碼流,所以在解碼MP4 Demux出的視頻流時,需要解析extradata,取出sps、pps,通過CSD(Codec-Specific Data
)來初始化解碼器;並且將AVCC碼流轉換為Annex-B,在ffmpeg中使用h264_mp4toannexb_filter
或hevc_mp4toannexb
做轉換。
- 硬解時間戳不准確的問題
硬解碼器解碼視頻到Surface,此時通過SurfaceTexture.getTimestamp()
獲得時間戳並不准確,某些機型會出現異常。所以還是要使用解碼輸入的時間戳,可將解碼過程由非同步轉為同步,或者將pts存儲到隊列中來實現。
- 音頻硬編硬解解的速度
MediaCodec的音頻編解碼具體實現和機型有關,許多機型的MediaCodec音頻編解碼工作仍然是軟體方案。經過測試MediaCodec音頻硬編碼較軟編碼有6%左右的提速,但MediaCodec音頻硬解反而比軟解的的速度慢,具體原因有待進一步調查。不過這隻是部分機型的測試結果,更多機型的比較大家可以使用我們demo的轉碼/合成功能進行測試。
4.5 轉碼提速對比
下麵以三星S8為例,短視頻SDK在轉碼速度上的進步,更多機型的對比數據,請移步github wiki查看。
將1分鐘1080p 18Mbps視頻,轉碼成540p 1.2Mbps,不同版本時間開銷大致如下:
機型 | 版本 | 編碼方式 | 第一次合成時長 | 第二次合成時長 | 第三次合成時長 | 平均值 |
---|---|---|---|---|---|---|
三星S8 | V1.0.4 | 軟編 | 52s | 54s | 58s | 54.7s |
V1.1.2 | 軟編 | 49s | 50s | 50s | 49.7s | |
V1.1.2 | 硬編 | 35s | 36s | 38s | 36.3s | |
V1.4.7 | 硬編 | 21.5s | 21.9s | 22.5s | 22.0s |
可以看到,使用了硬編、硬解等提速手段後,合成速度由54秒優化到22秒。
五. 模塊化的思考
金山雲短視頻SDK的基礎模塊是基於直播SDK,整體來說,是一套push模式的流水線。
流水線中的每個模塊都很好地實現瞭解耦,單獨模塊完成單一的功能,模塊的復用也非常方便。前置模塊在產生新的音視頻幀後,會立即push給後續模塊,後續模塊需要儘快把前置模塊產生的音視頻幀消化掉,最大程度上保證實時性。為了保證音視頻同步等邏輯,引入了大量同步鎖。在短視頻的開發中,遇到了不少的死鎖和不方便。對於短視頻這種非實時的場景,更多的時候,需要由後續模塊(而非前置模塊)來控制整個流程的進度。
當前處理過程中需要實現暫停,需要在前置模塊加鎖來實現。為了能方便以後的開發,我們會在接下來重新梳理這種push流水線的方式, 實現模塊化的同時,儘量減少同步鎖的使用。
六. 總結
轉碼對於普通用戶來說不可見的,但卻是短視頻SDK的一個重要過程。怎麼樣讓轉碼過程耗時更短,轉碼圖像質量更高,特效添加更靈活,減少我們團隊自身的開發和維護成本,同時也為開發者提供最方便易用的API,一直是金山雲多媒體SDK團隊的目標。
團隊在很用心的開發短視頻SDK,歡迎試用!