Obtén más información para usar requestVideoFrameCallback() y trabajar de manera más eficiente con videos en el navegador.
Publicado el 8 de enero de 2023
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 realizadas con esta API, como dibujar un fotograma de video en un lienzo con drawImage(), se sincronizan con el mejor esfuerzo posible con la velocidad de fotogramas del video que se reproduce en la pantalla. Esto es diferente 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.
Detección de características
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
// The API is supported!
}
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
Si usas el método requestAnimationFrame(), reconocerás el método requestVideoFrameCallback(). Registra una devolución de llamada inicial una vez y, luego, vuelve a registrarla cada vez que se active.
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 tipoDOMHighResTimeStamp: Es la fecha y hora en la que el agente de usuario envió el fotograma para su composición.expectedDisplayTime, de tipoDOMHighResTimeStamp: Es la hora en la que el agente de usuario espera que el fotograma sea visible.width, de tipounsigned long: Es el ancho del fotograma de video, en píxeles de medios.height, de tipounsigned long: Es la altura del fotograma de video, en píxeles de medios.mediaTime, de tipodouble: Es la marca de tiempo de presentación de medios (PTS) en segundos del fotograma presentado (como su marca de tiempo en la línea de tiempo devideo.currentTime).presentedFrames, de tipounsigned long: Es un recuento de la cantidad de fotogramas enviados para la composición. Permite a los clientes determinar si se perdieron fotogramas entre instancias deVideoFrameRequestCallback.processingDuration, de tipodouble: 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 quemediaTime) hasta que el decodificador tuvo listo el fotograma decodificado para su presentación.
En el caso de las aplicaciones de WebRTC, es posible que aparezcan propiedades adicionales:
captureTime, de tipoDOMHighResTimeStamp: En el caso de los fotogramas de video provenientes 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 tipoDOMHighResTimeStamp: 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 tipounsigned 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 con exactitud los fotogramas de una manera reproducible, incluso para identificar exactamente qué fotogramas omitiste.
Si parece que todo está desfasado en 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.
Es posible que la API esté un VSync atrasada 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.
Lo que sucede en realidad 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 se encuentra aproximadamente dieciséis milisegundos en el futuro (suponiendo que tu navegador se actualiza a 60 Hz), entonces 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.