了解如何使用 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 可能会晚一个垂直同步周期。通过 API 对网页所做的更改需要一个 vsync 才能显示在屏幕上(与 window.requestAnimationFrame() 相同)。因此,如果您不断更新网页上的 mediaTime 或帧号,并将其与带编号的视频帧进行比较,最终视频看起来会提前一帧。
实际情况是,帧在 vsync x 时准备就绪,回调被触发,帧在 vsync x+1 时渲染,回调中所做的更改在 vsync x+2 时渲染。您可以检查 metadata.expectedDisplayTime 是否大致为 now 或未来一个 vsync,从而检查回调是否为 vsync 延迟(并且帧已在屏幕上呈现)。如果它与 now 的时间差在 5 到 10 微秒左右,则表示帧已渲染;如果 expectedDisplayTime 大约在 16 毫秒之后(假设您的浏览器/屏幕以 60Hz 的频率刷新),则表示您与帧同步。
演示
我创建了一个小型演示,展示了如何以视频的准确帧速率在画布上绘制帧,以及如何记录帧元数据以进行调试。
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 拍摄。