作者| 阿裡文娛前端技術專家 歸影
這不是一篇基于 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。這樣的話網絡再波動也不會導緻卡頓了。