作者| 阿里文娛前端技術專家 歸影
這不是一篇基於 MSE 開發 Web 播放器的入門文章,而是圍繞 Web 播放器開發遇到的常見 問題與解決方案,畢竟入門文章常有而趟坑乾貨不常有。如果您有 Web 播放開發經驗和音視頻 技術基礎,讀起來會更有共鳴。
一、Web 播放器開發基礎知識
先介紹 Web 播放器開發的一些基礎知識。有人要問了,Web 播放器開發難道不是一個 video 標籤就夠了麼?非也!
1.瀏覽器 Video 支持的格式非常有限
在 W3C 的標準裡面 Video 只支持 MP4 格式 準確的說是 ISOBMFF(Fragment MP4)。當然 chrome 支持 WEBM,safari 支持 HLS(MPEG-TS)這都是自家的私有實現,做不得數。
2.瀏覽器 Video 無法逐個加載視頻切片
現在主流的流媒體點播/直播技術,都會把視頻切片。而 video 標籤 src 只能掛載整個 MP4
資源。沒法逐個的加載視頻分段。
所以我們的主角出場—— MediaSource Extenstion,簡稱 MSE,是一套能不斷的把音視頻 二進制數據塞給 video 標籤播放的 API。
(圖 1:MSE 簡明結構)
MSE 內部可以創建一系列的 sourcebuffer,一般是一個音頻 buffer,一個視頻 buffer。把 MSE 做成 blob url 之後綁定給 video 的 src。然後就可以通過appendBuffer 往 video 裡追加音視頻數據了。有了 MSE,播放器器的整體結構是什麼樣的呢,見下圖。
(圖 2:Web 播放器簡明結構)
首先在瀏覽器層面,主要使用 video 標籤、MSE、XHR 和 UI。
那麼播放器主要由 Manager 驅動加載視頻的playlist(比如 HLS 裡的 m3u8,dash 裡的 MPD, FLV 雖然不是 playlist 概念,但是是原理上差別不大,都是為了拿到視頻的一個個的片段的地址), 並通過數據服務加載這一個個的分片。然後通過 transmuxer 也就是所謂的轉封裝器,把分片的 封裝格式比如 TS 拆開(demux) 把連原始的音視頻數據解出來,再重新打包成 fmp4(remux), 最後通過 MSE API 餵給 video 標籤裡,讓 video 去播放。
因此播放器所做的事情最主要有兩點:
1)轉封裝。即將 video 不支持的封裝格式轉碼成 video 所支持的封裝格式;
2)如何驅動整個播放進行。即決定何時下載下一個分片,何時需要解碼插入到 video 的 buffer 裡。
二、時間戳對齊
轉封裝除了的封裝格式的解複用(demux)和再複用(remux)之外最重要的環節就是分片的 時間戳對齊策略,以及音視頻同步。
圖 3(傳說中的“開局一張圖 原理全靠猜”)
簡單講一下上圖:
紅色代表音頻的時間軸。藍色/青色是視頻的時間軸。PTS(Presentation Time Stamp) 指的是 這一幀需要渲染的時間。 DTS(Decoding Time Stamp) 指的是這一幀需要解碼的時間。
1.首片首幀的對齊策略
正常來說音頻 PTS 和 DTS 是一樣的,而視頻如果有 B 幀的話 DTS 往往要比 PTS 早一些(因 為要預留一定的時間解碼)。因此視頻的首幀會有一個洞(gap/shift 隨便你怎麼叫),如果不經處理插到 video 裡,那麼 video 裡的 buffer 也會呈現出一小段的洞,一般是 0.08s(比如 10s 的分片 插 進去可能出現 0.08~10.08 的情況)。現在主流的做法是削掉這個洞。就是把 DTS 跟 PTS 強行拉 平,一般來說 chrome 不會出現太大的問題。但是 safari 不行,如果不預留一定的 DTS/PTS 偏 移,safari 前兩幀的播放會明顯卡頓。
2.後續對齊策略
後續分片的對齊,會通過 DTS/PTS 兩個尾部指針來做。如果發現後續分片時間軸有間隔就 往前推從而填上間隔。如果發現重疊,就把重疊幀後移。這樣雖然會導致後續分片的前幾幀重疊。但在播放的過程中幾乎沒有影響。
三、音視頻同步
首先,什麼情況下會導致音畫不同步?
1)視頻源流壓根沒對齊。沒救了,看下一點。
2)還是因為有洞。很多時候視頻切出來的每個分片之間都不一定是嚴絲合縫的,分片間的 音視頻時間戳可能有洞。而且對於 TS 由於音頻每一幀的duration(≈23ms) 跟視頻每一幀的duration(40ms@25fps) 無法吻合(整除) 所以加劇了這種參差不齊的情況。 那麼,重點來了!chrome 有個特殊的機制,如果發現音頻之間有洞之後,為了保證音頻的平順, 會自動把後續音頻往前推抹平這個洞。如果每個分片都有洞,悲劇了,這種往前推的操作就會 積累越來越多導致音視頻不同步。
小 tips:
打開 chrome 的媒體調試頁面 chrome://media-internals 可以看到媒體播放相關的所有 debug 信息和 error 信息非常有用。其中就會有一條關於音頻處理的提示:
當然這條顯示的具體原因是自動切掉重疊 overlap 導致的。其實 gap/overlap 本質是一樣的。 怎麼辦?當然是播放器自己主動把洞填上。具體做法是插幀。目前主要是插靜音幀,或者複製 前一幀。靜音幀會帶來毛刺音,複製幀會導致拖音。我們目前的優化方案是判斷附近的音頻數 據量,數據量大時說明此處聲音豐富(其實不算靠譜,姑且這麼處理,因為沒有更好的判斷方式), 如果插靜音幀會毛刺很明顯,所以此時用複製幀,反之插靜音幀。
四、那些年我們躺過的坑
1.不同版本表現差異 容忍度不同
1)Chrome 35 分水嶺。chrome35 之前要求關鍵幀之後的第一幀 dts 不允許跟關鍵幀 dts 相 同,否則拋錯。
2)低延遲的模式。把轉封裝出來的 FMP4 中的視頻軌 duration(tkhd box) 設置成 0xffffffff 時 會讓 chrome 認為這是直播流,會開啟低延遲模式,所謂低延遲模式就是會極大的減少幀緩存, 基本上視頻幀立馬解碼立馬播放減少每個分片的起播延遲。但是呢在 CPU 負載過高的情況下(解不過來)會造成視頻頻繁卡頓(網絡無關的)。
2.不同瀏覽器表現有差異
1)timeupdate 事件。W3C 的標準是不能超過 250ms 觸發一次。windows 下 360 等瀏覽器會 達到 500ms 左右。
2)safari 對每一幀 duration 平順度更敏感。safari 需要對每一視頻幀的 duration 標準化處理, 例如 TS 下要處理成 3600。
3)對洞的容忍度不同。chrome 遇到 buffer 中有 0.08 的間隔以內會自動跳過去。像 IE edge 等瀏覽器不行會卡住,所以播放器一定要有跳洞邏輯。比如判斷當前卡在洞的邊界,要主動跳 過去(seek)。
3. 內存限制
通過 MSE push 給 video 的視頻數據會在內部維護一個 buffer,這個尺寸是有限制的。
1)chrome 系列約 100M 2)IE 系列約 30M
超過的話就會導致拋出 QuotaExceededError。所以需要處理好 buffer 的尺寸以及及時清除 不用的 buffer。比如已經播放過的,正常瀏覽器會自己清除,但是不那麼的及時。
五、優化
簡單說一下卡頓相關的優化。
多級 Buffer 控制
ABR 自適應碼率算法
基於 WebRTC 的 P2P
1.多級 buffer
為什麼要有多級的 buffer?因為 video 本身的解碼 buffer 有大小限制,而且 buffer 過長會導 致長時間解碼,會導致 CPU 一直佔用高。所以我們搞了兩級 buffer 一級就是 video 的 buffer 另 外一級是內存中的,只負責下載,二級很長。可以消除網絡抖動帶來的卡頓影響。
2.ABR 自適應碼率的算法
這個主要是來預測用戶本身的帶寬範圍,然後選用不同碼率的視頻流來無縫切換播放。當 然還有一些策略算法,比如根據用戶現在 buffer 的水位,或者檢測到用戶頻繁超時,來採用不 同的策略。
3.基於 WebRTC 的 P2P
因為 P2P 是基於 UDP 的傳輸,可以突破一些帶寬限制或網絡擁塞而導致的卡頓問題。不 過 P2P 不一定靠譜所以還是要輔以普通的 HTTP 傳輸相結合。我們一般是利用 P2P 加 indexDB 來變相延長視頻的緩衝區。因為 P2P 帶寬成本便宜,我們利用 P2P 做了一個非常長又很便宜的 buffer。這樣的話網絡再波動也不會導致卡頓了。