博客 / 詳情

返回

巧用 requestVideoFrameCallback 優化直播狀態檢測

作為保利威前端團隊的開發者,持續優化 Web 端的直播體驗是我們的核心目標。為此,我們深入研究了各種播放場景下的細節實現。本文將以兩個常見狀態的檢測為例,分享我們發現問題以及優化的過程。

基於 HTML Video 元素的直播播放器,通常需要在播放卡頓時呈現加載中的交互。

保利威直播播放器卡頓交互

它的代碼實現可能是這樣的:

video.addEventListener('waiting', function() {
  console.info('show loading');
}, false);

video.addEventListener('playing', function() {
  console.info('hide loading');
}, false);

然而,這個方案是不可靠的,在移動設備播放 HLS(M3U8)直播流的場景下會有諸多問題:

  • 部分機型或瀏覽器在緩衝視頻時不會觸發 waiting,恢復播放後不會觸發 playing。
  • 部分機型或瀏覽器在播放尚未恢復時就觸發了 playing。

於是,就有了基於 timeupdate 事件的改良方案:

let timer;
function onTimeUpdate() {
  if (timer) { clearTimeout(timer); }
  console.info('hide loading');
  timer = setTimeout(function() {
    if (!video.paused) {
      console.info('show loading');            
    }
  }, 1000);
}

video.addEventListener('timeupdate', onTimeUpdate, false);

只要視頻在播放,timeupdate 事件就會不斷觸發,從而清理上一次回調時創建的定時器,通過定時器設定的函數就不會執行。反之,只要 1 秒內沒有觸發 timeupdate 事件,通過定時器設定的函數就會執行,從而顯示加載中的交互。

這個方案在保利威直播播放器中運行了很長一段時間,直到在一次限速測試中發現:在個別 iOS 版本中,即使直播卡頓,timeupdate 仍在繼續觸發,從而導致加載中的交互沒有顯示。苦惱之際,我發現了 requestVideoFrameCallback。

requestVideoFrameCallback

requestVideoFrameCallback 是 HTML Video 元素的方法,它可以註冊一個回調函數。該回調函數在一個新的視頻幀發送到合成器時執行。

function callback() {
  console.info('requestVideoFrameCallback');
}
video.requestVideoFrameCallback(callback);

雖然視頻播放過程中會不斷產生新的視頻幀,但是通過 requestVideoFrameCallback 註冊的回調函數僅在下一幀發送到合成器時觸發一次。如果希望回調函數隨着視頻幀的產生不斷執行,就要繼續將其註冊為下一幀的回調。

function callbackAndRegisterNext(isFirst) {
  console.info('requestVideoFrameCallback');
  video.requestVideoFrameCallback(callbackAndRegisterNext);
}
video.requestVideoFrameCallback(callbackAndRegisterNext);

此時可以發現,與 timeupdate 方案的原理類似,通過在 requestVideoFrameCallback 註冊的回調函數中設定定時執行的函數,也可以判斷視頻是否正在播放,從而顯示或隱藏加載中的交互。

function checkPlayingByRVFC() {
  onTimeUpdate();
  video.requestVideoFrameCallback(checkPlaying);   
}
checkPlayingByRVFC();

然而,requestVideoFrameCallback 的兼容性相對較差,比如 iOS 最低支持版本是 15.4。有些瀏覽器甚至掛羊頭賣狗肉,雖然表面上支持這個接口,但註冊的回調根本不會執行。因此,使用 requestVideoFrameCallback 方案前要先檢查兼容性:

function canUseRVFC(video, cb) {
  if (video.requestVideoFrameCallback) {
    video.requestVideoFrameCallback(function() {
      // 觸發過一次即為支持
      cb(true);
    });
  } else {
    cb(false);
  }
}

video.addEventListener('timeupdate', onTimeUpdate, false);

canUseRVFC(video, function(result) {
  if (result) {
    // 支持 requesVideoFrameCallback 就不需要用 timeupdate 方案了
    video.removeEventListener('timeupdate', onTimeUpdate, false);
    checkPlayingByRVFC();
  }
});

直播結束的檢測

在直播場景下,HTML Video 元素的 ended 事件是不會觸發的,這就需要開發者以其他方式去判斷直播是否結束。

常用的方法是在後端維護直播狀態,開播端開播時將其設為直播中,開播端下播後將其設為直播結束。前端通過輪詢、SSE 或 WebSocket 獲取該狀態。然而,考慮到兼容性,移動 Web 端通常會採用 HLS 作為直播流協議,延遲通常會達到十幾甚至幾十秒。也就是説,開播端下播後,觀眾端也需要這麼長的時間,才能播完剩下的內容。由於後端維護的直播狀態是實時的,如果前端收到直播狀態為結束時就掐斷直播,剩下的這部分內容就無法播完。

根據主流雲服務廠商的表現,直播結束後的短時間內,HLS 拉流地址就會不存在,返回 404 狀態碼。不過終端已加載的 ts 片仍然可以繼續播放,直到播放完畢,畫面就會卡住,然後黑屏。因此,關鍵點還是在於檢測視頻是否在播放。只有後端返回的直播狀態是結束,視頻也沒有在播放,且拉流地址不存在,才可以判定為直播結束

let isStatusEnd;
let isPlaying;

// 檢查拉流地址是否存在
// 由於拉流地址可能不會馬上不存在,所以也要輪詢
let videoSrcTimer;
async function checkVideoSrc() {
  if (videoSrcTimer) { clearTimeout(videoSrcTimer); }
  if (isStatusEnd && !isPlaying) {
    // 僅需獲取狀態碼,用 HEAD 方法請求足矣
    const response = await fetch(video.src, { method: 'HEAD' });
    if (response.status === 404) {
      console.info('直播結束');
    } else {
      videoSrcTimer = setTimeout(checkStatus, 5 * 1000);
    }
  }
}

let rvfcTimer;
function checkEndedByRVFC() {
  if (rvfcTimer) { clearTimeout(rvfcTimer); }
  isPlaying = true;
  rvfcTimer = setTimeout(function() {
    isPlaying = false;
    checkVideoSrc(); // 直播不在播放時才去檢查拉流地址
  }, 1000);
  video.requestVideoFrameCallback(checkEndedByRVFC);   
}

// 每 10s 查詢一次後端的直播狀態
async function checkStatus() {
  const response = await fetch('後端獲取直播狀態的接口');
  const result = await response.json();
  if (result.status === 'end') {
    isStatusEnd = true;
    checkEndedByRVFC(); // 接口返回結束時才檢查視頻是否在播放
  } else {
    setTimeout(checkStatus, 10 * 1000);
  }
}

checkStatus();

總結

requestVideoFrameCallback 在支持的平台上確實能提供更準確的播放狀態反饋,是優化播放器體驗的重要方式。但其較差的兼容性要求我們必須有健壯的降級機制。

上述方案已在保利威直播播放器中穩定運行,有效處理了多種移動設備和複雜網絡環境下的交互問題。希望對遇到類似問題的朋友有所啓發。如果你有更好的思路或遇到其他“坑點”,歡迎在評論中交流。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.