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

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

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

透過此 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。

資料來源

聚酯纖維

您可以使用針對 requestVideoFrameCallback() 方法的 polyfill,其依據為 Window.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality()。使用前,請注意 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);

在回呼中,nowDOMHighResTimeStamp,而 metadataVideoFrameMetadata 字典,其中包含下列屬性:

  • 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。透過 API 對網頁所做的變更,需要一個 vsync 才能顯示在畫面上 (與 window.requestAnimationFrame() 相同)。因此,如果您持續更新網頁上的 mediaTime 或影格編號,並將其與編號的影片影格進行比較,最終影片看起來會比實際影格提前一個影格。

實際上,畫面會在 vsync x 時準備就緒,回呼會在 vsync x+1 時觸發,畫面會在 vsync x+2 時算繪,回呼中所做的變更會在 vsync x+2 時算繪。您可以檢查 metadata.expectedDisplayTime 是否大約為 now 或未來的一個 vsync,藉此確認回呼是否為 vsync 延遲 (且影格已在畫面上算繪)。如果 expectedDisplayTimenow 相差約五到十微秒,表示影格已算繪製完成;如果 expectedDisplayTime 約在未來十六毫秒後 (假設瀏覽器/螢幕以 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 上提供。