Узнайте, как использовать 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
, типаunsigned long
: временная метка RTP, связанная с этим видеокадром.
Особый интерес в этом списке представляет mediaTime
. Реализация Chromium использует аудиочасы в качестве источника времени, который поддерживает video.currentTime
, тогда как mediaTime
напрямую заполняется presentationTimestamp
кадра. mediaTime
— это то, что вам следует использовать, если вы хотите точно идентифицировать кадры воспроизводимым способом, в том числе для определения того, какие именно кадры вы пропустили.
Если что-то кажется не таким…
Вертикальная синхронизация (или просто vsync) — это графическая технология, которая синхронизирует частоту кадров видео и частоту обновления монитора. Поскольку requestVideoFrameCallback()
работает в основном потоке, но, под капотом, видеокомпозиция происходит в потоке компоновщика, все из этого API — это наилучшие усилия, и браузер не дает никаких строгих гарантий. Возможно, API может быть на один vsync позже относительно того, когда визуализируется видеокадр. Требуется один vsync для того, чтобы изменения, внесенные в веб-страницу через API, появились на экране (так же, как window.requestAnimationFrame()
). Поэтому, если вы продолжаете обновлять mediaTime
или номер кадра на своей веб-странице и сравнивать его с пронумерованными видеокадрами, в конечном итоге видео будет выглядеть так, как будто оно на один кадр впереди.
На самом деле происходит следующее: кадр готов в vsync x, запускается обратный вызов, и кадр визуализируется в vsync x+1, а изменения, внесенные в обратный вызов, визуализируются в vsync x+2. Вы можете проверить, является ли обратный вызов запоздалым на vsync (и кадр уже визуализирован на экране), проверив, является ли metadata.expectedDisplayTime
примерно now
или одним vsync в будущем. Если он находится в пределах примерно пяти-десяти микросекунд от 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.