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

了解如何使用 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!
}

浏览器支持

Browser Support

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

Source

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

在回调中,nowDOMHighResTimeStamp,而 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 MedleyKayce Basques 审核。 主打图片由 Unsplash 用户 Denise Jans 拍摄。