Elaborazione video con WebCodecs

Manipolazione dei componenti del video stream.

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

Le moderne tecnologie web offrono ampi modi per lavorare con i video. L'API Media Stream, l'API Media Recording, l'API Media Source e l'API WebRTC si aggiungono a un ricco set di strumenti per la registrazione, il trasferimento e la riproduzione di stream video. Per risolvere determinate attività di alto livello, queste API non consentono ai programmatori web di lavorare con i singoli componenti di uno stream video, come frame e blocchi non combinati di video o audio codificati. Per ottenere l'accesso di basso livello a questi componenti di base, gli sviluppatori hanno utilizzato WebAssembly per importare codec video e audio nel browser. Tuttavia, dal momento che i browser moderni sono già dotati di una varietà di codec (spesso accelerati dall'hardware), la loro ripacchettizzazione in WebAssembly sembra uno spreco di risorse umane e computer.

L'API WebCodecs elimina questa inefficienza offrendo ai programmatori un modo per utilizzare i componenti multimediali già presenti nel browser. In particolare:

  • Decodificatori video e audio
  • Codificatori video e audio
  • Frame video non elaborati
  • Decodificatori di immagini

L'API WebCodecs è utile per le applicazioni web che richiedono il controllo completo sulle modalità di elaborazione dei contenuti multimediali, come editor video, videoconferenze, streaming video e così via.

Flusso di lavoro dell'elaborazione video

I fotogrammi sono il pezzo forte dell'elaborazione video. Quindi in WebCodecs la maggior parte delle classi consuma o produce frame. I codificatori video convertono i fotogrammi in blocchi codificati. I decoder video fanno il contrario.

Inoltre VideoFrame funziona bene con altre API web in quanto CanvasImageSource e con un costruttore che accetta CanvasImageSource. Quindi può essere utilizzato in funzioni come drawImage() etexImage2D(). Inoltre, può essere creato a partire da canvas, bitmap, elementi video e altri frame video.

L'API WebCodecs funziona bene in coppia con le classi dell'API Insertable Streams, che collegano WebCodecs alle tracce degli stream multimediali.

  • MediaStreamTrackProcessor suddivide le tracce multimediali in singoli frame.
  • MediaStreamTrackGenerator crea una traccia multimediale a partire da uno stream di frame.

WebCodec e web worker

L'API WebCodecs è stata progettata per svolgere le attività più complesse in modo asincrono e fuori dal thread principale. Tuttavia, poiché i callback di frame e blocchi possono essere spesso chiamati più volte al secondo, potrebbero ingombrare il thread principale e quindi rendere meno reattivo il sito web. Pertanto è preferibile spostare la gestione dei singoli frame e dei blocchi codificati in un web worker.

Per aiutarti, ReadableStream fornisce un modo pratico per trasferire automaticamente tutti i frame provenienti da una traccia multimediale al worker. Ad esempio, MediaStreamTrackProcessor può essere utilizzato per ottenere un ReadableStream per una traccia di streaming multimediale proveniente dalla webcam. Successivamente, lo stream viene trasferito a un web worker, in cui i frame vengono letti uno alla volta e inseriti in coda in un VideoEncoder.

Con HTMLCanvasElement.transferControlToOffscreen è possibile eseguire il rendering anche al di fuori del thread principale. Tuttavia, nel caso in cui tutti gli strumenti di alto livello si rivelassero scomodi, VideoFrame stesso è trasferibile e potrebbe essere spostato da un worker a un altro.

WebCodec in azione

Codifica

Il percorso da Canvas o ImageBitmap alla rete o allo spazio di archiviazione
Il percorso da Canvas o ImageBitmap alla rete o allo spazio di archiviazione

Tutto inizia con un VideoFrame. Esistono tre modi per creare i fotogrammi video.

  • Da un'origine immagine come un canvas, una bitmap immagine o un elemento video.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Usa MediaStreamTrackProcessor per estrarre frame da una MediaStreamTrack

    const stream = await navigator.mediaDevices.getUserMedia({…});
    const track = stream.getTracks()[0];
    
    const trackProcessor = new MediaStreamTrackProcessor(track);
    
    const reader = trackProcessor.readable.getReader();
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      const frameFromCamera = result.value;
    }
    
  • Crea un frame dalla sua rappresentazione di pixel binari in una BufferSource.

    const pixelSize = 4;
    const init = {
      timestamp: 0,
      codedWidth: 320,
      codedHeight: 200,
      format: "RGBA",
    };
    const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
    for (let x = 0; x < init.codedWidth; x++) {
      for (let y = 0; y < init.codedHeight; y++) {
        const offset = (y * init.codedWidth + x) * pixelSize;
        data[offset] = 0x7f;      // Red
        data[offset + 1] = 0xff;  // Green
        data[offset + 2] = 0xd4;  // Blue
        data[offset + 3] = 0x0ff; // Alpha
      }
    }
    const frame = new VideoFrame(data, init);
    

Indipendentemente dalla provenienza, i frame possono essere codificati in oggetti EncodedVideoChunk con un VideoEncoder.

Prima della codifica, a VideoEncoder devono essere assegnati due oggetti JavaScript:

  • Dizionario init con due funzioni per la gestione di blocchi ed errori codificati. Queste funzioni sono definite dallo sviluppatore e non possono essere modificate dopo essere passate al costruttore VideoEncoder.
  • Oggetto di configurazione del codificatore, che contiene i parametri per lo stream video di output. Puoi modificare questi parametri in un secondo momento chiamando configure().

Il metodo configure() restituisce NotSupportedError se la configurazione non è supportata dal browser. Ti consigliamo di chiamare il metodo statico VideoEncoder.isConfigSupported() con la configurazione per verificare in anticipo se la configurazione è supportata e attendere la promessa.

const init = {
  output: handleChunk,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};

const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
  const encoder = new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

Una volta configurato, il codificatore è pronto ad accettare frame tramite il metodo encode(). Sia configure() che encode() ritornano immediatamente senza dover attendere il completamento del lavoro effettivo. Consente a diversi frame di mettere in coda per la codifica contemporaneamente, mentre encodeQueueSize mostra quante richieste sono in coda per completare le codifiche precedenti. Gli errori vengono segnalati generando immediatamente un'eccezione, nel caso in cui gli argomenti o l'ordine delle chiamate al metodo violino il contratto dell'API oppure richiamando il callback error() per i problemi riscontrati nell'implementazione del codec. Se la codifica viene completata correttamente, il callback output() viene chiamato con un nuovo blocco codificato come argomento. Un altro dettaglio importante è che è necessario avvisare i frame quando non sono più necessari chiamando close().

let frameCounter = 0;

const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);

const reader = trackProcessor.readable.getReader();
while (true) {
  const result = await reader.read();
  if (result.done) break;

  const frame = result.value;
  if (encoder.encodeQueueSize > 2) {
    // Too many frames in flight, encoder is overwhelmed
    // let's drop this frame.
    frame.close();
  } else {
    frameCounter++;
    const keyFrame = frameCounter % 150 == 0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

Infine, si completa il codice di codifica scrivendo una funzione che gestisca i blocchi di video codificati che escono dal codificatore. Di solito, questa funzione inviava blocchi di dati sulla rete o li mux in un container multimediale per l'archiviazione.

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }

  // actual bytes of encoded data
  const chunkData = new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);

  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

Se a un certo punto dovessi assicurarti che tutte le richieste di codifica in attesa siano state completate, puoi chiamare flush() e attendere l'esito.

await encoder.flush();

Decodifica

Il percorso dalla rete o dallo spazio di archiviazione a una Canvas o una ImageBitmap.
Il percorso dalla rete o dallo spazio di archiviazione a Canvas o ImageBitmap.

La configurazione di VideoDecoder è simile a quella di VideoEncoder: vengono passate due funzioni quando viene creato il decoder e i parametri del codec vengono assegnati a configure().

L'insieme dei parametri del codec varia da codec a codec. Ad esempio, il codec H.264 potrebbe richiedere un blob binario di AVCC, a meno che non sia codificato nel cosiddetto formato Annex B (encoderConfig.avc = { format: "annexb" }).

const init = {
  output: handleFrame,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};

const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder = new VideoDecoder(init);
  decoder.configure(config);
} else {
  // Try another config.
}

Una volta inizializzato il decoder, puoi iniziare a fornirlo con EncodedVideoChunk oggetti. Per creare un blocco, avrai bisogno di:

  • Un BufferSource di dati video codificati
  • timestamp di inizio del blocco in microsecondi (tempo multimediale del primo frame codificato nel blocco)
  • il tipo di blocco, uno dei seguenti:
    • key se il blocco può essere decodificato indipendentemente dai blocchi precedenti.
    • delta se il blocco può essere decodificato solo dopo che uno o più blocchi precedenti sono stati decodificati

Inoltre, tutti i blocchi emessi dall'encoder sono pronti per il decoder così come sono. Tutto quanto detto sopra sulla segnalazione degli errori e sulla natura asincrona dei metodi dell'encoder è altrettanto valido anche per i decoder.

const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
  const chunk = new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

Ora è il momento di mostrare come un frame appena decodificato può essere visualizzato nella pagina. È meglio assicurarsi che il callback di output del decoder (handleFrame()) restituisca rapidamente. Nell'esempio seguente, aggiunge solo un frame alla coda dei frame pronti per il rendering. Il rendering avviene separatamente ed è costituito da due passaggi:

  1. In attesa del momento giusto per mostrare l'inquadratura.
  2. Disegnamo il frame sulla tela.

Quando un frame non è più necessario, chiama close() per rilasciare la memoria sottostante prima che il garbage collector arrivi. In questo modo si riduce la quantità media di memoria utilizzata dall'applicazione web.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime == 0) baseTime = performance.now();
  let mediaTime = performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow = pendingFrames.length == 0;
  if (underflow) return;

  const frame = pendingFrames.shift();

  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r) => {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();

  // Immediately schedule rendering of the next frame
  setTimeout(renderFrame, 0);
}

Suggerimenti per sviluppatori

Usa il Pannello multimediale in Chrome DevTools per visualizzare i log multimediali ed eseguire il debug dei WebCodecs.

Screenshot del riquadro Media per il debug di WebCodecs
Media Panel in Chrome DevTools per il debug dei WebCodecs.

Demo

La demo riportata di seguito mostra come sono i frame dell'animazione di una tela:

  • acquisiti a 25 f/s in ReadableStream da MediaStreamTrackProcessor
  • viene trasferito a un web worker
  • Codificato nel formato video H.264
  • decodificato di nuovo in una sequenza di fotogrammi video
  • ed eseguito il rendering sul secondo canvas utilizzando transferControlToOffscreen()

Altre demo

Guarda anche le altre nostre demo:

Utilizzo dell'API WebCodecs

Rilevamento delle funzionalità

Per verificare il supporto per WebCodecs:

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

Tieni presente che l'API WebCodecs è disponibile solo in contesti sicuri, quindi il rilevamento non andrà a buon fine se self.isSecureContext è false.

Feedback

Il team di Chrome vuole conoscere la tua esperienza con l'API WebCodecs.

Parlaci della progettazione dell'API

C'è qualcosa nell'API che non funziona come previsto? Oppure mancano metodi o proprietà per implementare la tua idea? Hai domande o commenti sul modello di sicurezza? Segnala un problema relativo alle specifiche sul repository GitHub corrispondente o aggiungi le tue opinioni su un problema esistente.

Segnala un problema con l'implementazione

Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione è diversa dalle specifiche? Segnala un bug all'indirizzo new.crbug.com. Assicurati di includere il maggior numero di dettagli possibile e di semplici istruzioni per la riproduzione e di inserire Blink>Media>WebCodecs nella casella Componenti. Glitch funziona benissimo per condividere riproduzioni rapide e semplici.

Mostra il supporto dell'API

Hai intenzione di utilizzare l'API WebCodecs? Il tuo supporto pubblico aiuta il team di Chrome a dare la priorità alle funzionalità e mostra ad altri fornitori di browser l'importanza di supportarle.

Invia email a media-dev@chromium.org o un tweet a @ChromiumDev utilizzando l'hashtag #WebCodecs e facci sapere dove e come lo stai usando.

Immagine hero di Denise Jans su Unsplash.