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

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

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

requestAnimationFrame() 的差異

透過這個 API 製作的 drawImage() 等作業,系統會將影片影格速率同步至畫布,盡可能同步處理影片的畫面更新率。不同於 window.requestAnimationFrame() (通常是每秒觸發 60 次),requestVideoFrameCallback() 與實際影片影格速率不同,但有重要的例外狀況

執行回呼的有效速率,是指影片速率和瀏覽器速率之間較低的速率。也就是說,在以 60Hz 解析度的瀏覽器中播放 25 FPS 影片時,會以 25Hz 啟動回呼。若以同一個 60Hz 瀏覽器播放 120fps 影片,就會在 60Hz 時觸發回呼。

檔案名稱取名須知

由於與 window.requestAnimationFrame() 的相似之處,此方法最初是提議使用 video.requestAnimationFrame(),並重新命名為 requestVideoFrameCallback() (在經過長時間討論後達成共識)。

功能偵測

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

瀏覽器支援

瀏覽器支援

  • 83
  • 83
  • x
  • 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);

在回呼中,nowDOMHighResTimeStampmetadataVideoFrameMetadata 字典,且包含下列屬性:

  • presentationTime,類型為 DOMHighResTimeStamp:使用者代理程式提交影格用於組合的時間。
  • expectedDisplayTime,類型為 DOMHighResTimeStamp:使用者代理程式預期顯示頁框的時間。
  • unsigned long 類型的 width:影片影格的寬度,以媒體像素為單位。
  • unsigned long 類型的 height:影片影格的高度,以媒體像素為單位。
  • double 類型的 mediaTime:媒體顯示時間戳記 (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 狀態,畫面才會顯示該 API 的畫面 (與 window.requestAnimationFrame() 相同)。因此,如果您持續更新網頁上的 mediaTime 或影格編號,並將編號與影片影格數進行比較,影片最終看起來會像是前面一個影格。

實際的運作情形是,影格在 vsync x 時準備就緒,系統會觸發回呼,影格會在 vsync x+1 時轉譯,而回呼所做的變更則會在 vsync x+2 顯示。您可以檢查 metadata.expectedDisplayTime 是大約 now 還是未來一次 vsync,以檢查回呼是否為延遲的 vsync 情形,且影格是否已在螢幕上轉譯。如果位於 now 的大約 5 到 10 毫秒內,代表影格已經轉譯;如果 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 上提供。