Узнайте, как использовать 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 может отставать на один вертикальный синхронизм относительно момента рендеринга видеокадра. Для отображения на экране изменений, внесённых на веб-страницу через API (аналогично window.requestAnimationFrame() ), требуется один вертикальный синхронизм. Поэтому, если вы постоянно обновляете mediaTime или номер кадра на своей веб-странице и сравниваете его с номерами видеокадров, в конечном итоге видео будет выглядеть так, будто оно опережает один кадр.
 На самом деле происходит следующее: кадр готов в момент vsync x, запускается обратный вызов, и кадр визуализируется в момент vsync x+1, а изменения, внесенные в обратный вызов, визуализируются в момент vsync x+2. Вы можете проверить, является ли обратный вызов опозданием на vsync (и кадр уже визуализирован на экране), проверив, соответствует ли metadata.expectedDisplayTime now или одному vsync в будущем. Если он находится в пределах пяти-десяти микросекунд от now , кадр уже визуализирован; если expectedDisplayTime составляет примерно шестнадцать миллисекунд в будущем (при условии, что ваш браузер/экран обновляется с частотой 60 Гц), то вы синхронизированы с кадром.
Демо
Я создал небольшую демонстрацию , которая показывает, как кадры рисуются на холсте с той же частотой кадров, что и видео, и где метаданные кадра регистрируются для отладки.
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.
