目錄
- 1. 前言
- 2. 前端直播
- 2.1 常見直播協議
- 2.2 flv.js 的原理
- 2.3 flv.js 的簡單使用
- 3. flv.js 的優化方案
- 3.1 追幀-解決延遲累積問題
- 3.2 斷流重連
- 3.3 實時更新
- 3.4 解決 stuck 問題
- 4. 封裝插件 flvExtend.js
- 5. 其他問題
- 參考
1. 前言
最近在處理前端直播的業務,根據業務需要,使用 flv.js 的方案播放實時的flv視頻流,不得不承認,flv.js 是一個偉大的庫,
在使用flv.js開發的程序中,遇到了一些問題,也無外乎是視頻延遲,視頻卡頓等問題,經過在github issues里摸爬滾打,加上長時間的試錯,將這些問題歸納出了對應的解決方案,也自己封裝了一個擴展插件 flvExtend,
于是寫這篇文章來對我遇到的一些問題進行總結,我提出的解決方案不一定適合所有場景,如果有更好的解決方案,歡迎討論,這也是我寫這篇文章的目的,也是我寫文章的初心,
2. 前端直播
在講解 flv.js 的優化方案之前,我想先簡單的介紹一下前端直播的方案,為什么要使用 flv.js,方便大家理解以及作為一項技術來儲備,
2.1 常見直播協議
- RTMP: 底層基于 TCP,在瀏覽器端依賴 Flash,
- HTTP-FLV: 基于 HTTP 流式 IO 傳輸 FLV,依賴瀏覽器支持播放 FLV,
- WebSocket-FLV: 基于 WebSocket 傳輸 FLV,依賴瀏覽器支持播放 FLV,WebSocket 建立在 HTTP 之上,建立 WebSocket 連接前還要先建立 HTTP 連接,
- HLS: Http Live Streaming,蘋果提出基于 HTTP 的流媒體傳輸協議,HTML5 可以直接打開播放,
- RTP: 基于 UDP,延遲 1 秒,瀏覽器不支持,
可以看到,在瀏覽器端,可以考慮的方案有:HTTP-FLV、WebSocket-FLV 以及 HLS, 我們可以對比一下這幾個直播協議之間的性能:
(以下資料來源于網路,只做對比參考)
| 傳輸協議 | 播放器 | 延遲 | 記憶體 | CPU |
|---|---|---|---|---|
| RTMP | Flash | 1s | 430M | 11% |
| HTTP-FLV | Video | 1s | 310M | 4.4% |
| HLS | Video | 20s | 205M | 3% |
可以看出在瀏覽器里做直播,使用 HTTP-FLV 協議是不錯的,性能優于 RTMP+Flash,延遲可以做到和 RTMP+Flash 一樣甚至更好,
2.2 flv.js 的原理
flv.js 的主要作業就是,在獲取到 FLV 格式的音視頻資料后通過原生的 JS 去解碼 FLV 資料,再通過 Media Source Extensions API 喂給原生 HTML5 Video 標簽,(HTML5 原生僅支持播放 mp4/webm 格式,不支持 FLV)
flv.js 為什么要繞一圈,從服務器獲取 FLV 再解碼轉換后再喂給 Video 標簽呢?原因如下:
- 兼容目前的直播方案:目前大多數直播方案的音視頻服務都是采用 FLV 容器格式傳輸音視頻資料,
- FLV 容器格式相比于 MP4 格式更加簡單,決議起來更快更方便,
2.3 flv.js 的簡單使用
<script src="https://www.cnblogs.com/xiahj/archive/2022/07/20/flv.min.js"></script>
<video id="videoElement"></video>
<script>
if (flvjs.isSupported()) {
var videoElement = document.getElementById("videoElement");
var flvPlayer = flvjs.createPlayer({
type: "flv",
isLive: true,
url: "http://example.com/flv/video.flv",
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
</script>
主要流程就是:
- 創建
flvjs.Player物件,可以傳遞兩個引數:MediaDataSource,以及 Config,具體的可以看下官方檔案 - 掛載元素
- 加載視頻流
- 播放視頻流
附:官方 API 檔案
3. flv.js 的優化方案
我們根據官方的例子,可以很容易地把 flv 直播流播起來,但是在實際專案中使用時,還會遇到一些問題,我們需要手動對這些問題進行優化處理
3.1 追幀-解決延遲累積問題
flv.js 有一個最大的問題,就是延遲問題,一方面是直播端的延遲,一方面是瀏覽器的延遲,而且瀏覽器的延遲如果不做特殊處理,會造成延時累積的問題,對直播的實時性影響很大,
解決方案需要從以下兩部分入手:
3.1.1 修改 config 配置
{
enableWorker: true, // 啟用分離的執行緒進行轉換
enableStashBuffer: false, // 關閉IO隱藏緩沖區
stashInitialSize: 128, // 減少首幀顯示等待時長
}
- 開啟 flv.js 的 Worker,多執行緒運行 flv.js 提升決議速度可以優化延遲
- 關閉 buffer 快取,這個選項可以明顯地降低延遲,缺點就是由于關閉了 buffer 快取,網路不好的時候可能會出現 loading 加載
- 調低 IO 緩沖區的初始尺寸,減少首幀顯示的等待時長
3.1.2 追幀設定
解決延時累加最有效的方式就是進行追幀設定
追幀,就是去判斷緩沖區末尾的 buffer 值與當前播放時間的差值,如果大于某個值,就進行追幀設定,具體的思路如下:
- 首先,在 progress 事件,或者定時器中進行追幀邏輯
- 判斷 buffer 的差值
delta
let end = this.player.buffered.end(0); //獲取當前buffered值(緩沖區末尾)
let delta = end - this.player.currentTime; //獲取buffered與當前播放位置的差值
- 如果
delta值大于某個設定的值,則進行追幀操作 - 追幀有兩種方式
1)一種是直接更新當前的時間:this.player.currentTime = this.player.buffered.end(0) - 1,缺點是如果頻繁觸發會導致跳幀,觀感差;
2)一種是調快播放速度的方式來慢慢追幀:this.videoElement.playbackRate = 1.1,優點是穩定,缺點是如果 delta 值過大,通過這種方式追得太慢
在實際使用中兩種方式可以結合起來,
代碼實作:
videoElement.addEventListener("progress", () => {
let end = player.buffered.end(0); //獲取當前buffered值(緩沖區末尾)
let delta = end - player.currentTime; //獲取buffered與當前播放位置的差值
// 延遲過大,通過跳幀的方式更新視頻
if (delta > 10 || delta < 0) {
this.player.currentTime = this.player.buffered.end(0) - 1;
return;
}
// 追幀
if (delta > 1) {
videoElement.playbackRate = 1.1;
} else {
videoElement.playbackRate = 1;
}
});
3.2 斷流重連
斷流重連即在flvjs播放失敗的回呼中,進行重建視頻的操作
代碼實作:
this.player.on(flvjs.Events.ERROR, (e) => {
// destroy
this.player.pause();
this.player.unload();
this.player.detachMediaElement();
this.player.destroy();
this.player = null;
// 進行重建的邏輯,這里不再展開
this.init();
});
3.3 實時更新
直播需要保證視頻的實時性,以下兩種操作都會導致視頻的實時性得不到保證:
- 用戶點擊了暫停,過一段時間后再點播放,這時候的直播視頻不是最新的
- 網頁切到后臺,再重新切換回前臺,視頻不是最新的
所以需要根據這兩種情況來實時更新視頻
代碼實作:
// 點擊播放按鈕后,更新視頻
videoElement.addEventListener("play", () => {
let end = player.buffered.end(0) - 1;
this.player.currentTime = end;
});
// 網頁重新激活后,更新視頻
window.onfocus = () => {
let end = player.buffered.end(0) - 1;
this.player.currentTime = end;
};
3.4 解決 stuck 問題
有的時候,視頻在播放的程序中會突然卡住,或者控制臺有時會報錯 “Playback seems stuck at 0, seek to 1.1”,
我們需要判斷視頻是否卡住了,然后重建視頻實體
思路就是判斷 decodedFrames 是否產生變化,如果視頻是播放狀態并且該值沒有產生變化,則可以判斷視頻卡住了,
代碼實作:
function handleStuck() {
let lastDecodedFrames = 0;
let stuckTime = 0;
this.interval && clearInterval(this.interval);
this.interval = setInterval(() => {
const decodedFrames = this.player.statisticsInfo.decodedFrames;
if (!decodedFrames) return;
if (lastDecodedFrames === decodedFrames && !this.videoElement.paused) {
// 可能卡住了,多載
stuckTime++;
if (stuckTime > 1) {
console.log(`%c 卡住,重建視頻`, "background:red;color:#fff");
// 先destroy,再重建視頻實體
this.rebuild();
}
} else {
lastDecodedFrames = decodedFrames;
stuckTime = 0;
}
}, 800);
}
4. 封裝插件 flvExtend.js
我將這些優化方案封裝成了一個插件 flvExtend.js,它相當于是 flv.js 的一個功能擴展
插件地址:https://github.com/shady-xia/flvExtend
使用起來是這個樣子:
import FlvExtend from "flv-extend";
// 配置需要的功能
const flv = new FlvExtend({
element: videoElement, // *必傳
frameTracking: true, // 開啟追幀設定
updateOnStart: true, // 點擊播放后更新視頻
updateOnFocus: true, // 獲得焦點后更新視頻
reconnect: true, // 開啟斷流重連
reconnectInterval: 2000, // 斷流重連間隔
});
// 呼叫 init 方法初始化視頻
// init 方法的引數與 flvjs.createPlayer 相同,并回傳 flvjs.player 實體
const player = flv.init(
{
type: "flv",
url: "http://192.168.0.11/stream",
isLive: true,
},
{
enableStashBuffer: false, // 如果您需要實時(最小延遲)來進行實時流播放,則設定為false
stashInitialSize: 128, // 減少首幀顯示等待時長
}
);
// 直接呼叫play即可播放
player.play();
5. 其他問題
這里打算長期記錄一下遇到的問題以及解決思路,歡迎大家討論,我會更新補充
1)多路視頻同時直播
由于瀏覽器對 http 1.0 的限制,以Chrome為例,同一個瀏覽器下,最多只能播6路同源地址下的視頻(包括多個標簽頁也會被合算在內)
目前的解決方案有:
- 使用http 2.0,由于http 2.0的多路復用,可以同屏播放多個視頻流
- 使用 websocket
- 通過為流分配不同的服務端地址
參考
- github issues
- 使用 flv.js 做直播
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/499905.html
標籤:其他
