瞭解如何使用 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!
}
瀏覽器支援
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);
在回呼中,now
是 DOMHighResTimeStamp
,metadata
則是 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 Medley和 Kayce Basques 審查。主頁橫幅:Unsplash 上的 Denise Jans。