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

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

HTMLVideoElement.requestVideoFrameCallback() 方法允许网页作者注册回调 。 这样,开发者可以对视频、 例如视频处理、在画布上绘制、视频分析 或与外部音频源同步。

与 requestAnimationFrame() 的区别

操作,例如使用 drawImage() 通过该 API 进行的更新将尽最大努力同步 与屏幕上播放视频的帧速率进行比较 不同于 window.requestAnimationFrame()、 通常每秒触发 60 次左右 requestVideoFrameCallback() 会绑定到实际视频帧速率,并设置一个重要的 例外情况

回调的有效运行速率是视频速率之间的较小速率 以及浏览器的速率 这意味着 25fps 的视频在以 60Hz 渲染的浏览器中播放 就会以 25Hz 触发回调 同一 60Hz 浏览器中的 120fps 视频将触发 60Hz 回调。

如何命名?

由于它与 window.requestAnimationFrame() 相似,该方法最初 已提议为“video.requestAnimationFrame() 并重命名为 requestVideoFrameCallback(),已商定 。

功能检测

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

浏览器支持

浏览器支持

  • Chrome:83。 <ph type="x-smartling-placeholder">
  • Edge:83。 <ph type="x-smartling-placeholder">
  • Firefox:不支持。 <ph type="x-smartling-placeholder">
  • Safari:15.4. <ph type="x-smartling-placeholder">

来源

聚酯纤维

requestVideoFrameCallback() 方法的 polyfillWindow.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality() 可用。使用该功能之前,请注意 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);

在回调中,now 是一个 DOMHighResTimeStamp metadata的会员级别为VideoFrameMetadata 字典(具有以下属性):

  • DOMHighResTimeStamp 类型的 presentationTime: 用户代理提交框架进行合成的时间。
  • DOMHighResTimeStamp 类型的 expectedDisplayTime: 用户代理预计显示帧的时间。
  • unsigned long 类型的 width: 视频帧的宽度(以媒体像素为单位)。
  • unsigned long 类型的 height: 视频帧的高度(以媒体像素为单位)。
  • double 类型的 mediaTime: 以呈现帧的秒数表示的媒体呈现时间戳 (PTS),例如该帧在 video.currentTime 时间轴上的时间戳。
  • unsigned long 类型的 presentedFrames: 针对组合提交的帧数。允许客户端确定 VideoFrameRequestCallback 实例之间是否遗漏了帧。
  • double 类型的 processingDuration: 从向解码器提交与此帧具有相同呈现时间戳 (PTS) 的编码包(例如,与 mediaTime 相同)到解码帧可以呈现之前经过的时长(以秒为单位)。

对于 WebRTC 应用,可能还会显示其他属性:

  • DOMHighResTimeStamp 类型的 captureTime: 对于来自本地或远程源的视频帧,这是指相机捕获帧的时间。 对于远程来源,捕获时间是根据时钟同步和 RTCP 发送器报告估算的时间 将 RTP 时间戳转换为捕获时间。
  • DOMHighResTimeStamp 类型的 receiveTime: 对于来自远程来源的视频帧,这是指接收编码帧的时间 由平台指定,即通过网络接收属于该帧的最后一个数据包的时间。
  • unsigned long 类型的 rtpTimestamp: 与此视频帧关联的 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+1 时。 并且回调中所做的更改将在 Vsync x+2 处呈现。 您可以检查回调是否延迟了 vsync(并且帧已呈现在屏幕上) 方法是检查 metadata.expectedDisplayTime 大约是 now 还是一个 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.currentTime 个结果。 requestVideoFrameCallback() 方法在此解决方法的基础上进行了极大改进。

致谢

requestVideoFrameCallback API 由 Thomas Guilbert。 此帖子由 Joe Medley 审核 以及 Kayce Basques 的作品。 主打图片,作者 Unsw 的 Denise Jans