有效執行每個影片影格的作業

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

發布日期:2023 年 1 月 8 日

Browser Support

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

Source

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

與 requestAnimationFrame() 的差異

使用這個 API 執行的作業 (例如使用 drawImage() 將影片影格繪製到畫布上),會盡量與螢幕上播放的影片影格速率同步。這與 window.requestAnimationFrame() 不同,後者通常每秒會觸發約 60 次。

requestVideoFrameCallback() 會受到實際影片影格速率的限制,但有重要的例外狀況

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

特徵偵測

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

Polyfill

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

使用方法

如果您使用 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 大約在十六毫秒後 (假設瀏覽器以 60Hz 重新整理),則影格會同步。

示範

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

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 審查。