春天的時候花椒作了一個創新項目, 這是一個直播綜藝節目的項目,前端的工做主要是作出一個PC主站點,在這個站點中的首頁須要一個播放器, 既能播放FLV直播視頻流,還要在用戶點擊視頻回顧按鈕的時候, 彈出窗口播放HLS視頻流;咱們開始開發這個播放器的時候也沒有多想, 直接使用了你們都能想到的 最簡單的套路,flv.js和hls.js一塊兒用!在播放視頻時,調用中間件video.js來輸出的Player來實現播放,這個Player根據視頻地址的結尾字符來初始化播放器:new HLS 或者 flvjs.createPlayer, 對外提供一致的接口,對HLS.js和FLV.js建立的播放器進行調用。完美的實現了產品的需求,不過寫代碼的時候總感受有點蠢,HLS.js(208KB)和FLV.js(169KB)體積加起來有點太讓人熱淚盈眶了。 這時咱們就有了一個想法,這兩能不能合起來成爲一個lib,既能播放flv視頻,又能播放hls視頻。理想很豐滿,現實很骨感,這2個lib雖然都是JavaScript寫的,可是它們的範疇都是視頻類,之前只是調用, 徹底沒有深刻了解過,不過咱們仍是在領導的大(wei)力(bi)支(li)持(you)下,開始了嘗試。javascript
FLV.js的工做原理是下載flv文件轉碼成IOS BMFF(MP4碎片)片斷, 而後經過Media Source Extensions
將MP4片斷傳輸給HTML5的Video標籤進行播放; 它的結構以下圖所示:前端
src/flv.js 是對外輸出FLV.js的一些組件, 事件和錯誤, 方便用戶根據拋出的事件進行各類操做和獲取相應的播放信息; 最主要是flv.js下返回的2個player: NativePlayer
和 FLVPlayer
; NativePlayer
是對瀏覽器自己播放器的一個再包裝, 使之能和FLVPlayer
同樣, 相應共同的事件和操做; 你們最主要使用的仍是FLVPlayer
這個播放器; 而 FLVPlayer
中最重要東西可分爲兩塊: 1. MSEController; 2. Transmuxer;java
這個MSEController負責給HTML Video Element 和 SourceBuffer之間創建鏈接, 接受 InitSegment(ISO BMFF 片斷中的 FTYP + MOOV)和 MediaSegment (ISO BMFF 片斷中的 MOOF + MDATA); 將這2個片斷按照順序添加到SourceBuffer中, 和對SouceBuffer的一些控制和狀態反饋;git
Transmuxer 主要負責的就是下載, 解碼, 轉碼, 發送Segment的工做; 它的下面主要包含了 2個模塊, TransmuxingWorker
和 TransmuxingController
; TransmuxingWorker
是啓用多線程執行 TransmuxingController
, 並對 TransmuxingController
拋出的事件就行轉發; TransmuxingController
纔是真正執行 下載, 解碼, 轉碼, 發送Segment的苦力部門, 苦活累活都是這個部門乾的, Transmuxer
(真上級) 和 TransmuxingController
(僞上級)都是在調用它的功能和傳遞它的輸出;程序員
下面有請這個勞苦功高的部門登場github
TransmuxingController
也是一個大部門, 他的手下有三個小組: IOController
, demuxer
和 remuxer
;瀏覽器
IOController主要有三個功能, 一是負責遴選他手下的小小弟(loaders), 選出最適合當前瀏覽器環境的loader, 去從服務器搬運媒體流; 二是存儲小小弟(loader)發上來的數據; 三是把數據發送給demuxer(解碼)並存儲demuxer未處理完的數據;服務器
demuxer 是負責解碼工做的員工, 他須要把IOController發送過來的FLV data, 解析整理成 videoTrack 和 audioTrack; 並把解析後的數據發送給 remuxer
轉碼器; 解碼完成後, 他會把已經處理的數據的長度返回給 IOController, IOController會把未處理的數據(總數據 - 已經處理的數據)存儲, 等待下次發送數據的時候發從頭部追加未處理的數據, 一塊兒發送給 demuxer.markdown
remuxer 是負責將 videoTrack 和 audioTrack 轉成 InitSegment 和 MediaSegment並向上發送, 並在轉化的過程當中進行音視頻同步的操做.網絡
總的流程就是 FLVPlayer喊了一聲啓動以後, loader 加載數據 => IOController 存儲和轉發數據 => demuxer 解碼數據 => remuxer 轉碼數據 => TransmuxingWorker 和 Transmuxer 轉發數據 =>
MSEController 接受數據 => SourceBuffer; 一系列操做以後視頻就能夠播放了;
HLS.js的工做原理是先下載index.m3u8文件, 而後解析該文檔, 取出Level, 再根據Levels中的片斷(Fragments)信息去下載相應的TS文件, 轉碼成IOS BMFF(MP4碎片)片斷, 而後經過Media Source Extensions
將MP4片斷傳輸給HTML5的Video標籤進行播放;
HLS.js
的結構以下
相對於 flv.js的多層分級, hls.js到是有一點扁平化的味道, hls這個公司老總在繼承 Observer 的trigger功能以後, 深刻各個部門(即各類controller和loader)發號施令(進行hls.trigger(HlsEvents.xxx, data)
的操做); 而各個部門繼承EventHandler以後, 實例化時就分配好本身所負責的工做; 以 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
這個部門主要負責如下功能:
buffer-controller.js
初始化時就定義了本身只響應 Events.MEDIA_ATTACHING
, Events.MEDIA_DETACHING
等等這些工做, 它會本身實現 onMediaAttaching
, onMediaDetaching
等方法來響應和完成這些工做, 其餘的一律無論, 它完成本身的任務後會經過hls向其餘部門告知已經完成了本身的工做, 並將工做結果移交給其餘部門, 例如 buffer-controller.js
中的 581行 this.hls.trigger(Events.BUFFER_FLUSHED)
, 這行代碼就是向其餘部門(其餘controllers)告知已經完成BUFFER_FLUSHED
的工做;
注: 你們在讀取hls.js的源碼的時候, 看到 `this.hls.trigger(Events.xxxx)`時, 查找下一步驟時, 只要在所有代碼中搜索 onXXX(去掉事件中的下劃線) 方法便可找到下一步操做
複製代碼
明白了HLS.JS代碼的讀取套路以後咱們能夠更清晰的瞭解hls.js實現播放HLS流的大體過程了;
FLVPlayer
, 直接提供API, 響應外界的各類操做和發送信息; 在開始準備播放的時候它會發令HlsEvents.MANIFEST_LOADING
,LEVEL_LOADED
的事件並攜帶level信息;FRAG_LOADING
事件, 並初始化 解碼器和轉碼器 (Demuxer對象, Remuxer會在Demuxer實例化中初始化)FRAG_LOADING
以後會去加載相應的TS文件, 並在加載TS文件完畢以後發出 FRAG_LOADED
事件, 並把TS的Uint8數據和fragment的其餘信息一併發送出;stream-controller
接收 FRAG_LOADED
事件後, 他會調用它的 onFragLoaded
方法, 在這個方法中 demuxer 會解析 TS 的文件, 通過demuxer和remuxer的通力協做, 生成InitSegment(FRAG_PARSING_INIT_SEGMENT事件 所攜帶的數據) 和 MediaSegment(FRAG_PARSING_DATA事件 所攜帶的數據), 經由 steam-controller 傳輸給 buffer-controller, 最後添加進SourceBuffer;經過對FLV.js和HLS.js 進行分析, 它們共同的流程都是 下載, 解碼, 轉碼, 傳輸給SourceBuffer; 同樣的loader(FragmentLoader和FetchStreamLoader), 同樣的解碼和轉碼(demuxer和remuxer), 同樣的 SourceBuffer Controller (MSEController 和 Buffer-controller ); 不一樣的就是他們的控制流程不同, 還有hls流多了一步解析文檔的步驟;
下面咱們就思考怎麼去結合兩個lib:
根據項目目的: 項目是一個主直播, 次點播的站點; FLV直播功能是最重要的功能, HLS流的回放只在用戶點擊視頻回顧和查看過去節目視頻纔會使用;
根據其餘項目的需求: 花椒的主站如今也是HTTP-FLV的形式去進行直播展現, 而HLS流計劃用於播放主播小視頻(點播);
根據業界狀況: 如今業界直播基本仍是用的HTTP-FLV這種形式(基礎設施成熟, 技術簡單, 延遲小), 而HLS流通常仍是用在移動端直播;
因此咱們決定採用在 FLV.js 的基礎上, 加上HLS.js中的 loader, demuxer 和 remuxer 這三部分去組成一個新的播放器library, 既能播放FLV視頻, 也能播放HLS流(根據項目的須要只包含單碼率流的直播和點播, 不包含多碼率流, 自動切換碼率, 解密等功能);
首先咱們先規劃了一下內嵌的功能怎麼接入:
HLS.js中加載HLS流須要 FragmentLoader, XHRLoader, M3U8Parser, LevelController, StreamController 這些, 其中 FragmentLoader 是控制XHR加載TS文件和反饋Fragment加載狀態的組件, XHRLoader是執行加載 TS 文件和 playlist 文件 的組件, LevelController 是 選擇符合當前碼率的level 和 playlist加載間隔的, streamController是負責判斷加載當前Level中哪一個TS文件的組件; 在接入FLV.js時, 須要 FragmentLoader 本身去承擔 LevelController 和 StreamController 中相應的工做, 當 IOController 調用 startLoad 方法時, 它本身要去獲取並解析playlist, 存儲 Level的詳細信息, 選擇Level, 經過判斷 Fragment 的 sequenceNum 來獲取下一個TS文件地址, 讓XHRLoader 去加載; (FragmentLoader 這娃來到了新公司, 身上擔子變重了).
由於FLV和TS文件的解析方式不一樣, 可是在TransmuxingController中, 兩個都要接入IOController這個統一數據源, 因此把FLV的解碼和轉碼放入到一個FLVCodec的對象中對外輸出功能, TS的解碼和轉碼則集中放入TSCodec中對外輸出功能; 根據傳進來媒體類型實例化解碼器和轉碼器.
在 TransmuxingController 中則用 一個 _mediaCodec 對象來管理FLVCodec和TSCodec, 接入數據源IOController時調用二者都擁有的bindDataSource方法; 這裏有一點須要注意的是 FLVCodec功能會返回一個 number 類型 consumed; 此參數表示FLVCodec功能已解碼和轉碼的輸出長度, 須要返回給 IOController, 讓 IOController 刨除已解碼的數據, 存儲未解碼的數據, 等下次一塊兒再傳給 FLVCodec 功能, 而TSCodec由於TS的文件結構特色(每一個TS包都是188字節的整數倍), 因此每次都是所有處理, 只須要返回 consumed = 0 便可;
在FLV.js中, 每當SEEK操做時都會MediaInfo中的KeyFrame信息, 去查找相應的Range點, 而後從Range點去加載; 對於hls點播流, 須要對FragmentLoader中的Level信息進行查詢, 對每一個Fragment進行循環判斷 seek的時間點是否處於當前 Fragment 的播放時間, 若是是, 就當即加載便可;
在嵌入的組件中加入logger打印日誌, 並將錯誤返回接入到FLV.JS框架中, 使之能返回響應的錯誤信息和日誌信息;
具體結構以下圖:
除此以外, 咱們還作了如下幾點:
HJPlayer.Events.GET_SEI_INFO
事件能夠獲得自定義SEI信息, 格式爲Uint8Array;在項目中, 主持人會在節目播放過程當中提供事件發展方向的選項, 而後前端會彈出面板, 讓用戶選擇方向, 節目根據答案的方向進行直播表演; 按照以往的方案, 通常這種狀況都是選擇由 Socket 服務器下發消息, 前端接到消息後展現選項, 而後用戶選擇, 點擊提交答案這麼一個流程; 去年阿里雲推出了一項新穎的直播答題解決方案; 選項再也不由Socket服務器下發, 而是由視頻雲服務器隨視頻下發; 播放SDK解析視頻中的視頻補充加強信息, 展現選項; 咱們對此方案進行了實踐, 大概流程以下:
當主持人提出問題後, 後臺人員會在後臺填寫問題, 經視頻雲SDK傳輸給360視頻雲, 視頻雲對視頻進行處理, 加入視頻補充加強信息, 當播放SDK收到帶有SEI信息的視頻後, 通過解碼去重, 將其中包含的信息傳遞給綜藝直播間的互動組件, 互動組件展現, 用戶點擊選擇答案後提交給後臺進行彙總, 節目根據彙總後的答案進行節目內容的變動;
與傳統方案相比, 採用視頻SEI信息傳遞互動的方案有如下幾項優勢:
視頻補充加強信息的內容通常由雲服務器來指定內容, 除前16位UUID以外, 內容不盡相同, 因此本播放器直接將SEI信息(Uint8Array格式數據)經GET_SEI_INFO
事件拋出, 用戶需自行按照己方視頻雲給定的格式去解析信息; 另外注意SEI信息是一段時間內重複發送的, 因此用戶須要自行去重.
咱們完成了此項目後, 將它應用到花椒的主站播放FLV直播, 除此以外咱們還將項目開源HJPlayer, 但願能幫助那些遇見一樣項目需求的程序員; 若是使用中有問題, 能夠在ISSUES中提出, 讓咱們共同討論解決.
答: 點擊視頻回顧的時候, 須要播放過去5分鐘播過的內容, 若是採用 FLV 文件的話, 那麼每次就要從存儲的視頻中截取一段視頻生成 FLV 文件, 而後前端拉取文件播放, 這樣會增長一大堆的視頻碎片文件, 隨之會帶來一系列的存儲問題; 若是採用HLS流的話, 能夠根據前端傳回的時間戳, 在存儲的HLS回顧文件中查找相應的TS文件, 並生成一份m3u8文檔就能夠了;
視頻補充加強信息是H.264視頻壓縮標準的特性之一, 提供了向視頻碼流中加入信息的辦法; 它並非解碼過程當中的必須存在的, 有可能對解碼有幫助, 可是沒有也沒有關係; 在視頻內容的生成端、傳輸過程當中,均可以插入SEI 信息。插入的信息,和其餘視頻內容一塊兒通過網絡傳輸到播放SDK; 在H264/AVC編碼格式中NAL uint 中的頭部, 有type字段指明 NAL uint的類型, 當 type = 6 時 該NAL uint 攜帶的信息即爲 補充加強信息(SEI);
NAL uint type 後下一位即爲 SEI 的type, 通常自定義的SEI信息的type 爲 5, 即 user_data_unregistered; SEI type 的下一位直到0xFF爲止即爲所攜帶的數據的長度, 而後就是16位的UUID, 在16位的UUID以後一直到0x00的結束符之間, 即爲自定義信息內容, 因此信息內容長度 = SEI信息所攜帶的數據的長度 - 16位UUID; 自定義信息內容的解析方式就要根據己方視頻雲給定的數據格式定義了;