了解如何使用 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!
}
浏览器支持
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);
在回调中,now
是 DOMHighResTimeStamp
,而 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 Medley 和 Kayce Basques 审核。
主打图片由 Unsplash 用户 Denise Jans 拍摄。