Узнайте, как использовать requestVideoFrameCallback()
для более эффективной работы с видео в браузере.
Метод HTMLVideoElement.requestVideoFrameCallback()
позволяет веб-авторам регистрировать обратный вызов, который выполняется на этапах рендеринга, когда новый видеокадр отправляется в наборщик. Это позволяет разработчикам выполнять эффективные покадровые операции с видео, такие как обработка видео и рисование на холсте, анализ видео или синхронизация с внешними источниками звука.
Разница с requestAnimationFrame()
Такие операции, как рисование видеокадра на холсте с помощью drawImage()
выполняемые через этот API, будут максимально синхронизированы с частотой кадров видео, воспроизводимого на экране. В отличие от window.requestAnimationFrame()
, который обычно срабатывает примерно 60 раз в секунду, requestVideoFrameCallback()
привязан к фактической частоте кадров видео — за одним важным исключением :
Эффективная скорость выполнения обратных вызовов – это меньшая скорость между скоростью видео и скоростью браузера. Это означает, что видео со скоростью 25 кадров в секунду, воспроизводимое в браузере, который рисует с частотой 60 Гц, будет вызывать обратные вызовы с частотой 25 Гц. Видео со скоростью 120 кадров в секунду в том же браузере с частотой 60 Гц будет вызывать обратные вызовы с частотой 60 Гц.
Что в имени?
Из-за схожести с window.requestAnimationFrame()
метод изначально был предложен как video.requestAnimationFrame()
и переименован в requestVideoFrameCallback()
, что и было согласовано после длительного обсуждения .
Обнаружение функций
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
// The API is supported!
}
Поддержка браузера
Полифилл
Доступен полифилл для метода requestVideoFrameCallback()
на основе Window.requestAnimationFrame()
и HTMLVideoElement.getVideoPlaybackQuality()
. Прежде чем использовать это, обратите внимание на ограничения, упомянутые в 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
, of typeunsigned long
: The RTP timestamp associated with this video frame.
Особый интерес в этом списке представляет mediaTime
. Реализация Chromium использует звуковые часы в качестве источника времени, который поддерживает video.currentTime
, тогда как mediaTime
напрямую заполняется presentationTimestamp
кадра. The mediaTime
is what you should use if you want to exactly identify frames in a reproducible way, including to identify exactly which frames you missed.
Если что-то кажется неправильным на один кадр…
Вертикальная синхронизация (или просто vsync) — это графическая технология, которая синхронизирует частоту кадров видео и частоту обновления монитора. Поскольку requestVideoFrameCallback()
выполняется в основном потоке, но под капотом компоновка видео происходит в потоке композитора, все, что касается этого API, — это лучшее, что можно сделать, и браузер не дает каких-либо строгих гарантий. Возможно, происходит то, что API может отставать на одну vsync относительно момента рендеринга видеокадра. It takes one vsync for changes made to the web page through the API to appear on screen (same as window.requestAnimationFrame()
). Поэтому, если вы продолжаете обновлять значение mediaTime
или номер кадра на своей веб-странице и сравнивать его с пронумерованными видеокадрами, в конечном итоге видео будет выглядеть так, как будто оно на один кадр впереди.
На самом деле происходит то, что кадр готов при vsync x, запускается обратный вызов и кадр визуализируется при vsync x+1, а изменения, внесенные в обратный вызов, обрабатываются при vsync x+2. Вы можете проверить, является ли обратный вызов поздней вертикальной синхронизацией (и кадр уже отображается на экране), проверив, соответствует ли metadata.expectedDisplayTime
примерно now
или одной вертикальной синхронизации в будущем. Если это произойдет примерно через пять-десять микросекунд после now
, кадр уже визуализируется; если expectedDisplayTime
в будущем составит примерно шестнадцать миллисекунд (при условии, что ваш браузер/экран обновляется с частотой 60 Гц), то вы синхронизированы с кадром.
Демо
Я создал небольшую демонстрацию на Glitch , которая показывает, как кадры рисуются на холсте с точной частотой кадров видео и где метаданные кадра регистрируются для целей отладки.
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()
значительно улучшает этот обходной путь.
Благодарности
API requestVideoFrameCallback
был указан и реализован Томасом Гильбертом . Этот пост был рассмотрен Джо Медли и Кейси Баскс . Изображение героя , созданное Дениз Янс на Unsplash.