了解如何使用 requestVideoFrameCallback()
在浏览器中更高效地处理视频。
借助 HTMLVideoElement.requestVideoFrameCallback()
方法,Web 作者可以注册一个回调,以便在向合成器发送新视频帧时在渲染步骤中运行该回调。这样,开发者就可以对视频执行高效的每视频帧操作,例如视频处理和绘制到画布、视频分析或与外部音频源同步。
与 requestAnimationFrame() 的区别
通过此 API 执行的操作(例如使用 drawImage()
将视频帧绘制到画布)会尽可能与屏幕上播放的视频的帧速率同步。与通常每秒触发约 60 次的 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 相对于渲染视频帧的时间可能会延迟一个 vsync。通过 API 对网页所做的更改需要 1 个 vsync 才能显示在屏幕上(与 window.requestAnimationFrame()
相同)。因此,如果您不断更新网页上的 mediaTime
或帧编号,并将其与编号的视频帧进行比较,最终视频看起来会比实际提前 1 帧。
实际发生的情况是,帧在 vsync x 时准备就绪,系统会在 vsync x+1 时触发回调并渲染帧,并且在 vsync x+2 时渲染回调中进行的更改。您可以通过检查 metadata.expectedDisplayTime
是否大约为 now
或 1 个 vsync 后,来检查回调是否延迟了 vsync(并且帧已渲染到屏幕上)。如果 expectedDisplayTime
在 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 的审核。主打图片:Unsplash 用户 Denise Jans 提供。