Videoverwerking met WebCodecs

Videostreamcomponenten manipuleren.

Eugène Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

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

WebCodecs API elimineert deze inefficiëntie door programmeurs een manier te bieden om mediacomponenten te gebruiken die al in de browser aanwezig zijn. Specifiek:

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

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

Workflow voor videoverwerking

Frames vormen het middelpunt van videoverwerking. In WebCodecs verbruiken of produceren de meeste klassen dus frames. Video-encoders zetten frames om in gecodeerde brokken. Videodecoders doen het tegenovergestelde.

VideoFrame speelt ook goed samen met andere web-API's omdat het een CanvasImageSource is en een constructor heeft die CanvasImageSource accepteert. Het kan dus worden gebruikt in functies als drawImage() en texImage2D() . Het kan ook worden opgebouwd uit 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 creëert een mediatrack uit een stroom frames.

Webcodecs en webwerkers

Door het ontwerp doet de WebCodecs API al het zware werk asynchroon en buiten de hoofdlijnen. Maar omdat frame- en chunk-callbacks vaak meerdere keren per seconde kunnen worden aangeroepen, kunnen ze de hoofdlijn onoverzichtelijk maken en de website dus minder responsief maken. Daarom verdient het de voorkeur om de verwerking van individuele frames en gecodeerde stukken naar een webwerker te verplaatsen.

Om daarbij te helpen biedt ReadableStream een ​​handige manier om automatisch alle frames die van een mediatrack komen naar de werknemer over te dragen. MediaStreamTrackProcessor kan bijvoorbeeld worden gebruikt om een ReadableStream te verkrijgen voor een mediastreamtrack afkomstig van de webcamera. Daarna wordt de stream overgebracht naar een webwerker waar frames één voor één worden gelezen en in een VideoEncoder worden geplaatst.

Met HTMLCanvasElement.transferControlToOffscreen kan zelfs rendering buiten de hoofdthread worden uitgevoerd. Maar als alle tools op hoog niveau ongemakkelijk blijken te zijn, is VideoFrame zelf overdraagbaar en kan deze tussen werknemers worden verplaatst.

Webcodecs in actie

Codering

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

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

  • Van een afbeeldingsbron zoals een canvas, een afbeeldingsbitmap 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);
    

Waar ze ook vandaan komen, frames kunnen worden gecodeerd in EncodedVideoChunk objecten met een VideoEncoder .

Vóór het coderen moet VideoEncoder twee JavaScript-objecten krijgen:

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

De methode configure() genereert NotSupportedError als de configuratie niet door de browser wordt ondersteund. U wordt aangemoedigd om de statische methode VideoEncoder.isConfigSupported() aan te roepen met de configuratie om vooraf te controleren of de configuratie wordt ondersteund en te wachten op de belofte.

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 de methode encode() . Zowel configure() als encode() keren onmiddellijk terug zonder te wachten tot het daadwerkelijke werk is voltooid. Hiermee kunnen meerdere frames tegelijkertijd in de wachtrij staan ​​voor codering, terwijl encodeQueueSize laat zien hoeveel verzoeken in de wachtrij wachten totdat eerdere coderingen zijn voltooid. Fouten worden gerapporteerd door onmiddellijk een uitzondering te genereren, in het geval dat de argumenten of de volgorde van de methodeaanroepen het API-contract schenden, of door de callback error() aan te roepen voor problemen die zich voordoen bij de codec-implementatie. Als het coderen met succes is voltooid, wordt de callback output() aangeroepen met een nieuw gecodeerd stuk als argument. Een ander belangrijk detail hier is dat frames moeten worden geïnformeerd 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();
  }
}

Eindelijk is het tijd om het coderen van de code af te ronden door een functie te schrijven die stukjes gecodeerde video verwerkt zodra deze uit de encoder komen. Normaal gesproken zou deze functie gegevensbrokken over het netwerk verzenden of deze in een mediacontainer samenvoegen voor opslag.

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 er op een gegeven moment zeker van wilt zijn dat alle lopende coderingsverzoeken zijn voltooid, kunt u flush() aanroepen en op de belofte wachten.

await encoder.flush();

Decodering

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 er voor de VideoEncoder is gedaan: er worden twee functies doorgegeven wanneer de decoder wordt gemaakt, en er worden codecparameters gegeven aan configure() .

De set codecparameters varieert van codec tot codec. De H.264-codec heeft bijvoorbeeld mogelijk een binaire blob van AVCC nodig, tenzij deze is gecodeerd 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, kunt u deze gaan voeden met EncodedVideoChunk -objecten. Om een ​​chunk te maken, heb je het volgende nodig:

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

Ook alle chunks die door de encoder worden uitgezonden, zijn ongewijzigd klaar voor de decoder. Alle dingen die hierboven zijn gezegd over foutrapportage en het asynchrone karakter van de methoden van encoders gelden eveneens 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 callback van de decoderuitvoer ( handleFrame() ) snel terugkeert. In het onderstaande voorbeeld wordt alleen een frame toegevoegd aan de wachtrij met frames die gereed zijn voor weergave. Het renderen gebeurt afzonderlijk en bestaat uit twee stappen:

  1. Wachten op het juiste moment om het frame te laten zien.
  2. Het frame op het canvas tekenen.

Zodra een frame niet langer nodig is, roept u close() aan om het onderliggende geheugen vrij te geven voordat de garbage collector er bij komt. Dit vermindert 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 medialogboeken te bekijken en fouten in WebCodecs op te sporen.

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

Demo

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

  • vastgelegd met 25 fps in een ReadableStream door MediaStreamTrackProcessor
  • overgedragen aan een webwerker
  • gecodeerd in H.264-videoformaat
  • opnieuw gedecodeerd in een reeks videoframes
  • en weergegeven op het tweede canvas met behulp van transferControlToOffscreen()

Andere demo's

Bekijk ook onze andere demo's:

Met behulp van de WebCodecs-API

Functiedetectie

Controleren op ondersteuning voor WebCodecs:

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

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

Feedback

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

Vertel ons over het API-ontwerp

Is 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? Heeft u een vraag of opmerking over het beveiligingsmodel? Dien een spec issue in op de corresponderende GitHub repo , of voeg uw gedachten toe aan een bestaand issue.

Meld een probleem met de implementatie

Heeft u een bug gevonden in de implementatie van Chrome? Of wijkt de uitvoering af van de specificaties? Dien een bug in op new.crbug.com . Zorg ervoor dat u zoveel mogelijk details en eenvoudige instructies voor het reproduceren opneemt, en voer Blink>Media>WebCodecs in het vak Componenten in. Glitch werkt uitstekend voor het delen van snelle en gemakkelijke reproducties.

Toon ondersteuning voor de API

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

Stuur e-mails naar media-dev@chromium.org of stuur een tweet naar @ChromiumDev met de hashtag #WebCodecs en laat ons weten waar en hoe u deze gebruikt.

Hero-afbeelding door Denise Jans op Unsplash .