跳到主要内容

场景

2023年03月19日
柏拉文
越努力,越幸运

一、获取视频帧


1.1 认识

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

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

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

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

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

1.2 实现

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

二、实现一个 2s 的动画


let start = null;
const box = document.getElementById("box");
function workLoop(timestamp) {
if (!start) {
start = timestamp;
}
let process = timestamp - start;
box.style.left = Math.min(process / 10, 100) + 'px';
if (process < 2000) {
window.requestAnimationFrame(workLoop);
}
}
window.requestAnimationFrame(workLoop);

三、实现一个无线循环的动画


const box = document.getElementById("circle");
let flag = false;
let width = box.clientWidth;
function loop() {
if (flag) {
if (width >= 400) {
flag = false;
}
width += 1;
} else {
if (width <= 100) {
flag = true;
}
width -= 1;
}
box.style.width = `${width}px`;
box.style.height = `${width}px`;
}

function animationFrame() {
loop();
window.requestAnimationFrame(animationFrame);
}
animationFrame();