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

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

HTMLVideoElement.requestVideoFrameCallback() 方法可讓網頁作者註冊一個回呼,在新影格傳送至合成器時於轉譯步驟中執行。這可讓開發人員針對每個影格,有效地執行影片處理和繪製至畫布、影片分析或與外部音訊來源同步的作業。

與 requestAnimationFrame() 的差異

透過此 API 使用 drawImage() 將影片影格繪製至畫布上的作業,會盡可能與螢幕上播放的影片影格率同步。與 window.requestAnimationFrame() 不同,requestVideoFrameCallback() 通常每秒觸發約 60 次,但會綁定至實際的影片影格速率,但有一個重要的例外狀況

回呼執行的有效速率,是影片速率和瀏覽器速率中較低的速率。也就是說,在以 60Hz 繪圖的瀏覽器中播放的 25fps 影片,會以 25Hz 觸發回呼。在同一個 60Hz 瀏覽器中,120fps 的影片會以 60Hz 的頻率觸發回呼。

檔案名稱取名須知

由於與 window.requestAnimationFrame() 相似,這個方法最初video.requestAnimationFrame() 命名,後來改為 requestVideoFrameCallback(),這是在長時間討論後達成的共識。

特徵偵測

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

瀏覽器支援

瀏覽器支援

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

資料來源

聚酯纖維

您可以使用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 可能會相較於轉譯影片影格時延遲一個 vsync。這個 vsync 作業會取得透過 API 對網頁進行的變更 (與 window.requestAnimationFrame() 相同)。因此,如果您持續更新網頁上的 mediaTime 或影格編號,並將這組號碼與編號的影片影格比較,最終影片看起來會像前一個影格。

實際上,畫面會在 vsync x 時準備就緒,回呼會在 vsync x+1 時觸發,畫面會在 vsync x+2 時算繪。您可以檢查 metadata.expectedDisplayTime 是否大約等於 now,或在未來的一個 vsync,藉此確認回呼是否為 vsync 延遲 (且影格已在畫面上算繪)。如果 expectedDisplayTimenow 相差約五到十微秒,表示影格已算繪製完成;如果 expectedDisplayTime 約在 16 毫秒之後 (假設瀏覽器/螢幕以 60Hz 的速度重新整理),表示您與影格同步。

示範

我已在 Glitch 上建立一個小型示範,說明如何以精確的影片影格速率在畫布上繪製影格,以及將影格中繼資料記錄在何處,以利偵錯。

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 審查。主頁橫幅Denise Jans 在 Unsplash 上提供。