360视频云Web前端HEVC播放器实践剖析
360視頻云前端團隊圍繞HEVC前端播放及解密實現了一套基于WebAssembly、WebWorker的通用模塊化Web播放器,在LiveVideoStackCon2019深圳的演講中360奇舞團Web前端技術經理胡尊杰對其架構設計、核心原理,具體痛點問題的解決方式進行了詳細剖析。?
文 / 胡尊杰
整理 / LiveVideoStack
奇舞團是360集團最大的大前端團隊,同樣也是TC39和W3C會員,擁有Web前端、服務端、Android、iOS、設計、產品、運營等崗位人員,旗下的開源框架和技術品牌有SpriteJS、ThinkJS、MeshJS、Chimee、QiShare、聲享、即視、奇字庫、眾成翻譯、奇舞學院、奇舞周刊、泛前端分享等。
?
奇舞團支持的業務基本上涵蓋了360大部分業務線。我個人最開始的時候也曾帶隊負責360核心安全平臺的Web前端支持,包括大家耳熟能詳的安全衛士、殺毒軟件等。隨著公司的業務發展,后面也負責了IoT業務前端支持,最近兩年主要配合360視頻云的一些Web前端支持工作。基于HEVC的播放器,實際上就是來源于我們最近做的一個叫QHWWPlayer的播放器。HEVC并不是一個新鮮事物,但對于我們團隊來說,Web前端的HEVC播放器一直是個亟待優化的領域。雖然移動終端或PC端HEVC播放器已經遍地開花,但在Web端仍舊有很多地方需要改進。包括現存一系列智能硬件產品,也在固件采集端已經應用了HEVC的編碼,不過如果想讓其在Web端呈現并達到用戶需求仍需加倍的努力。本次分享將從以下幾個維度展開,希望能給大家帶來一定的參考價值。
?
1. 需求背景
1.1 瀏覽器端HEVC的支持情況
?
上圖展示了HEVC在瀏覽器端的支持情況,其中紅色代表不支持的瀏覽器對應版本,綠色代表對HEVC具有良好的支持,青色代表無法保證瀏覽器可以很好地支持HEVC。總體上來說HEVC在瀏覽器端并不是一個得到廣泛支持的靠譜方案。
?
一般情況下,PC端瀏覽器都給我們提供了相應的API,如果我們的業務場景是支持HEVC的瀏覽器,可嘗試有效利用瀏覽器的原生能力。
基于瀏覽器原生video,配置source時指定解碼器,告知瀏覽器當前視頻采取的是哪一種編碼方案。如果瀏覽器自身有能力進行解碼那么其自然會走入“支持HEVC”的邏輯分支當中。
也可以另外通過JS實現檢測功能,JS也提供了相應API——canPlayType來判斷當前瀏覽器環境是否支持HEVC解碼。
?
但如果以上流程無法得到有效支持呢?這也是本次分享我們討論的重點。
?
1.2 Web端解碼方案
?
瀏覽器端視頻解碼總共有以上三種方案,首先就是前文我們提到的基于瀏覽器原生能力的播放,例如基于video標簽拉流、解碼以及渲染播放,整個過程完全由瀏覽器實現。第二種方案是首先通過JS來下載視頻流、對視頻流進行解封裝與轉封裝處理,最后再通過瀏覽器提供的相關API,交由瀏覽器原生video進行解碼與渲染播放。如開源社區當中的HLS.JS或FLV.JS等就是基于該思路。
?
但是HEVC不能僅靠解封裝與轉封裝來實現,因為其本質上在解碼層就不支持。因此第三種方案就是:JS下載的視頻流首先經由解封裝(解密)處理,并在接下來進行解碼,解碼完成后渲染播放。如果我們這里轉成瀏覽器普遍支持的解碼格式并讓video標簽進行播放,盡管理論上可行,但成本顯然是非常高的,并且中間存在一個無端的浪費。因此這里通常直接采用瀏覽器端Canvas+WebAudio API實現視頻與音頻的渲染,而不再使用瀏覽器原生video能力。這里如果使用純瀏覽器原生的JS,由于 JS天生單線程執行的弱勢,會導致整個處理的效率比較差。
?
近期,萬維網標準化委員會正式推出了WebAssembly規范。一方面我們可以借助WebAssembly高于JS的能力,實現更加出色的大規模數據處理與解碼,另一方面基于WebAssembly,我們也能方便地將傳統媒體處理中基于C或C++開發的一些媒體處理能力集成在瀏覽器端執行,并且可通過JS來調用API。對于熟悉傳統Web前端開發的我們來說,這也是一個值得我們堅持探索與實踐的全新領域。有了WebAssembly之后,我們就可以讓部門內擅長視頻處理的專家級同事來配合實現更加出色的瀏覽器端視頻播放,相對以往的開發流程來說,無論是能力、成本控制還是效率與靈活程度都有十分顯著的提升。
?
1.3 瀏覽器端WebAssembly的支持情況
?
上圖展現了瀏覽器端WebAssembly的支持情況,盡管個別低版本的瀏覽器有一些支持限制,但隨著標準化委員會對該標準的不斷推進,情況會變得越來越好。在包括一些混合式場景,例如APP內嵌(比如聊天工具或通訊工具當中打開一個鏈接)等情況,是否支持也取決于WebView本身提供的能力以及WebAssembly的支持情況,總體上來說趨于向好。
?
1.4 HEVC播放器需求目標
?
HEVC播放器的需求目標,就是基于 JavaScript 相關API,配合FFmpeg+WASM達成 HEVC 在瀏覽器端的解碼&解密、渲染播放的需求,接下來我們就開始研究如何落地這一目標。
?
2. 架構設計
?
總體架構設計思路如上圖所示,首先我們需要一個專門負責下載的下載器,該下載器也是基于瀏覽器的JS Fetch或XHR API,以實現文件獲取或直播拉流等操作。成功拉取的視頻流會被存儲在一個數據隊列當中,隨后基于WebAssembly(WASM)+FFmpeg的解碼器會來消費處理隊列里這些流數據,解碼出音視頻數據,并放置在音視頻幀數據隊列當中,等待隨后的渲染器對其進行渲染處理。渲染器基于WebGL+Canvas與WebAudio調用硬件渲染出圖像與音頻。
最后則是控制層用于貫穿整體流程中下載、解碼、渲染等獨立模塊,同時實現底層一些基本功能:如之前我們提到JS為單線程,而瀏覽器提供的WebWork API可拉起一個子線程。該流程中每一個模塊都是獨立的,隊列中的生產與消費過程也是異步進行的。(我們可基于JS本身一些比較好的特性實現諸多便捷的功能。例如基于Promise可以將異步過程進行較為合理的封裝,并呈現一些異步處理邏輯流程的關鍵環節的控制到UI層。)
除此之外,還有控制層的一些基礎配置選項,包括播放器本身的一些事件或消息的管理,都可以基于控制層來實現。
?
3. 分解實現
3.1 下載器
下載器作為一個基本模塊獨立存在,具有初始配置、啟動、暫停、停止、隊列管理與Seek響應(用于進度條拖拽)等基本功能。上圖左側圖標是在開發完成后,基于下載器的事件消息呈現的數據可視化結果。(柱狀圖表示單位時間下載量,這里我們可以看到的是,下載量并不均勻,其中的變化可能取決于推流端、服務端、用戶端,也可能取決于整個網絡環境。)
?
下載器方面需要留意五個關鍵問題點:
?
線性的數據流的合并與拆分
?
我們應當進行線性數據流的合并與拆分。理論上瀏覽器從服務端下載一個視頻流的過程是線性的,但瀏覽器的表現實際上并非如此,二者的差異可能會很大。
例如當一個瀏覽器啟動并基于JSFetch API抓取流,其過程也是通過API監聽數據回調來實現,每次回調可能間隔會很短、數據量也只是一個很小的一千字節左右的數據包。但有些瀏覽器的表現并非如此,它們會等抓取到一個1M或2M的數據包之后才反饋給API回調。
而那些過于零碎的數據直接丟給隊列或之后的流程來處理,這樣勢必導致更頻繁的數據處理;數據包體積大的直接隊列和后續流程勢必增加單次處理成本。
因此對線性數據流的合理合并與拆分十分必要,整個過程也是結合初始配置來實現閾值控制。
?
通過閾值調節控制,我們希望能夠做好用戶端瀏覽器硬件資源消耗,與該業務場景下媒體播放產品服務體驗之間的取舍與平衡。
內部維護管理 range 狀態
除此之外,下載器實際上也需要內部維護管理range 狀態。例如當用戶選擇點播時,我們需要明確是從哪一個字節位置到另一個字節位置下載傳輸中間這一片數據。而在直播過程中,則可能出現由網絡環境造成卡頓或用戶端主動暫停的現象,此時下載器需要明確知道播放或當前下載的位置。
不同媒體類型數據獲取的差異
?
第三點是不同媒體類型數據獲取的差異,也就是下載器針對不同的媒體類型開發不同的下載功能。例如一個FLV直播流可以理解為是一個連續的線性的數據獲取,而點播則以包為單位獲取。對于HLS流需要獲取m3u8列表,完成分析之后再從中選取數據包的地址并單獨下載,隨后進行流的合并或拆分。總地來說,我們需要保證數據的最終產出盡量均勻存儲到隊列中,以便于后續的一系列處理。
MOOV 前置或后置
在媒體處理中像MOOV等的索引數據有前置與后置兩種情況,這里需要注意的是,我們的播放器基于Web端。
?
若索引文件為后置,如果播放器直接下載了一部分數據就直接丟給FFmpeg解碼器進行解碼,由于FFmpeg解碼器無法獲取索引,當然也就無法解碼成功。除非解碼器等待整體媒體源下載完畢,實際上這樣是不現實的。
?
另外由于我們無法控制MOOV索引數據的體量,前置索引的大小無法確定,尤其對于一些特殊情況,這種邏輯會帶來很多問題。(但是這里有一個取巧的辦法,就是我們可以嘗試首先抓取前面幾個數據包,探測MOOV邊界,并基于此得到MOOV的長度,從而判斷取舍在什么時機啟動后續的解碼。)
慎重并折中的控制內存消耗
最后,慎重并折中控制內存消耗也至關重要。例如盡管較大的緩存能帶來流暢的播放,但在Seek時就會帶來很大的浪費,我們則需要根據服務所在的應用場景、幀率碼率等來實現合理的折中與取舍。
?
3.2 解碼器
?
?
下載器之后,整個流程的核心能力就是解碼器。解碼器的基本功能與下載器相比大同小異,需要特別關注的是解碼器并不是像下載器完全是去調用一個原生的JS Fetch API或XHR,而是在啟動WebWorker之后再啟動WebAssembly(這里的WebAssembly依賴中是引入了定制化的FFmpeg API,以解決解容器、解碼等需求),并實現一些API的交互。上圖左側展現了音頻與視頻幀解碼數據隊列的可視化結果。
?
解碼器方面,需要關注的關鍵問題主要有以下幾點:
啟動解碼前依賴數據量控制
剛才講到MOOV前置與后置時我們也提及這一點,也就是在啟動解碼前做好數據量控制,明確其數據量是否已經達到FFmpeg的基本需求。如果索引文件的數據還沒有完全給到就直接使用命令行啟動FFmpeg,那么就會出現報錯的情況。我們應當結合數據量的精準控制來對解碼器的啟動時機做合理的判斷。
主動向下載器獲取數據
解碼器需要主動獲取下載器生成的數據隊列,這樣系統便可根據數據消費效率獲知當前解碼器是否處于繁忙的狀態。同時,主動向下載器獲取數據也能在一定程度上減輕CPU的負擔,并可根據CPU的負載來決定當前從下載端應該獲取多少數據。例如如果CPU負載較大則數據隊列自然會出現累積,我們可以在下載器初始化時設置一個閾值,如果數據隊列積累達到該閾值則下載器暫停下載,這樣就可合理控制處理的整體流程并確保播放的正常。
動態解碼模式控制CPU消耗
整個解碼過程實際上還依賴CPU的性能,如果單幀解碼的時間較長,例如一個幀率是25的視頻,僅單幀解碼就需耗費半秒鐘甚至更長時間,此時如果我們依然按照這樣半秒鐘或更久的頻度解碼,則解碼數據生產效率完全跟不上渲染的自然時間進度,效果肯定不符合預期,播放也會斷斷續續。因此我們需要針對不同的應用場景,使用動態解碼模式(主動丟幀)控制好CPU的消耗。例如在直播或安防場景下,我們可以舍棄一些指標以保證解碼與傳輸的時效性。
獨立的音頻、畫面幀數據隊列
如上圖左側所示,獨立的音頻與畫面幀數據隊列分別管理;比如我們啟動丟幀策略的話,會看到畫面幀數據量變少,但聲音沒有變化。
音頻重新采樣
采集端編碼數據的音頻采樣率需要結合播放端的支持情況來留意兼容問題。
瀏覽器是一個比較特殊的應用場景,各瀏覽器對音頻渲染中采樣率的支持程度也是不同的。
例如安防場景對聲音的要求并不是很高,通常16,000的采樣率即可,但是如果想在瀏覽器端播放視頻,則部分瀏覽器要求至少22,050的采樣率,否則瀏覽器端播放無法成功識別并渲染音頻數據。FFmpeg本身可以進行音頻重新采樣,因此我們可以在解碼器端加入相應的配置項,如果用戶有該需求那么就可以啟動音頻重新采樣,重新把16,000的音頻采樣率重采樣成符合瀏覽器所要求的22050采樣率。有了符合要求的獨立的音頻與視頻數據幀隊列,接下來也自然就能基于瀏覽器實現對音視頻的渲染與呈現。
?
3.3 渲染器
渲染器的基本功能與下載器、解碼器相似,不同之處在于以下幾個關鍵點:
依賴解碼、UI提供畫布
渲染器需要瀏覽器提供一個獨立的畫布用于繪制相應的視覺畫面內容。在UI模塊初始化時呈現出一個畫布的容器,渲染器渲染生成的畫面才能表現在網頁上。
除此之外,渲染器依賴解碼器解碼生產出的音視頻幀數據才能進行音畫渲染。
主動向解碼器獲取幀數據
這一點與解碼器向下載器主動拿數據相似。
?
分緩存隊列、渲染隊列
渲染器會消費處理等待渲染的幀數據隊列,只不過幀數據會被分為緩存隊列與渲染隊列。
而之前我們介紹的下載器與解碼器,本身只有一組數據隊列。為什么要這樣呢?渲染器調用WebAudio API將音頻數據傳輸給瀏覽器進行PCM渲染時,無法將已經通過該API傳輸給瀏覽器的數據做取回控制,因此就需要記錄當前已經給了多少數據到瀏覽器,這就是“渲染隊列”。而“緩存隊列”則是從進程中獲取一部分數據先存儲在一個臨時隊列當中,從而避免頻繁地向處于另一個獨立WebWorker中的解碼器索取其音畫幀隊列數據,而帶來不必要的時間消耗。
?
音畫同步、倍速播放、Waiting
音畫同步、倍速播放以及判定是否處于等待狀態至關重要。比如要追求直播的低延時,網絡抖動導致數據堆積發生的時候,倍速追幀是個有效的辦法。
?
動態碼率變化
一個視頻在播放的過程中,可能隨網絡狀態的波動出現碼率的動態變化,例如為適應較差的網絡狀況,播放器可以主動將媒體流獲取從一個較為清晰的高分辨率變化到一個比較模糊的低分辨率源。
而再渲染中,基于WebGLCanavas的渲染器,我們首先需要對YUV著色器進行初始化操作,而YUV著色器的初始化,依賴于其所繪制的數據對應的分辨率、比例與尺寸。如果最開始的分辨率、比例和尺寸與之后要渲染的數據不一樣,而我們又未對此做相應的響應適配,那么就會出現畫面繪制花屏的情況。而動態碼率變化就是要隨時響應每一畫面幀所對應的分辨率變化,對YUV著色器作動態調整,從而保證畫面的實時性與穩定性。
?
從下載、解碼到渲染,視頻播放器的基本流程就此建立,播放器便有了獲取媒體數據、完成解碼、呈現音畫效果的基本能力。
?
3.4 UI
?
基本的UI如上圖左側所示,上半部分是整個播放器在實例化之前我們可以去做的一系列初始化配置。圖中所示的僅是一小部分參數,例如媒體源的地址、是否啟用了加密Key、對應的解密算法,包括渲染時為滿足某些特定場景下的需求,音視頻是同時進行渲染還是在主動控制下僅渲染音頻或視頻——例如在安防監控業務場景,會有一些設備需要音頻采集、另一些不需要,或者干脆播放時就不想播放源流音頻等等。若在這里播放器不做判定支持,則存在由于音畫同步控制依賴音頻幀視頻幀時間戳比對,但沒有音頻幀數據的原因導致無法正常播放,而播放器使用者能進行主動控制則可以避免該問題。
?
UI的基本功能包括實例化、用戶操作觸發后續流程涉及的各模塊接下來要做什么,還有狀態信息響應展現,也就是根據用戶交互行為和播放器工作狀態作出反饋與信息傳遞。
?
另外,UI也需要對相應的狀態變化作出響應,例如用戶控制當前播放器從正在播放切換到暫停,那么UI層面則需要針對用戶操作進行相應的變化。還有快進、拖拽進度條等等。
?
3.5 控制層
?
最后的控制層至關重要,首先控制層隔離校驗對外暴露的參數及方法。播放器可實現或具備的特性有很多,不可能全部暴露給用戶。在播放視頻時,下載與解碼的數據實際上存在一個前后呼應的關系,如果我們不考慮用戶行為與需求,在網頁上呈現播放器的所有特性。而用戶也不對其進行科學性選擇與判斷,而是隨意調用API,勢必會帶來矛盾、沖突與混亂。因此我們需要隔離配置信息、校驗對外暴露的控制參數及方法,以避免可能存在的沖突。
?
另外根據之前的介紹我們可以看到,不同模塊的基本功能大致相同。因此在控制層我們需要統一各模塊的生命周期,并完成用于調度各模塊工作的基礎類的實現。
每個獨立的模塊什么時刻可以實例裝載?什么時刻銷毀?該模塊是否支持熱插拔?各模塊生命周期狀態的管控與事件消息的監聽與調度…… 這些都由控制層進行管理。
?
有時我們需要做一些取舍,例如編碼器并不是基于FFmpeg,而是基于我們自己的解碼解決方案,那么就可以嘗試在播放器實例化時候,更換對應模塊當中相對應的部分依賴為自己的解碼方案;如果我們需要調整播放器UI層界面樣式,那么就可能需要定制自己的UI模塊……
?
在這個播放器實現中,為了規避單線程一些弊端,我們基于WebWorker API對重點模塊開啟子線程。
?
而WebWorker本身的設計存在各種不便:
首先,要求我們必須單獨打包一個JS文件,基于 new Worker(“*.js”)引入到項目中。
但我們整個播放器作為SDK項目的構建來說,通常只產生一個JS文件發布出去,才是合理的。如果同時產生多個JS文件,這對我們的調試、開發或后續應用等來說都不方便。
針對這個問題我們結合Promise 實現了PromiseWebWorker,PromiseWebWorker 相對于原生Worker,參數不再必須是傳入一個JS引用路徑,而是可以傳入一個函數。
這樣以來我們就可以在項目編譯時生成一個獨立的JS文件,在播放器的執行過程中將其中worker依賴的那部分函數內容生成一個虛擬的文件依賴地址,作為WebWorker執行的資源。
?
其次,WebWorker原生能力實現父子線程之間數據傳遞通訊,只能通過postMessage傳送數據、通過onMessage獲取傳送過來的數據,這對于頻繁的數據交互中想保證上下文關聯對應關系是比較麻煩的。PromiseWebWorker則借助了Promise的優勢,對以上整個數據交換過程做嚴格的應答封裝處理,從而實現播放器功能的健壯可靠。
?
上圖鏈接 http://lab.pyzy.net/qhww中是播放器DEMO展示地址
若對此感興趣可以前往試用研究
?
要點回顧
調度控制層控制下載器、解碼器、渲染器與UI&交互四大模塊,如果要做某功能模塊的業務定制化開發、功能增強補充,對對應獨立的模塊內部進行優化并做出相應的功能擴展或者調整即可。
?
4. 難點突破
開發過程所遇到的難點總體可以用以上三點來概括:首先基于WebAssembly 工具鏈(emscripten.org),借助EMSC編譯器我們可以直接將一個C和C++編譯成JS可用。這一過程本身存在諸多不便之處,主要是因為其本身對系統一些底層庫的依賴或對于開發環境的要求,導致可移植性并沒有那么好,當需要跨機器協作時容易出現諸多問題。現在我們內部的解決方案是自己找一臺專用機器來配置做為編譯發布使用。
?
第二點是隊列管理與狀態控制,只有精確實現隊列管理與狀態控制,我們才能保證整個程序能合理穩定的執行。
?
第三點就是項目構建打包,我們要解決前端一些構建打包的習慣以及其在邏輯需求上存在的一些沖突。
5. 未來展望
展望未來,我希望未來瀏覽器能對HEVC有更加出色的支持。本次分享雖然是一個播放器,但我們知道FFmpeg的能力不只是解碼播放,還可以做更多實用工具的發掘實現。同時我也希望未來媒體類型百花齊放,甚至私有編解碼也能夠形成Web端場景更規范靈活的解決方案。WASM成熟、標準化完善、各業務領域對應解決能力的細分,也是很值得期待的一件事情;而回到播放器本身,字幕、AI、互動交互等都是能進一步提升音視頻播放服務的可玩性與用戶體驗方向值得研究的方向。
點擊【閱讀原文】訪問購票頁面
總結
以上是生活随笔為你收集整理的360视频云Web前端HEVC播放器实践剖析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Zoom并非端到端加密、TikTok第一
- 下一篇: LiveVideoStack线上分享第五