Videostream-Komponenten manipulieren
Moderne Webtechnologien bieten zahlreiche Möglichkeiten, mit Video zu arbeiten. Die Media Stream API, die Media Recording API, die Media Source API und die WebRTC API bilden ein umfangreiches Tool-Set zum Aufzeichnen, Übertragen und Abspielen von Videostreams. Diese APIs eignen sich zwar für bestimmte allgemeine Aufgaben, aber Webentwickler können damit nicht mit einzelnen Komponenten eines Videostreams wie Frames und nicht demuxten Chunks von codiertem Video oder Audio arbeiten. Entwickler nutzen WebAssembly, um Video- und Audio-Codecs in den Browser zu integrieren, um Low-Level-Zugriff auf diese grundlegenden Komponenten zu erhalten. Da moderne Browser jedoch bereits eine Vielzahl von Codecs enthalten, die oft durch Hardware beschleunigt werden, erscheint es als Verschwendung von Personal- und Computerressourcen, sie als WebAssembly neu zu verpacken.
Die WebCodecs API beseitigt diese Ineffizienz, indem sie Programmierern die Möglichkeit bietet, Medienkomponenten zu verwenden, die bereits im Browser vorhanden sind. Im Detail:
- Video- und Audiodekoder
- Video- und Audioencoder
- Rohvideoframes
- Bilddekoder
Die WebCodecs API ist nützlich für Webanwendungen, bei denen die Verarbeitung von Medieninhalten vollständig gesteuert werden muss, z. B. Video-Editoren, Videokonferenzen und Videostreaming.
Workflow für die Videoverarbeitung
Frames sind das Herzstück der Videoverarbeitung. Daher verbrauchen oder produzieren die meisten Klassen in WebCodecs Frames. Video-Encoder wandeln Frames in codierte Blöcke um. Videodecoder machen das Gegenteil.
Außerdem lässt sich VideoFrame
gut mit anderen Web-APIs kombinieren, da es sich um eine CanvasImageSource
handelt und einen Konstruktor hat, der CanvasImageSource
akzeptiert.
Sie kann daher in Funktionen wie drawImage()
und texImage2D()
verwendet werden. Es kann auch aus Canvases, Bitmaps, Videoelementen und anderen Videoframes erstellt werden.
Die WebCodecs API funktioniert gut in Kombination mit den Klassen aus der Insertable Streams API, die WebCodecs mit Mediastream-Tracks verbinden.
MediaStreamTrackProcessor
teilt Medientracks in einzelne Frames auf.MediaStreamTrackGenerator
erstellt einen Medientrack aus einem Frame-Stream.
WebCodecs und Web Worker
Die WebCodecs API erledigt alle Aufgaben asynchron und außerhalb des Hauptthreads. Da Frame- und Chunk-Callbacks jedoch häufig mehrmals pro Sekunde aufgerufen werden können, können sie den Hauptthread überladen und die Website dadurch weniger responsiv machen. Daher ist es besser, die Verarbeitung einzelner Frames und codierter Chunks in einen Webworker zu verschieben.
Dazu bietet ReadableStream eine praktische Möglichkeit, alle Frames, die aus einem Medientrack stammen, automatisch an den Worker zu übertragen. Mit MediaStreamTrackProcessor
kann beispielsweise ein ReadableStream
für einen Mediastream-Track abgerufen werden, der von der Webcam stammt. Danach wird der Stream an einen Webworker übertragen, in dem die Frames einzeln gelesen und in einer VideoEncoder
-Warteschlange angeordnet werden.
Mit HTMLCanvasElement.transferControlToOffscreen
kann sogar das Rendering außerhalb des Hauptthreads erfolgen. Wenn sich jedoch alle Tools auf höherer Ebene als unpraktisch erweisen, kann VideoFrame
selbst übertragen und zwischen Mitarbeitern verschoben werden.
WebCodecs in Aktion
Codierung
Alles beginnt mit einem VideoFrame
.
Es gibt drei Möglichkeiten, Videoframes zu erstellen.
Aus einer Bildquelle wie einem Canvas, einer Bild-Bitmap oder einem Videoelement.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
MediaStreamTrackProcessor
verwenden, um Frames aus einerMediaStreamTrack
abzurufenconst 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; }
Frame aus der binären Pixeldarstellung in einer
BufferSource
erstellenconst 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);
Unabhängig von ihrer Herkunft können Frames mit einem VideoEncoder
in EncodedVideoChunk
-Objekte codiert werden.
Vor der Codierung müssen VideoEncoder
zwei JavaScript-Objekte zugewiesen werden:
- Initialisierungs-Wörterbuch mit zwei Funktionen zur Verarbeitung codierter Blöcke und von Fehlern Diese Funktionen werden vom Entwickler definiert und können nicht mehr geändert werden, nachdem sie an den
VideoEncoder
-Konstruktor übergeben wurden. - Encoder-Konfigurationsobjekt, das Parameter für den Ausgabevideostream enthält. Sie können diese Parameter später ändern, indem Sie
configure()
aufrufen.
Die Methode configure()
gibt NotSupportedError
aus, wenn die Konfiguration vom Browser nicht unterstützt wird. Wir empfehlen, die statische Methode VideoEncoder.isConfigSupported()
mit der Konfiguration aufzurufen, um vorher zu prüfen, ob die Konfiguration unterstützt wird, und auf das entsprechende Promise zu warten.
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.
}
Nachdem der Encoder eingerichtet wurde, kann er Frames über die encode()
-Methode akzeptieren.
Sowohl configure()
als auch encode()
werden sofort zurückgegeben, ohne auf den Abschluss der eigentlichen Arbeit zu warten. Sie ermöglicht, dass mehrere Frames gleichzeitig zur Codierung in die Warteschlange gestellt werden, während encodeQueueSize
anzeigt, wie viele Anfragen sich in der Warteschlange befinden, bis vorherige Codierungen abgeschlossen sind.
Fehler werden entweder durch sofortiges Auslösen einer Ausnahme gemeldet, wenn die Argumente oder die Reihenfolge der Methodenaufrufe gegen den API-Vertrag verstoßen, oder durch Aufrufen des error()
-Callbacks bei Problemen bei der Codec-Implementierung.
Wenn die Codierung erfolgreich abgeschlossen wurde, wird der output()
-Callback mit einem neuen codierten Block als Argument aufgerufen.
Ein weiteres wichtiges Detail ist, dass Frames durch Aufrufen von close()
mitgeteilt werden müssen, wenn sie nicht mehr benötigt werden.
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();
}
}
Jetzt ist es an der Zeit, den Codierungscode fertigzustellen. Schreiben Sie dazu eine Funktion, die die codierten Video-Chunks verarbeitet, sobald sie aus dem Encoder kommen. Normalerweise sendet diese Funktion Datenblöcke über das Netzwerk oder muxt sie zu einem Mediencontainer für die Speicherung zusammen.
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,
});
}
Wenn Sie sich vergewissern möchten, dass alle ausstehenden Codierungsanfragen abgeschlossen wurden, können Sie flush()
aufrufen und auf die Zusicherung warten.
await encoder.flush();
Decodierung
Das Einrichten eines VideoDecoder
ähnelt dem für VideoEncoder
: Beim Erstellen des Decoders werden zwei Funktionen übergeben und es werden Codec-Parameter an configure()
übergeben.
Die Codec-Parameter variieren je nach Codec. Für den H.264-Codec ist beispielsweise ein binärer Blob von AVCC erforderlich, es sei denn, er ist im sogenannten Annex-B-Format (encoderConfig.avc = { format: "annexb" }
) codiert.
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.
}
Sobald der Decoder initialisiert ist, kannst du ihm EncodedVideoChunk
-Objekte zuführen.
Zum Erstellen eines Chunks benötigen Sie Folgendes:
BufferSource
codierter Videodaten- Der Startzeitstempel des Chunks in Mikrosekunden (Medienzeit des ersten codierten Frames im Chunk)
- den Typ des Chunks, einer der folgenden:
key
, wenn der Chunk unabhängig von vorherigen Blöcken decodiert werden kanndelta
, wenn der Block erst decodiert werden kann, nachdem ein oder mehrere vorherige Blöcke decodiert wurden
Außerdem sind alle vom Encoder gesendeten Chunks direkt für den Decoder bereit. Alles, was oben über die Fehlermeldung und die asynchrone Natur der Encoder-Methoden gesagt wurde, gilt auch für 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();
Jetzt ist es an der Zeit, zu zeigen, wie ein neu decodierter Frame auf der Seite angezeigt werden kann. Es ist besser, dafür zu sorgen, dass der Decoder-Ausgabe-Callback (handleFrame()
) schnell zurückgegeben wird. Im folgenden Beispiel wird der Warteschlange der Frames, die zum Rendern bereit sind, nur ein Frame hinzugefügt.
Das Rendering erfolgt separat und besteht aus zwei Schritten:
- Wartet auf den richtigen Zeitpunkt, um den Frame anzuzeigen.
- Zeichnen Sie den Frame auf dem Canvas.
Wenn ein Frame nicht mehr benötigt wird, rufen Sie close()
auf, um den zugrunde liegenden Arbeitsspeicher freizugeben, bevor der Garbage Collector ihn erreicht. Dadurch wird der durchschnittliche Arbeitsspeicherverbrauch der Webanwendung reduziert.
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);
}
Tipps für Entwickler
Verwende den Medienbereich in den Chrome-Entwicklertools, um Medienprotokolle aufzurufen und WebCodecs zu debuggen.
Demo
Die folgende Demo zeigt, wie Animationsframes aus einem Canvas aussehen:
- von
MediaStreamTrackProcessor
mit 25 fps in einerReadableStream
aufgenommen - an einen Webworker übertragen wird.
- im H.264-Videoformat codiert
- wieder in eine Sequenz von Videoframes decodiert.
- und auf dem zweiten Canvas mit
transferControlToOffscreen()
gerendert
Andere Demos
Sehen Sie sich auch unsere anderen Demos an:
- GIFs mit ImageDecoder decodieren
- Kameraeingabe in einer Datei erfassen
- MP4-Wiedergabe
- Weitere Beispiele
WebCodecs API verwenden
Funktionserkennung
So prüfen Sie, ob WebCodecs unterstützt werden:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Die WebCodecs API ist nur in sicheren Kontexten verfügbar. Die Erkennung schlägt daher fehl, wenn self.isSecureContext
falsch ist.
Feedback
Das Chrome-Team möchte mehr über Ihre Erfahrungen mit der WebCodecs API erfahren.
Informationen zum API-Design
Funktioniert die API nicht wie erwartet? Fehlen Methoden oder Eigenschaften, die Sie für die Implementierung Ihrer Idee benötigen? Haben Sie eine Frage oder einen Kommentar zum Sicherheitsmodell? Reichen Sie ein Problem mit der Spezifikation im entsprechenden GitHub-Repository ein oder fügen Sie Ihre Anmerkungen zu einem vorhandenen Problem hinzu.
Problem mit der Implementierung melden
Haben Sie bei der Implementierung von Chrome einen Fehler gefunden? Oder unterscheidet sich die Implementierung von der Spezifikation? Melden Sie einen Fehler unter new.crbug.com.
Geben Sie so viele Details wie möglich und eine einfache Anleitung für die Reproduktion an und geben Sie Blink>Media>WebCodecs
in das Feld Komponenten ein.
Glitch eignet sich hervorragend, um schnell und einfach Reproduktionen zu teilen.
Unterstützung für die API anzeigen
Beabsichtigen Sie, die WebCodecs API zu verwenden? Ihr öffentlicher Support hilft dem Chrome-Team dabei, Funktionen zu priorisieren, und zeigt anderen Browseranbietern, wie wichtig der Support für sie ist.
Senden Sie eine E-Mail an media-dev@chromium.org oder einen Tweet an @ChromiumDev mit dem Hashtag #WebCodecs
und teilen Sie uns mit, wo und wie Sie die Funktion verwenden.
Hero-Image von Denise Jans auf Unsplash