使用 requestVideoFrameCallback() 对视频执行高效的每视频帧操作

了解如何使用 requestVideoFrameCallback() 在浏览器中更高效地处理视频。

借助 HTMLVideoElement.requestVideoFrameCallback() 方法,Web 作者可以注册一个回调,以便在向合成器发送新视频帧时在渲染步骤中运行该回调。这样,开发者就可以对视频执行高效的每视频帧操作,例如视频处理和绘制到画布、视频分析或与外部音频源同步。

使用通过此 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!
}

浏览器支持

浏览器支持

  • Chrome:83。
  • Edge:83.
  • Firefox:132.
  • Safari:15.4.

来源

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);

在回调中,nowDOMHighResTimeStampmetadata 是具有以下属性的 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(并且帧已呈现到屏幕上)。如果 expectedDisplayTimenow 之后约 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 MedleyKayce Basques 审核。主打图片:Unsplash 用户 Denise Jans 提供。