Выполняйте эффективные покадровые операции с видео с помощью requestVideoFrameCallback().

Узнайте, как использовать 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!
}

Поддержка браузера

Поддержка браузера

  • Хром: 83.
  • Край: 83.
  • Предварительная версия технологии Firefox: поддерживается.
  • Сафари: 15.4.

Источник

Полифилл

Доступен полифилл для метода 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 относительно момента рендеринга видеокадра. Чтобы изменения, внесенные на веб-страницу через API, появились на экране, требуется одна виртуальная синхронизация (так же, как 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.