Videoverarbeitung mit WebCodecs

Videostreamkomponenten bearbeiten

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

Moderne Webtechnologien bieten viele Möglichkeiten, mit Videos zu arbeiten. Die Media Stream API, die Media Recording API, die Media Source API und die WebRTC API bilden zusammen ein umfangreiches Toolset zum Aufzeichnen, Übertragen und Abspielen von Videostreams. Beim Lösen bestimmter Aufgaben auf hoher Ebene ermöglichen diese APIs Webprogrammierern nicht, mit einzelnen Komponenten eines Videostreams wie Frames und demultiplexten Chunks von codiertem Video oder Audio zu arbeiten. Um Low-Level-Zugriff auf diese grundlegenden Komponenten zu erhalten, haben Entwickler WebAssembly verwendet, um Video- und Audio-Codecs in den Browser zu bringen. Da moderne Browser jedoch bereits mit einer Vielzahl von Codecs ausgeliefert werden, die oft durch Hardware beschleunigt werden, scheint es eine Verschwendung von menschlichen und Computerressourcen zu sein, sie als WebAssembly neu zu verpacken.

Die WebCodecs API beseitigt diese Ineffizienz, indem sie Programmierern die Möglichkeit gibt, Medienkomponenten zu verwenden, die bereits im Browser vorhanden sind. Im Detail:

  • Video- und Audiodecoder
  • Video- und Audio-Encoder
  • Unbearbeitete Videoframes
  • Bild-Decoder

Die WebCodecs API ist nützlich für Webanwendungen, die die vollständige Kontrolle über die Verarbeitung von Media-Inhalten erfordern, z. B. Video-Editoren, Videokonferenzen und Videostreaming.

Workflow für die Videoverarbeitung

Frames sind das Herzstück der Videoverarbeitung. In WebCodecs werden die meisten Klassen daher entweder zum Verarbeiten oder Erstellen von Frames verwendet. Video-Encoder wandeln Frames in codierte Chunks um. Video-Decoder machen das Gegenteil.

Außerdem lässt sich VideoFrame gut mit anderen Web-APIs kombinieren, da es sich um ein CanvasImageSource handelt und einen Konstruktor hat, der CanvasImageSource akzeptiert. Sie kann also in Funktionen wie drawImage() und texImage2D() verwendet werden. Außerdem kann es aus Arbeitsbereichen, Bitmaps, Videoelementen und anderen Videoframes bestehen.

Die WebCodecs API funktioniert gut in Verbindung mit den Klassen aus der Insertable Streams API, die WebCodecs mit Media Stream-Tracks verbinden.

  • Mit MediaStreamTrackProcessor werden Mediatracks in einzelne Frames aufgeteilt.
  • Mit MediaStreamTrackGenerator wird ein Mediatrack aus einem Stream von Frames erstellt.

WebCodecs und Web Worker

Die WebCodecs API ist so konzipiert, dass alle rechenintensiven Aufgaben asynchron und außerhalb des Hauptthreads ausgeführt werden. Da Frame- und Chunk-Callbacks jedoch oft mehrmals pro Sekunde aufgerufen werden können, kann es sein, dass sie den Hauptthread überlasten und die Website dadurch weniger reaktionsschnell wird. Daher ist es besser, die Verarbeitung einzelner Frames und codierter Chunks in einen Webworker zu verlagern.

Um dies zu erleichtern, bietet ReadableStream eine praktische Möglichkeit, alle Frames eines Media-Tracks automatisch an den Worker zu übertragen. MediaStreamTrackProcessor kann beispielsweise verwendet werden, um einen ReadableStream für einen Media-Stream-Track von der Webcam zu erhalten. Danach wird der Stream an einen Web-Worker übertragen, in dem Frames einzeln gelesen und in eine VideoEncoder eingereiht werden.

Mit HTMLCanvasElement.transferControlToOffscreen kann das Rendern sogar außerhalb des Hauptthreads erfolgen. Wenn sich alle Tools auf hoher Ebene als unpraktisch erweisen, ist VideoFrame selbst übertragbar und kann zwischen Mitarbeitern verschoben werden.

WebCodecs in Aktion

Codierung

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

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 });
    
  • Mit MediaStreamTrackProcessor 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 der binären Pixel-Darstellung 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 davon, woher sie stammen, können Frames mit einem VideoEncoder in EncodedVideoChunk-Objekte codiert werden.

Vor der Codierung müssen VideoEncoder zwei JavaScript-Objekte übergeben werden:

  • Initialisieren Sie das Dictionary mit zwei Funktionen zum Verarbeiten von codierten Chunks und 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() löst NotSupportedError aus, wenn die Konfiguration vom Browser nicht unterstützt wird. Es wird empfohlen, die statische Methode VideoEncoder.isConfigSupported() mit der Konfiguration aufzurufen, um vorab zu prüfen, ob die Konfiguration unterstützt wird, und auf das 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 empfangen. Sowohl configure() als auch encode() werden sofort zurückgegeben, ohne auf den Abschluss der eigentlichen Arbeit zu warten. So können mehrere Frames gleichzeitig für die Codierung in die Warteschlange gestellt werden. encodeQueueSize gibt an, wie viele Anfragen in der Warteschlange darauf warten, dass vorherige Codierungen abgeschlossen werden. 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()-Rückrufs für Probleme, die in der Codec-Implementierung auftreten. Wenn die Codierung erfolgreich abgeschlossen ist, wird der output()-Callback mit einem neuen codierten Chunk als Argument aufgerufen. Ein weiteres wichtiges Detail ist, dass Frames mit dem Aufruf von close() mitgeteilt werden muss, 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. Dazu schreiben Sie eine Funktion, die Chunks von codiertem Video verarbeitet, wenn sie aus dem Encoder kommen. Normalerweise werden mit dieser Funktion Datenblöcke über das Netzwerk gesendet oder für die Speicherung in einem Media-Container gemuxt.

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 zu einem bestimmten Zeitpunkt sicherstellen müssen, dass alle ausstehenden Codierungsanfragen abgeschlossen sind, können Sie flush() aufrufen und auf das Promise warten.

await encoder.flush();

Dekodierung

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

Die Einrichtung von VideoDecoder ähnelt der Einrichtung von VideoEncoder: Beim Erstellen des Decoders werden zwei Funktionen übergeben und configure() werden Codec-Parameter übergeben.

Die Menge der Codec-Parameter variiert von Codec zu Codec. Für den H.264-Codec ist beispielsweise möglicherweise ein binärer Blob von AVCC erforderlich, sofern er nicht im sogenannten Annex B-Format (encoderConfig.avc = { format: "annexb" }) codiert ist.

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, können Sie ihn mit EncodedVideoChunk-Objekten füttern. Zum Erstellen eines Chunks benötigen Sie Folgendes:

  • Ein BufferSource codierter Videodaten
  • Der Startzeitstempel des Chunks in Mikrosekunden (Media-Zeit des ersten codierten Frames im Chunk)
  • Der Typ des Chunks, einer der folgenden:
    • key, wenn der Chunk unabhängig von vorherigen Chunks decodiert werden kann
    • delta, wenn der Chunk erst decodiert werden kann, nachdem ein oder mehrere vorherige Chunks decodiert wurden

Außerdem sind alle vom Encoder ausgegebenen Chunks sofort für den Decoder verfügbar. Alle oben genannten Punkte zur Fehlerberichterstattung und zum asynchronen Charakter der Methoden des Encoders gelten 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, wenn der Callback für die Decoder-Ausgabe (handleFrame()) schnell zurückgegeben wird. Im Beispiel unten wird der Warteschlange der Frames, die gerendert werden können, nur ein Frame hinzugefügt. Das Rendern erfolgt separat und besteht aus zwei Schritten:

  1. Warten auf den richtigen Zeitpunkt, um den Frame zu präsentieren.
  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 der Garbage Collector ihn erreicht. Dadurch wird die durchschnittliche Menge an Arbeitsspeicher, die von der Webanwendung verwendet wird, 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

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

Screenshot des Media-Bereichs zum Debuggen von WebCodecs
Media-Bereich in den Chrome-Entwicklertools zum Debuggen von WebCodecs.

Demo

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

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

Andere Demos

Sehen Sie sich auch unsere anderen Demos an:

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 also fehl, wenn self.isSecureContext „false“ ist.

Feedback

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

Informationen zum API-Design

Funktioniert etwas an der API nicht wie erwartet? Oder fehlen Methoden oder Attribute, die Sie für die Umsetzung Ihrer Idee benötigen? Haben Sie eine Frage oder einen Kommentar zum Sicherheitsmodell? Reichen Sie ein Spezifikationsproblem im entsprechenden GitHub-Repository ein oder fügen Sie Ihre Gedanken zu einem bestehenden Problem hinzu.

Problem mit der Implementierung melden

Haben Sie einen Fehler in der Chrome-Implementierung gefunden? Oder weicht die Implementierung von der Spezifikation ab? Melden Sie einen Fehler unter new.crbug.com. Geben Sie dabei so viele Details wie möglich an, einschließlich einer einfachen Anleitung zum Reproduzieren des Fehlers, und geben Sie Blink>Media>WebCodecs in das Feld Components ein.

API-Support zeigen

Möchten Sie die WebCodecs API verwenden? Ihr öffentlicher Support 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 [email protected] 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.