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 viene inviato un nuovo frame video al compositore. Ciò consente agli sviluppatori di eseguire operazioni efficienti per fotogramma video sui video, ad esempio l'elaborazione e la pittura di video su un canvas, l'analisi video o la sincronizzazione con sorgenti audio esterne.

Differenza con requestAnimationFrame()

Operazioni come il disegno di un frame video su un canvas utilizzando drawImage() effettuate tramite questa API verranno sincronizzate nel modo più efficiente possibile con la frequenza fotogrammi del video in riproduzione sullo schermo. A differenza di window.requestAnimationFrame(), che di solito si attiva circa 60 volte al secondo, requestVideoFrameCallback() è vincolato alla frequenza fotogrammi effettiva del video, con un'eccezione importante:

La frequenza effettiva con cui vengono eseguiti i callback è la minore frequenza tra la frequenza del video e quella del browser. Ciò significa che un video a 25 f/s riprodotto in un browser che dipinge a 60 Hz attiva callback a 25 Hz. Un video a 120 Hz nello stesso browser a 60 Hz attivava callback a 60 Hz.

Cosa si nasconde dietro a un nome?

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

Rilevamento delle funzionalità

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

Supporto del browser

Supporto dei browser

  • 83
  • 83
  • x
  • 15,4

Fonte

Polyfill

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

Utilizzo del metodo requestVideoFrameCallback()

Se ti è già capitato di usare il metodo requestAnimationFrame(), ti sentirai subito a tuo agio con il metodo requestVideoFrameCallback(). Puoi registrare il callback iniziale una volta, quindi ripetere la registrazione ogni volta che viene attivato il callback.

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: il momento 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 della presentazione multimediale (PTS) in secondi del frame presentato (ad es. il timestamp sulla sequenza temporale video.currentTime).
  • presentedFrames, di tipo unsigned long: un conteggio del numero di fotogrammi inviati per la composizione. Consente ai client di determinare se sono stati mancati 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 esempio, lo stesso valore 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 un'origine locale o remota, si tratta del momento in cui il fotogramma è stato acquisito dalla fotocamera. Per un'origine remota, il tempo di acquisizione viene stimato 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 un'origine 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 a supporto di video.currentTime, mentre mediaTime viene compilato direttamente dal valore presentationTimestamp del frame. mediaTime è ciò che devi usare se vuoi identificare esattamente i frame in modo riproducibile, anche per identificare esattamente quali frame sono persi.

Se sembra che qualcosa non veda un frame...

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, in background, avviene il compositing video nel thread del compositor, ogni cosa di questa API è il massimo possibile e il browser non offre garanzie rigide. Ciò che potrebbe accadere è che l'API può essere in ritardo di vsync rispetto al rendering di un frame video. Occorre un vsync per le modifiche apportate alla pagina web tramite l'API per essere visualizzate sullo schermo (come per window.requestAnimationFrame()). Quindi, se continui ad aggiornare mediaTime o il numero di frame sulla pagina web e confrontarlo con i frame video numerati, alla fine il video risulterà un frame avanti.

Ciò che accade in realtà è che il frame è pronto in vsync x, il callback viene attivato e il frame viene visualizzato in vsync x+1 e le modifiche apportate nella callback vengono visualizzate in vsync x+2. Puoi verificare se il callback è in ritardo con vsync (e il frame è già visualizzato sullo schermo) controllando se metadata.expectedDisplayTime è all'incirca di now o di un vsync in futuro. Se è compresa tra circa 5 e 10 microsecondi da now, il frame è già stato visualizzato; se expectedDisplayTime è di circa sedici millisecondi nel futuro (supponendo che il browser/schermo si aggiorni a 60 Hz), il frame è già sincronizzato con il frame.

Demo

Ho creato una piccola demo su Glitch che mostra come i fotogrammi vengono disegnati su una tela con la stessa 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 hanno eseguito 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.