JavaScript: надежное извлечение видеокадров
Я работаю над клиентским проектом, который позволяет пользователю поставлять видеофайл и применять к нему основные манипуляции. Я пытаюсь извлечь кадры из видео надежно. В настоящий момент у меня есть <video>
который я загружаю в выбранное видео, а затем вытягиваю каждый кадр следующим образом:
- Ищите начало
- Приостановить видео
- Нарисуйте
<video>
на <canvas>
- Захват кадра из холста с помощью
.toDataUrl()
- Ищите вперед на 1/30 секунд (1 кадр).
- Промыть и повторить
Это довольно неэффективный процесс, а более конкретно, доказывает ненадежность, поскольку я часто получаю застревание кадров. Кажется, это от него не обновляется фактический элемент <video>
прежде чем он рисует на холст.
Я бы предпочел не загружать исходное видео на сервер, чтобы разделить фреймы, а затем загрузить их обратно клиенту.
Любые предложения для лучшего способа сделать это очень ценятся. Единственное предостережение в том, что мне нужно, чтобы он работал с любым форматом, поддерживаемым браузером (декодирование в JS не является отличным вариантом).
Ответы
Ответ 1
В основном взяты из этого великолепного ответа GameAlchemist:
Поскольку браузеры не учитывают частоту кадров видео, а вместо этого "используют некоторые приемы, чтобы сопоставить частоту кадров фильма и частоту обновления экрана", вы предполагаете, что каждые 30-е секунды появляется новая Рамка будет окрашена довольно неточно.
Однако событие timeupdate
должно срабатывать при изменении currentTime, и мы можем предположить, что новый кадр был нарисован.
Итак, я бы сделал это так:
document.querySelector('input').addEventListener('change', extractFrames, false);
function extractFrames() {
var video = document.createElement('video');
var array = [];
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var pro = document.querySelector('#progress');
function initCanvas(e) {
canvas.width = this.videoWidth;
canvas.height = this.videoHeight;
}
function drawFrame(e) {
this.pause();
ctx.drawImage(this, 0, 0);
/*
this will save as a Blob, less memory consumptive than toDataURL
a polyfill can be found at
https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Polyfill
*/
canvas.toBlob(saveFrame, 'image/jpeg');
pro.innerHTML = ((this.currentTime / this.duration) * 100).toFixed(2) + ' %';
if (this.currentTime < this.duration) {
this.play();
}
}
function saveFrame(blob) {
array.push(blob);
}
function revokeURL(e) {
URL.revokeObjectURL(this.src);
}
function onend(e) {
var img;
// do whatever with the frames
for (var i = 0; i < array.length; i++) {
img = new Image();
img.onload = revokeURL;
img.src = URL.createObjectURL(array[i]);
document.body.appendChild(img);
}
// we don't need the video objectURL anymore
URL.revokeObjectURL(this.src);
}
video.muted = true;
video.addEventListener('loadedmetadata', initCanvas, false);
video.addEventListener('timeupdate', drawFrame, false);
video.addEventListener('ended', onend, false);
video.src = URL.createObjectURL(this.files[0]);
video.play();
}
<input type="file" accept="video/*" />
<p id="progress"></p>
Ответ 2
Вот рабочая функция, которая была подправлена из этого вопроса:
async function extractFramesFromVideo(videoUrl, fps=25) {
return new Promise(async (resolve) => {
// fully download it first (no buffering):
let videoBlob = await fetch(videoUrl).then(r => r.blob());
let videoObjectUrl = URL.createObjectURL(videoBlob);
let video = document.createElement("video");
let seekResolve;
video.addEventListener('seeked', async function() {
if(seekResolve) seekResolve();
});
video.src = videoObjectUrl;
// workaround chromium metadata bug (https://stackoverflow.com/q/38062864/993683)
while((video.duration === Infinity || isNaN(video.duration)) && video.readyState < 2) {
await new Promise(r => setTimeout(r, 1000));
video.currentTime = 10000000*Math.random();
}
let duration = video.duration;
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
let [w, h] = [video.videoWidth, video.videoHeight]
canvas.width = w;
canvas.height = h;
let frames = [];
let interval = 1 / fps;
let currentTime = 0;
while(currentTime < duration) {
video.currentTime = currentTime;
await new Promise(r => seekResolve=r);
context.drawImage(video, 0, 0, w, h);
let base64ImageData = canvas.toDataURL();
frames.push(base64ImageData);
currentTime += interval;
}
resolve(frames);
});
});
}
Использование:
let frames = await extractFramesFromVideo("https://example.com/video.webm");
Обратите внимание, что в настоящее время нет простого способа определить фактическую/естественную частоту кадров видео, если, возможно, вы не используете ffmpeg.js, но это мегабайтный javascript файл 10+ (поскольку он является портом emscripten фактической библиотеки ffmpeg, которая, очевидно, огромный).