Esegui operazioni efficienti per frame video sul video con requestVideoFrameCallback()

Scopri come utilizzare requestVideoFrameCallback() per lavorare in modo più efficiente con i video nel browser.

Il metodo HTMLVideoElement.requestVideoFrameCallback() consente agli autori web di registrare un callback che viene eseguito nei passaggi di rendering quando un nuovo frame video viene inviato al compositore. Ciò consente agli sviluppatori di eseguire operazioni efficienti per fotogramma video, come l'elaborazione e il disegno su un canvas, l'analisi o la sincronizzazione con fonti audio esterne.

Differenza rispetto a requestAnimationFrame()

Le operazioni come il disegno di un fotogramma video su un canvas utilizzando drawImage() eseguite tramite questa API verranno sincronizzate al meglio con il frame rate del video riprodotto sullo schermo. A differenza di window.requestAnimationFrame(), che in genere si attiva circa 60 volte al secondo, requestVideoFrameCallback() è associato al frame rate video effettivo, con un'importante eccezione:

La frequenza effettiva con cui vengono eseguite le richiamate è la frequenza più bassa tra quella del video e quella del browser. Ciò significa che un video a 25 fps riprodotto in un browser che esegue il rendering a 60 Hz attiverebbe i callback a 25 Hz. Un video a 120 fps nello stesso browser a 60 Hz attiverebbe i callback a 60 Hz.

Cosa si nasconde dietro a un nome?

A causa della sua somiglianza con window.requestAnimationFrame(), il metodo inizialmente è stato proposto come video.requestAnimationFrame() e rinominato in requestVideoFrameCallback(), come concordato dopo una lunga discussione.

Rilevamento delle funzionalità

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

Supporto browser

Browser Support

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

Source

Polyfill

È disponibile un polyfill per il metodo requestVideoFrameCallback() basato su Window.requestAnimationFrame() e HTMLVideoElement.getVideoPlaybackQuality(). Prima di utilizzarlo, tieni presente le limitazioni menzionate in README.

Utilizzo del metodo requestVideoFrameCallback()

Se hai mai utilizzato il metodo requestAnimationFrame(), avrai subito familiarità con il metodo requestVideoFrameCallback(). Registri un callback iniziale una sola volta, poi lo registri di nuovo ogni volta che viene attivato.

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

Nel callback, now è un DOMHighResTimeStamp e metadata è un dizionario VideoFrameMetadata con le seguenti proprietà:

  • presentationTime, di tipo DOMHighResTimeStamp: L'ora in cui lo user agent ha inviato il frame per la composizione.
  • expectedDisplayTime, di tipo DOMHighResTimeStamp: L'ora in cui lo user agent prevede che il frame sia visibile.
  • width, di tipo unsigned long: La larghezza del frame video, in pixel multimediali.
  • height, di tipo unsigned long: L'altezza del frame video, in pixel multimediali.
  • mediaTime, di tipo double: Il timestamp di presentazione dei contenuti multimediali (PTS) in secondi del frame presentato (ad es. il timestamp sulla cronologia video.currentTime).
  • presentedFrames, di tipo unsigned long: Un conteggio del numero di frame inviati per la composizione. Consente ai client di determinare se sono stati persi frame tra le istanze di VideoFrameRequestCallback.
  • processingDuration, di tipo double: La durata trascorsa in secondi dall'invio del pacchetto codificato con lo stesso timestamp di presentazione (PTS) di questo frame (ad es. lo stesso di mediaTime) al decodificatore fino a quando il frame decodificato non è stato pronto per la presentazione.

Per le applicazioni WebRTC, potrebbero essere visualizzate proprietà aggiuntive:

  • captureTime, di tipo DOMHighResTimeStamp: Per i fotogrammi video provenienti da una sorgente locale o remota, questo è il momento in cui il fotogramma è stato acquisito dalla videocamera. Per una sorgente remota, il tempo di acquisizione viene stimato utilizzando la sincronizzazione dell'orologio e i report del mittente RTCP per convertire i timestamp RTP in tempo di acquisizione.
  • receiveTime, di tipo DOMHighResTimeStamp: Per i fotogrammi video provenienti da una fonte remota, questo è il momento in cui il fotogramma codificato è stato ricevuto dalla piattaforma, ovvero il momento in cui l'ultimo pacchetto appartenente a questo fotogramma è stato ricevuto tramite la rete.
  • rtpTimestamp, di tipo unsigned long: Il timestamp RTP associato a questo frame video.

Di particolare interesse in questo elenco è mediaTime. L'implementazione di Chromium utilizza l'orologio audio come origine temporale che supporta video.currentTime, mentre mediaTime viene compilato direttamente da presentationTimestamp del frame. L'mediaTime è ciò che devi utilizzare se vuoi identificare con precisione i frame in modo riproducibile, incluso per identificare esattamente i frame che hai perso.

Se le cose sembrano sfasate di un fotogramma…

La sincronizzazione verticale (o semplicemente vsync) è una tecnologia grafica che sincronizza la frequenza fotogrammi di un video e la frequenza di aggiornamento di un monitor. Poiché requestVideoFrameCallback() viene eseguito sul thread principale, ma, internamente, la composizione video avviene sul thread del compositore, tutto ciò che proviene da questa API è un tentativo e il browser non offre garanzie rigorose. Ciò che potrebbe accadere è che l'API possa essere in ritardo di una sincronizzazione verticale rispetto al rendering di un frame video. È necessario un vsync affinché le modifiche apportate alla pagina web tramite l'API vengano visualizzate sullo schermo (come window.requestAnimationFrame()). Pertanto, se continui ad aggiornare mediaTime o il numero di frame sulla tua pagina web e lo confronti con i frame numerati del video, alla fine il video sembrerà avere un frame di anticipo.

In realtà, il frame è pronto alla sincronizzazione verticale x, il callback viene attivato e il frame viene visualizzato alla sincronizzazione verticale x+1 e le modifiche apportate nel callback vengono visualizzate alla sincronizzazione verticale x+2. Puoi verificare se il callback è un ritardo della sincronizzazione verticale (e il frame è già stato visualizzato sullo schermo) controllando se metadata.expectedDisplayTime è circa now o una sincronizzazione verticale in futuro. Se si trova a circa 5-10 microsecondi da now, il frame è già stato sottoposto a rendering; se expectedDisplayTime si trova a circa 16 millisecondi nel futuro (supponendo che il browser/schermo si aggiorni a 60 Hz), allora è sincronizzato con il frame.

Demo

Ho creato una piccola demo che mostra come vengono disegnati i frame su un canvas esattamente alla frequenza fotogrammi del video e dove vengono registrati i metadati dei frame a scopo di debug.

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

Conclusioni

Le persone eseguono l'elaborazione a livello di frame da molto tempo, senza avere accesso ai frame effettivi, ma solo in base a video.currentTime. Il metodo requestVideoFrameCallback() migliora notevolmente questa soluzione alternativa.

Ringraziamenti

L'API requestVideoFrameCallback è stata specificata e implementata da Thomas Guilbert. Questo post è stato rivisto da Joe Medley e Kayce Basques. Immagine hero di Denise Jans su Unsplash.