我正在开发一个客户端项目,该项目允许用户提供视频文件并对其应用基本操作。我正在尝试可靠地从视频中提取帧。目前我有一个 <video>
我正在将选定的视频加载到其中,然后按如下方式拉出每一帧:
- 寻找起点
- 暂停视频
- 画
<video>
到<canvas>
- 使用
.toDataUrl()
从 Canvas 中捕获帧 - 向前搜索 1/30 秒(1 帧)。
- 冲洗并重复
这是一个相当低效的过程,更具体地说,事实证明它不可靠,因为我经常遇到帧卡住的情况。这似乎是因为它没有更新实际的 <video>
元素绘制到 Canvas 之前。
我宁愿不必为了拆分帧而将原始视频上传到服务器,然后将它们下载回客户端。
非常感谢任何关于更好的方法的建议。唯一需要注意的是,我需要它来处理浏览器支持的任何格式(在 JS 中解码不是一个很好的选择)。
最佳答案
[2021 更新]:自从这个问题(和答案)首次发布以来,这方面的事情发生了变化,终于到了更新的时候了; the method that was exposed here已经过时了,但幸运的是,一些新的或传入的 API 可以帮助我们更好地提取视频帧:
最有前途和最强大的,但仍在开发中,有很多限制:WebCodecs
这个新的 API 释放了对媒体解码器和编码器的访问权限,使我们能够从视频帧(YUV 平面)访问原始数据,这对于许多应用程序来说可能比渲染帧更有用;对于那些需要渲染帧的人,VideoFrame此 API 公开的接口(interface)可以直接绘制到
可以找到一个完整的例子on the proposal's repo .
关键部分由
const decoder = new VideoDecoder({
output: onFrame, // the callback to handle all the VideoFrame objects
error: e => console.error(e),
});
decoder.configure(config); // depends on the input file, your demuxer should provide it
demuxer.start((chunk) => { // depends on the demuxer, but you need it to return chunks of video data
decoder.decode(chunk); // will trigger our onFrame callback
})
请注意,我们甚至可以抓取 MediaStream 的帧,感谢 MediaCapture Transform的 MediaStreamTrackProcessor .
这意味着我们应该能够结合 HTMLMediaElement.captureStream()和这个 API 来获取我们的 VideoFrames,而不需要多路分解器。然而,这仅适用于少数编解码器,这意味着我们将以读取速度提取帧...
无论如何,这是一个在最新的基于 Chromium 的浏览器上运行的示例,打开了 chrome://flags/#enable-experimental-web-platform-features
:
const frames = [];
const button = document.querySelector("button");
const select = document.querySelector("select");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
button.onclick = async(evt) => {
if (window.MediaStreamTrackProcessor) {
let stopped = false;
const track = await getVideoTrack();
const processor = new MediaStreamTrackProcessor(track);
const reader = processor.readable.getReader();
readChunk();
function readChunk() {
reader.read().then(async({ done, value }) => {
if (value) {
const bitmap = await createImageBitmap(value);
const index = frames.length;
frames.push(bitmap);
select.append(new Option("Frame #" + (index + 1), index));
value.close();
}
if (!done && !stopped) {
readChunk();
} else {
select.disabled = false;
}
});
}
button.onclick = (evt) => stopped = true;
button.textContent = "stop";
} else {
console.error("your browser doesn't support this API yet");
}
};
select.onchange = (evt) => {
const frame = frames[select.value];
canvas.width = frame.width;
canvas.height = frame.height;
ctx.drawImage(frame, 0, 0);
};
async function getVideoTrack() {
const video = document.createElement("video");
video.crossOrigin = "anonymous";
video.src = "https://upload.wikimedia.org/wikipedia/commons/a/a4/BBH_gravitational_lensing_of_gw150914.webm";
document.body.append(video);
await video.play();
const [track] = video.captureStream().getVideoTracks();
video.onended = (evt) => track.stop();
return track;
}
video,canvas {
max-width: 100%
}
<button>start</button>
<select disabled>
</select>
<canvas></canvas>
最容易使用,但浏览器支持相对较差,并且容易出现浏览器掉帧:HTMLVideoElement.requestVideoFrameCallback
这个方法允许我们在 HTMLVideoElement 上绘制新帧时安排回调。
它比 WebCodecs 级别更高,因此可能会有更多延迟,而且我们只能以读取速度提取帧。
const frames = [];
const button = document.querySelector("button");
const select = document.querySelector("select");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
button.onclick = async(evt) => {
if (HTMLVideoElement.prototype.requestVideoFrameCallback) {
let stopped = false;
const video = await getVideoElement();
const drawingLoop = async(timestamp, frame) => {
const bitmap = await createImageBitmap(video);
const index = frames.length;
frames.push(bitmap);
select.append(new Option("Frame #" + (index + 1), index));
if (!video.ended && !stopped) {
video.requestVideoFrameCallback(drawingLoop);
} else {
select.disabled = false;
}
};
// the last call to rVFC may happen before .ended is set but never resolve
video.onended = (evt) => select.disabled = false;
video.requestVideoFrameCallback(drawingLoop);
button.onclick = (evt) => stopped = true;
button.textContent = "stop";
} else {
console.error("your browser doesn't support this API yet");
}
};
select.onchange = (evt) => {
const frame = frames[select.value];
canvas.width = frame.width;
canvas.height = frame.height;
ctx.drawImage(frame, 0, 0);
};
async function getVideoElement() {
const video = document.createElement("video");
video.crossOrigin = "anonymous";
video.src = "https://upload.wikimedia.org/wikipedia/commons/a/a4/BBH_gravitational_lensing_of_gw150914.webm";
document.body.append(video);
await video.play();
return video;
}
video,canvas {
max-width: 100%
}
<button>start</button>
<select disabled>
</select>
<canvas></canvas>
对于您的 Firefox 用户,Mozilla 的非标准 HTMLMediaElement.seekToNextFrame()
顾名思义,这将使您的
const frames = [];
const button = document.querySelector("button");
const select = document.querySelector("select");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
button.onclick = async(evt) => {
if (HTMLMediaElement.prototype.seekToNextFrame) {
let stopped = false;
const video = await getVideoElement();
const requestNextFrame = (callback) => {
video.addEventListener("seeked", () => callback(video.currentTime), {
once: true
});
video.seekToNextFrame();
};
const drawingLoop = async(timestamp, frame) => {
if(video.ended) {
select.disabled = false;
return; // FF apparently doesn't like to create ImageBitmaps
// from ended videos...
}
const bitmap = await createImageBitmap(video);
const index = frames.length;
frames.push(bitmap);
select.append(new Option("Frame #" + (index + 1), index));
if (!video.ended && !stopped) {
requestNextFrame(drawingLoop);
} else {
select.disabled = false;
}
};
requestNextFrame(drawingLoop);
button.onclick = (evt) => stopped = true;
button.textContent = "stop";
} else {
console.error("your browser doesn't support this API yet");
}
};
select.onchange = (evt) => {
const frame = frames[select.value];
canvas.width = frame.width;
canvas.height = frame.height;
ctx.drawImage(frame, 0, 0);
};
async function getVideoElement() {
const video = document.createElement("video");
video.crossOrigin = "anonymous";
video.src = "https://upload.wikimedia.org/wikipedia/commons/a/a4/BBH_gravitational_lensing_of_gw150914.webm";
document.body.append(video);
await video.play();
return video;
}
video,canvas {
max-width: 100%
}
<button>start</button>
<select disabled>
</select>
<canvas></canvas>
最不可靠的,随着时间的推移确实停止工作:HTMLVideoElement.ontimeupdate
策略暂停 - 绘制 - 播放 - 等待时间更新曾经是(在 2015 年)一种非常可靠的方法来了解何时将新框架绘制到元素上,但从那时起,浏览器已经对这个以极快的速度触发的事件进行了严格的限制,现在我们无法从中获取太多信息......
我不确定我是否仍然可以提倡使用它,我没有检查 Safari(这是目前唯一没有解决方案的)如何处理这个事件(他们对媒体的处理对我来说很奇怪),并且有很有可能一个简单的 setTimeout(fn, 1000/30)
循环在大多数情况下实际上更可靠。
关于JavaScript:可靠地提取视频帧,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32699721/