通過flv.js播放監(jiān)控示例深入探究直播流技術(shù)
引言
究其原因,一方面 GitHub 上文檔比較晦澀,說明也比較簡陋;另一方面是受“視頻播放”思維的影響,沒有對流的足夠認(rèn)識以及缺乏處理流的經(jīng)驗(yàn)。
點(diǎn)播與直播
啥是直播?啥是點(diǎn)播?
直播就不用說了,抖音普及之下大家都知道直播是干嘛的。點(diǎn)播其實(shí)就是視頻播放,和咱們嗶哩嗶哩看視頻一摸一樣沒區(qū)別,就是把提前做好的視頻放出來,就叫點(diǎn)播。
點(diǎn)播對于我們前端來說,就是拿一個(gè) mp4 的鏈接地址,放到 video
標(biāo)簽里面,瀏覽器會幫我們處理好視頻解析播放等一些列事情,我們可以拖動進(jìn)度條選擇想看的任意一個(gè)時(shí)間。
但是直播不一樣,直播有兩個(gè)特點(diǎn):
- 獲取的是流數(shù)據(jù)
- 要求實(shí)時(shí)性
先看一下什么叫流數(shù)據(jù)。大部分沒有做過音視頻的前端同學(xué),我們常接觸的數(shù)據(jù)就是 ajax 從接口獲取的 json 數(shù)據(jù),特別一點(diǎn)的可能是文件上傳。這些數(shù)據(jù)的特點(diǎn)是,它們都屬于一次性就能拿到的數(shù)據(jù)。我們一個(gè)請求,一個(gè)響應(yīng),完整的數(shù)據(jù)就拿回來了。
但是流不一樣,流數(shù)據(jù)獲取是一幀一幀的,你可以理解為是一小塊一小塊的。像直播流的數(shù)據(jù),它并不是一個(gè)完整的視頻片段,它就是很小的二進(jìn)制數(shù)據(jù),需要你一點(diǎn)一點(diǎn)的拼接起來,才有可能輸出一段視頻。
再看它的實(shí)時(shí)性。如果是點(diǎn)播的話,我們直接將完整的視頻存儲在服務(wù)器上,然后返回鏈接,前端用 video 或播放器播就行了。但是直播的實(shí)時(shí)性,就決定了數(shù)據(jù)源不可能在服務(wù)器上,而是在某一個(gè)客戶端。
數(shù)據(jù)源在客戶端,那么又是怎么到達(dá)其他客戶端的呢?
這個(gè)問題,請看下面這張流程圖:
如圖所示,發(fā)起直播的客戶端,向上連著流媒體服務(wù)器,直播產(chǎn)生的視頻流會被實(shí)時(shí)推送到服務(wù)端,這個(gè)過程叫做推流
。其他客戶端同樣也連接著這個(gè)流媒體服務(wù)器,不同的是它們是播放端,會實(shí)時(shí)拉取直播客戶端的視頻流,這個(gè)過程叫做拉流
。
推流—> 服務(wù)器-> 拉流,這是目前流行的也是標(biāo)準(zhǔn)的直播解決方案??吹搅税?,直播的整個(gè)流程全都是流數(shù)據(jù)傳輸,數(shù)據(jù)處理直面二進(jìn)制,要比點(diǎn)播復(fù)雜了幾個(gè)量級。
具體到我們業(yè)務(wù)當(dāng)中的攝像頭實(shí)時(shí)監(jiān)控預(yù)覽,其實(shí)和上面的完全一致,只不過發(fā)起直播的客戶端是攝像頭,觀看直播的客戶端是瀏覽器而已。
靜態(tài)數(shù)據(jù)與流數(shù)據(jù)
我們常接觸的文本,json,圖片等等,都屬于靜態(tài)數(shù)據(jù),前端用 ajax 向接口請求回來的數(shù)據(jù)就是靜態(tài)數(shù)據(jù)。
像上面說到的,直播產(chǎn)生的視頻和音頻,都屬于流數(shù)據(jù)。流數(shù)據(jù)是一幀一幀的,它的本質(zhì)是二進(jìn)制數(shù)據(jù),因?yàn)楹苄?,?shù)據(jù)像水流一樣連綿不斷的流動,因此非常適合實(shí)時(shí)傳輸。
靜態(tài)數(shù)據(jù),在前端代碼中有對應(yīng)的數(shù)據(jù)類型,比如 string,json,array
等等。那么流數(shù)據(jù)(二進(jìn)制數(shù)據(jù))的數(shù)據(jù)類型是什么?在前端如何存儲?又如何操作?
首先明確一點(diǎn),前端是可以存儲和操作二進(jìn)制的。最基本的二進(jìn)制對象是 ArrayBuffer
,它表示一個(gè)固定長度,如:
let buffer = new ArrayBuffer(16) // 創(chuàng)建一個(gè) 16 字節(jié) 的 buffer,用 0 填充 alert(buffer.byteLength) // 16
ArrayBuffer
只是用于存儲二進(jìn)制數(shù)據(jù),如果要操作,則需要使用 視圖對象。
視圖對象,不存儲任何數(shù)據(jù),作用是將 ArrayBuffer 的數(shù)據(jù)做了結(jié)構(gòu)化的處理,便于我們操作這些數(shù)據(jù),說白了它們是操作二進(jìn)制數(shù)據(jù)的接口。
視圖對象包括:
- 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)化的視圖對象和其長度為:
- Uint8Array:長度 16
- Uint16Array:長度 8
- Uint32Array:長度 4
- Float64Array:長度 2
這里只是簡單介紹流數(shù)據(jù)在前端如何存儲,為的是避免你在瀏覽器看到一個(gè)長長的 ArrayBuffer 不知道它是什么,記住它一定是二進(jìn)制數(shù)據(jù)。
為什么選 flv?
前面說到,直播需要實(shí)時(shí)性,延遲當(dāng)然越短越好。當(dāng)然決定傳輸速度的因素有很多,其中一個(gè)就是視頻數(shù)據(jù)本身的大小。
點(diǎn)播場景我們最常見的 mp4 格式,對前端是兼容性最好的。但是相對來說 mp4 的體積比較大,解析會復(fù)雜一些。在直播場景下這就是 mp4 的劣勢。
flv 就不一樣了,它的頭部文件非常小,結(jié)構(gòu)簡單,解析起來又塊,在直播的實(shí)時(shí)性要求下非常有優(yōu)勢,因此它成了最常用的直播方案之一。
當(dāng)然除了 flv 之外還有其他格式,對應(yīng)直播協(xié)議,我們一一對比一下:
- RTMP: 底層基于 TCP,在瀏覽器端依賴 Flash。
- HTTP-FLV: 基于 HTTP 流式 IO 傳輸 FLV,依賴瀏覽器支持播放 FLV。
- WebSocket-FLV: 基于 WebSocket 傳輸 FLV,依賴瀏覽器支持播放 FLV。
- HLS: Http Live Streaming,蘋果提出基于 HTTP 的流媒體傳輸協(xié)議。HTML5 可以直接打開播放。
- RTP: 基于 UDP,延遲 1 秒,瀏覽器不支持。
其實(shí)早期常用的直播方案是 RTMP
,兼容性也不錯(cuò),但是它依賴 Flash,而目前瀏覽器下 Flash 默認(rèn)是被禁用的狀態(tài),已經(jīng)被時(shí)代淘汰的技術(shù),因此不做考慮。
HLS
協(xié)議也很常見,對應(yīng)視頻格式就是 m3u8
。它是由蘋果推出,對手機(jī)支持非常好,但是致命缺點(diǎn)是延遲高(10~30 秒),因此也不做考慮。
RTP 不必說,瀏覽器不支持,剩下的就只有 flv 了。
但是 flv 又分為 HTTP-FLV
和 WebSocket-FLV
,它兩看著像兄弟,又有什么區(qū)別呢?
前面我們說過,直播流是實(shí)時(shí)傳輸,連接創(chuàng)建后不會斷,需要持續(xù)的推拉流。這種需要長連接的場景我們首先想到的方案自然是 WebSocket,因?yàn)?WebSocket 本來就是長連接實(shí)時(shí)互傳的技術(shù)。
不過呢隨著 js 原生能力擴(kuò)展,出現(xiàn)了像 fetch
這樣比 ajax 更強(qiáng)的黑科技。它不光支持對我們更友好的 Promise,并且天生可以處理流數(shù)據(jù),性能很好,而且使用起來也足夠簡單,對我們開發(fā)者來說更方便,因此就有了 http 版的 flv 方案。
綜上所述,最適合瀏覽器直播的是 flv,但是 flv 也不是萬金油,它的缺點(diǎn)是前端 video 標(biāo)簽不能直接播放,需要經(jīng)過處理才行。
處理方案,就是我們今天的主角:flv.js
協(xié)議與基礎(chǔ)實(shí)現(xiàn)
前面我們說到,flv 同時(shí)支持 WebSocket 和 HTTP 兩種傳輸方式,幸運(yùn)的是,flv.js 也同時(shí)支持這兩種協(xié)議。
選擇用 http 還是 ws,其實(shí)功能和性能上差別不大,關(guān)鍵看后端同學(xué)給我們什么協(xié)議吧。我這邊的選擇是 http,前后端處理起來都比較方便。
接下來我們介紹 flv.js 的具體接入流程,官網(wǎng)在這里
假設(shè)現(xiàn)在有一個(gè)直播流地址:http://test.stream.com/fetch-media.flv
,第一步我們按照官網(wǎng)的快速開始建一個(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
,代碼的第一行是檢測瀏覽器是否支持 flv.js,其實(shí)大部分瀏覽器是支持的。接下來就是獲取 video
標(biāo)簽的 DOM 元素。flv 會把處理后的 flv 流輸出給 video 元素,然后在 video 上實(shí)現(xiàn)視頻流播放。
接下來是關(guān)鍵之處,就是創(chuàng)建 flvjs.Player
對象,我們稱之為播放器實(shí)例。播放器實(shí)例通過 flvjs.createPlayer
函數(shù)創(chuàng)建,參數(shù)是一個(gè)配置對象,常用如下:
type
:媒體類型,flv
或mp4
,默認(rèn) flvisLive
:可選,是否是直播流,默認(rèn) truehasAudio
:是否有音頻hasVideo
:是否有視頻url
:指定流地址,可以是https(s)
orws(s)
上面的是否有音頻,視頻的配置,還是要看流地址是否有音視頻。比如監(jiān)控流只有視頻流沒有音頻,那即便你配置 hasAudio: true 也是不可能有聲音的。
播放器實(shí)例創(chuàng)建之后,接下來就是三步走:
掛載元素
:flvPlayer.attachMediaElement(videoEl)加載流
:flvPlayer.load()播放流
:flvPlayer.play()
基礎(chǔ)實(shí)現(xiàn)流程就這么多,下面再說一下處理過程中的細(xì)節(jié)和要點(diǎn)。
細(xì)節(jié)處理要點(diǎn)
上面說了基本的用法,下面說一下實(shí)踐中的關(guān)鍵問題。
暫停與播放
點(diǎn)播中的暫停與播放很容易,播放器下面會有一個(gè)播放/暫停按鍵,想什么時(shí)候暫停都可以,再點(diǎn)播放的時(shí)候會接著上次暫停的地方繼續(xù)播放。但是直播中就不一樣了。
正常情況下直播應(yīng)該是沒有播放/暫停按鈕以及進(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)擊暫停按鈕,視頻也是會停住的,這與點(diǎn)播邏輯一致。但是如果你再點(diǎn)播放,視頻還是會從暫停處繼續(xù)播放,這就不對了。
那么我們換個(gè)角度,重新審視一下直播的播放/暫停邏輯。
直播為什么需要暫停?拿我們視頻監(jiān)控來說,一個(gè)頁面會放好幾個(gè)攝像頭的監(jiān)控視頻,如果每個(gè)播放器一直與服務(wù)器保持連接,持續(xù)拉流,這會造成大量的連接和消耗,流失的都是白花花的銀子。
那我們是不是可以這樣:進(jìn)去網(wǎng)頁的時(shí)候,找到想看的攝像頭,點(diǎn)擊播放再拉流。當(dāng)你不想看的時(shí)候,點(diǎn)擊暫停,播放器斷開連接,這樣是不是就會節(jié)省無用的流量消耗。
因此,直播中的播放/暫停,核心邏輯是拉流/斷流。
理解到這里,那我們的方案應(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 接入直播流的過程會遇到各種問題,有的是后端數(shù)據(jù)流的問題,有的是前端處理邏輯的問題。因?yàn)榱魇菍?shí)時(shí)獲取,flv 也是實(shí)時(shí)轉(zhuǎn)化輸出,因此一旦發(fā)生錯(cuò)誤,瀏覽器控制臺會循環(huán)連續(xù)的打印異常。
如果你用 react 和 ts,滿屏異常,你都無法開發(fā)下去了。再有直播流本來就可能發(fā)生許多異常,因此錯(cuò)處理非常關(guān)鍵。
官方對異常處理的說明不太明顯,我簡單總結(jié)一下:
首先,flv.js 的異常分為兩個(gè)級別,可以看作是 一級異常
和 二級異常
。
再有,flv.js 有一個(gè)特殊之處,它的 事件
和 錯(cuò)誤
都是用枚舉來表示,如下:
flvjs.Events
:表示事件flvjs.ErrorTypes
:表示一級異常flvjs.ErrorDetails
:表示二級異常
下面介紹的異常和事件,都是基于上述枚舉,你可以理解為是枚舉下的一個(gè) key
值。
一級異常有三類:
NETWORK_ERROR
:網(wǎng)絡(luò)錯(cuò)誤,表示連接問題MEDIA_ERROR
:媒體錯(cuò)誤,格式或解碼問題OTHER_ERROR
:其他錯(cuò)誤
二級級異常常用的有三類:
NETWORK_STATUS_CODE_INVALID
:HTTP 狀態(tài)碼錯(cuò)誤,說明 url 地址有誤NETWORK_TIMEOUT
:連接超時(shí),網(wǎng)絡(luò)或后臺問題MEDIA_FORMAT_UNSUPPORTED
:媒體格式不支持,一般是流數(shù)據(jù)不是 flv 的格式
了解這些之后,我們在播放器實(shí)例上監(jiān)聽異常:
// 監(jiān)聽錯(cuò)誤事件 flvPlayer.on(flvjs.Events.ERROR, (err, errdet) => { // 參數(shù) err 是一級異常,errdet 是二級異常 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)??梢酝ㄟ^以下方法監(jiān)聽視頻流加載完成:
player.on(flvjs.Events.METADATA_ARRIVED, () => { console.log('視頻加載完成') })
樣式定制
為什么會有樣式定制?前面我們說了,直播流的播放/暫停邏輯與點(diǎn)播不同,因此我們要隱藏 video
的操作欄元素,通過自定義元素來實(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è)全屏按鈕,看一下全屏的邏輯怎么寫:
const fullPage = () => { let dom = document.querySelector('.video') if (dom.requestFullscreen) { dom.requestFullscreen() } else if (dom.webkitRequestFullScreen) { dom.webkitRequestFullScreen() } }
其他自定義樣式,比如你要做彈幕,在 video 上面蓋一層元素自行實(shí)現(xiàn)就可以了。
以上就是通過flv.js播放監(jiān)控示例深入探究直播流技術(shù)的詳細(xì)內(nèi)容,更多關(guān)于flv.js播放監(jiān)控直播流的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
googlemap 之 javascript實(shí)現(xiàn)方法
googlemap 之 javascript實(shí)現(xiàn)方法...2007-01-01JavaScript實(shí)現(xiàn)獲取本機(jī)IP地址
這篇文章主要介紹了JavaScript實(shí)現(xiàn)獲取本機(jī)IP地址方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07學(xué)習(xí)使用Bootstrap輸入框、導(dǎo)航、分頁等常用組件
這篇文章主要教大家學(xué)習(xí)使用Bootstrap輸入框、導(dǎo)航、分頁等常用組件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05JavaScript實(shí)現(xiàn)一個(gè)電子小蜘蛛
這篇文章主要介紹了JavaScript實(shí)現(xiàn)一個(gè)電子小蜘蛛,具體的樣子就是讓它會跟隨著我們的鼠標(biāo)進(jìn)行移動,那么我們?nèi)绾螌?shí)現(xiàn)這樣的效果呢,下面來詳細(xì)講解實(shí)現(xiàn)方法,需要的朋友可以參考下2024-10-10javascript獲得服務(wù)器端控件的ID的實(shí)現(xiàn)代碼
javascript獲得服務(wù)器端控件的ID的實(shí)現(xiàn)代碼,需要的朋友可以參考下。2011-12-12