Videoverarbeitung mit WebCodecs

Video-Stream-Komponenten bearbeiten

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

Moderne Webtechnologien bieten zahlreiche Möglichkeiten zur Arbeit mit Videos. Die Media Stream API, die Media Recording API, die Media Source API und die WebRTC API bilden zusammen ein umfangreiches Tool zum Aufzeichnen, Übertragen und Abspielen von Videostreams. Diese APIs lösen zwar bestimmte übergeordnete Aufgaben, aber Webprogrammierern verwehren Webprogrammierern die Arbeit mit einzelnen Komponenten eines Videostreams wie Frames und nicht gemischten Blöcken codierter Video- oder Audioelemente. Um Low-Level-Zugriff auf diese grundlegenden Komponenten zu erhalten, haben Entwickler WebAssembly verwendet, um Video- und Audio-Codecs im Browser zu integrieren. Moderne Browser verfügen jedoch bereits über eine Vielzahl von Codecs, die häufig durch die Hardware beschleunigt werden. Daher scheint es so, als ob eine Neubündelung in WebAssembly eine Verschwendung von Personal- und Computerressourcen scheint.

Die WebCodecs API beseitigt diese Ineffizienz, da Programmierer die Möglichkeit haben, Medienkomponenten zu verwenden, die bereits im Browser vorhanden sind. Insbesondere:

  • Video- und Audiodecoder
  • Video- und Audioencoder
  • Raw-Videoframes
  • Bilddecoder

Die WebCodecs API eignet sich für Webanwendungen, die die vollständige Kontrolle über die Verarbeitung von Medieninhalten erfordern, z. B. Video-Editoren, Videokonferenzen, Videostreaming usw.

Videoverarbeitungsworkflow

Frames sind das Herzstück der Videoverarbeitung. Daher nehmen oder erzeugen die meisten Klassen in WebCodecs Frames. Video-Encoder wandeln Frames in codierte Blöcke um. Videodecoder haben genau das Gegenteil.

VideoFrame funktioniert auch gut mit anderen Web-APIs, da es ein CanvasImageSource ist und einen Konstruktor hat, der CanvasImageSource akzeptiert. Sie kann also 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 zusammen mit den Klassen der Insertable Streams API, die WebCodecs mit Medienstream-Tracks verbinden.

  • Bei MediaStreamTrackProcessor werden Medienspuren in einzelne Frames unterteilt.
  • MediaStreamTrackGenerator erstellt einen Medientrack aus einem Stream von Frames.

WebCodecs und Web Worker

Die WebCodecs API übernimmt die meiste Arbeit asynchron und aus dem Hauptthread entfernt. Da Frame- und Chunk-Callbacks jedoch häufig mehrmals pro Sekunde aufgerufen werden, überladen sie möglicherweise den Hauptthread, wodurch die Website weniger responsiv wird. Daher ist es besser, die Verarbeitung einzelner Frames und codierter Blöcke in einen Web Worker zu verschieben.

Zu diesem Zweck bietet ReadableStream eine bequeme Möglichkeit, alle Frames, die von einer Medienspur kommen, automatisch an den Worker zu übertragen. Mit MediaStreamTrackProcessor kann beispielsweise ein ReadableStream für einen Medienstream-Track abgerufen werden, der von der Webcam stammt. Anschließend wird der Stream an einen Web Worker übertragen, in dem die Frames einzeln gelesen und in einer VideoEncoder in die Warteschlange gestellt werden.

Mit HTMLCanvasElement.transferControlToOffscreen ist sogar das Rendering außerhalb des Hauptthreads möglich. Wenn sich jedoch alle übergeordneten Tools als umständlich erwiesen haben, ist VideoFrame selbst übertragbar und kann auf andere Worker verschoben werden.

WebCodecs in Aktion

Codierung

Der Pfad von einem Canvas oder einer ImageBitmap zum Netzwerk oder Speicher
Der Pfad von einem Canvas- oder ImageBitmap zum Netzwerk oder Speicher

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 kannst du Frames aus einem MediaStreamTrack abrufen

    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;
    }
    
  • Erstellt einen Frame aus seiner binären Pixeldarstellung in einem 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);
    

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 erhalten:

  • Init-Wörterbuch mit zwei Funktionen für die Verarbeitung codierter Blöcke und Fehler Diese Funktionen sind vom Entwickler definiert und können nicht mehr geändert werden, nachdem sie an den Konstruktor VideoEncoder übergeben wurden.
  • Encoder-Konfigurationsobjekt, das Parameter für den Videostream enthält Sie können diese Parameter später durch Aufrufen von configure() ändern.

Die Methode configure() gibt NotSupportedError aus, wenn die Konfiguration vom Browser nicht unterstützt wird. Es empfiehlt sich, die statische Methode VideoEncoder.isConfigSupported() mit der Konfiguration aufzurufen, um vorab zu prüfen, ob die Konfiguration unterstützt wird, und auf ihr Versprechen 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.
}

Nach der Einrichtung des Encoders 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. Es können mehrere Frames gleichzeitig zur Codierung in die Warteschlange gestellt werden, während encodeQueueSize anzeigt, wie viele Anfragen in der Warteschlange auf den Abschluss vorheriger Codierungen warten. Fehler werden entweder durch sofortiges Auslösen einer Ausnahme gemeldet, falls die Argumente oder die Reihenfolge der Methodenaufrufe gegen den API-Vertrag verstoßen, oder durch Aufrufen des error()-Callbacks bei Problemen, die bei der Codec-Implementierung aufgetreten sind. Wenn die Codierung erfolgreich abgeschlossen wurde, wird der output()-Callback mit einem neuen codierten Chunk als Argument aufgerufen. Ein weiteres wichtiges Detail hier 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, die Codierung des Codes abzuschließen, indem Sie eine Funktion schreiben, die Blöcke codierten Videos verarbeitet, wenn sie aus dem Encoder kommen. Normalerweise würde diese Funktion Datenblöcke über das Netzwerk senden oder sie zur Speicherung in einem Mediencontainer muxieren.

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 du dafür sorgen musst, dass alle ausstehenden Codierungsanfragen abgeschlossen wurden, kannst du flush() aufrufen und auf das Versprechen warten.

await encoder.flush();

Decodierung

Der Pfad vom Netzwerk oder Speicher zu einem Canvas oder einer ImageBitmap.
Der Pfad vom Netzwerk oder Speicher zu einem Canvas- oder ImageBitmap.

Das Einrichten von VideoDecoder ähnelt dem für VideoEncoder: Beim Erstellen des Decoders werden zwei Funktionen übergeben und configure() werden Codec-Parameter zugewiesen.

Der Satz der Codec-Parameter variiert von Codec zu Codec. Zum Beispiel benötigt der H.264-Codec möglicherweise ein binäres Blob von AVCC, 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 wurde, kannst du ihn mit EncodedVideoChunk-Objekten versorgen. Zum Erstellen eines Chunks benötigen Sie Folgendes:

  • BufferSource mit codierten Videodaten
  • Startzeitstempel des Blocks in Mikrosekunden (Medienzeit des ersten codierten Frames in dem Chunk)
  • den Typ des Chunks, einen der folgenden Werte:
    • key, wenn der Block unabhängig von vorherigen Blöcken decodiert werden kann
    • delta, wenn der Chunk erst decodiert werden kann, nachdem ein oder mehrere vorherige Chunks decodiert wurden

Alle vom Encoder ausgegebenen Blöcke sind für den Decoder in der vorliegenden Form bereit. Alle oben genannten Informationen über Fehlerberichte und die asynchrone Natur der Methoden von Encodern gelten gleichermaßen für Decodierer.

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 dargestellt werden kann. Wir empfehlen, dafür zu sorgen, dass der Callback für die Decoderausgabe (handleFrame()) schnell zurückgegeben wird. Im folgenden Beispiel wird nur ein Frame zur Warteschlange der Frames hinzugefügt, die bereit für das Rendering sind. Das Rendern erfolgt separat und umfasst zwei Schritte:

  1. Warte, bis der richtige Zeitpunkt für die Anzeige des Frames steht.
  2. Zeichnen Sie den Rahmen auf dem Canvas.

Wenn ein Frame nicht mehr benötigt wird, rufen Sie close() auf, um den zugrunde liegenden Arbeitsspeicher freizugeben, bevor die automatische Speicherbereinigung erreicht wird. Dadurch wird der durchschnittlich von der Webanwendung verwendete Arbeitsspeicher 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);
}

Entwicklertipps

Verwenden Sie das Media Panel in den Chrome-Entwicklertools, um Medienprotokolle anzusehen und WebCodecs zu debuggen.

Screenshot des Bereichs „Media Panel“ für das Debugging von WebCodecs
Medienbereich in den Chrome-Entwicklertools für das Debugging von WebCodecs.

Demo

In der folgenden Demo sehen Sie, wie Animationsframes aus einem Canvas aussehen:

  • aufgenommen mit 25 fps in ReadableStream von MediaStreamTrackProcessor
  • an einen Web Worker übertragen,
  • im H.264-Videoformat codiert
  • in eine Folge von Videoframes decodiert.
  • und wird mit transferControlToOffscreen() auf dem zweiten Canvas gerendert.

Andere Demos

Sehen Sie sich auch unsere anderen Demos an:

WebCodecs API verwenden

Funktionserkennung

So prüfen Sie, ob WebCodecs unterstützt wird:

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

Die WebCodecs API ist nur in sicheren Kontexten verfügbar. Daher schlägt die Erkennung fehl, wenn self.isSecureContext auf „false“ gesetzt ist.

Feedback

Das Chrome-Team möchte mehr über Ihre Erfahrungen mit der WebCodecs API erfahren.

Informationen zum API-Design

Gibt es etwas an der API, das nicht so funktioniert, wie Sie es erwartet hatten? Oder fehlen Methoden oder Eigenschaften, um Ihre Idee zu implementieren? Haben Sie eine Frage oder einen Kommentar zum Sicherheitsmodell? Sie können ein Spezifikationsproblem über das entsprechende GitHub-Repository melden oder Ihre Gedanken zu einem vorhandenen Problem hinzufügen.

Problem mit der Implementierung melden

Haben Sie einen Fehler bei der Implementierung in Chrome gefunden? Oder unterscheidet sich die Implementierung von der Spezifikation? Melde einen Fehler unter new.crbug.com. Gib so viele Details wie möglich und eine einfache Anleitung zum Reproduzieren an. Gib Blink>Media>WebCodecs in das Feld Komponenten ein. Glitch eignet sich perfekt, um schnelle und einfache Reproduzierungen zu teilen.

Unterstützung für die API zeigen

Möchten Sie die WebCodecs API verwenden? Ihre öffentliche Unterstützung hilft dem Chrome-Team, Funktionen zu priorisieren, und zeigt anderen Browseranbietern, wie wichtig es ist, sie zu unterstützen.

Sende E-Mails an media-dev@chromium.org oder einen Tweet an @ChromiumDev. Verwende dabei den Hashtag #WebCodecs und teile uns mit, wo und wie du den Code verwendest.

Hero-Image von Denise Jans auf Unsplash