使用 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 提供。