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