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. Um Low-Level-Zugriff auf diese grundlegenden Komponenten zu erhalten, haben Entwickler WebAssembly verwendet, um Video- und Audio-Codecs in den Browser einzubinden. 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
- Unbearbeitete Videoframes
- 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 Chunks 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. Außerdem können sie 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 Webworker
Die WebCodecs API erledigt alle Aufgaben asynchron und außerhalb des Hauptthreads. Da Frame- und Chunk-Callbacks jedoch oft mehrmals pro Sekunde aufgerufen werden können, können sie den Hauptthread überlasten und so die Reaktionsfähigkeit der Website beeinträchtigen. 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 einer 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 });
Mit
MediaStreamTrackProcessor
Frames aus einerMediaStreamTrack
abrufenconst 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:
- Wörterbuch mit zwei Funktionen zum Umgang mit codierten Chunks und Fehlern initialisieren. 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 configure()
-Methode löst 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. So können mehrere Frames gleichzeitig zur Codierung in die Warteschlange gestellt werden. Mit encodeQueueSize
wird angezeigt, wie viele Anfragen in der Warteschlange auf die Fertigstellung der vorherigen Codierungen warten.
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 Chunk 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();
Dekodierung
Die Einrichtung eines VideoDecoder
ähnelt der Einrichtung eines VideoEncoder
: Beim Erstellen des Decoders werden zwei Funktionen übergeben und configure()
werden Codec-Parameter zugewiesen.
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
codierte Videodaten- Der Startzeitstempel des Chunks in Mikrosekunden (Medienzeit des ersten codierten Frames im Chunk)
- den Typ des Chunks, einer der folgenden:
key
, wenn der Block 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 frisch 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
Im Media Panel in den Chrome-Entwicklertools können Sie sich Medienprotokolle ansehen und WebCodecs beheben.
Demo
In der folgenden Demo wird gezeigt, wie Animationsframes aus einem Canvas:
- 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 mit
transferControlToOffscreen()
auf dem zweiten Canvas 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 Fragen oder Kommentare zum Sicherheitsmodell? Reichen Sie ein Problem mit der Spezifikation im entsprechenden GitHub-Repository ein oder fügen Sie Ihre Gedanken zu einem vorhandenen Problem hinzu.
Problem mit der Implementierung melden
Haben Sie einen Fehler in der Chrome-Implementierung gefunden? Oder unterscheidet sich die Implementierung von der Spezifikation? Melden Sie den Fehler unter new.crbug.com. Geben Sie dabei so viele Details wie möglich an, eine einfache Anleitung zur Reproduktion und geben Sie Blink>Media>WebCodecs
in das Feld Components 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? Ihre öffentliche Unterstützung hilft dem Chrome-Team, Funktionen zu priorisieren, und zeigt anderen Browseranbietern, wie wichtig es ist, sie zu unterstützen.
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.