flv.js的追幀、斷流重連及實時更新的直播優化方案

2022-07-20 15:00:18

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-FLVWebSocket-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 標籤呢?原因如下:

  1. 相容目前的直播方案:目前大多數直播方案的音視訊服務都是採用 FLV 容器格式傳輸音視訊資料。
  2. 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>

主要流程就是:

  1. 建立flvjs.Player物件,可以傳遞兩個引數:MediaDataSource,以及 Config,具體的可以看下官方檔案
  2. 掛載元素
  3. 載入視訊流
  4. 播放視訊流

附:官方 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 值與當前播放時間的差值,如果大於某個值,就進行追幀設定,具體的思路如下:

  1. 首先,在 progress 事件,或者定時器中進行追幀邏輯
  2. 判斷 buffer 的差值 delta
let end = this.player.buffered.end(0); //獲取當前buffered值(緩衝區末尾)
let delta = end - this.player.currentTime; //獲取buffered與當前播放位置的差值
  1. 如果 delta 值大於某個設定的值,則進行追幀操作
  2. 追幀有兩種方式
    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路同源地址下的視訊(包括多個分頁也會被合算在內)

目前的解決方案有:

  1. 使用http 2.0,由於http 2.0的多路複用,可以同屏播放多個視訊流
  2. 使用 websocket
  3. 通過為流分配不同的伺服器端地址

參考