最近在處理前端直播的業務,根據業務需要,使用 flv.js 的方案播放實時的flv視訊流。不得不承認,flv.js 是一個偉大的庫。
在使用flv.js開發的過程中,遇到了一些問題,也無外乎是視訊延遲,視訊卡頓等問題,經過在github issues裡摸爬滾打,加上長時間的試錯,將這些問題歸納出了對應的解決方案,也自己封裝了一個擴充套件外掛 flvExtend。
於是寫這篇文章來對我遇到的一些問題進行總結,我提出的解決方案不一定適合所有場景,如果有更好的解決方案,歡迎討論,這也是我寫這篇文章的目的,也是我寫文章的初心。
在講解 flv.js 的優化方案之前,我想先簡單的介紹一下前端直播的方案,為什麼要使用 flv.js,方便大家理解以及作為一項技術來儲備。
可以看到,在瀏覽器端,可以考慮的方案有: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 一樣甚至更好。
flv.js 的主要工作就是,在獲取到 FLV 格式的音視訊資料後通過原生的 JS 去解碼 FLV 資料,再通過 Media Source Extensions API 餵給原生 HTML5 Video 標籤。(HTML5 原生僅支援播放 mp4/webm 格式,不支援 FLV)
flv.js 為什麼要繞一圈,從伺服器獲取 FLV 再解碼轉換後再餵給 Video 標籤呢?原因如下:
<script src="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,具體的可以看下官方檔案我們根據官方的例子,可以很容易地把 flv 直播流播起來,但是在實際專案中使用時,還會遇到一些問題,我們需要手動對這些問題進行優化處理
flv.js 有一個最大的問題,就是延遲問題,一方面是直播端的延遲,一方面是瀏覽器的延遲,而且瀏覽器的延遲如果不做特殊處理,會造成延時累積的問題,對直播的實時性影響很大。
解決方案需要從以下兩部分入手:
3.1.1 修改 config 設定
{
enableWorker: true, // 啟用分離的執行緒進行轉換
enableStashBuffer: false, // 關閉IO隱藏緩衝區
stashInitialSize: 128, // 減少首幀顯示等待時長
}
3.1.2 追幀設定
解決延時累加最有效的方式就是進行追幀設定
追幀,就是去判斷緩衝區末尾的 buffer 值與當前播放時間的差值,如果大於某個值,就進行追幀設定,具體的思路如下:
delta
let end = this.player.buffered.end(0); //獲取當前buffered值(緩衝區末尾)
let delta = end - this.player.currentTime; //獲取buffered與當前播放位置的差值
delta
值大於某個設定的值,則進行追幀操作this.player.currentTime = this.player.buffered.end(0) - 1
,缺點是如果頻繁觸發會導致跳幀,觀感差;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;
}
});
斷流重連即在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();
});
直播需要保證視訊的實時性,以下兩種操作都會導致視訊的實時性得不到保證:
所以需要根據這兩種情況來實時更新視訊
程式碼實現:
// 點選播放按鈕後,更新視訊
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;
};
有的時候,視訊在播放的過程中會突然卡住,或者控制檯有時會報錯 「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);
}
我將這些優化方案封裝成了一個外掛 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();
這裡打算長期記錄一下遇到的問題以及解決思路,歡迎大家討論,我會更新補充
1)多路視訊同時直播
由於瀏覽器對 http 1.0 的限制,以Chrome為例,同一個瀏覽器下,最多隻能播6路同源地址下的視訊(包括多個分頁也會被合算在內)
目前的解決方案有: