概述
介紹移動端Android/iOS硬解用法的文章也是比較多的,本文將以筆者在實際開發工做中的經驗爲基礎,抽出幾個比較關鍵的部分來跟你們分享,旨在解決實際工做中可能遇到的花屏、(半邊)綠屏、播放不完整等問題。
本文將以目前普遍應用的H264編碼的視頻爲例來講明,主要包含:H264碼流數據結構說明、解碼器的初始化、seek、先後臺切換、無縫分辨率切換、播放結束時的處理以及iOS如何避免下半部分綠屏的問題。
瞭解
網易雲信,來自網易核心架構的通訊與視頻雲服務。
H264碼流數據結構說明
理解碼流數據結構的重要性
咱們講支持硬解,提升硬解兼容性,實際上就是對碼流數據的結構進行處理以符合平臺硬解要求,所以對碼流數據結構的理解是必不可少的。
SPS/PPS與IDR幀
SPS(Sequence Parameter Set)序列參數集、PPS(Picture Parameter Set)圖像參數集,包含了圖像編碼的各類參數信息,是做爲解碼器初始化所必須的參數信息。
IDR(Instantaneous Decoding Refresh)幀,也就是即時解碼刷新幀,直觀意思就是解碼器在接收到IDR幀後會刷新參考幀緩存。IDR幀先後的視頻幀不會有任何參考關係,解碼器能夠從任何一個IDR幀開始解碼。
H264的NAL單元
H264標準中,視頻流是由NAL(Network Abstraction Layer)單元組成的(簡稱NALU),每一個NALU中多是IDR圖像、SPS、PPS、non-IDR圖像等。
上圖中示意的NALU單元是以startcode方式分割的,關於NALU的分割方式將在後面說明。 另外,NALU內容中添加了防競爭字節,也就是說在一個NALU中,咱們不可能再找到匹配的startcode.
從上圖能夠看到,一個視頻幀中可能可能包含多個NALU, 此時能夠稱該視頻幀爲多slice視頻幀(一個NALU中包含該視頻幀的一個slice)。
其中nal_unit_type是咱們關心的字段,該字段標識了當前NALU的類型,咱們能夠經過將NALU中第一個字節&0x1F的方式來獲得NALU類型。NALU類型的具體定義以下圖所示:
其中5表明上面提到的IDR幀數據,七、8分別表明SPS/PPS數據。
AVCC與Annex-B
H264碼流分爲AVCC與Annex-B兩種組織格式。
-
AVCC格式 也叫AVC1格式,MPEG-4格式,字節對齊,所以也叫Byte-Stream Format。用於mp4/flv/mkv等封裝中。
-
Annex-B格式 也叫MPEG-2 transport stream format格式(ts格式), ElementaryStream格式。用於TS流中(以及使用TS做爲切片的hls格式中)。
-
AVCC格式使用NALU長度(固定字節,字節數由extradata中的信息給定)進行分割,在封裝文件或者直播流的頭部包含extradata信息(非NALU),extradata中包含NALU長度的字節數以及SPS/PPS信息。
-
Annex-B格式使用start code進行分割,start code爲0x000001或0x00000001,SPS/PPS做爲通常NALU單元以start code做爲分隔符的方式放在文件或者直播流的頭部。
AVCC格式的extradata格式定義在「ISO_IEC_14496-15"文檔中,Annex-B格式的SPS/PPS定義能夠在"ISO_IEC_14496-10"文檔中找到。
MediaCodec與VideoToolBox使用的數據格式
Android的硬解碼接口MediaCodec只能接收Annex-B格式的H264數據,而iOS平臺的VideoToolBox則相反,只支持AVCC格式。
解碼器的初始化及數據輸入
初始化解碼器,除了配置輸入視頻流的的編碼格式、寬高以及輸出格式以外,還須要配置一些額外的信息。 對於H264視頻,須要填充的就是咱們前面提到的SPS/PPS信息。
Android平臺MediaCodec的初始化
咱們須要將Annex-B格式的兩個SPS/PPS NALU單元經過setByteBuffer方法,以"csd-0"爲名稱(或SPS設爲"csd-0", PPS設爲"csd-1")設置到MediaFormat對象中,並調用configure接口配置到MediaCodec中去。
MediaCodec設置SPS/PPS信息的示例代碼
MediaCodec mediaCodec = MediaCodec.createDecoderByType("video/avc");
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
// extradata中是Annex-B格式的SPS、PPS NALU數據
mediaFormat.setByteBuffer("csd-0", extradata);
// ...
mediaCodec.configure(mediaFormat, surface, 0, 0);
// ...複製代碼
如上節所述,對於mp4/flv/mkv等封裝,咱們獲得的是AVCC格式的extradata,須要先將該extradata轉換爲Annex-B格式的兩個NALU, 而後用startcode進行分割。
Android平臺在配置解碼方式時,最好使用MediaCodec直接渲染到Surface的方式,一是能夠避免不一樣硬件平臺繁雜的YUV格式兼容,二是在解碼渲染高分辨率的視頻時能夠有很是明顯的效率提高。緩存
iOS平臺VideoToolBox接口的初始化
VideoToolBox針對AVCC格式和Annex-B格式的SPS/PPS信息設置,分別提供了兩個方法:
須要注意,iOS平臺不支持隔行H264視頻的解碼,須要在建立videoToolBox前從SPS中判斷當前視頻是否隔行編碼。
數據格式的轉換
如前所述,Android平臺只接受Annex-B格式以startcode分割的H264 NALU;iOS平臺則相反,只接受AVCC格式以size分割的NALU. 在原視頻流格式不匹配時須要進行相應的轉換。
-
若是源視頻流自己已是AVCC格式,但NALU size的大小是3個字節,而非4字節時,須要轉爲4字節格式。具體的話,須要先更改extradata中標識NALU size的字段,而後每一個視頻幀中的NALU size都要改爲4個字節。
-
若是一個視頻幀內有多個NALU(多slice),那必須將這些NALU打包到一個CMSampleBuffer中,一次性送給解碼器。
seek時的處理
編碼後的視頻幀之間存在着參考關係,咱們沒法直接從任意一幀開始解碼,只能從可隨機訪問幀開始,在H264中就是IDR幀。
從IDR幀開始解碼
對於點播視頻,mp4/flv/mkv的頭信息中都會保存整個視頻的IDR幀索引,seek時須要定位到原seek位置附近的IDR幀再送數據給解碼器。 若是要實現短視頻中的精確seek邏輯,能夠先seek到離目標位置最近的上一個IDR幀開始解碼,但不輸出圖像,直到目標位置的視頻被解碼出來。
刷新解碼器
進行seek操做時,除了要保證從IDR幀開始以外,還須要在送新的IDR幀數據前對解碼器進行刷新操做。
先後臺切換
對Android、iOS平臺,都存在App切後臺,播放器渲染View被銷燬而致使解碼出錯的狀況。
切回前臺的處理
App切到後臺時,iOS的videoToolBox session會失效,切回前臺後原session也不能繼續使用,需從新建立videoToolBox實例;Android平臺在配置了Surface的狀況下,若是Surface被銷燬,則在切回前臺時也須要配置新的Surface來從新建立並初始化MediaCodec.
若是咱們要提升用戶體驗,實現先後臺切換時的無縫播放,而不是從新拉流,那麼能夠在用戶切後臺的時候暫停播放,切回前臺時從新建立解碼器,繼續從原位置開始播放。
不過參考前面seek章節的說明,咱們恢復播放的位置極可能不是IDR幀,這種狀況下就會出現切回前臺後畫面會先黑一段時間,直到下一個IDR幀被解碼。黑屏的時間會跟視頻流的IDR幀間隔有關,最差狀況下黑屏時間接近IDR幀間隔。 爲了儘可能避免黑屏現象的出現,咱們能夠參考前面精確seek的處理,在解碼過程當中一直緩存當前GOP(Group Of Picture)的視頻幀數據,在恢復時從當前GOP的IDR幀開始解碼但不輸出圖像,直到恢復點。
不過上述方案也沒法100%解決黑屏問題,解碼恢復點前的視頻數據自己會有時間消耗,GOP越大,解碼恢復可能須要的時間也就越長,黑屏時間也就會越長。
Android平臺使用TextureView避免Surface被銷燬
對Android平臺,咱們也能夠經過使用TextureView渲染來儘可能避免Surface被銷燬。
-
在TextureView的onSurfaceTextureAvailable回調中保存當前建立的SurfaceTexture;
-
App切後臺時,TextureView的onSurfaceTextureDestroyed回調中返回false,不讓系統銷燬當前的SurfaceTexture;
-
在下一次App切回前臺,onSurfaceTextureAvailable回調中,將前面保存的SurfaceTexture經過setSurfaceTexture接口設置給TextureView,並銷燬回調參數中傳回的surfaceTexture;
-
播放器銷燬時,須要銷燬保存的surfaceTexture.
無縫分辨率切換的處理
考慮到用戶網絡的差別性,以及不一樣時間段的擁堵情況不一樣,爲了兼顧拉流清晰度與流暢度,咱們能夠經過實時檢測用戶的網絡狀況,並動態切換視頻的分辨率、碼率來提升播放體驗。
rtmp直播,http/flv直播,hls直播以及hls點播能夠支持動態分辨率切換。
分辨率切換時須要拿到新的SPS/PPS並重啓解碼器
-
對於rtmp, http/flv直播,以及mp4分片的hls視頻,分辨率切換時咱們可以拿到新的AVCC格式的extradata(使用ffmpeg解封裝時這個信息是在AVPacket的sidedata中), 此時須要用新的extradata數據從新建立解碼器,所需的分辨率信息能夠從extradata中解析出來。
-
而對於ts切片的hls直播點播視頻,SPS/PPS信息是以Annex-B格式保存在正常的NALU中,並且每一個IDR幀前都會有SPS/PPS的NALU。對此,咱們須要監控每一個收到的視頻包,獲取其NALU類型,若是是SPS/PPS, 則從中解析出分辨率等信息,若是有變化,則用新的SPS/PPS從新建立解碼器。
播放完成時避免遺漏最後幾幀
前面咱們提到過,編碼後的視頻幀之間存在着參考關係,並且存在雙向參考幀(B幀)的視頻流其解碼輸出順序和輸入的順序是不一樣的,同時解碼器在異步模式下也不會當即返回解碼後的視頻幀,這就致使咱們在輸入最後一幀數據給解碼器後,可能還會有一些視頻幀沒有輸出。
-
Android平臺須要給MediaCodec送入一個帶有BUFFER_FLAG_END_OF_STREAM標記的buffer數據(能夠是空buffer),而後等待MediaCodec輸出帶有該標記的內容,再銷燬解碼器,結束播放。
-
iOS平臺須要在送完最後一幀數據後,調用VTDecompressionSessionWaitForAsynchronousFrames接口,該接口會等待全部未輸出的視頻幀輸出結束後再返回。
VideoToolBox兼容不標準的多slice視頻
在iOS平臺的硬解的實踐中,咱們可能會遇到以下圖的這種狀況(上面一部分有畫面,下面部分是綠屏):
這種現象實際上就是多slice視頻的組織格式不符合VideoToolBox的要求引發的。
以上圖的視頻爲例,該視頻流的每一幀是由3個slice構成的,對於VideoToolBox能夠正常解碼的組織格式應該以下圖所示:
能夠看出,該視頻混用了AVCC與Annex-B格式的分隔符,致使iOS VideoToolBox只能解碼第一個slice單元,從而出現下半部分綠屏的狀況。
瞭解
網易雲信,來自網易核心架構的通訊與視頻雲服務。
網易雲信(NeteaseYunXin)是集網易18年IM以及音視頻技術打造的PaaS服務產品,來自網易核心技術架構的通訊與視頻雲服務,穩定易用且功能全面,致力於提供全球領先的技術能力和場景化解決方案。開發者經過集成客戶端SDK和雲端OPEN API,便可快速實現包含IM、音視頻通話、直播、點播、互動白板、短信等功能。