Elaborazione video con WebCodecs

Manipolare i componenti dello stream video.

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

Le moderne tecnologie web offrono numerosi modi per lavorare con i video. L'API Media Stream, l'API Media Recording, l'API Media Source e l'API WebRTC formano un ricco set di strumenti per la registrazione, il trasferimento e la riproduzione di stream video. Sebbene risolvano determinate attività di alto livello, queste API non consentono ai programmatori web di lavorare con i singoli componenti di uno stream video, come i frame e i chunk non muxati di video o audio codificati. Per ottenere l'accesso a basso livello a questi componenti di base, gli sviluppatori utilizzano WebAssembly per integrare i codec video e audio nel browser. Tuttavia, dato che i browser moderni sono già dotati di una varietà di codec (che sono spesso accelerati dall'hardware), la loro ripacchettizzazione come WebAssembly sembra uno spreco di risorse umane e informatiche.

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
  • Fotogrammi video non elaborati
  • Decodificatori di immagini

L'API WebCodecs è utile per le applicazioni web che richiedono il controllo completo sul modo in cui vengono elaborati i contenuti multimediali, come editor video, videoconferenze, streaming video e così via.

Flusso di lavoro di elaborazione dei video

I fotogrammi sono il fulcro dell'elaborazione video. Pertanto, in WebCodecs la maggior parte delle classi consumerà o produrrà frame. I codificatori video convertono i frame in chunk codificati. I decodificatori video fanno il contrario.

Inoltre, VideoFrame è compatibile con altre API web perché è un CanvasImageSource e ha un costruttore che accetta CanvasImageSource. Pertanto, può essere utilizzato in funzioni come drawImage() e texImage2D(). Inoltre, può essere creato da canvas, bitmap, elementi video e altri frame video.

L'API WebCodecs funziona bene insieme alle 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 da uno stream di frame.

WebCodec e web worker

Per impostazione predefinita, l'API WebCodecs esegue tutte le operazioni più complesse in modo asincrono e al di fuori del thread principale. Tuttavia, poiché i callback di frame e chunk possono essere chiamati spesso più volte al secondo, potrebbero ingombrare il thread principale e rendere il sito web meno reattivo. Pertanto, è preferibile spostare la gestione dei singoli frame e dei chunk codificati in un worker web.

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

Con HTMLCanvasElement.transferControlToOffscreen è possibile eseguire anche il rendering al di fuori del thread principale. Tuttavia, se tutti gli strumenti di alto livello risultano scomodi, VideoFrame stesso è trasferibile e può essere spostato da un utente all'altro.

WebCodecs in azione

Codifica

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

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

  • Da un'origine immagine, ad esempio un canvas, una bitmap dell'immagine o un elemento video.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Utilizza MediaStreamTrackProcessor per estrarre frame da un 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;
    }
    
  • Creare un frame dalla sua rappresentazione in pixel binari in un 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 loro provenienza, i frame possono essere codificati in oggetti EncodedVideoChunk con un VideoEncoder.

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

  • Inizializza il dizionario con due funzioni per gestire i chunk codificati e gli errori. Queste funzioni sono definite dallo sviluppatore e non possono essere modificate dopo essere state 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() restituirà NotSupportedError se la configurazione non è supportata dal browser. Ti invitiamo a 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 i frame tramite il metodo encode(). Sia configure() che encode() restituiscono immediatamente senza attendere il completamento del lavoro effettivo. Consente di mettere in coda più frame per la codifica contemporaneamente, mentre encodeQueueSize mostra quante richieste sono in attesa in coda per il completamento delle codifiche precedenti. Gli errori vengono segnalati generando immediatamente un'eccezione, nel caso in cui gli argomenti o l'ordine delle chiamate ai metodi violino il contratto API, o 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 i frame devono essere informati 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();
  }
}

Finalmente è il momento di completare il codice di codifica scrivendo una funzione che gestisca i blocchi di video codificati quando escono dal codificatore. In genere questa funzione invia 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, devi assicurarti che tutte le richieste di codifica in attesa siano state completate, puoi chiamare flush() e attendere la promessa.

await encoder.flush();

Decodifica

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

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

L'insieme di parametri del codec varia da un codec all'altro. Ad esempio, il codec H.264 potrebbe richiedere un blob binario di AVCC, a meno che non sia codificato nel cosiddetto formato allegato 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 decodificatore, puoi iniziare a fornirgli oggetti EncodedVideoChunk. Per creare un chunk, ti serviranno:

  • Un BufferSource di dati video codificati
  • il timestamp di inizio del chunk in microsecondi (tempo medio del primo frame codificato nel chunk)
  • il tipo di chunk, uno dei seguenti:
    • key se il chunk può essere decodificato indipendentemente dai chunk precedenti
    • delta se il chunk può essere decodificato solo dopo la decodifica di uno o più chunk precedenti

Inoltre, tutti i chunk emessi dall'encoder sono pronti per il decoder così come sono. Tutto ciò che è stato detto sopra sulla generazione di report sugli errori e sulla natura asincrona dei metodi dell'encoder vale 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 dell'output del decodificatore (handleFrame()) ritorni rapidamente. Nell'esempio riportato di seguito, viene aggiunto un solo frame alla coda di frame pronti per il rendering. Il rendering viene eseguito separatamente e prevede due passaggi:

  1. In attesa del momento giusto per mostrare il frame.
  2. Disegno del frame sul canvas.

Quando un frame non è più necessario, chiama close() per rilasciare la memoria sottostante prima che il garbage collector vi acceda, si ridurrà 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 gli sviluppatori

Utilizza il riquadro Media in Chrome DevTools per visualizzare i log multimediali e eseguire il debug di WebCodecs.

Screenshot del riquadro Media per il debug di WebCodecs
Riquadro multimediale in Chrome DevTools per il debug di WebCodecs.

Demo

La demo seguente mostra come sono i frame di animazione di una tela:

  • acquisito a 25 f/s in ReadableStream di MediaStreamTrackProcessor
  • trasferito a un worker web
  • codificati in formato video H.264
  • sono nuovamente decodificati in una sequenza di fotogrammi
  • e visualizzato sulla seconda tela utilizzando transferControlToOffscreen()

Altre demo

Dai un'occhiata anche alle altre nostre demo:

Utilizzo dell'API WebCodecs

Rilevamento di funzionalità

Per verificare il supporto di 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 è falso.

Feedback

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

Fornisci informazioni sul design dell'API

C'è qualcosa nell'API che non funziona come previsto? Oppure mancano metodi o proprietà per implementare la tua idea? Hai una domanda o un commento sul modello di sicurezza? Invia una segnalazione relativa alle specifiche nel corrispondente repository GitHub o aggiungi il tuo parere a una segnalazione esistente.

Segnalare un problema con l'implementazione

Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione è diversa dalla specifica? Segnala un bug all'indirizzo new.crbug.com. Assicurati di includere il maggior numero di dettagli possibile, di semplici istruzioni per la riproduzione e di inserire Blink>Media>WebCodecs nella casella Componenti. Glitch è la soluzione perfetta per condividere riproduzioni in modo facile e veloce.

Mostra il supporto per l'API

Intendi utilizzare l'API WebCodecs? Il tuo sostegno pubblico aiuta il team di Chrome a dare la priorità alle funzionalità e mostra ad altri fornitori di browser quanto sia importante supportarle.

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

Immagine hero di Denise Jans su Unsplash.