通過(guò)flv.js播放監(jiān)控示例深入探究直播流技術(shù)
引言
究其原因,一方面 GitHub 上文檔比較晦澀,說(shuō)明也比較簡(jiǎn)陋;另一方面是受“視頻播放”思維的影響,沒(méi)有對(duì)流的足夠認(rèn)識(shí)以及缺乏處理流的經(jīng)驗(yàn)。
點(diǎn)播與直播
啥是直播?啥是點(diǎn)播?
直播就不用說(shuō)了,抖音普及之下大家都知道直播是干嘛的。點(diǎn)播其實(shí)就是視頻播放,和咱們嗶哩嗶哩看視頻一摸一樣沒(méi)區(qū)別,就是把提前做好的視頻放出來(lái),就叫點(diǎn)播。
點(diǎn)播對(duì)于我們前端來(lái)說(shuō),就是拿一個(gè) mp4 的鏈接地址,放到 video 標(biāo)簽里面,瀏覽器會(huì)幫我們處理好視頻解析播放等一些列事情,我們可以拖動(dòng)進(jìn)度條選擇想看的任意一個(gè)時(shí)間。
但是直播不一樣,直播有兩個(gè)特點(diǎn):
- 獲取的是流數(shù)據(jù)
- 要求實(shí)時(shí)性
先看一下什么叫流數(shù)據(jù)。大部分沒(méi)有做過(guò)音視頻的前端同學(xué),我們常接觸的數(shù)據(jù)就是 ajax 從接口獲取的 json 數(shù)據(jù),特別一點(diǎn)的可能是文件上傳。這些數(shù)據(jù)的特點(diǎn)是,它們都屬于一次性就能拿到的數(shù)據(jù)。我們一個(gè)請(qǐng)求,一個(gè)響應(yīng),完整的數(shù)據(jù)就拿回來(lái)了。
但是流不一樣,流數(shù)據(jù)獲取是一幀一幀的,你可以理解為是一小塊一小塊的。像直播流的數(shù)據(jù),它并不是一個(gè)完整的視頻片段,它就是很小的二進(jìn)制數(shù)據(jù),需要你一點(diǎn)一點(diǎn)的拼接起來(lái),才有可能輸出一段視頻。
再看它的實(shí)時(shí)性。如果是點(diǎn)播的話,我們直接將完整的視頻存儲(chǔ)在服務(wù)器上,然后返回鏈接,前端用 video 或播放器播就行了。但是直播的實(shí)時(shí)性,就決定了數(shù)據(jù)源不可能在服務(wù)器上,而是在某一個(gè)客戶端。
數(shù)據(jù)源在客戶端,那么又是怎么到達(dá)其他客戶端的呢?
這個(gè)問(wèn)題,請(qǐng)看下面這張流程圖:

如圖所示,發(fā)起直播的客戶端,向上連著流媒體服務(wù)器,直播產(chǎn)生的視頻流會(huì)被實(shí)時(shí)推送到服務(wù)端,這個(gè)過(guò)程叫做推流。其他客戶端同樣也連接著這個(gè)流媒體服務(wù)器,不同的是它們是播放端,會(huì)實(shí)時(shí)拉取直播客戶端的視頻流,這個(gè)過(guò)程叫做拉流。
推流—> 服務(wù)器-> 拉流,這是目前流行的也是標(biāo)準(zhǔn)的直播解決方案。看到了吧,直播的整個(gè)流程全都是流數(shù)據(jù)傳輸,數(shù)據(jù)處理直面二進(jìn)制,要比點(diǎn)播復(fù)雜了幾個(gè)量級(jí)。
具體到我們業(yè)務(wù)當(dāng)中的攝像頭實(shí)時(shí)監(jiān)控預(yù)覽,其實(shí)和上面的完全一致,只不過(guò)發(fā)起直播的客戶端是攝像頭,觀看直播的客戶端是瀏覽器而已。
靜態(tài)數(shù)據(jù)與流數(shù)據(jù)
我們常接觸的文本,json,圖片等等,都屬于靜態(tài)數(shù)據(jù),前端用 ajax 向接口請(qǐng)求回來(lái)的數(shù)據(jù)就是靜態(tài)數(shù)據(jù)。
像上面說(shuō)到的,直播產(chǎn)生的視頻和音頻,都屬于流數(shù)據(jù)。流數(shù)據(jù)是一幀一幀的,它的本質(zhì)是二進(jìn)制數(shù)據(jù),因?yàn)楹苄?,?shù)據(jù)像水流一樣連綿不斷的流動(dòng),因此非常適合實(shí)時(shí)傳輸。
靜態(tài)數(shù)據(jù),在前端代碼中有對(duì)應(yīng)的數(shù)據(jù)類(lèi)型,比如 string,json,array 等等。那么流數(shù)據(jù)(二進(jìn)制數(shù)據(jù))的數(shù)據(jù)類(lèi)型是什么?在前端如何存儲(chǔ)?又如何操作?
首先明確一點(diǎn),前端是可以存儲(chǔ)和操作二進(jìn)制的。最基本的二進(jìn)制對(duì)象是 ArrayBuffer,它表示一個(gè)固定長(zhǎng)度,如:
let buffer = new ArrayBuffer(16) // 創(chuàng)建一個(gè) 16 字節(jié) 的 buffer,用 0 填充 alert(buffer.byteLength) // 16
ArrayBuffer 只是用于存儲(chǔ)二進(jìn)制數(shù)據(jù),如果要操作,則需要使用 視圖對(duì)象。
視圖對(duì)象,不存儲(chǔ)任何數(shù)據(jù),作用是將 ArrayBuffer 的數(shù)據(jù)做了結(jié)構(gòu)化的處理,便于我們操作這些數(shù)據(jù),說(shuō)白了它們是操作二進(jìn)制數(shù)據(jù)的接口。
視圖對(duì)象包括:
- Uint8Array:每個(gè) item 1 個(gè)字節(jié)
- Uint16Array:每個(gè) item 2 個(gè)字節(jié)
- Uint32Array:每個(gè) item 4 個(gè)字節(jié)
- Float64Array:每個(gè) item 8 個(gè)字節(jié)
按照上面的標(biāo)準(zhǔn),一個(gè) 16 字節(jié) ArrayBuffer,可轉(zhuǎn)化的視圖對(duì)象和其長(zhǎng)度為:
- Uint8Array:長(zhǎng)度 16
- Uint16Array:長(zhǎng)度 8
- Uint32Array:長(zhǎng)度 4
- Float64Array:長(zhǎng)度 2
這里只是簡(jiǎn)單介紹流數(shù)據(jù)在前端如何存儲(chǔ),為的是避免你在瀏覽器看到一個(gè)長(zhǎng)長(zhǎng)的 ArrayBuffer 不知道它是什么,記住它一定是二進(jìn)制數(shù)據(jù)。
為什么選 flv?
前面說(shuō)到,直播需要實(shí)時(shí)性,延遲當(dāng)然越短越好。當(dāng)然決定傳輸速度的因素有很多,其中一個(gè)就是視頻數(shù)據(jù)本身的大小。
點(diǎn)播場(chǎng)景我們最常見(jiàn)的 mp4 格式,對(duì)前端是兼容性最好的。但是相對(duì)來(lái)說(shuō) mp4 的體積比較大,解析會(huì)復(fù)雜一些。在直播場(chǎng)景下這就是 mp4 的劣勢(shì)。
flv 就不一樣了,它的頭部文件非常小,結(jié)構(gòu)簡(jiǎn)單,解析起來(lái)又塊,在直播的實(shí)時(shí)性要求下非常有優(yōu)勢(shì),因此它成了最常用的直播方案之一。
當(dāng)然除了 flv 之外還有其他格式,對(duì)應(yīng)直播協(xié)議,我們一一對(duì)比一下:
- RTMP: 底層基于 TCP,在瀏覽器端依賴 Flash。
- HTTP-FLV: 基于 HTTP 流式 IO 傳輸 FLV,依賴瀏覽器支持播放 FLV。
- WebSocket-FLV: 基于 WebSocket 傳輸 FLV,依賴瀏覽器支持播放 FLV。
- HLS: Http Live Streaming,蘋(píng)果提出基于 HTTP 的流媒體傳輸協(xié)議。HTML5 可以直接打開(kāi)播放。
- RTP: 基于 UDP,延遲 1 秒,瀏覽器不支持。
其實(shí)早期常用的直播方案是 RTMP,兼容性也不錯(cuò),但是它依賴 Flash,而目前瀏覽器下 Flash 默認(rèn)是被禁用的狀態(tài),已經(jīng)被時(shí)代淘汰的技術(shù),因此不做考慮。
HLS 協(xié)議也很常見(jiàn),對(duì)應(yīng)視頻格式就是 m3u8。它是由蘋(píng)果推出,對(duì)手機(jī)支持非常好,但是致命缺點(diǎn)是延遲高(10~30 秒),因此也不做考慮。
RTP 不必說(shuō),瀏覽器不支持,剩下的就只有 flv 了。
但是 flv 又分為 HTTP-FLV 和 WebSocket-FLV,它兩看著像兄弟,又有什么區(qū)別呢?
前面我們說(shuō)過(guò),直播流是實(shí)時(shí)傳輸,連接創(chuàng)建后不會(huì)斷,需要持續(xù)的推拉流。這種需要長(zhǎng)連接的場(chǎng)景我們首先想到的方案自然是 WebSocket,因?yàn)?WebSocket 本來(lái)就是長(zhǎng)連接實(shí)時(shí)互傳的技術(shù)。
不過(guò)呢隨著 js 原生能力擴(kuò)展,出現(xiàn)了像 fetch 這樣比 ajax 更強(qiáng)的黑科技。它不光支持對(duì)我們更友好的 Promise,并且天生可以處理流數(shù)據(jù),性能很好,而且使用起來(lái)也足夠簡(jiǎn)單,對(duì)我們開(kāi)發(fā)者來(lái)說(shuō)更方便,因此就有了 http 版的 flv 方案。
綜上所述,最適合瀏覽器直播的是 flv,但是 flv 也不是萬(wàn)金油,它的缺點(diǎn)是前端 video 標(biāo)簽不能直接播放,需要經(jīng)過(guò)處理才行。
處理方案,就是我們今天的主角:flv.js
協(xié)議與基礎(chǔ)實(shí)現(xiàn)
前面我們說(shuō)到,flv 同時(shí)支持 WebSocket 和 HTTP 兩種傳輸方式,幸運(yùn)的是,flv.js 也同時(shí)支持這兩種協(xié)議。
選擇用 http 還是 ws,其實(shí)功能和性能上差別不大,關(guān)鍵看后端同學(xué)給我們什么協(xié)議吧。我這邊的選擇是 http,前后端處理起來(lái)都比較方便。
接下來(lái)我們介紹 flv.js 的具體接入流程,官網(wǎng)在這里
假設(shè)現(xiàn)在有一個(gè)直播流地址:http://test.stream.com/fetch-media.flv,第一步我們按照官網(wǎng)的快速開(kāi)始建一個(gè) demo:
import flvjs from 'flv.js'
if (flvjs.isSupported()) {
var videoEl = document.getElementById('videoEl')
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: 'http://test.stream.com/fetch-media.flv'
})
flvPlayer.attachMediaElement(videoEl)
flvPlayer.load()
flvPlayer.play()
}首先安裝 flv.js,代碼的第一行是檢測(cè)瀏覽器是否支持 flv.js,其實(shí)大部分瀏覽器是支持的。接下來(lái)就是獲取 video 標(biāo)簽的 DOM 元素。flv 會(huì)把處理后的 flv 流輸出給 video 元素,然后在 video 上實(shí)現(xiàn)視頻流播放。
接下來(lái)是關(guān)鍵之處,就是創(chuàng)建 flvjs.Player 對(duì)象,我們稱之為播放器實(shí)例。播放器實(shí)例通過(guò) flvjs.createPlayer 函數(shù)創(chuàng)建,參數(shù)是一個(gè)配置對(duì)象,常用如下:
type:媒體類(lèi)型,flv或mp4,默認(rèn) flvisLive:可選,是否是直播流,默認(rèn) truehasAudio:是否有音頻hasVideo:是否有視頻url:指定流地址,可以是https(s)orws(s)
上面的是否有音頻,視頻的配置,還是要看流地址是否有音視頻。比如監(jiān)控流只有視頻流沒(méi)有音頻,那即便你配置 hasAudio: true 也是不可能有聲音的。
播放器實(shí)例創(chuàng)建之后,接下來(lái)就是三步走:
掛載元素:flvPlayer.attachMediaElement(videoEl)加載流:flvPlayer.load()播放流:flvPlayer.play()
基礎(chǔ)實(shí)現(xiàn)流程就這么多,下面再說(shuō)一下處理過(guò)程中的細(xì)節(jié)和要點(diǎn)。
細(xì)節(jié)處理要點(diǎn)
上面說(shuō)了基本的用法,下面說(shuō)一下實(shí)踐中的關(guān)鍵問(wèn)題。
暫停與播放
點(diǎn)播中的暫停與播放很容易,播放器下面會(huì)有一個(gè)播放/暫停按鍵,想什么時(shí)候暫停都可以,再點(diǎn)播放的時(shí)候會(huì)接著上次暫停的地方繼續(xù)播放。但是直播中就不一樣了。
正常情況下直播應(yīng)該是沒(méi)有播放/暫停按鈕以及進(jìn)度條的。因?yàn)槲覀兛吹氖菍?shí)時(shí)信息,你暫停了視頻,再點(diǎn)播放的時(shí)候是不能從暫停的地方繼續(xù)播放的。為啥?因?yàn)槟闶菍?shí)時(shí)的嘛,再點(diǎn)播放的時(shí)候應(yīng)該是獲取最新的實(shí)時(shí)流,播放最新的視頻。
具體到技術(shù)細(xì)節(jié),前端的 video 標(biāo)簽?zāi)J(rèn)是帶有進(jìn)度條和暫停按鈕的,flv.js 將直播流輸出到 video 標(biāo)簽,此時(shí)如果點(diǎn)擊暫停按鈕,視頻也是會(huì)停住的,這與點(diǎn)播邏輯一致。但是如果你再點(diǎn)播放,視頻還是會(huì)從暫停處繼續(xù)播放,這就不對(duì)了。
那么我們換個(gè)角度,重新審視一下直播的播放/暫停邏輯。
直播為什么需要暫停?拿我們視頻監(jiān)控來(lái)說(shuō),一個(gè)頁(yè)面會(huì)放好幾個(gè)攝像頭的監(jiān)控視頻,如果每個(gè)播放器一直與服務(wù)器保持連接,持續(xù)拉流,這會(huì)造成大量的連接和消耗,流失的都是白花花的銀子。
那我們是不是可以這樣:進(jìn)去網(wǎng)頁(yè)的時(shí)候,找到想看的攝像頭,點(diǎn)擊播放再拉流。當(dāng)你不想看的時(shí)候,點(diǎn)擊暫停,播放器斷開(kāi)連接,這樣是不是就會(huì)節(jié)省無(wú)用的流量消耗。
因此,直播中的播放/暫停,核心邏輯是拉流/斷流。
理解到這里,那我們的方案應(yīng)該是隱藏 video 的暫停/播放按鈕,然后自己實(shí)現(xiàn)播放和暫停的邏輯。
還是以上述代碼為例,播放器實(shí)例(上面的 flvPlayer 變量)不用變,播放/暫停代碼如下:
const onClick = isplay => {
// 參數(shù) isplay 表示當(dāng)前是否正在播放
if (isplay) {
// 在播放,斷流
player.unload()
player.detachMediaElement()
} else {
// 已斷流,重新拉流播放
player.attachMediaElement(videoEl.current)
player.load()
player.play()
}
}異常處理
用 flv.js 接入直播流的過(guò)程會(huì)遇到各種問(wèn)題,有的是后端數(shù)據(jù)流的問(wèn)題,有的是前端處理邏輯的問(wèn)題。因?yàn)榱魇菍?shí)時(shí)獲取,flv 也是實(shí)時(shí)轉(zhuǎn)化輸出,因此一旦發(fā)生錯(cuò)誤,瀏覽器控制臺(tái)會(huì)循環(huán)連續(xù)的打印異常。
如果你用 react 和 ts,滿屏異常,你都無(wú)法開(kāi)發(fā)下去了。再有直播流本來(lái)就可能發(fā)生許多異常,因此錯(cuò)處理非常關(guān)鍵。
官方對(duì)異常處理的說(shuō)明不太明顯,我簡(jiǎn)單總結(jié)一下:
首先,flv.js 的異常分為兩個(gè)級(jí)別,可以看作是 一級(jí)異常 和 二級(jí)異常。
再有,flv.js 有一個(gè)特殊之處,它的 事件 和 錯(cuò)誤 都是用枚舉來(lái)表示,如下:
flvjs.Events:表示事件flvjs.ErrorTypes:表示一級(jí)異常flvjs.ErrorDetails:表示二級(jí)異常
下面介紹的異常和事件,都是基于上述枚舉,你可以理解為是枚舉下的一個(gè) key 值。
一級(jí)異常有三類(lèi):
NETWORK_ERROR:網(wǎng)絡(luò)錯(cuò)誤,表示連接問(wèn)題MEDIA_ERROR:媒體錯(cuò)誤,格式或解碼問(wèn)題OTHER_ERROR:其他錯(cuò)誤
二級(jí)級(jí)異常常用的有三類(lèi):
NETWORK_STATUS_CODE_INVALID:HTTP 狀態(tài)碼錯(cuò)誤,說(shuō)明 url 地址有誤NETWORK_TIMEOUT:連接超時(shí),網(wǎng)絡(luò)或后臺(tái)問(wèn)題MEDIA_FORMAT_UNSUPPORTED:媒體格式不支持,一般是流數(shù)據(jù)不是 flv 的格式
了解這些之后,我們?cè)诓シ牌鲗?shí)例上監(jiān)聽(tīng)異常:
// 監(jiān)聽(tīng)錯(cuò)誤事件
flvPlayer.on(flvjs.Events.ERROR, (err, errdet) => {
// 參數(shù) err 是一級(jí)異常,errdet 是二級(jí)異常
if (err == flvjs.ErrorTypes.MEDIA_ERROR) {
console.log('媒體錯(cuò)誤')
if(errdet == flvjs.ErrorDetails.MEDIA_FORMAT_UNSUPPORTED) {
console.log('媒體格式不支持')
}
}
if (err == flvjs.ErrorTypes.NETWORK_ERROR) {
console.log('網(wǎng)絡(luò)錯(cuò)誤')
if(errdet == flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID) {
console.log('http狀態(tài)碼異常')
}
}
if(err == flvjs.ErrorTypes.OTHER_ERROR) {
console.log('其他異常:', errdet)
}
}除此之外,自定義播放/暫停邏輯,還需要知道加載狀態(tài)。可以通過(guò)以下方法監(jiān)聽(tīng)視頻流加載完成:
player.on(flvjs.Events.METADATA_ARRIVED, () => {
console.log('視頻加載完成')
})樣式定制
為什么會(huì)有樣式定制?前面我們說(shuō)了,直播流的播放/暫停邏輯與點(diǎn)播不同,因此我們要隱藏 video 的操作欄元素,通過(guò)自定義元素來(lái)實(shí)現(xiàn)相關(guān)功能。
首先要隱藏播放/暫停按鈕,進(jìn)度條,以及音量按鈕,用 css 實(shí)現(xiàn)即可:
/* 所有控件 */
video::-webkit-media-controls-enclosure {
display: none;
}
/* 進(jìn)度條 */
video::-webkit-media-controls-timeline {
display: none;
}
video::-webkit-media-controls-current-time-display {
display: none;
}
/* 音量按鈕 */
video::-webkit-media-controls-mute-button {
display: none;
}
video::-webkit-media-controls-toggle-closed-captions-button {
display: none;
}
/* 音量的控制條 */
video::-webkit-media-controls-volume-slider {
display: none;
}
/* 播放按鈕 */
video::-webkit-media-controls-play-button {
display: none;
}播放和暫停的邏輯上面講了,樣式這邊自定義一個(gè)按鈕即可。除此之外我們還可能需要一個(gè)全屏按鈕,看一下全屏的邏輯怎么寫(xiě):
const fullPage = () => {
let dom = document.querySelector('.video')
if (dom.requestFullscreen) {
dom.requestFullscreen()
} else if (dom.webkitRequestFullScreen) {
dom.webkitRequestFullScreen()
}
}其他自定義樣式,比如你要做彈幕,在 video 上面蓋一層元素自行實(shí)現(xiàn)就可以了。
以上就是通過(guò)flv.js播放監(jiān)控示例深入探究直播流技術(shù)的詳細(xì)內(nèi)容,更多關(guān)于flv.js播放監(jiān)控直播流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
讓div層隨鼠標(biāo)移動(dòng)的實(shí)現(xiàn)代碼 ie ff
隨鼠標(biāo)移動(dòng)的div層使用ie ff ,大家可以注意下兼容性的問(wèn)題。2009-12-12
JS使用正則表達(dá)式過(guò)濾多個(gè)詞語(yǔ)并替換為相同長(zhǎng)度星號(hào)的方法
這篇文章主要介紹了JS使用正則表達(dá)式過(guò)濾多個(gè)詞語(yǔ)并替換為相同長(zhǎng)度星號(hào)的方法,涉及javascript字符串與正則替換操作相關(guān)技巧,需要的朋友可以參考下2016-08-08
使用JS操作文件(FileReader讀取--node的fs)
這篇文章主要介紹了使用JS操作文件(FileReader讀取--node的fs),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
javascript頁(yè)面動(dòng)態(tài)顯示時(shí)間變化示例代碼
頁(yè)面動(dòng)態(tài)顯示時(shí)間變化的方法有很多,本文為大家介紹下使用javascript的具體實(shí)現(xiàn),感興趣的朋友不要錯(cuò)過(guò)2013-12-12
javascript 學(xué)習(xí)筆記(onchange等)
javascript 學(xué)習(xí)筆記,一些簡(jiǎn)單的小技巧,學(xué)習(xí)js的朋友可以看下。2010-11-11
一文教你如何實(shí)現(xiàn)localStorage的過(guò)期機(jī)制
要知道localStorage本身并沒(méi)有提供過(guò)期機(jī)制,既然如此那就只能我們自己來(lái)實(shí)現(xiàn)了,這篇文章主要給大家介紹了關(guān)于如何實(shí)現(xiàn)localStorage過(guò)期機(jī)制的相關(guān)資料,需要的朋友可以參考下2022-02-02
js open() 與showModalDialog()方法使用介紹
項(xiàng)目開(kāi)發(fā)中經(jīng)常要用到j(luò)s open() 與showModalDialog()方法,下面有個(gè)不錯(cuò)的示例,喜歡的朋友可以研究下2013-09-09

