瞭解如何使用 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!
}
瀏覽器支援
聚酯纖維
您可以使用以 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 可能會相較於轉譯影片影格時延遲一個 vsync。這個 vsync 作業會取得透過 API 對網頁進行的變更 (與 window.requestAnimationFrame()
相同)。因此,如果您持續更新網頁上的 mediaTime
或影格編號,並將這組號碼與編號的影片影格比較,最終影片看起來會像前一個影格。
實際上,畫面會在 vsync x 時準備就緒,回呼會在 vsync x+1 時觸發,畫面會在 vsync x+2 時算繪。您可以檢查 metadata.expectedDisplayTime
是否大約等於 now
,或在未來的一個 vsync,藉此確認回呼是否為 vsync 延遲 (且影格已在畫面上算繪)。如果 expectedDisplayTime
與 now
相差約五到十微秒,表示影格已算繪製完成;如果 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 Medley 和 Kayce Basques 審查。主頁橫幅:Denise Jans 在 Unsplash 上提供。