跳到主要内容

Canvas

2024年02月19日
柏拉文
越努力,越幸运

一、认识


开发者通常使用以下方法获取视频帧:

  1. setInterval: 无法精准同步视频帧,可能出现帧丢失或重复

  2. requestAnimationFrame: 无法精准同步视频帧,可能出现帧丢失或重复。requestAnimationFrame 不保证 每次都能精准对应视频帧,可能有丢帧或重复帧问题。

  3. requestVideoFrameCallback 通过视频帧驱动回调,确保每一帧都能精准捕获,并提供额外的帧元数据(timestamppresentationTimeexpectedDisplayTime)。而且 requestVideoFrameCallback 保证只在新视频帧可用时触发,避免多余计算。因此, requestVideoFrameCallback 可以用于高效同步视频帧,不会错过或重复帧, 帧驱动 回调,更适用于 视频分析、AI 计算、滤镜渲染。比 requestAnimationFrame 更精准,但需要 浏览器支持。

二、实现


目前 requestVideoFrameCallback 仅在 ChromeEdgeOpera(基于 Chromium)上支持,SafariFirefox 仍未实现。下面是一个完整的 优雅降级 方案,优先使用 requestVideoFrameCallback,然后是 requestAnimationFrame,最后降级到 setInterval

<video id="video" src="./video.mp4" controls></video>
<canvas id="canvas"></canvas>
const video = document.getElementById("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

function isVideoPlaying(video) {
return !video.paused && !video.ended;
}

function captureVideoFrame(video, callback) {
if (!video) {
return;
}

let requestFrame;

function processFrame(now, metadata) {
if (!isVideoPlaying(video)) {
return;
}

callback?.(video, now, metadata);
requestFrame(processFrame);
}

if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
requestFrame = (cb) => video.requestVideoFrameCallback(cb);
} else if (window.requestAnimationFrame) {
requestFrame = (cb) => requestAnimationFrame(cb);
} else {
requestFrame = (cb) => {
setInterval(() => {
const now = performance.now();
cb(now);
}, 1000 / 30);
};
}

requestFrame(processFrame);
}

function drawVideoFrame(video, now, metadata) {
ctx.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
}

video.onloadeddata = () => {
canvas.width = video.offsetWidth;
canvas.height = video.offsetHeight;
};

video.onplay = () => {
captureVideoFrame(video, drawVideoFrame);
};