Realiza operaciones eficientes por fotograma en video con requestVideoFrameCallback()

Aprende a usar el requestVideoFrameCallback() para trabajar de manera más eficiente con los videos en el navegador.

El método HTMLVideoElement.requestVideoFrameCallback() permite a los autores web registrar 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 en el 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 en un lienzo con drawImage() realizadas a través de esta API se sincronizarán como mejor esfuerzo con la velocidad de fotogramas del video que se reproduce en la pantalla. A diferencia de window.requestAnimationFrame(), que suele activarse aproximadamente 60 veces por segundo, requestVideoFrameCallback() está vinculado a la velocidad de fotogramas real del video, con una excepción importante:

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

¿Qué debe incluir un nombre?

Debido a su similitud con window.requestAnimationFrame(), al principio se propuso el método como video.requestAnimationFrame() y se le cambió el nombre por requestVideoFrameCallback(), que se acordó luego de un largo debate.

Detección de funciones

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

Navegadores compatibles

Navegadores compatibles

  • 83
  • 83
  • x
  • 15.4

Origen

Polyfill

Está disponible un polyfill para el método requestVideoFrameCallback() basado en Window.requestAnimationFrame() y HTMLVideoElement.getVideoPlaybackQuality(). Antes de usar esto, ten en cuenta las limitaciones que se mencionan en README.

Cómo usar el método requestVideoFrameCallback()

Si alguna vez usaste el método requestAnimationFrame(), te familiarizarás de inmediato con el método requestVideoFrameCallback(). Registras una devolución de llamada inicial una vez y vuelves a registrarte cada vez que se activa la devolución de llamada.

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, del tipo DOMHighResTimeStamp: Es la hora a la que el usuario-agente envió el fotograma para la composición.
  • expectedDisplayTime, del tipo DOMHighResTimeStamp: Es la hora a la que el usuario-agente espera que el fotograma sea visible.
  • width, del tipo unsigned long: Es el ancho del fotograma del video, en píxeles multimedia.
  • height, del tipo unsigned long: La altura del fotograma del video, en píxeles multimedia
  • mediaTime, del tipo double: Es la marca de tiempo de la presentación multimedia (PTS) en segundos del fotograma presentado (p.ej., su marca de tiempo en la línea de tiempo de video.currentTime).
  • presentedFrames, del tipo unsigned long: Es un recuento de la cantidad de fotogramas que se enviaron para la composición. Permite a los clientes determinar si se perdieron marcos entre las instancias de VideoFrameRequestCallback.
  • processingDuration, del tipo double: Es la duración en segundos que transcurre desde el envío del paquete codificado con la misma marca de tiempo de presentación (PTS) que este marco (p.ej., igual que el mediaTime) al decodificador hasta que el marco decodificado estuvo listo para presentarse.

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

  • captureTime, del tipo DOMHighResTimeStamp: Para los fotogramas de video que provienen de una fuente local o remota, es el momento en que la cámara capturó el fotograma. Para una fuente remota, la hora de captura se estima mediante la sincronización del reloj y los informes de remitentes de RTCP a fin de convertir las marcas de tiempo de RTP a fin de convertir la hora de captura.
  • receiveTime, del tipo DOMHighResTimeStamp: Para los fotogramas de video que provienen de una fuente remota, es el momento en que la plataforma recibió el fotograma codificado, es decir, la hora en la que el último paquete que pertenece a este fotograma se recibió por la red.
  • rtpTimestamp, del tipo unsigned long: Es la marca de tiempo de RTP asociada con este fotograma del video.

Esta lista tiene un interés especial: 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 con el presentationTimestamp del fotograma. mediaTime es lo que debes usar si deseas identificar con exactitud los fotogramas de manera reproducible, incluso para identificar exactamente qué fotogramas omitiste.

Si todo parece estar fuera de cuadro...

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 de actualización de un monitor. Dado que requestVideoFrameCallback() se ejecuta en el subproceso principal, pero, de forma interna, la composición de videos ocurre en el subproceso del compositor, todo lo de esta API es el mejor esfuerzo y el navegador no ofrece ninguna garantía estricta. Lo que podría suceder es que la API pueda estar un retraso de vsync en relación con cuando se renderiza un fotograma de video. Se necesita una vsync 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 mediaTime o el número de fotograma en tu página web y lo comparas con los fotogramas numerados del video, finalmente el video se verá como si estuviera un fotograma más adelante.

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

Demostración

Creé una pequeña demostración en Glitch en la que se muestra cómo se dibujan los fotogramas en un lienzo con la misma velocidad de fotogramas del video y dónde se registran los metadatos con 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

Los usuarios han realizado procesamiento a nivel de fotograma durante mucho tiempo (sin tener acceso a los fotogramas reales, solo que se basan en video.currentTime). El método requestVideoFrameCallback() mejora en gran medida esta solución alternativa.

Agradecimientos

Thomas Guilbert especificó y luego implementó la API de requestVideoFrameCallback. Joe Medley y Kayce Basques revisaron esta publicación. Hero image de Denise Jans en Unsplash.