用一個 flv.js 播放監控的例子,帶你深撅直播流技術

作者:楊成功

簡介:專注前端工程與架構產出

本文記錄一下在使用 flv。js 播放監控影片時踩過的各種各樣的坑。雖然官網給的

Getting Started

只有短短几行程式碼,跑一個能播影片的 demo 很容易,但是播放時各種各樣的異常會搞到你懷疑人生。

究其原因,一方面 GitHub 上文件比較晦澀,說明也比較簡陋;另一方面是受“影片播放”思維的影響,沒有對流的足夠認識以及缺乏處理流的經驗。

下面我將自己踩過的坑,以及踩坑過程中補充的相關知識,詳細總結一下。

大綱預覽

本文介紹的內容包括以下方面:

直播與點播

靜態資料與流資料

為什麼選 flv?

協議與基礎實現

細節處理要點

樣式定製

點播與直播

啥是直播?啥是點播?

直播就不用說了,抖音普及之下大家都知道直播是幹嘛的。點播其實就是影片播放,和咱們嗶哩嗶哩看影片一摸一樣沒區別,就是把提前做好的影片放出來,就叫點播。

點播對於我們前端來說,就是拿一個 mp4 的連結地址,放到 video 標籤裡面,瀏覽器會幫我們處理好影片解析播放等一些列事情,我們可以拖動進度條選擇想看的任意一個時間。

但是直播不一樣,直播有兩個特點:

獲取的是流資料

要求實時性

先看一下什麼叫流資料。大部分沒有做過音影片的前端同學,我們常接觸的資料就是 ajax 從介面獲取的 json 資料,特別一點的可能是檔案上傳。這些資料的特點是,它們都屬於一次性就能拿到的資料。我們一個請求,一個響應,完整的資料就拿回來了。

但是流不一樣,流資料獲取是一幀一幀的,你可以理解為是一小塊一小塊的。像直播流的資料,它並不是一個完整的影片片段,它就是很小的二進位制資料,需要你一點一點的拼接起來,才有可能輸出一段影片。

再看它的實時性。如果是點播的話,我們直接將完整的影片儲存在伺服器上,然後返回連結,前端用 video 或播放器播就行了。但是直播的實時性,就決定了資料來源不可能在伺服器上,而是在某一個客戶端。

資料來源在客戶端,那麼又是怎麼到達其他客戶端的呢?

這個問題,請看下面這張流程圖:

用一個 flv.js 播放監控的例子,帶你深撅直播流技術

如圖所示,發起直播的客戶端,向上連著流媒體伺服器,直播產生的影片流會被實時推送到服務端,這個過程叫做推流。其他客戶端同樣也連線著這個流媒體伺服器,不同的是它們是播放端,會實時拉取直播客戶端的影片流,這個過程叫做拉流。

推流—> 伺服器-> 拉流

,這是目前流行的也是標準的直播解決方案。看到了吧,直播的整個流程全都是流資料傳輸,資料處理直面二進位制,要比點播複雜了幾個量級。

具體到我們業務當中的攝像頭實時監控預覽,其實和上面的完全一致,只不過發起直播的客戶端是攝像頭,觀看直播的客戶端是瀏覽器而已。

靜態資料與流資料

我們常接觸的文字,json,圖片等等,都屬於靜態資料,前端用 ajax 向介面請求回來的資料就是靜態資料。

像上面說到的,直播產生的影片和音訊,都屬於流資料。流資料是一幀一幀的,它的本質是二進位制資料,因為很小,資料像水流一樣連綿不斷的流動,因此非常適合實時傳輸。

靜態資料,在前端程式碼中有對應的資料型別,比如 string,json,array 等等。那麼流資料(二進位制資料)的資料型別是什麼?在前端如何儲存?又如何操作?

首先明確一點,前端是可以儲存和操作二進位制的。最基本的二進位制物件

是 ArrayBuffer,它表示一個固定長度,如:

ArrayBuffer 只是用於儲存二進位制資料,如果要操作,則需要使用

檢視物件

檢視物件,不儲存任何資料,作用是將 ArrayBuffer 的資料做了結構化的處理,便於我們操作這些資料,說白了它們是操作二進位制資料的介面。

檢視物件包括:

Uint8Array

:每個 item 1 個位元組

Uint16Array

:每個 item 2 個位元組

Uint32Array

:每個 item 4 個位元組

Float64Array

:每個 item 8 個位元組

按照上面的標準,一個 16 位元組 ArrayBuffer,可轉化的檢視物件和其長度為:

Uint8Array:長度 16

Uint16Array:長度 8

Uint32Array:長度 4

Float64Array:長度 2

這裡只是簡單介紹流資料在前端如何儲存,為的是避免你在瀏覽器看到一個長長的 ArrayBuffer 不知道它是什麼,記住它一定是二進位制資料。

為什麼選 flv?

前面說到,直播需要實時性,延遲當然越短越好。當然決定傳輸速度的因素有很多,其中一個就是影片資料本身的大小。

點播場景我們最常見的 mp4 格式,對前端是相容性最好的。但是相對來說 mp4 的體積比較大,解析會複雜一些。在直播場景下這就是 mp4 的劣勢。

flv 就不一樣了,它的頭部檔案非常小,結構簡單,解析起來又塊,在直播的實時性要求下非常有優勢,因此它成了最常用的直播方案之一。

當然除了 flv 之外還有其他格式,對應直播協議,我們一一對比一下:

RTMP:

底層基於 TCP,在瀏覽器端依賴 Flash。

HTTP-FLV:

基於 HTTP 流式 IO 傳輸 FLV,依賴瀏覽器支援播放 FLV。

WebSocket-FLV:

基於 WebSocket 傳輸 FLV,依賴瀏覽器支援播放 FLV。

HLS

: Http Live Streaming,蘋果提出基於 HTTP 的流媒體傳輸協議。HTML5 可以直接開啟播放。

RTP

: 基於 UDP,延遲 1 秒,瀏覽器不支援。

其實早期常用的直播方案是 RTMP,相容性也不錯,但是它依賴 Flash,而目前瀏覽器下 Flash 預設是被禁用的狀態,已經被時代淘汰的技術,因此不做考慮。

HLS 協議也很常見,對應影片格式就是 m3u8。它是由蘋果推出,對手機支援非常好,但是致命缺點是延遲高(10~30 秒),因此也不做考慮。

RTP 不必說,瀏覽器不支援,剩下的就只有 flv 了。

但是 flv 又分為 HTTP-FLV 和 WebSocket-FLV,它兩看著像兄弟,又有什麼區別呢?

前面我們說過,直播流是實時傳輸,連線建立後不會斷,需要持續的推拉流。這種需要長連線的場景我們首先想到的方案自然是 WebSocket,因為 WebSocket 本來就是長連線實時互傳的技術。

不過呢隨著 js 原生能力擴充套件,出現了像 fetch 這樣比 ajax 更強的黑科技。它不光支援對我們更友好的 Promise,並且天生可以處理流資料,效能很好,而且使用起來也足夠簡單,對我們開發者來說更方便,因此就有了 http 版的 flv 方案。

綜上所述,最適合瀏覽器直播的是 flv,但是 flv 也不是萬金油,它的缺點是前端 video 標籤不能直接播放,需要經過處理才行。

處理方案,就是我們今天的主角:flv。js

協議與基礎實現

前面我們說到,flv 同時支援 WebSocket 和 HTTP 兩種傳輸方式,幸運的是,flv。js 也同時支援這兩種協議。

選擇用 http 還是 ws,其實功能和效能上差別不大,關鍵看後端同學給我們什麼協議吧。我這邊的選擇是 http,前後端處理起來都比較方便。

接下來我們介紹 flv。js 的具體接入流程。

官網地址:

https://github。com/Bilibili/flv。js/

首先安裝flv。js,程式碼的第一行是檢測瀏覽器是否支援 flv。js,其實大部分瀏覽器是支援的。接下來就是獲取 video 標籤的 DOM 元素。flv 會把處理後的 flv 流輸出給 video 元素,然後在 video 上實現影片流播放。

接下來是關鍵之處,就是建立flvjs。Player物件,我們稱之為播放器例項。播放器例項透過 flvjs。createPlayer 函式建立,引數是一個配置物件,常用如下:

type:媒體型別,flv 或 mp4,預設 flv

isLive:可選,是否是直播流,預設 true

hasAudio:是否有音訊

hasVideo:是否有影片

url:指定流地址,可以是 https(s) or ws(s)

上面的是否有音訊,影片的配置,還是要看流地址是否有音影片。比如監控流只有影片流沒有音訊,那即便你配置 hasAudio: true 也是不可能有聲音的。

播放器例項建立之後,接下來就是三步走:

掛載元素:flvPlayer。attachMediaElement(videoEl)

載入流:flvPlayer。load()

播放流:flvPlayer。play()

基礎實現流程就這麼多,下面再說一下處理過程中的細節和要點。

細節處理要點

上面說了基本的用法,下面說一下實踐中的關鍵問題。

暫停與播放

點播中的暫停與播放很容易,播放器下面會有一個播放/暫停按鍵,想什麼時候暫停都可以,再點播放的時候會接著上次暫停的地方繼續播放。但是直播中就不一樣了。

正常情況下直播應該是沒有播放/暫停按鈕以及進度條的。因為我們看的是實時資訊,你暫停了影片,再點播放的時候是不能從暫停的地方繼續播放的。為啥?因為你是實時的嘛,再點播放的時候應該是獲取最新的實時流,播放最新的影片。

具體到技術細節,前端的 video 標籤預設是帶有進度條和暫停按鈕的,flv。js 將直播流輸出到 video 標籤,此時如果點選暫停按鈕,影片也是會停住的,這與點播邏輯一致。但是如果你再點播放,影片還是會從暫停處繼續播放,這就不對了。

那麼我們換個角度,重新審視一下直播的播放/暫停邏輯。

直播為什麼需要暫停?拿我們影片監控來說,一個頁面會放好幾個攝像頭的監控影片,如果每個播放器一直與伺服器保持連線,持續拉流,這會造成大量的連線和消耗,流失的都是白花花的銀子。

那我們是不是可以這樣:進去網頁的時候,找到想看的攝像頭,點選播放再拉流。當你不想看的時候,點選暫停,播放器斷開連線,這樣是不是就會節省無用的流量消耗。

因此,

直播中的播放/暫停,核心邏輯是拉流/斷流。

理解到這裡,那我們的方案應該是隱藏 video 的暫停/播放按鈕,然後自己實現播放和暫停的邏輯。

還是以上述程式碼為例,播放器例項(上面的 flvPlayer 變數)不用變,播放/暫停程式碼如下:

異常處理

用 flv。js 接入直播流的過程會遇到各種問題,有的是後端資料流的問題,有的是前端處理邏輯的問題。因為流是實時獲取,flv 也是實時轉化輸出,因此一旦發生錯誤,瀏覽器控制檯會迴圈連續的列印異常。

如果你用 react 和 ts,滿屏異常,你都無法開發下去了。再有直播流本來就可能發生許多異常,因此錯處理非常關鍵。

官方對異常處理的說明不太明顯,我簡單總結一下:

首先,flv。js 的異常分為兩個級別,可以看作是 一級異常 和 二級異常。

再有,flv。js 有一個特殊之處,它的 事件 和 錯誤 都是用列舉來表示,如下:

flvjs。Events:表示事件

flvjs。ErrorTypes:表示一級異常

flvjs。ErrorDetails:表示二級異常

下面介紹的異常和事件,都是基於上述列舉,你可以理解為是列舉下的一個key值。

一級異常有三類:

NETWORK_ERROR:網路錯誤,表示連線問題

MEDIA_ERROR:媒體錯誤,格式或解碼問題

OTHER_ERROR:其他錯誤

二級級異常常用的有三類:

NETWORK_STATUS_CODE_INVALID:HTTP 狀態碼錯誤,說明 url 地址有誤

NETWORK_TIMEOUT:連線超時,網路或後臺問題

MEDIA_FORMAT_UNSUPPORTED:媒體格式不支援,一般是流資料不是 flv 的格式

瞭解這些之後,我們在播放器例項上監聽異常:

除此之外,自定義播放/暫停邏輯,還需要知道載入狀態。可以透過以下方法監聽影片流載入完成:

樣式定製

為什麼會有樣式定製?前面我們說了,直播流的播放/暫停邏輯與點播不同,因此我們要隱藏 video 的操作欄元素,透過自定義元素來實現相關功能。

首先要隱藏播放/暫停按鈕,進度條,以及音量按鈕,用 css 實現即可:

播放和暫停的邏輯上面講了,樣式這邊自定義一個按鈕即可。除此之外我們還可能需要一個全屏按鈕,看一下全屏的邏輯怎麼寫:

其他自定義樣式,比如你要做彈幕,在 video 上面蓋一層元素自行實現就可以了。

TAG: 直播flv播放暫停影片