了解如何使用 requestVideoFrameCallback() 在浏览器中更高效地处理视频。
发布时间:2023 年 1 月 8 日
借助 HTMLVideoElement.requestVideoFrameCallback() 方法,Web 作者可以注册一个回调,当新的视频帧发送到合成器时,该回调会在渲染步骤中运行。这使开发者能够对视频执行高效的逐视频帧操作,例如视频处理和绘制到画布、视频分析或与外部音频源同步。
与 requestAnimationFrame() 的区别
使用此 API 进行的操作(例如使用 drawImage() 将视频帧绘制到画布)会尽最大努力与屏幕上播放的视频的帧速率同步。这与 window.requestAnimationFrame() 不同,后者通常每秒触发约 60 次。
requestVideoFrameCallback() 与实际视频帧速率相关联,但有一个重要的例外情况:
回调的有效运行速率是视频的速率和浏览器的速率中的较小者。 这意味着,如果浏览器以 60Hz 的频率绘制,那么以 25fps 播放的视频将以 25Hz 的频率触发回调。 在同一 60Hz 浏览器中,120fps 视频将以 60Hz 的频率触发回调。
功能检测
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
// The API is supported!
}
Polyfill
我们提供了一个基于 Window.requestAnimationFrame() 和 HTMLVideoElement.getVideoPlaybackQuality() 的 requestVideoFrameCallback() 方法的 polyfill。在使用此功能之前,请注意README中提及的限制。
使用该方法
如果您使用 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。
如果画面似乎延迟了一帧
垂直同步(或简称“垂直同步”)是一种图形技术,可同步视频的帧速率和显示器的刷新速率。由于 requestVideoFrameCallback() 在主线程上运行,但实际上视频合成发生在合成器线程上,因此此 API 中的所有内容都是尽力而为,浏览器不提供任何严格的保证。
API 可能比视频帧的渲染时间晚一个垂直同步信号。通过 API 对网页所做的更改需要一个垂直同步周期才能显示在屏幕上(与 window.requestAnimationFrame() 相同)。因此,如果您不断更新网页上的 mediaTime 或帧编号,并将其与带编号的视频帧进行比较,最终视频看起来会提前一帧。
实际情况是,帧在 Vsync x 时准备就绪,回调在 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 审核。