使用 requestVideoFrameCallback() 對影片執行每個影片影格的有效作業

瞭解如何使用 requestVideoFrameCallback(),在瀏覽器中更有效率地處理影片。

網頁作者可透過 HTMLVideoElement.requestVideoFrameCallback() 方法註冊回呼,在新的影片影格傳送至合成器時,於轉譯步驟中執行。開發人員可藉此對影片執行有效率的逐影格作業,例如影片處理和繪製到畫布、影片分析,或是與外部音訊來源同步。

與 requestAnimationFrame() 的差異

透過這個 API 執行的作業 (例如使用 drawImage() 將影片影格繪製到畫布上),會盡量與螢幕上播放影片的影格速率同步。與 window.requestAnimationFrame()不同,requestVideoFrameCallback()會繫結至實際影片影格速率,但有重要的例外狀況

回呼的有效執行率是影片的速率和瀏覽器的速率兩相比較後,較低的速率。也就是說,如果瀏覽器以 60Hz 繪製,但播放的影片為 25fps,回呼就會以 25Hz 觸發。在 60Hz 瀏覽器中,120fps 影片會以 60Hz 觸發回呼。

檔案名稱取名須知

由於與 window.requestAnimationFrame() 相似,這個方法最初提議命名為 video.requestAnimationFrame(),後來重新命名為 requestVideoFrameCallback(),經過長時間討論後達成共識。

特徵偵測

if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
  // The API is supported!
}

瀏覽器支援

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: 132.
  • Safari: 15.4.

Source

Polyfill

Window.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality() 為基礎,提供 requestVideoFrameCallback() 方法的 polyfill。使用這項功能前,請注意README中提及的限制。

使用 requestVideoFrameCallback() 方法

如果您曾使用 requestAnimationFrame() 方法,會立即熟悉 requestVideoFrameCallback() 方法。您只需註冊一次初始回呼,之後每當回呼觸發時,再重新註冊即可。

const doSomethingWithTheFrame = (now, metadata) => {
  // Do something with the frame.
  console.log(now, metadata);
  // Re-register the callback to be notified about the next frame.
  video.requestVideoFrameCallback(doSomethingWithTheFrame);
};
// Initially register the callback to be notified about the first frame.
video.requestVideoFrameCallback(doSomethingWithTheFrame);

在回呼中,nowDOMHighResTimeStampmetadata 則是 VideoFrameMetadata 字典,包含下列屬性:

  • presentationTime,類型為 DOMHighResTimeStamp: 使用者代理程式提交影格以進行組合的時間。
  • expectedDisplayTime,類型為 DOMHighResTimeStamp:使用者代理程式預期影格顯示的時間。
  • width,型別為 unsigned long: 影片影格的寬度,以媒體像素為單位。
  • height,型別為 unsigned long: 影片影格的高度,以媒體像素為單位。
  • mediaTime,類型為 double: 顯示影格的媒體呈現時間戳記 (PTS),以秒為單位 (例如在 video.currentTime 時間軸上的時間戳記)。
  • presentedFrames,類型為 unsigned long: 提交用於合成的影格數量。允許用戶端判斷 VideoFrameRequestCallback 執行個體之間是否遺失影格。
  • processingDuration,類型為 double: 從提交編碼封包 (與這個影格具有相同的呈現時間戳記 (PTS),例如與 mediaTime 相同),到解碼器準備好呈現解碼影格為止,經過的時間長度 (以秒為單位)。

如果是 WebRTC 應用程式,可能會顯示其他屬性:

  • captureTime,類型為 DOMHighResTimeStamp: 如果是來自本機或遠端來源的影片影格,這是指攝影機擷取影格的時間。 如果是遠端來源,系統會使用時鐘同步和 RTCP 傳送端報表估算擷取時間,將 RTP 時間戳記轉換為擷取時間。
  • receiveTime,類型為 DOMHighResTimeStamp: 如果是來自遠端來源的影片影格,這是平台收到編碼影格的時間,也就是透過網路收到屬於這個影格的最後一個封包的時間。
  • rtpTimestamp,類型為 unsigned long: 與這個視訊影格相關聯的 RTP 時間戳記。

這個清單中特別值得注意的項目是 mediaTime。 Chromium 的實作會使用音訊時鐘做為支援 video.currentTime 的時間來源,而 mediaTime 則會直接由影格的 presentationTimestamp 填入。如果您想以可重現的方式準確識別影格,包括準確找出錯過的影格,就應該使用 mediaTime

如果畫面似乎差了一格…

垂直同步 (或簡稱 VSync) 是一種圖像技術,可同步處理影片的畫面更新率和螢幕的更新率。 由於 requestVideoFrameCallback() 會在主執行緒上執行,但視訊合成作業會在合成器執行緒上進行,因此這個 API 的所有作業都是盡力而為,瀏覽器不會提供任何嚴格的保證。可能的原因是,相較於影片影格的算繪時間,API 可能會延遲一個垂直同步訊號。透過 API 對網頁所做的變更,需要一個垂直同步訊號才能顯示在畫面上 (與 window.requestAnimationFrame() 相同)。 因此,如果您持續更新網頁上的 mediaTime 或影格編號,並與編號的影片影格進行比較,最終影片看起來會領先一個影格。

實際情況是,影格會在垂直同步 x 時準備就緒,回呼會觸發,影格會在垂直同步 x+1 時算繪,而回呼中進行的變更會在垂直同步 x+2 時算繪。您可以檢查 metadata.expectedDisplayTime 是否大約是 now 或一個 VSync 週期後的時間,判斷回呼是否為 VSync 延遲 (且影格已在畫面上算繪)。如果時間在 now 的五到十微秒內,影格就已算繪完成;如果 expectedDisplayTime 大約在十六毫秒後 (假設瀏覽器/螢幕以 60 Hz 重新整理),則表示您與影格同步。

示範

我建立了一個小型示範,說明如何以影片的確切畫面更新率在畫布上繪製影格,以及如何記錄影格中繼資料以進行偵錯。

let paintCount = 0;
let startTime = 0.0;

const updateCanvas = (now, metadata) => {
  if (startTime === 0.0) {
    startTime = now;
  }

  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

  const elapsed = (now - startTime) / 1000.0;
  const fps = (++paintCount / elapsed).toFixed(3);
  fpsInfo.innerText = `video fps: ${fps}`;
  metadataInfo.innerText = JSON.stringify(metadata, null, 2);

  video.requestVideoFrameCallback(updateCanvas);
};

video.requestVideoFrameCallback(updateCanvas);

結論

長期以來,使用者都是在沒有實際影格的情況下,僅根據 video.currentTime 進行影格層級的處理。requestVideoFrameCallback() 方法大幅改善了這個解決方法。

特別銘謝

requestVideoFrameCallback API 由 Thomas Guilbert 指定及實作。本文由 Joe MedleyKayce Basques 審查。主頁橫幅:Unsplash 上的 Denise Jans