瞭解如何使用 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!
}
瀏覽器支援
聚酯纖維
可使用以 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
:使用者代理程式預期顯示頁框的時間。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 Medley 和 Kayce Basques 審查。主頁橫幅由 Denise Jans 在 Unsplash 上提供。