Videoverwerking met WebCodecs

Manipuleren van videostreamcomponenten.

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

Moderne webtechnologieën bieden talloze manieren om met video te werken. De Media Stream API , Media Recording API , Media Source API en WebRTC API vormen samen een uitgebreide toolset voor het opnemen, overbrengen en afspelen van videostreams. Hoewel deze API's bepaalde taken op hoog niveau oplossen, laten ze webprogrammeurs niet werken met afzonderlijke componenten van een videostream, zoals frames en niet-gemixte fragmenten van gecodeerde video of audio. Om toegang op laag niveau tot deze basiscomponenten te krijgen, gebruiken ontwikkelaars WebAssembly om video- en audiocodecs in de browser te integreren. Maar aangezien moderne browsers al met verschillende codecs worden geleverd (die vaak door hardware worden versneld), lijkt het herverpakken ervan als WebAssembly een verspilling van menselijke en computerbronnen.

De WebCodecs API elimineert deze inefficiëntie door programmeurs de mogelijkheid te bieden om mediacomponenten te gebruiken die al in de browser aanwezig zijn. Meer specifiek:

  • Video- en audiodecoders
  • Video- en audio-encoders
  • Ruwe videoframes
  • Beelddecoders

De WebCodecs API is handig voor webapplicaties die volledige controle nodig hebben over de manier waarop media-inhoud wordt verwerkt, zoals video-editors, videoconferenties, videostreaming, enzovoort.

Workflow voor videoverwerking

Frames vormen de kern van videoverwerking. Daarom consumeren of produceren de meeste klassen in WebCodecs frames. Video-encoders zetten frames om in gecodeerde fragmenten. Video-decoders doen het tegenovergestelde.

VideoFrame werkt ook goed samen met andere web-API's doordat het een CanvasImageSource is en een constructor heeft die CanvasImageSource accepteert. Het kan dus gebruikt worden in functies zoals drawImage() en texImage2D() . Het kan ook geconstrueerd worden met canvassen, bitmaps, video-elementen en andere videoframes.

De WebCodecs API werkt goed samen met de klassen van de Insertable Streams API , die WebCodecs verbinden met mediastreamtracks .

  • MediaStreamTrackProcessor verdeelt mediatracks in afzonderlijke frames.
  • MediaStreamTrackGenerator maakt een mediatrack van een stroom frames.

WebCodecs en webworkers

De WebCodecs API doet al het zware werk asynchroon en los van de hoofdthread. Maar omdat frame- en chunk-callbacks vaak meerdere keren per seconde worden aangeroepen, kunnen ze de hoofdthread overbelasten en de website daardoor minder responsief maken. Daarom is het beter om de verwerking van individuele frames en gecodeerde chunks naar een webworker te verplaatsen.

Om daarbij te helpen, biedt ReadableStream een ​​handige manier om automatisch alle frames van een mediatrack naar de worker over te brengen. MediaStreamTrackProcessor kan bijvoorbeeld worden gebruikt om een ReadableStream te verkrijgen voor een mediastreamtrack die afkomstig is van de webcam. Vervolgens wordt de stream doorgestuurd naar een webworker, waar de frames één voor één worden gelezen en in een VideoEncoder worden geplaatst.

Met HTMLCanvasElement.transferControlToOffscreen kan zelfs rendering buiten de hoofdthread worden uitgevoerd. Mochten alle geavanceerde tools echter onhandig blijken, dan is VideoFrame zelf overdraagbaar en kan het tussen workers worden verplaatst.

WebCodecs in actie

Codering

Het pad van een Canvas of een ImageBitmap naar het netwerk of naar de opslag
Het pad van een Canvas of een ImageBitmap naar het netwerk of naar de opslag

Het begint allemaal met een VideoFrame . Er zijn drie manieren om videoframes te maken.

  • Vanuit een beeldbron zoals een canvas, een bitmap van een afbeelding of een video-element.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Gebruik MediaStreamTrackProcessor om frames uit een MediaStreamTrack te halen

    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;
    }
    
  • Maak een frame van de binaire pixelrepresentatie in een 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);
    

Ongeacht waar ze vandaan komen, kunnen frames met een VideoEncoder worden gecodeerd in EncodedVideoChunk -objecten.

Voordat VideoEncoder kan coderen, moeten er twee JavaScript-objecten worden opgegeven:

  • Initialiseer een woordenboek met twee functies voor het verwerken van gecodeerde chunks en fouten. Deze functies zijn door de ontwikkelaar gedefinieerd en kunnen niet meer worden gewijzigd nadat ze zijn doorgegeven aan de VideoEncoder constructor.
  • Encoderconfiguratieobject, dat parameters bevat voor de uitvoervideostream. U kunt deze parameters later wijzigen door configure() aan te roepen.

De configure() -methode genereert NotSupportedError als de configuratie niet door de browser wordt ondersteund. Het is raadzaam om de statische methode VideoEncoder.isConfigSupported() met de configuratie aan te roepen om vooraf te controleren of de configuratie wordt ondersteund en te wachten op de toezegging.

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.
}

Nadat de encoder is ingesteld, is deze klaar om frames te accepteren via encode() methode. Zowel configure() als encode() retourneren direct zonder te wachten tot het eigenlijke werk is voltooid. Het staat meerdere frames tegelijk in de wachtrij voor codering toe, terwijl encodeQueueSize laat zien hoeveel verzoeken er in de wachtrij staan ​​voor eerdere codering. Fouten worden gerapporteerd door direct een uitzondering te genereren, in het geval dat de argumenten of de volgorde van methodeaanroepen het API-contract schendt, of door de error() callback aan te roepen bij problemen die zich voordoen in de codec-implementatie. Als de codering succesvol is voltooid, wordt de output() -callback aangeroepen met een nieuw gecodeerd blok als argument. Een ander belangrijk detail hierbij is dat frames moeten worden verteld wanneer ze niet langer nodig zijn door close() aan te roepen.

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();
  }
}

Ten slotte is het tijd om de code te voltooien door een functie te schrijven die fragmenten van gecodeerde video verwerkt zodra ze uit de encoder komen. Meestal zou deze functie fragmenten van data via het netwerk versturen of ze in een mediacontainer opslaan .

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,
  });
}

Als u op een gegeven moment zeker wilt weten dat alle openstaande coderingsaanvragen zijn voltooid, kunt u flush() aanroepen en wachten op de belofte.

await encoder.flush();

Decoderen

Het pad van het netwerk of de opslag naar een Canvas of een ImageBitmap.
Het pad van het netwerk of de opslag naar een Canvas of een ImageBitmap .

Het instellen van een VideoDecoder is vergelijkbaar met wat is gedaan voor de VideoEncoder : er worden twee functies doorgegeven wanneer de decoder wordt aangemaakt en er worden codecparameters meegegeven aan configure() .

De set codecparameters varieert per codec. Zo kan een H.264-codec bijvoorbeeld een binaire blob van AVCC nodig hebben, tenzij deze gecodeerd is in het zogenaamde Annex B-formaat ( 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.
}

Zodra de decoder is geïnitialiseerd, kun je hem voeden met EncodedVideoChunk -objecten. Om een ​​chunk te maken, heb je het volgende nodig:

  • Een BufferSource van gecodeerde videogegevens
  • het starttijdstempel van het blok in microseconden (mediatijd van het eerste gecodeerde frame in het blok)
  • het type van het stuk, een van:
    • key als het fragment onafhankelijk van eerdere fragmenten kan worden gedecodeerd
    • delta als het fragment alleen kan worden gedecodeerd nadat een of meer eerdere fragmenten zijn gedecodeerd

Ook alle fragmenten die door de encoder worden uitgezonden, zijn klaar voor de decoder. Alles wat hierboven is gezegd over foutrapportage en de asynchrone aard van de methoden van de encoder, geldt ook voor decoders.

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();

Nu is het tijd om te laten zien hoe een vers gedecodeerd frame op de pagina kan worden weergegeven. Het is beter om ervoor te zorgen dat de decoder-uitvoercallback ( handleFrame() ) snel retourneert. In het onderstaande voorbeeld wordt er alleen een frame toegevoegd aan de wachtrij met frames die klaar zijn voor rendering. Rendering gebeurt apart en bestaat uit twee stappen:

  1. Wacht op het juiste moment om het frame te tonen.
  2. Het frame op het canvas tekenen.

Zodra een frame niet meer nodig is, roept u close() aan om het onderliggende geheugen vrij te geven voordat de garbage collector het frame kan vinden. Hiermee verlaagt u de gemiddelde hoeveelheid geheugen die door de webapplicatie wordt gebruikt.

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);
}

Ontwikkelaarstips

Gebruik het Mediapaneel in Chrome DevTools om medialogs te bekijken en WebCodecs te debuggen.

Schermafbeelding van het mediapaneel voor het debuggen van webcodecs
Mediapaneel in Chrome DevTools voor het debuggen van WebCodecs.

Demonstratie

De onderstaande demo laat zien hoe animatieframes van een canvas eruit zien:

  • vastgelegd met 25 fps in een ReadableStream door MediaStreamTrackProcessor
  • overgebracht naar een webworker
  • gecodeerd in H.264-videoformaat
  • opnieuw gedecodeerd in een reeks videoframes
  • en weergegeven op het tweede canvas met transferControlToOffscreen()

Andere demo's

Bekijk ook onze andere demo's:

De WebCodecs API gebruiken

Functiedetectie

Controleren op WebCodecs-ondersteuning:

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

Houd er rekening mee dat de WebCodecs API alleen beschikbaar is in beveiligde contexten . Detectie mislukt dus als self.isSecureContext false is.

Feedback

Het Chrome-team wil graag uw ervaringen met de WebCodecs API horen.

Vertel ons over het API-ontwerp

Werkt er iets aan de API dat niet werkt zoals je had verwacht? Of ontbreken er methoden of eigenschappen die je nodig hebt om je idee te implementeren? Heb je een vraag of opmerking over het beveiligingsmodel? Dien een spec-issue in op de betreffende GitHub-repository of voeg je mening toe aan een bestaand issue.

Meld een probleem met de implementatie

Heb je een bug gevonden in de implementatie van Chrome? Of wijkt de implementatie af van de specificatie? Meld een bug op new.crbug.com . Zorg ervoor dat je zoveel mogelijk details opgeeft, eenvoudige instructies voor reproductie, en voer Blink>Media>WebCodecs in het vak Componenten in.

Toon ondersteuning voor de API

Bent u van plan de WebCodecs API te gebruiken? Uw publieke steun helpt het Chrome-team om functies te prioriteren en laat andere browserleveranciers zien hoe belangrijk het is om deze te ondersteunen.

Stuur e-mails naar [email protected] of stuur een tweet naar @ChromiumDev met de hashtag #WebCodecs en laat ons weten waar en hoe u het gebruikt.

Hero-afbeelding door Denise Jans op Unsplash .