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

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

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

与 requestAnimationFrame() 的差异

使用通过此 API 执行的 drawImage() 将视频帧绘制到画布等操作会尽可能地与屏幕上所播放视频的帧速率同步。与 window.requestAnimationFrame()(通常每秒触发约 60 次)不同,requestVideoFrameCallback() 受实际视频帧速率约束,但有一个重要的例外情况

运行回调的有效速率是视频速率和浏览器速率之间的较小速率。 这意味着,如果浏览器中以 60Hz 的刷新率播放 25fps 的视频,则会以 25Hz 的刷新率触发回调。 同一个 60Hz 浏览器中的 120fps 视频会以 60Hz 的频率触发回调。

如何命名?

由于该方法与 window.requestAnimationFrame() 类似,因此该方法最初被提出为 video.requestAnimationFrame() 并重命名为 requestVideoFrameCallback(),后者经过长时间讨论后达成一致。

功能检测

if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
  // The API is supported!
}

浏览器支持

浏览器支持

  • 83
  • 83
  • x
  • 15.4

来源

聚酯纤维

我们提供了一个 polyfill,用于基于 Window.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality()requestVideoFrameCallback() 方法。在使用此函数之前,请注意 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 对网页所做的更改需要执行一次 vsync 操作才能显示在屏幕上(与 window.requestAnimationFrame() 相同)。因此,如果您不断更新网页上的 mediaTime 或帧号,并将其与已编号的视频帧进行比较,最终视频看起来会比前一帧。

实际发生的情况是,帧在 vsync x 下已就绪,触发了回调,以 vsync x+1 渲染该帧,然后在 vsync x+2 下渲染在回调中所做的更改。您可以通过检查 metadata.expectedDisplayTime 是大约为 now 还是 1 个未来的 vsync,来检查回调是否出现 vsync 延迟(以及帧已在屏幕上渲染)。如果其在 now 的 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.currentTimerequestVideoFrameCallback() 方法大大改进了这种解决方法。

致谢

requestVideoFrameCallback API 由 Thomas Guilbert 指定和实现。这篇博文由 Joe MedleyKayce Basques 审核。 主打图片,作者:Denise Jans,来源:Unsplash。