Videostreamcomponenten manipuleren.
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 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 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);
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 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:
- Wachten op het juiste moment om het frame te laten zien.
- 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.
Demo
De onderstaande demo laat zien hoe animatieframes van een canvas zijn:
- vastgelegd met 25 fps in een
ReadableStream
doorMediaStreamTrackProcessor
- 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 .