Realiza operaciones eficientes por fotograma en video con requestVideoFrameCallback()

Obtén información para usar requestVideoFrameCallback() y trabajar de forma más eficiente con los 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 de video y la pintura en un lienzo, el análisis de video 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() que se realizan a través de esta API se sincronizarán de la mejor manera posible con la velocidad de fotogramas del video que se reproduce en la pantalla. A diferencia de window.requestAnimationFrame(), que suele activarse alrededor de 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 pinta 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 inicialmente se propuso como video.requestAnimationFrame() y se le cambió el nombre a requestVideoFrameCallback(), que se acordó después de una larga discusión.

Detección de atributos

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

Navegadores compatibles

Navegadores compatibles

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

Origen

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.

Usa el método requestVideoFrameCallback().

Si alguna vez usaste el método requestAnimationFrame(), te sentirás familiarizado de inmediato con el método requestVideoFrameCallback(). 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 hora en la que el usuario-agente envió el fotograma para la composición.
  • expectedDisplayTime, de tipo DOMHighResTimeStamp: Es la hora en la que el usuario-agente espera que el fotograma sea visible.
  • width, de tipo unsigned long: Es el ancho del fotograma de video, en píxeles de contenido multimedia.
  • height, de tipo unsigned long: Es la altura del fotograma de video, en píxeles de media.
  • mediaTime, de tipo double: Es la marca de tiempo de presentación de contenido multimedia (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 omitieron 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) al decodificador hasta que el fotograma decodificado estuvo listo para la presentación.

En el caso de las aplicaciones de WebRTC, pueden aparecer propiedades adicionales:

  • captureTime, de tipo DOMHighResTimeStamp: Para 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. Para una fuente remota, el tiempo de captura se estima mediante la sincronización del reloj y los informes del remitente de RTCP para convertir las marcas de tiempo de RTP en el tiempo de captura.
  • receiveTime, de tipo DOMHighResTimeStamp: Para los fotogramas de video que provienen de una fuente remota, este es el momento en que la plataforma recibió el fotograma codificado, es decir, el momento en que se recibió el último paquete que pertenece a este fotograma a través de la red.
  • rtpTimestamp, de tipo unsigned long: Es la marca de tiempo de RTP asociada con esta trama de video.

De especial interés en esta lista es mediaTime. 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 desde el presentationTimestamp del fotograma. mediaTime es lo que debes usar si deseas identificar exactamente los fotogramas de una manera reproducible, incluso para identificar exactamente qué fotogramas te faltaron.

Si parece que hay un fotograma de diferencia…

La sincronización vertical (o solo vsync) es una tecnología gráfica 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 de esta API es un esfuerzo máximo, y el navegador no ofrece garantías estrictas. Es posible que la API tenga un vsync de retraso en relación con el momento en que se renderiza un fotograma de video. Se necesita un vsync para que los cambios realizados en la página web a través de la API aparezcan en la pantalla (al igual que window.requestAnimationFrame()). Por lo tanto, si sigues actualizando el mediaTime o el número de fotogramas en tu página web y lo comparas con los fotogramas de video numerados, con el tiempo, el video se verá como si estuviera un fotograma por delante.

Lo que realmente sucede es que la fotograma está lista en vsync x, se activa la devolución de llamada y se renderiza la fotograma en vsync x+1, y los cambios realizados en la devolución de llamada se renderizan en vsync x+2. Para verificar si la devolución de llamada es una vsync tardía (y el fotograma ya se renderizó en la pantalla), verifica si metadata.expectedDisplayTime es aproximadamente now o una vsync en el futuro. Si está dentro de un rango de cinco a diez microsegundos de now, el fotograma ya se renderizó. Si el expectedDisplayTime está aproximadamente dieciséis milisegundos en el futuro (suponiendo que tu navegador o pantalla se actualice a 60 Hz), significa que estás sincronizado con el fotograma.

Demostración

Creé una pequeña demo en Glitch que muestra cómo se dibujan los fotogramas en un lienzo a la velocidad de fotogramas exacta del video y dónde se registran los metadatos de los fotogramas para depurar.

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 llevan mucho tiempo realizando el procesamiento a nivel de fotogramas 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

Thomas Guilbert especificó e implementó la API de requestVideoFrameCallback. Joe Medley y Kayce Basques revisaron esta publicación. Imagen hero de Denise Jans en Unsplash.