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

Scopri come utilizzare il pulsante 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. In questo modo, gli sviluppatori possono eseguire operazioni efficienti sui singoli fotogrammi dei video, come l'elaborazione e la pittura su una tela, l'analisi video o la sincronizzazione con sorgenti audio esterne.

Differenza con requestAnimationFrame()

Operazioni come il disegno di un frame video su una tela utilizzando drawImage() effettuate tramite questa API verranno sincronizzate secondo il criterio del massimo impegno con la frequenza dei fotogrammi del video riprodotto sullo schermo. A differenza di window.requestAnimationFrame(), che di solito viene attivato circa 60 volte al secondo, requestVideoFrameCallback() è vincolato alla frequenza fotogrammi effettiva del video, con un'importante eccezione:

La frequenza effettiva con cui vengono eseguiti i callback è 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 attiva 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 poi rinominato in requestVideoFrameCallback(), dopo una lunga discussione.

Rilevamento di funzionalità

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

Supporto browser

Supporto dei browser

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

Origine

Polyfill

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

Utilizzo del metodo requestVideoFrameCallback()

Se hai già utilizzato il metodo requestAnimationFrame(), il metodo requestVideoFrameCallback() ti risulterà subito familiare. Devi registrare un callback iniziale una volta e poi registrarlo 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: La data e l'ora in cui l'user agent ha inviato il frame per la composizione.
  • expectedDisplayTime, di tipo DOMHighResTimeStamp: Il momento in cui l'user agent si aspetta che il frame sia visibile.
  • width, di tipo unsigned long: la larghezza del frame del video, in pixel medi.
  • height, di tipo unsigned long: l'altezza del frame del video, in pixel medi.
  • mediaTime, di tipo double: Il timestamp (PTS) della presentazione dei contenuti multimediali in secondi dell'inquadratura presentata (ad es. il timestamp nella sequenza temporale 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 dal momento dell'invio al decodificatore del pacchetto codificato con lo stesso timestamp di presentazione (PTS) di questo frame (ad es. lo stesso di mediaTime) fino al momento in cui il frame decodificato era 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, si tratta dell'ora in cui il fotogramma è stato acquisito dalla videocamera. Per un'origine remota, la data e l'ora di acquisizione vengono stimate utilizzando la sincronizzazione dell'orologio e i report del mittente RTCP per convertire i timestamp RTP in data e ora di acquisizione.
  • receiveTime, di tipo DOMHighResTimeStamp: per i frame video provenienti da una sorgente remota, si tratta dell'ora in cui il frame codificato è stato ricevuto dalla piattaforma, ovvero l'ora in cui l'ultimo pacchetto appartenente a questo frame è stato ricevuto sulla 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 dal presentationTimestamp del frame. mediaTime è ciò che devi utilizzare se vuoi identificare esattamente i frame in modo riproducibile, incluso identificare esattamente i frame mancanti.

Se le cose sembrano fuori 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 nel thread principale, ma sotto il cofano il compositing video avviene nel thread del compositore, tutto ciò che proviene da questa API è secondo il criterio del massimo impegno e il browser non offre garanzie rigorose. È possibile che l'API sia in ritardo di un vsync rispetto al momento in cui viene visualizzato un frame video. È necessario un vsync affinché le modifiche apportate alla pagina web tramite l'API vengano visualizzate sullo schermo (come per window.requestAnimationFrame()). Pertanto, se continui ad aggiornare mediaTime o il numero di frame nella pagina web e lo confronti con i frame video numerati, alla fine il video sembrerà essere in anticipo di un frame.

In realtà, il frame è pronto a vsync x, il callback viene attivato e il frame viene visualizzato a vsync x+1, e le modifiche apportate nel callback vengono visualizzate a vsync x+2. Puoi verificare se il callback è in ritardo rispetto a vsync (e il frame è già stato visualizzato sullo schermo) controllando se metadata.expectedDisplayTime è approssimativamente now o un vsync in futuro. Se è compreso tra circa cinque e dieci microsecondi di now, il frame è già stato visualizzato; se expectedDisplayTime è approssimativamente sedici millisecondi nel futuro (supponendo che il browser/lo schermo si aggiorni a 60 Hz), allora sei in sincronia con il frame.

Demo

Ho creato una piccola demo su Glitch che mostra come i fotogrammi vengono disegnati su una tela esattamente con la frequenza fotogrammi del video e dove vengono registrati i metadati dei fotogrammi 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, 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 esaminato da Joe Medley e Kayce Basques. Immagine hero di Denise Jans su Unsplash.