Realiza operaciones eficientes por fotograma en video con requestVideoFrameCallback()

Obtén más información para usar requestVideoFrameCallback() y trabajar de manera más eficiente con videos en el navegador.

El método HTMLVideoElement.requestVideoFrameCallback() permite que los autores web registren una devolución de llamada que se ejecuta en los pasos de renderización cuando se envía un nuevo fotograma de video al compositor. Esto permite a los desarrolladores realizar operaciones eficientes por fotograma de video, como el procesamiento y la pintura de videos en un lienzo, el análisis de videos o la sincronización con fuentes de audio externas.

Diferencia con requestAnimationFrame()

Las operaciones como dibujar un fotograma de video en un lienzo con drawImage() realizadas a través de esta API se sincronizarán con la frecuencia de fotogramas del video que se reproduce en la pantalla como un mejor esfuerzo. A diferencia de window.requestAnimationFrame(), que suele activarse unas 60 veces por segundo, requestVideoFrameCallback() está vinculado a la velocidad de fotogramas real del video, con una excepción importante:

La tasa efectiva a la que se ejecutan las devoluciones de llamada es la menor entre la tasa del video y la del navegador. Esto significa que un video de 25 FPS que se reproduce en un navegador que renderiza a 60 Hz activaría devoluciones de llamada a 25 Hz. Un video de 120 FPS en ese mismo navegador de 60 Hz activaría devoluciones de llamada a 60 Hz.

¿Qué debe incluir un nombre?

Debido a su similitud con window.requestAnimationFrame(), el método se propuso inicialmente como video.requestAnimationFrame() y se cambió su nombre a requestVideoFrameCallback(), lo que se acordó después de una larga discusión.

Detección de características

if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
  // The API is supported!
}

Navegadores compatibles

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: 132.
  • Safari: 15.4.

Source

Polyfill

Hay disponible un polyfill para el método requestVideoFrameCallback() basado en Window.requestAnimationFrame() y HTMLVideoElement.getVideoPlaybackQuality(). Antes de usar esta función, ten en cuenta las limitaciones mencionadas en README.

Cómo usar el método requestVideoFrameCallback()

Si alguna vez usaste el método requestAnimationFrame(), te resultará familiar el método requestVideoFrameCallback() de inmediato. Registras una devolución de llamada inicial una vez y, luego, vuelves a registrarla cada vez que se activa.

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);

En la devolución de llamada, now es un DOMHighResTimeStamp y metadata es un diccionario VideoFrameMetadata con las siguientes propiedades:

  • presentationTime, de tipo DOMHighResTimeStamp: Es la fecha y hora en la que el agente de usuario envió el fotograma para su composición.
  • expectedDisplayTime, de tipo DOMHighResTimeStamp: Es la hora en la que el agente de usuario espera que el fotograma sea visible.
  • width, de tipo unsigned long: Es el ancho del fotograma de video, en píxeles de medios.
  • height, de tipo unsigned long: Es la altura del fotograma de video, en píxeles de medios.
  • mediaTime, de tipo double: Es la marca de tiempo de presentación de medios (PTS) en segundos del fotograma presentado (p.ej., su marca de tiempo en la línea de tiempo de video.currentTime).
  • presentedFrames, de tipo unsigned long: Es un recuento de la cantidad de fotogramas enviados para la composición. Permite que los clientes determinen si se perdieron fotogramas entre instancias de VideoFrameRequestCallback.
  • processingDuration, de tipo double: Es la duración transcurrida en segundos desde el envío del paquete codificado con la misma marca de tiempo de presentación (PTS) que este fotograma (p.ej., igual que mediaTime) hasta que el fotograma decodificado estuvo listo para la presentación.

En el caso de las aplicaciones de WebRTC, es posible que aparezcan propiedades adicionales:

  • captureTime, de tipo DOMHighResTimeStamp: En el caso de los fotogramas de video que provienen de una fuente local o remota, esta es la hora en la que la cámara capturó el fotograma. En el caso de una fuente remota, la hora de captura se estima con la sincronización del reloj y los informes del emisor de RTCP para convertir las marcas de tiempo de RTP en la hora de captura.
  • receiveTime, de tipo DOMHighResTimeStamp: En el caso de los fotogramas de video que provienen de una fuente remota, esta es la hora en la que la plataforma recibió el fotograma codificado, es decir, la hora en la que se recibió el último paquete perteneciente a este fotograma a través de la red.
  • rtpTimestamp, de tipo unsigned long: Es la marca de tiempo de RTP asociada a este fotograma de video.

En esta lista, mediaTime es de especial interés. La implementación de Chromium usa el reloj de audio como la fuente de tiempo que respalda video.currentTime, mientras que mediaTime se propaga directamente con el presentationTimestamp del fotograma. El mediaTime es lo que debes usar si deseas identificar exactamente los fotogramas de una manera reproducible, incluso para identificar exactamente qué fotogramas omitiste.

Si parece que todo está desfasado un fotograma…

La sincronización vertical (o simplemente vsync) es una tecnología de gráficos que sincroniza la velocidad de fotogramas de un video y la frecuencia de actualización de un monitor. Dado que requestVideoFrameCallback() se ejecuta en el subproceso principal, pero, en segundo plano, la composición de video se realiza en el subproceso del compositor, todo lo que proviene de esta API es un esfuerzo óptimo, y el navegador no ofrece ninguna garantía estricta. Lo que puede estar sucediendo es que la API puede estar un vsync tarde en relación con el momento en que se renderiza un fotograma de video. Se necesita una sincronización vertical para que los cambios realizados en la página web a través de la API aparezcan en la pantalla (igual que window.requestAnimationFrame()). Por lo tanto, si sigues actualizando el número de mediaTime o de fotograma en tu página web y lo comparas con los fotogramas numerados del video, eventualmente el video parecerá estar un fotograma adelantado.

En realidad, lo que sucede es que el fotograma está listo en la sincronización vertical x, se activa la devolución de llamada y el fotograma se renderiza en la sincronización vertical x+1, y los cambios realizados en la devolución de llamada se renderizan en la sincronización vertical x+2. Puedes verificar si la devolución de llamada es tarde para la sincronización vertical (y el fotograma ya se renderizó en la pantalla) verificando si metadata.expectedDisplayTime es aproximadamente now o una sincronización vertical en el futuro. Si se encuentra entre cinco y diez microsegundos de now, el fotograma ya se renderizó. Si expectedDisplayTime está aproximadamente a dieciséis milisegundos en el futuro (suponiendo que tu navegador o pantalla se actualizan a 60 Hz), estás sincronizado con el fotograma.

Demostración

Creé una pequeña demostración que muestra cómo se dibujan los fotogramas en un lienzo exactamente con la velocidad de fotogramas del video y dónde se registran los metadatos de los fotogramas para fines de depuración.

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);

Conclusiones

Las personas han realizado el procesamiento a nivel de fotogramas durante mucho tiempo, sin tener acceso a los fotogramas reales, solo en función de video.currentTime. El método requestVideoFrameCallback() mejora en gran medida esta solución alternativa.

Agradecimientos

La API de requestVideoFrameCallback fue especificada e implementada por Thomas Guilbert. Joe Medley y Kayce Basques revisaron esta publicación. Imagen hero de Denise Jans en Unsplash.