Manipuleren van videostreamcomponenten.
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

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 eenMediaStreamTrack
te halenconst 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

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:
- Wacht op het juiste moment om het frame te tonen.
- 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.

Demonstratie
De onderstaande demo laat zien hoe animatieframes van een canvas eruit zien:
- vastgelegd met 25 fps in een
ReadableStream
doorMediaStreamTrackProcessor
- 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:
- Gifs decoderen met ImageDecoder
- Camera-invoer vastleggen in een bestand
- MP4-weergave
- Andere monsters
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 .