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