1. 前言 最近在處理前端直播的業務,根據業務需要,使用 flv.js 的方案播放實時的flv視頻流。不得不承認,flv.js 是一個偉大的庫。 在使用flv.js開發的過程中,遇到了一些問題,也無外乎是視頻延遲,視頻卡頓等問題,經過在github issues里摸爬滾打,加上長時間的試錯,將這些問 ...
目錄
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="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,具體的可以看下官方文檔 - 掛載元素
- 載入視頻流
- 播放視頻流
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
- 通過為流分配不同的服務端地址