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端呈現並達到用戶需求仍需加倍的努力。本次分享將從如下幾個維度展開,但願能給你們帶來必定的參考價值。緩存
上圖展現了HEVC在瀏覽器端的支持狀況,其中紅色表明不支持的瀏覽器對應版本,綠色表明對HEVC具備良好的支持,青色表明沒法保證瀏覽器能夠很好地支持HEVC。整體上來講HEVC在瀏覽器端並非一個獲得普遍支持的靠譜方案。安全
通常狀況下,PC端瀏覽器都給咱們提供了相應的API,若是咱們的業務場景是支持HEVC的瀏覽器,可嘗試有效利用瀏覽器的原生能力。網絡
基於瀏覽器原生video,配置source時指定解碼器,告知瀏覽器當前視頻採起的是哪種編碼方案。若是瀏覽器自身有能力進行解碼那麼其天然會走入「支持HEVC」的邏輯分支當中。架構
也能夠另外經過JS實現檢測功能,JS也提供了相應API——canPlayType來判斷當前瀏覽器環境是否支持HEVC解碼。框架
但若是以上流程沒法獲得有效支持呢?這也是本次分享咱們討論的重點。異步
瀏覽器端視頻解碼總共有以上三種方案,首先就是前文咱們提到的基於瀏覽器原生能力的播放,例如基於video標籤拉流、解碼以及渲染播放,整個過程徹底由瀏覽器實現。第二種方案是首先經過JS來下載視頻流、對視頻流進行解封裝與轉封裝處理,最後再經過瀏覽器提供的相關API,交由瀏覽器原生video進行解碼與渲染播放。如開源社區當中的HLS.JS或FLV.JS等就是基於該思路。ide
可是HEVC不能僅靠解封裝與轉封裝來實現,由於其本質上在解碼層就不支持。所以第三種方案就是:JS下載的視頻流首先經由解封裝(解密)處理,並在接下來進行解碼,解碼完成後渲染播放。若是咱們這裏轉成瀏覽器廣泛支持的解碼格式並讓video標籤進行播放,儘管理論上可行,但成本顯然是很是高的,而且中間存在一個無故的浪費。所以這裏一般直接採用瀏覽器端Canvas+WebAudio API實現視頻與音頻的渲染,而再也不使用瀏覽器原生video能力。這裏若是使用純瀏覽器原生的JS,因爲 JS天生單線程執行的弱勢,會致使整個處理的效率比較差。
近期,萬維網標準化委員會正式推出了WebAssembly規範。一方面咱們能夠藉助WebAssembly高於JS的能力,實現更加出色的大規模數據處理與解碼,另外一方面基於WebAssembly,咱們也能方便地將傳統媒體處理中基於C或C++開發的一些媒體處理能力集成在瀏覽器端執行,而且可經過JS來調用API。對於熟悉傳統Web前端開發的咱們來講,這也是一個值得咱們堅持探索與實踐的全新領域。有了WebAssembly以後,咱們就可讓部門內擅長視頻處理的專家級同事來配合實現更加出色的瀏覽器端視頻播放,相對以往的開發流程來講,不管是能力、成本控制仍是效率與靈活程度都有十分顯著的提高。
上圖展示了瀏覽器端WebAssembly的支持狀況,儘管個別低版本的瀏覽器有一些支持限制,但隨着標準化委員會對該標準的不斷推動,狀況會變得愈來愈好。在包括一些混合式場景,例如APP內嵌(好比聊天工具或通信工具當中打開一個連接)等狀況,是否支持也取決於WebView自己提供的能力以及WebAssembly的支持狀況,整體上來講趨於向好。
HEVC播放器的需求目標,就是基於 JavaScript 相關API,配合FFmpeg+WASM達成 HEVC 在瀏覽器端的解碼&解密、渲染播放的需求,接下來咱們就開始研究如何落地這一目標。
整體架構設計思路如上圖所示,首先咱們須要一個專門負責下載的下載器,該下載器也是基於瀏覽器的JS Fetch或XHR API,以實現文件獲取或直播拉流等操做。成功拉取的視頻流會被存儲在一個數據隊列當中,隨後基於WebAssembly(WASM)+FFmpeg的解碼器會來消費處理隊列裏這些流數據,解碼出音視頻數據,並放置在音視頻幀數據隊列當中,等待隨後的渲染器對其進行渲染處理。渲染器基於WebGL+Canvas與WebAudio調用硬件渲染出圖像與音頻。
最後則是控制層用於貫穿總體流程中下載、解碼、渲染等獨立模塊,同時實現底層一些基本功能:如以前咱們提到JS爲單線程,而瀏覽器提供的WebWork API可拉起一個子線程。該流程中每個模塊都是獨立的,隊列中的生產與消費過程也是異步進行的。(咱們可基於JS自己一些比較好的特性實現諸多便捷的功能。例如基於Promise能夠將異步過程進行較爲合理的封裝,並呈現一些異步處理邏輯流程的關鍵環節的控制到UI層。)
除此以外,還有控制層的一些基礎配置選項,包括播放器自己的一些事件或消息的管理,均可以基於控制層來實現。
下載器做爲一個基本模塊獨立存在,具備初始配置、啓動、暫停、中止、隊列管理與Seek響應(用於進度條拖拽)等基本功能。上圖左側圖標是在開發完成後,基於下載器的事件消息呈現的數據可視化結果。(柱狀圖表示單位時間下載量,這裏咱們能夠看到的是,下載量並不均勻,其中的變化可能取決於推流端、服務端、用戶端,也可能取決於整個網絡環境。)
下載器方面須要留意五個關鍵問題點:
線性的數據流的合併與拆分
咱們應當進行線性數據流的合併與拆分。理論上瀏覽器從服務端下載一個視頻流的過程是線性的,但瀏覽器的表現實際上並不是如此,兩者的差別可能會很大。
例如當一個瀏覽器啓動並基於JSFetch API抓取流,其過程也是經過API監聽數據回調來實現,每次回調可能間隔會很短、數據量也只是一個很小的一千字節左右的數據包。但有些瀏覽器的表現並不是如此,它們會等抓取到一個1M或2M的數據包以後才反饋給API回調。
而那些過於零碎的數據直接丟給隊列或以後的流程來處理,這樣勢必致使更頻繁的數據處理;數據包體積大的直接隊列和後續流程勢必增長單次處理成本。
所以對線性數據流的合理合併與拆分十分必要,整個過程也是結合初始配置來實現閾值控制。
經過閾值調節控制,咱們但願可以作好用戶端瀏覽器硬件資源消耗,與該業務場景下媒體播放產品服務體驗之間的取捨與平衡。
內部維護管理 range 狀態
除此以外,下載器實際上也須要內部維護管理range 狀態。例如當用戶選擇點播時,咱們須要明確是從哪個字節位置到另外一個字節位置下載傳輸中間這一片數據。而在直播過程當中,則可能出現由網絡環境形成卡頓或用戶端主動暫停的現象,此時下載器須要明確知道播放或當前下載的位置。
不一樣媒體類型數據獲取的差別
第三點是不一樣媒體類型數據獲取的差別,也就是下載器針對不一樣的媒體類型開發不一樣的下載功能。例如一個FLV直播流能夠理解爲是一個連續的線性的數據獲取,而點播則以包爲單位獲取。對於HLS流須要獲取m3u8列表,完成分析以後再從中選取數據包的地址並單獨下載,隨後進行流的合併或拆分。總地來講,咱們須要保證數據的最終產出儘可能均勻存儲到隊列中,以便於後續的一系列處理。
MOOV 前置或後置
在媒體處理中像MOOV等的索引數據有前置與後置兩種狀況,這裏須要注意的是,咱們的播放器基於Web端。
若索引文件爲後置,若是播放器直接下載了一部分數據就直接丟給FFmpeg解碼器進行解碼,因爲FFmpeg解碼器沒法獲取索引,固然也就沒法解碼成功。除非解碼器等待總體媒體源下載完畢,實際上這樣是不現實的。
另外因爲咱們沒法控制MOOV索引數據的體量,前置索引的大小沒法肯定,尤爲對於一些特殊狀況,這種邏輯會帶來不少問題。(可是這裏有一個取巧的辦法,就是咱們能夠嘗試首先抓取前面幾個數據包,探測MOOV邊界,並基於此獲得MOOV的長度,從而判斷取捨在什麼時機啓動後續的解碼。)
慎重並折中的控制內存消耗
最後,慎重並折中控制內存消耗也相當重要。例如儘管較大的緩存能帶來流暢的播放,但在Seek時就會帶來很大的浪費,咱們則須要根據服務所在的應用場景、幀率碼率等來實現合理的折中與取捨。
下載器以後,整個流程的核心能力就是解碼器。解碼器的基本功能與下載器相比大同小異,須要特別關注的是解碼器並非像下載器徹底是去調用一個原生的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採樣率。有了符合要求的獨立的音頻與視頻數據幀隊列,接下來也天然就能基於瀏覽器實現對音視頻的渲染與呈現。
渲染器的基本功能與下載器、解碼器類似,不一樣之處在於如下幾個關鍵點:
依賴解碼、UI提供畫布
渲染器須要瀏覽器提供一個獨立的畫布用於繪製相應的視覺畫面內容。在UI模塊初始化時呈現出一個畫布的容器,渲染器渲染生成的畫面才能表如今網頁上。
除此以外,渲染器依賴解碼器解碼生產出的音視頻幀數據才能進行音畫渲染。
主動向解碼器獲取幀數據
這一點與解碼器向下載器主動拿數據類似。
分緩存隊列、渲染隊列
渲染器會消費處理等待渲染的幀數據隊列,只不過幀數據會被分爲緩存隊列與渲染隊列。
而以前咱們介紹的下載器與解碼器,自己只有一組數據隊列。爲何要這樣呢?渲染器調用WebAudio API將音頻數據傳輸給瀏覽器進行PCM渲染時,沒法將已經經過該API傳輸給瀏覽器的數據作取回控制,所以就須要記錄當前已經給了多少數據到瀏覽器,這就是「渲染隊列」。而「緩存隊列」則是從進程中獲取一部分數據先存儲在一個臨時隊列當中,從而避免頻繁地向處於另外一個獨立WebWorker中的解碼器索取其音畫幀隊列數據,而帶來沒必要要的時間消耗。
音畫同步、倍速播放、Waiting
音畫同步、倍速播放以及斷定是否處於等待狀態相當重要。好比要追求直播的低延時,網絡抖動致使數據堆積發生的時候,倍速追幀是個有效的辦法。
動態碼率變化
一個視頻在播放的過程當中,可能隨網絡狀態的波動出現碼率的動態變化,例如爲適應較差的網絡情況,播放器能夠主動將媒體流獲取從一個較爲清晰的高分辨率變化到一個比較模糊的低分辨率源。
而再渲染中,基於WebGLCanavas的渲染器,咱們首先須要對YUV着色器進行初始化操做,而YUV着色器的初始化,依賴於其所繪製的數據對應的分辨率、比例與尺寸。若是最開始的分辨率、比例和尺寸與以後要渲染的數據不同,而咱們又未對此作相應的響應適配,那麼就會出現畫面繪製花屏的狀況。而動態碼率變化就是要隨時響應每一畫面幀所對應的分辨率變化,對YUV着色器做動態調整,從而保證畫面的實時性與穩定性。
從下載、解碼到渲染,視頻播放器的基本流程就此創建,播放器便有了獲取媒體數據、完成解碼、呈現音畫效果的基本能力。
基本的UI如上圖左側所示,上半部分是整個播放器在實例化以前咱們能夠去作的一系列初始化配置。圖中所示的僅是一小部分參數,例如媒體源的地址、是否啓用了加密Key、對應的解密算法,包括渲染時爲知足某些特定場景下的需求,音視頻是同時進行渲染仍是在主動控制下僅渲染音頻或視頻——例如在安防監控業務場景,會有一些設備須要音頻採集、另外一些不須要,或者乾脆播放時就不想播放源流音頻等等。若在這裏播放器不作斷定支持,則存在因爲音畫同步控制依賴音頻幀視頻幀時間戳比對,但沒有音頻幀數據的緣由致使沒法正常播放,而播放器使用者能進行主動控制則能夠避免該問題。
UI的基本功能包括實例化、用戶操做觸發後續流程涉及的各模塊接下來要作什麼,還有狀態信息響應展示,也就是根據用戶交互行爲和播放器工做狀態做出反饋與信息傳遞。
另外,UI也須要對相應的狀態變化做出響應,例如用戶控制當前播放器從正在播放切換到暫停,那麼UI層面則須要針對用戶操做進行相應的變化。還有快進、拖拽進度條等等。
最後的控制層相當重要,首先控制層隔離校驗對外暴露的參數及方法。播放器可實現或具有的特性有不少,不可能所有暴露給用戶。在播放視頻時,下載與解碼的數據實際上存在一個先後呼應的關係,若是咱們不考慮用戶行爲與需求,在網頁上呈現播放器的全部特性。而用戶也不對其進行科學性選擇與判斷,而是隨意調用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中是...
若對此感興趣能夠前往試用研究
調度控制層控制下載器、解碼器、渲染器與UI&交互四大模塊,若是要作某功能模塊的業務定製化開發、功能加強補充,對對應獨立的模塊內部進行優化並作出相應的功能擴展或者調整便可。
開發過程所遇到的難點整體能夠用以上三點來歸納:首先基於WebAssembly 工具鏈(emscripten.org),藉助EMSC編譯器咱們能夠直接將一個C和C++編譯成JS可用。這一過程自己存在諸多不便之處,主要是由於其自己對系統一些底層庫的依賴或對於開發環境的要求,致使可移植性並無那麼好,當須要跨機器協做時容易出現諸多問題。如今咱們內部的解決方案是本身找一臺專用機器來配置作爲編譯發佈使用。
第二點是隊列管理與狀態控制,只有精確實現隊列管理與狀態控制,咱們才能保證整個程序能合理穩定的執行。
第三點就是項目構建打包,咱們要解決前端一些構建打包的習慣以及其在邏輯需求上存在的一些衝突。
展望將來,我但願將來瀏覽器能對HEVC有更加出色的支持。本次分享雖然是一個播放器,但咱們知道FFmpeg的能力不僅是解碼播放,還能夠作更多實用工具的發掘實現。同時我也但願將來媒體類型百花齊放,甚至私有編解碼也可以造成Web端場景更規範靈活的解決方案。WASM成熟、標準化完善、各業務領域對應解決能力的細分,也是很值得期待的一件事情;而回到播放器自己,字幕、AI、互動交互等都是能進一步提高音視頻播放服務的可玩性與用戶體驗方向值得研究的方向。