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 molti modi per lavorare con i video. API Media Stream, l'API Media Recording, API Media Source, e l'API WebRTC si sommano a un ricco set di strumenti per la registrazione, il trasferimento e la riproduzione di stream video. Queste API, pur risolvendo alcune attività di alto livello, i programmatori lavorano con i singoli componenti di uno stream video, come i fotogrammi frammenti di video o audio codificati e non associati. Per ottenere un accesso di basso livello a questi componenti di base, gli sviluppatori hanno usato WebAssembly per integrare codec video e audio nel browser. Ma dato i browser moderni dispongono già di una varietà di codec (spesso accelerata dall'hardware), ripacchettizzandole come WebAssembly sembra uno spreco di risorse umane e informatiche.

L'API WebCodecs elimina questa inefficienza dando ai programmatori un modo per usare i componenti multimediali già presenti il 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 del modalità di elaborazione dei contenuti multimediali, ad esempio editor video, videoconferenze, streaming e così via.

Flusso di lavoro di elaborazione dei video

I fotogrammi sono il fulcro dell'elaborazione video. Di conseguenza, in WebCodec gran parte delle classi consumare o produrre frame. I codificatori video convertono i fotogrammi in o blocchi di testo. I decoder video fanno il contrario.

Inoltre, VideoFrame funziona perfettamente con altre API web in quanto è un CanvasImageSource e ha un costruttore che accetta CanvasImageSource. Pertanto, può essere utilizzata in funzioni come drawImage() e texImage2D(). Può anche essere creato da canvas, bitmap, elementi video e altri fotogrammi.

L'API WebCodecs funziona bene insieme alle classi dell'API Insertable Streams che collegano i WebCodec 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

L'API WebCodecs si occupa di tutte le operazioni più gravose in modo asincrono e al di fuori del thread principale. Ma poiché i callback frame e chunk possono essere chiamati più volte al secondo, potrebbero ingombrare il thread principale e rendere il sito web meno reattivo. Perciò è preferibile spostare la gestione dei singoli frame e di blocchi codificati in un di Google.

Per aiutarti, ReadableStream offre un modo conveniente per trasferire automaticamente tutti i frame provenienti da un contenuto multimediale il tracciamento per il worker. Ad esempio, MediaStreamTrackProcessor può essere utilizzato per ottenere un ReadableStream per una traccia di stream multimediale proveniente dalla webcam. In seguito lo stream viene trasferito a un worker web dove i frame vengono letti uno alla volta e messi in coda in un VideoEncoder.

Con HTMLCanvasElement.transferControlToOffscreen è possibile eseguire il rendering anche dal thread principale. Ma se tutti gli strumenti di alto livello non sia conveniente, pertanto l'entità VideoFrame stessa è trasferibile e potrebbe essere si spostano da un worker all'altro.

WebCodec in azione

Codifica

Il percorso da un elemento Canvas o ImageBitmap alla rete o allo spazio di archiviazione
Il percorso da un Canvas o 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 });
    
  • Usa MediaStreamTrackProcessor per eseguire il pull dei 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;
    }
    
  • Consente di creare un frame dalla relativa rappresentazione in pixel binari in un file 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 EncodedVideoChunk oggetti con VideoEncoder.

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

  • Inizia il dizionario con due funzioni per la gestione di blocchi codificati e errori. Queste funzioni sono definite dallo sviluppatore e non possono essere modificate dopo vengono passate al costruttore VideoEncoder.
  • Oggetto di configurazione encoder, che contiene i parametri per l'output stream video. Puoi modificare questi parametri in un secondo momento richiamando configure().

Il metodo configure() restituisce NotSupportedError se la configurazione non è supportate 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.
}

Dopo la configurazione, il codificatore è pronto per accettare frame con il metodo encode(). Sia configure() che encode() ritornano immediatamente senza attendere il il lavoro effettivo da completare. Consente a diversi frame di inserire i dati in coda per la codifica contemporaneamente, mentre encodeQueueSize mostra quante richieste sono in attesa in coda per le codifica precedenti. Gli errori vengono segnalati generando immediatamente un'eccezione, se gli argomenti o l'ordine delle chiamate ai metodi viola il contratto API oppure chiamando il error() per problemi riscontrati nell'implementazione del codec. Se la codifica ha esito positivo, output() viene chiamato con un nuovo blocco codificato come argomento. Un altro dettaglio importante è che i frame devono essere informati quando non sono più necessario chiamando il numero 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, è il momento di completare il codice di codifica scrivendo una funzione che gestisca frammenti di video codificati quando escono dal codificatore. Di solito questa funzione invia blocchi di dati sulla rete o li mux in un supporto container 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 è stata completata, 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 a un ImageBitmap.

La configurazione di VideoDecoder è simile a quella che è stata eseguita per VideoEncoder: vengono passate due funzioni al momento della creazione del decoder e il codec vengono assegnati a configure().

L'insieme di parametri del codec varia da codec a codec. Ad esempio codec H.264 potrebbe aver bisogno di un blob binario di AVCC, a meno che non siano codificati 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 decoder, puoi iniziare a alimentarlo con oggetti EncodedVideoChunk. Per creare un blocco, avrai bisogno di:

  • Un BufferSource di dati video codificati
  • il timestamp di inizio del blocco in microsecondi (tempo dei media del primo frame codificato nel blocco)
  • il tipo di chunk, 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. Tutte le considerazioni precedenti relative alla segnalazione degli errori e alla natura asincrona dei metodi dell'encoder sono ugualmente veri 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 può essere mostrato nella pagina un frame appena decodificato. È è meglio assicurarsi che il callback di output del decoder (handleFrame()) che ritorna rapidamente. Nell'esempio riportato di seguito, viene aggiunto solo un frame alla coda di pronti per il rendering. Il rendering viene eseguito separatamente ed è costituito da due passaggi:

  1. In attesa del momento giusto per mostrare il frame.
  2. Disegno della cornice sull'area di lavoro.

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 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 Contenuti multimediali in Chrome DevTools per visualizzare i log multimediali ed eseguire il debug dei WebCodec.

Screenshot del riquadro Media per il debug dei WebCodec
Riquadro multimediale in Chrome DevTools per il debug dei codec web.

Demo

La demo riportata di seguito mostra come sono i frame di animazione da un canvas:

  • acquisito a 25 f/s in ReadableStream di MediaStreamTrackProcessor
  • trasferito a un lavoratore web
  • con codifica nel formato video H.264
  • sono nuovamente decodificati in una sequenza di fotogrammi
  • e visualizzato sul secondo canvas utilizzando transferControlToOffscreen()

Altre demo

Guarda anche le altre nostre demo:

Utilizzo dell'API WebCodecs

Rilevamento delle caratteristiche

Per verificare il supporto dei WebCodec:

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

Tieni presente che l'API WebCodecs è disponibile solo in contesti sicuri, pertanto 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 sono mancano metodi o proprietà per implementare la tua idea? Avere un domanda o commento sul modello di sicurezza? Segnala un problema con le specifiche sul un repository GitHub corrispondente oppure aggiungi le tue opinioni su un problema esistente.

Segnalare un problema con l'implementazione

Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione rispetto alle specifiche? Segnala un bug all'indirizzo new.crbug.com. Includi il maggior numero di dettagli possibile, istruzioni semplici per viene riprodotto e inserisci 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 supporto pubblico aiuta Il team di Chrome assegna la priorità alle funzionalità e mostra ad altri fornitori di browser quanto sono importanti è sostenerli.

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

Immagine hero di Denise Jans su Unsplash.