Android短視頻SDK轉碼實踐

**一. 前言

一些涉及的基本概念:**

轉碼:一般指多媒體文件格式的轉換,比如分辨率、碼率、封裝格式等;
解複用(demux):從某種封裝中分離出視頻track和音頻track,然後交給後續模塊進行處理;

複用(mux):將視頻壓縮數據(例如H.264)和音頻壓縮數據(例如AAC)合併到某種封裝格式的文件中去。常提到的MP4即是一種封裝;

編碼(encode):通過專門的算法(例如H.264或AAC)來對原始音視頻數據進行壓縮;

解碼(decode):對壓縮後的數據進行解壓縮。

短視頻APP中錄製完成後,爲什麼要做轉碼:

原始視頻文件碼率較大,上傳下載都需要很長時間,不利於傳播;

編輯時增加特效、轉場效果後,只是在預覽中有效,原始文件並未改變,需要進行一次轉碼來把這些效果合成進最終的文件;

多段視頻進行編輯前轉碼拼接爲一個文件,方便後續的編輯;

目標格式和源文件格式不一致,比如需要從mp4轉成gif。

爲什麼不在服務端做轉碼呢?

短視頻需要加入濾鏡等效果,在移動端轉碼可以充分利用手機的GPU等資源,實現實時添加濾鏡實時看到效果;

原始視頻碼率較大,上傳下載都需要很長時間。

轉碼的主要流程如下:
Android短視頻SDK轉碼實踐

圖1. 轉碼基本流程

其中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
Android短視頻SDK轉碼實踐
可以看到,使用了硬編、硬解等提速手段後,合成速度由54秒優化到22秒。

五. 模塊化的思考

金山雲短視頻SDK的基礎模塊是基於直播SDK,整體來說,是一套push模式的流水線。

流水線中的每個模塊都很好地實現瞭解耦,單獨模塊完成單一的功能,模塊的複用也非常方便。前置模塊在產生新的音視頻幀後,會立即push給後續模塊,後續模塊需要儘快把前置模塊產生的音視頻幀消化掉,最大程度上保證實時性。爲了保證音視頻同步等邏輯,引入了大量同步鎖。在短視頻的開發中,遇到了不少的死鎖和不方便。對於短視頻這種非實時的場景,更多的時候,需要由後續模塊(而非前置模塊)來控制整個流程的進度。

當前處理過程中需要實現暫停,需要在前置模塊加鎖來實現。爲了能方便以後的開發,我們會在接下來重新梳理這種push流水線的方式, 實現模塊化的同時,儘量減少同步鎖的使用。

六. 總結

轉碼對於普通用戶來說不可見的,但卻是短視頻SDK的一個重要過程。怎麼樣讓轉碼過程耗時更短,轉碼圖像質量更高,特效添加更靈活,減少我們團隊自身的開發和維護成本,同時也爲開發者提供最方便易用的API,一直是金山雲多媒體SDK團隊的目標。
團隊在很用心的開發短視頻SDK,歡迎試用!

轉載請註明:
作者金山視頻雲,首發簡書 Jianshu.com

Android短視頻SDK:https://github.com/ksvc/KSYMediaEditorKit_Android
有關音視頻的更多精彩內容,請參考https://github.com/ksvc

金山雲SDK相關的QQ交流羣:

視頻雲技術交流羣:574179720
視頻雲Android技術交流:620036233

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章