花椒web端实时互动流媒体播放器
項目背景
春天的時候花椒做了一個創(chuàng)新項目, 這是一個直播綜藝節(jié)目的項目,前端的工作主要是做出一個PC主站點,在這個站點中的首頁需要一個播放器,既能播放FLV直播視頻流,還要在用戶點擊視頻回顧按鈕的時候, 彈出窗口播放HLS視頻流;我們開始開發(fā)這個播放器的時候也沒有多想, 直接使用了大家都能想到的
最簡單的套路,flv.js和hls.js一起用!在播放視頻時,調(diào)用中間件video.js來輸出的Player來實現(xiàn)播放,這個Player根據(jù)視頻地址的結(jié)尾字符來初始化播放器:new HLS 或者 flvjs.createPlayer,對外提供一致的接口,對HLS.js和FLV.js創(chuàng)建的播放器進行調(diào)用。完美的實現(xiàn)了產(chǎn)品的需求,不過寫代碼的時候總感覺有點蠢,HLS.js(208KB)和FLV.js(169KB)體積加起來有點太讓人熱淚盈眶了。
這時我們就有了一個想法,這兩能不能合起來成為一個lib,既能播放flv視頻,又能播放hls視頻。理想很豐滿,現(xiàn)實很骨感,這2個lib雖然都是JavaScript寫的,但是它們的范疇都是視頻類,以前只是調(diào)用,完全沒有深入了解過,不過我們還是在領(lǐng)導(dǎo)的大(wei)力(bi)支(li)持(you)下,開始了嘗試。
FLV.JS分析
FLV.js的工作原理是下載flv文件轉(zhuǎn)碼成IOS BMFF(MP4碎片)片段, 然后通過Media Source Extensions將MP4片段傳輸給HTML5的Video標(biāo)簽進行播放;
它的結(jié)構(gòu)如下圖所示:
src/flv.js 是對外輸出FLV.js的一些組件, 事件和錯誤, 方便用戶根據(jù)拋出的事件進行各種操作和獲取相應(yīng)的播放信息; 最主要是flv.js下返回的2個player:?NativePlayer?和?FLVPlayer;
NativePlayer?是對瀏覽器本身播放器的一個再包裝, 使之能和FLVPlayer一樣, 相應(yīng)共同的事件和操作; 大家最主要使用的還是FLVPlayer這個播放器;
而?FLVPlayer中最重要東西可分為兩塊: 1. MSEController; 2. Transmuxer;?
MSEController這個MSEController負(fù)責(zé)給HTML Video Element 和 SourceBuffer之間建立連接, 接受 InitSegment(ISO BMFF 片段中的 FTYP + MOOV)和 MediaSegment (ISO BMFF 片段中的 MOOF + MDATA); 將這2個片段按照順序添加到SourceBuffer中, 和對SouceBuffer的一些控制和狀態(tài)反饋;
Transmuxer
Transmuxer 主要負(fù)責(zé)的就是下載, 解碼, 轉(zhuǎn)碼, 發(fā)送Segment的工作; 它的下面主要包含了 2個模塊,?TransmuxingWorker?和?TransmuxingController;?
TransmuxingWorker是啟用多線程執(zhí)行?TransmuxingController, 并對?TransmuxingController拋出的事件就行轉(zhuǎn)發(fā);
TransmuxingController?才是真正執(zhí)行 下載, 解碼, 轉(zhuǎn)碼, 發(fā)送Segment的苦力部門, 苦活累活都是這個部門干的,?Transmuxer(真上級) 和?TransmuxingController(偽上級)都是在調(diào)用它的功能和傳遞它的輸出;
下面有請這個勞苦功高的部門登場
TransmuxingController
TransmuxingController也是一個大部門, 他的手下有三個小組:?IOController,?demuxer和?remuxer;
IOController?
IOController主要有三個功能, 一是負(fù)責(zé)遴選他手下的小小弟(loaders), 選出最適合當(dāng)前瀏覽器環(huán)境的loader, 去從服務(wù)器搬運媒體流; 二是存儲小小弟(loader)發(fā)上來的數(shù)據(jù); 三是把數(shù)據(jù)發(fā)送給demuxer(解碼)并存儲demuxer未處理完的數(shù)據(jù);?
demuxer?
demuxer 是負(fù)責(zé)解碼工作的員工, 他需要把IOController發(fā)送過來的FLV data, 解析整理成 videoTrack 和 audioTrack; 并把解析后的數(shù)據(jù)發(fā)送給?remuxer?轉(zhuǎn)碼器; 解碼完成后, 他會把已經(jīng)處理的數(shù)據(jù)的長度返回給 IOController, IOController會把未處理的數(shù)據(jù)(總數(shù)據(jù) - 已經(jīng)處理的數(shù)據(jù))存儲, 等待下次發(fā)送數(shù)據(jù)的時候發(fā)從頭部追加未處理的數(shù)據(jù), 一起發(fā)送給 demuxer.
remuxer
remuxer 是負(fù)責(zé)將 videoTrack 和 audioTrack 轉(zhuǎn)成 InitSegment 和 MediaSegment并向上發(fā)送, 并在轉(zhuǎn)化的過程中進行音視頻同步的操作.?
總的流程就是 FLVPlayer喊了一聲啟動之后, loader 加載數(shù)據(jù) => IOController 存儲和轉(zhuǎn)發(fā)數(shù)據(jù) => demuxer 解碼數(shù)據(jù) => remuxer 轉(zhuǎn)碼數(shù)據(jù) => TransmuxingWorker 和 Transmuxer 轉(zhuǎn)發(fā)數(shù)據(jù) =>MSEController 接受數(shù)據(jù) => SourceBuffer; 一系列操作之后視頻就可以播放了;
HLS.JS分析
HLS.js的工作原理是先下載index.m3u8文件, 然后解析該文檔, 取出Level, 再根據(jù)Levels中的片段(Fragments)信息去下載相應(yīng)的TS文件, 轉(zhuǎn)碼成IOS BMFF(MP4碎片)片段, 然后通過Media Source Extensions將MP4片段傳輸給HTML5的Video標(biāo)簽進行播放;
HLS.js的結(jié)構(gòu)如下:
相對于 flv.js的多層分級, hls.js到是有一點扁平化的味道, hls這個公司老總在繼承 Observer 的trigger功能之后, 深入各個部門(即各種controller和loader)發(fā)號施令(進行hls.trigger(HlsEvents.xxx, data)的操作); 而各個部門繼承EventHandler之后, 實例化時就分配好自己所負(fù)責(zé)的工作; 以?buffer-controller.js?為例:
constructor (hls: any) { super(hls, Events.MEDIA_ATTACHING, Events.MEDIA_DETACHING, Events.MANIFEST_PARSED, Events.BUFFER_RESET, Events.BUFFER_APPENDING, Events.BUFFER_CODECS, Events.BUFFER_EOS, Events.BUFFER_FLUSHING, Events.LEVEL_PTS_UPDATED, Events.LEVEL_UPDATED); this.config = hls.config; }buffer-controller.js?這個部門主要負(fù)責(zé)以下功能:?
響應(yīng)BUFFER_RESET事件, 重置媒體緩沖區(qū)
響應(yīng)BUFFER_CODECS事件, 接收時使用適當(dāng)?shù)木幗獯a器信息初始化SourceBuffer
響應(yīng)BUFFER_APPENDING事件, 給SourceBuffer中添加MP4 片段
成功添加緩沖區(qū)后觸發(fā)BUFFER_APPENDED事件
響應(yīng)BUFFER_FLUSHING事件, 刷新指定的緩沖區(qū)范圍
成功刷新緩沖區(qū)后觸發(fā)BUFFER_FLUSHED事件
buffer-controller.js?初始化時就定義了自己只響應(yīng)?Events.MEDIA_ATTACHING,?Events.MEDIA_DETACHING?等等這些工作, 它會自己實現(xiàn)?onMediaAttaching,??onMediaDetaching等方法來響應(yīng)和完成這些工作, 其他的一概不管, 它完成自己的任務(wù)后會通過hls向其他部門告知已經(jīng)完成了自己的工作, 并將工作結(jié)果移交給其他部門, 例如?buffer-controller.js?中的 581行?this.hls.trigger(Events.BUFFER_FLUSHED), 這行代碼就是向其他部門(其他controllers)告知已經(jīng)完成BUFFER_FLUSHED的工作;
注: 大家在讀取hls.js的源碼的時候, 看到 `this.hls.trigger(Events.xxxx)`時, 查找下一步驟時, 只要在全部代碼中搜索 onXXX(去掉事件中的下劃線) 方法即可找到下一步操作明白了HLS.JS代碼的讀取套路之后我們可以更清晰的了解hls.js實現(xiàn)播放HLS流的大致過程了;?
hls.js只播放HLS流, 沒有NativePlayer, 所以頂級src/hls.js 對應(yīng)著 flv.js中的?FLVPlayer, 直接提供API, 響應(yīng)外界的各種操作和發(fā)送信息; 在開始準(zhǔn)備播放的時候它會發(fā)令HlsEvents.MANIFEST_LOADING,
playlist-loader 收到 HlsEvents.MANIFEST_LOADING 后, 它會使用XHRLoader去加載 M3U8文檔, 文檔經(jīng)過解析之后會得到該文檔含有的level(對于直播行業(yè)來說一般就是一個level, level[0] 就是我們想要的數(shù)據(jù)); playlist-loader 會發(fā)出?LEVEL_LOADED?的事件并攜帶level信息;
level-controller會記錄level信息, 并計算更新m3u8的時間間隔, 不斷加載m3u8文件更新level; 而 stream-controller 則會經(jīng)過一系列的操作之后去加載 fragment(即m3u8文檔中的ts文件); 發(fā)出?FRAG_LOADING事件, 并初始化 解碼器和轉(zhuǎn)碼器 (Demuxer對象, Remuxer會在Demuxer實例化中初始化)
FragmentLoader 收到??FRAG_LOADING?之后會去加載相應(yīng)的TS文件, 并在加載TS文件完畢之后發(fā)出?FRAG_LOADED?事件, 并把TS的Uint8數(shù)據(jù)和fragment的其他信息一并發(fā)送出;
在?stream-controller?接收?FRAG_LOADED事件后, 他會調(diào)用它的?onFragLoaded?方法, 在這個方法中 demuxer 會解析 TS 的文件, 經(jīng)過demuxer和remuxer的通力協(xié)作, 生成InitSegment(FRAGPARSINGINITSEGMENT事件 所攜帶的數(shù)據(jù)) 和 MediaSegment(FRAGPARSING_DATA事件 所攜帶的數(shù)據(jù)), 經(jīng)由 steam-controller 傳輸給 buffer-controller, 最后添加進SourceBuffer;
怎么結(jié)合
通過對FLV.js和HLS.js 進行分析, 它們共同的流程都是 下載, 解碼, 轉(zhuǎn)碼, 傳輸給SourceBuffer; 一樣的loader(FragmentLoader和FetchStreamLoader), 一樣的解碼和轉(zhuǎn)碼(demuxer和remuxer), 一樣的 SourceBuffer Controller (MSEController 和 Buffer-controller ); 不同的就是他們的控制流程不一樣, 還有hls流多了一步解析文檔的步驟;?
下面我們就思考怎么去結(jié)合兩個lib:
根據(jù)項目目的: 項目是一個主直播, 次點播的站點; FLV直播功能是最重要的功能, HLS流的回放只在用戶點擊視頻回顧和查看過去節(jié)目視頻才會使用;
根據(jù)其他項目的需求:?花椒PC端主站(https://www.huajiao.com/)現(xiàn)在也是HTTP-FLV的形式去進行直播展示, 而HLS流計劃用于播放主播小視頻(點播);
根據(jù)業(yè)界情況: 現(xiàn)在業(yè)界直播基本還是用的HTTP-FLV這種形式(基礎(chǔ)設(shè)施成熟, 技術(shù)簡單, 延遲小), 而HLS流一般還是用在移動端直播;?
所以我們決定采用在 FLV.js 的基礎(chǔ)上, 加上HLS.js中的 loader, demuxer 和 remuxer 這三部分去組成一個新的播放器library, 既能播放FLV視頻, 也能播放HLS流(根據(jù)項目的需要只包含單碼率流的直播和點播, 不包含多碼率流, 自動切換碼率, 解密等功能);
具體實施過程
首先我們先規(guī)劃了一下內(nèi)嵌的功能怎么接入:
Loader的接入
HLS.js中加載HLS流需要 FragmentLoader, XHRLoader, M3U8Parser, LevelController, StreamController 這些, 其中 FragmentLoader 是控制XHR加載TS文件和反饋Fragment加載狀態(tài)的組件,?
XHRLoader是執(zhí)行加載 TS 文件和 playlist 文件 的組件, LevelController 是 選擇符合當(dāng)前碼率的level 和 playlist加載間隔的, streamController是負(fù)責(zé)判斷加載當(dāng)前Level中哪個TS文件的組件;
在接入FLV.js時, 需要 FragmentLoader 自己去承擔(dān) LevelController 和 StreamController 中相應(yīng)的工作, 當(dāng) IOController 調(diào)用 startLoad 方法時, 它自己要去獲取并解析playlist, 存儲 Level的詳細(xì)信息, 選擇Level, 通過判斷 Fragment 的 sequenceNum 來獲取下一個TS文件地址, 讓XHRLoader 去加載; (FragmentLoader 這娃來到了新公司, 身上擔(dān)子變重了).
demuxer和remuxer的接入
因為FLV和TS文件的解析方式不同, 但是在TransmuxingController中, 兩個都要接入IOController這個統(tǒng)一數(shù)據(jù)源, 所以把FLV的解碼和轉(zhuǎn)碼放入到一個FLVCodec的對象中對外輸出功能, TS的解碼和轉(zhuǎn)碼則集中放入TSCodec中對外輸出功能; 根據(jù)傳進來媒體類型實例化解碼器和轉(zhuǎn)碼器.
IOController和 _mediaCodec 的接入
在 TransmuxingController 中則用 一個 _mediaCodec 對象來管理FLVCodec和TSCodec, 接入數(shù)據(jù)源IOController時調(diào)用兩者都擁有的bindDataSource方法; 這里有一點需要注意的是FLVCodec功能會返回一個 number 類型 consumed; 此參數(shù)表示FLVCodec功能已解碼和轉(zhuǎn)碼的輸出長度, 需要返回給 IOController, 讓 IOController 刨除已解碼的數(shù)據(jù), 存儲未解碼的數(shù)據(jù), 等下次一起再傳給 FLVCodec 功能, 而TSCodec因為TS的文件結(jié)構(gòu)特點(每個TS包都是188字節(jié)的整數(shù)倍), 所以每次都是全部處理, 只需要返回 consumed = 0 即可;
hls流的點播seek功能的接入
在FLV.js中, 每當(dāng)SEEK操作時都會MediaInfo中的KeyFrame信息, 去查找相應(yīng)的Range點, 然后從Range點去加載; 對于hls點播流, 需要對FragmentLoader中的Level信息進行查詢, 對每個Fragment進行循環(huán)判斷 seek的時間點是否處于當(dāng)前 Fragment 的播放時間, 如果是, 就立即加載即可;?
對各種意外情況的處理
在嵌入的組件中加入logger打印日志, 并將錯誤返回接入到FLV.JS框架中, 使之能返回響應(yīng)的錯誤信息和日志信息;
具體結(jié)構(gòu)如下圖:?
除此之外, 我們還做了以下幾點:
我們在進行改造的時候還接入了Typescript , 實現(xiàn)對功能參數(shù)的類型檢查;
在FLV-MP4Remuxer中集成了?jamken?(感謝? jamken)
(https://github.com/jamken)
對 FLV.js 推送的?354PR
(https://github.com/bilibili/flv.js/pull/354), 修正FLV.JS中音視頻不同步的問題;
還加入了視頻補充增強信息(Supplemental Enhancement Information)的解析, 通過監(jiān)聽HJPlayer.Events.GET_SEI_INFO事件可以得到自定義SEI信息, 格式為Uint8Array;?
對視頻直播實時互動的嘗試
在項目中, 主持人會在節(jié)目播放過程中提供事件發(fā)展方向的選項, 然后前端會彈出面板, 讓用戶選擇方向, 節(jié)目根據(jù)答案的方向進行直播表演; 按照以往的方案, 一般這種情況都是選擇由 Socket 服務(wù)器下發(fā)消息, 前端接到消息后展示選項, 然后用戶選擇, 點擊提交答案這么一個流程; 去年阿里云推出了一項新穎的直播答題解決方案;?
選項不再由Socket服務(wù)器下發(fā), 而是由視頻云服務(wù)器隨視頻下發(fā); 播放SDK解析視頻中的視頻補充增強信息, 展示選項; 我們對此方案進行了實踐, 大概流程如下:
當(dāng)主持人提出問題后, 后臺人員會在后臺填寫問題, 經(jīng)視頻云SDK傳輸給360視頻云, 視頻云對視頻進行處理, 加入視頻補充增強信息, 當(dāng)播放SDK收到帶有SEI信息的視頻后, 經(jīng)過解碼去重, 將其中包含的信息傳遞給綜藝直播間的互動組件, 互動組件展示, 用戶點擊選擇答案后提交給后臺進行匯總, 節(jié)目根據(jù)匯總后的答案進行節(jié)目內(nèi)容的變更;
與傳統(tǒng)方案相比, 采用視頻SEI信息傳遞互動的方案有以下幾項優(yōu)點:
可以實現(xiàn)與主持人的音視頻同步出現(xiàn), 避免因服務(wù)器群發(fā)消息不及時導(dǎo)致主持人已經(jīng)宣布開始, 但是面板遲遲不出現(xiàn)的問題.
成本低, 問題是由視頻下發(fā)而不是由服務(wù)器下發(fā), 但延遲會高一點(可提前在視頻中插入, 主持人后提出問題, 減少延遲);
視頻補充增強信息的內(nèi)容一般由云服務(wù)器來指定內(nèi)容, 除前16位UUID之外, 內(nèi)容不盡相同, 所以本播放器直接將SEI信息(Uint8Array格式數(shù)據(jù))經(jīng)GET_SEI_INFO事件拋出, 用戶需自行按照己方視頻云給定的格式去解析信息; 另外注意SEI信息是一段時間內(nèi)重復(fù)發(fā)送的, 所以用戶需要自行去重.
最后
我們完成了此項目后, 將它應(yīng)用到花椒PC端主站(https://www.huajiao.com/)播放FLV直播, 除此之外我們還將項目開源HJPlayer(https://github.com/huajiaofrontend/HJPlayer), 希望能幫助那些碰見同樣項目需求的程序員; 如果使用中有問題, 可以在ISSUES中提出, 讓我們共同討論解決。
題外
有人可能會問 為什么你們的視頻回顧不采用FLV文件, 這樣就只使用FLV.JS不就可以播放了嗎?
答:點擊視頻回顧的時候, 需要播放過去5分鐘播過的內(nèi)容, 如果采用 FLV 文件的話, 那么每次就要從存儲的視頻中截取一段視頻生成 FLV 文件, 然后前端拉取文件播放, 這樣會增加一大堆的視頻碎片文件, 隨之會帶來一系列的存儲問題; 如果采用HLS流的話, 可以根據(jù)前端傳回的時間戳, 在存儲的HLS回顧文件中查找相應(yīng)的TS文件, 并生成一份m3u8文檔就可以了;?
視頻補充增強信息(Supplemental Enhancement Information) 是什么?
答:視頻補充增強信息是H.264視頻壓縮標(biāo)準(zhǔn)的特性之一, 提供了向視頻碼流中加入信息的辦法; 它并不是解碼過程中的必須存在的, 有可能對解碼有幫助, 但是沒有也沒有關(guān)系;?
在視頻內(nèi)容的生成端、傳輸過程中,都可以插入SEI 信息。插入的信息,和其他視頻內(nèi)容一起經(jīng)過網(wǎng)絡(luò)傳輸?shù)讲シ臩DK;?
在H264/AVC編碼格式中NAL uint 中的頭部, 有type字段指明 NAL uint的類型, 當(dāng) type = 6 時 該NAL uint 攜帶的信息即為 補充增強信息(SEI);
關(guān)于 SEI信息的解析:
NAL uint type 后下一位即為 SEI 的type, 一般自定義的SEI信息的type 為 5, 即 userdataunregistered; SEI type 的下一位直到0xFF為止即為所攜帶的數(shù)據(jù)的長度, 然后就是16位的UUID,
在16位的UUID之后一直到0x00的結(jié)束符之間, 即為自定義信息內(nèi)容, 所以信息內(nèi)容長度 = SEI信息所攜帶的數(shù)據(jù)的長度 - 16位UUID; 自定義信息內(nèi)容的解析方式就要根據(jù)己方視頻云給定的數(shù)據(jù)格式定義了;
總結(jié)
以上是生活随笔為你收集整理的花椒web端实时互动流媒体播放器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 音视频技术开发周刊(第123期)
- 下一篇: 用Elevator优化AV1视频播放