Videoverarbeitung mit WebCodecs

Manipulieren von Video-Stream-Komponenten

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

Moderne Webtechnologien bieten viele Möglichkeiten, mit Videos zu arbeiten. Media Stream API Media Recording API Media Source API und WebRTC API addiert ein umfangreiches Tool zum Aufzeichnen, Übertragen und Wiedergeben von Videostreams. Diese APIs verhindern bei der Lösung bestimmter übergeordneter Aufgaben Programmierer arbeiten mit einzelnen Komponenten eines Videostreams, z. B. Frames. und nicht gemischt codierte Video- oder Audio-Blöcke. Um Low-Level-Zugriff auf diese grundlegenden Komponenten zu erhalten, haben Entwickler WebAssembly, um Video- und Audio-Codecs in den Browser zu integrieren. Aber in Anbetracht dass moderne Browser bereits mit einer Vielzahl von Codecs ausgeliefert werden (die häufig durch Hardware beschleunigt werden), sie als WebAssembly neu zu verpacken, Personal- und Computerressourcen.

WebCodecs API beseitigt diese Ineffizienz. indem sie Programmierern die Möglichkeit geben, Medienkomponenten zu verwenden, die bereits in im Browser. Im Detail:

  • Video- und Audiodecoder
  • Video- und Audio-Encoder
  • Rohvideoframes
  • Bilddecoder

Das WebCodecs-API eignet sich für Webanwendungen, die vollständige Kontrolle über den wie Medieninhalte verarbeitet werden, z. B. Videoeditoren, Videokonferenzen, Streaming usw.

Videoverarbeitungs-Workflow

Frames sind das Herzstück der Videoverarbeitung. Daher werden in WebCodecs die meisten Klassen entweder Frames verbrauchen oder produzieren. Video-Encoder wandeln Frames in codierte Blöcke. Bei Videodecoder ist das Gegenteil der Fall.

VideoFrame ist außerdem gut mit anderen Web-APIs kompatibel, da es ein CanvasImageSource ist und einen Konstruktor hat, der CanvasImageSource akzeptiert. Daher kann es 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.

  • MediaStreamTrackProcessor unterteilt die Media-Tracks in einzelne Frames.
  • MediaStreamTrackGenerator erstellt einen Medien-Track aus einem Frame-Stream.

WebCodecs und Web Worker

Die WebCodecs API erledigt die schwierigen Aufgaben asynchron und außerhalb des Hauptthreads. Da Frame- und Chunk-Callbacks jedoch häufig mehrmals pro Sekunde aufgerufen werden können, könnten sie den Hauptthread überladen und die Website dadurch weniger responsiv machen. Daher ist es besser, die Verarbeitung einzelner Frames und codierte Blöcke Web Worker.

ReadableStream ermöglicht die bequeme Übertragung aller Frames, die von einem Medium stammen, an den Worker senden. Mit MediaStreamTrackProcessor kann beispielsweise ein Wert für ReadableStream für einen Mediastream-Track, der von der Webcam stammt. Danach Der Stream wird an einen Web Worker übertragen, wo Frames einzeln gelesen und in die Warteschlange gestellt werden. in VideoEncoder.

Mit HTMLCanvasElement.transferControlToOffscreen kann sogar außerhalb des Hauptthreads gerendert werden. Aber wenn sich all die High-Level-Tools nicht praktikabel ist, ist VideoFrame selbst übertragbar und unter Umständen zwischen Workern verschoben.

WebCodecs in Aktion

Codierung

<ph type="x-smartling-placeholder">
</ph> Der Pfad von einer Canvas- oder ImageBitmap zum Netzwerk oder zum Speicher
Pfad von einem Canvas oder 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;
    }
    
  • Frame aus seiner binären Pixeldarstellung in einem BufferSource erstellen

    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);
    

Ganz gleich, woher sie kommen, Frames können in EncodedVideoChunk-Objekte mit einem VideoEncoder.

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

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

Die Methode configure() gibt NotSupportedError aus, wenn die Konfiguration nicht die vom Browser unterstützt werden. Es empfiehlt sich, die statische Methode aufzurufen, VideoEncoder.isConfigSupported() durch die Konfiguration, um vorab zu prüfen, die Konfiguration unterstützt wird, und warten auf ihr Promise.

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 Methode encode() annehmen. Sowohl configure() als auch encode() werden sofort zurückgegeben, ohne auf den zu erledigen. Es ermöglicht, dass mehrere Frames an der während encodeQueueSize anzeigt, wie viele Anfragen in der Warteschlange bis die vorherigen Codierungen abgeschlossen sind. Fehler werden gemeldet, indem entweder sofort eine Ausnahme ausgelöst wird, falls die Argumente oder die Reihenfolge der Methodenaufrufe verstößt gegen den API-Vertrag oder das Aufrufen der error() Callback für Probleme bei der Codec-Implementierung. Wenn die Codierung erfolgreich abgeschlossen wurde, hat output() -Callback mit einem neuen codierten Chunk als Argument aufgerufen. Ein weiteres wichtiges Detail ist, dass Frames informiert werden müssen, wenn sie mehr benötigt wird, indem Sie close() aufrufen.

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();
  }
}

Schließlich ist es an der Zeit, den Codierungscode abzuschließen, indem Sie eine Funktion schreiben, die codierten Videoblöcken, die über den Encoder ausgegeben werden. Normalerweise werden mit dieser Funktion Datenblöcke über das Netzwerk gesendet oder in ein Medium mux. für die Speicherung.

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,
  });
}

Solltest du irgendwann einmal sicherstellen, dass alle ausstehenden Codierungsanfragen abgeschlossen wurde, können Sie flush() aufrufen und auf das Versprechen warten.

await encoder.flush();

Decodierung

<ph type="x-smartling-placeholder">
</ph> Der Pfad vom Netzwerk oder Speicher zu einer Canvas- oder ImageBitmap.
Der Pfad vom Netzwerk oder Speicher zu einem Canvas oder einem ImageBitmap.

Die Einrichtung eines VideoDecoder ähnelt dem Vorgang für den VideoEncoder: Beim Erstellen des Decoders werden zwei Funktionen übergeben und der Codec configure() übergeben werden.

Die Codec-Parameter sind von Codec zu Codec unterschiedlich. Beispiel: H.264-Codec benötigt möglicherweise ein binäres Blob von AVCC, es sei denn, sie sind im so genannten Anhang 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 EncodedVideoChunk-Objekte in ihn übergeben. Zum Erstellen eines Blocks benötigen Sie Folgendes:

  • BufferSource codierter Videodaten
  • Startzeitstempel des Blocks in Mikrosekunden (Medienzeit des ersten codierten Frames im Block)
  • den Chunk-Typ, einer der folgenden: <ph type="x-smartling-placeholder">
      </ph>
    • key, wenn der Chunk unabhängig von vorherigen Blöcken decodiert werden kann
    • delta, wenn der Block erst decodiert werden kann, nachdem mindestens ein vorheriger Block decodiert wurde

Außerdem sind alle vom Encoder ausgegebenen Blöcke bereit für den Decoder. Alle oben Gesagten über Error Reporting und die asynchrone Natur der Encoder-Methoden gelten auch 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 neu decodierter Frame auf der Seite angezeigt werden kann. Es ist um sicherzustellen, dass der Decoder-Callback (handleFrame()) ausgegeben wird kehrt schnell zurück. Im Beispiel unten wird nur ein Frame die zum Rendern bereit sind. Das Rendering erfolgt separat und umfasst zwei Schritte:

  1. Es wird auf den richtigen Zeitpunkt gewartet, um den Frame anzuzeigen.
  2. Zeichnen des Rahmens auf dem Canvas

Sobald ein Frame nicht mehr benötigt wird, rufen Sie close() auf, um den zugrunde liegenden Speicher freizugeben bevor die automatische Speicherbereinigung an die automatische Speicherbereinigung gelangt, von der Webanwendung verwendet wird.

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

Den Medienbereich verwenden in den Chrome-Entwicklertools, um Medienprotokolle aufzurufen und WebCodecs zu debuggen.

<ph type="x-smartling-placeholder">
</ph> Screenshot des Medienbereichs 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 eine ReadableStream von MediaStreamTrackProcessor
  • an einen Web Worker übertragen
  • im H.264-Videoformat codiert
  • wieder in eine Folge von Videoframes decodiert wird
  • 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. sodass die Erkennung fehlschlägt, 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

Funktioniert die API nicht wie erwartet? Oder sind fehlende Methoden oder Eigenschaften, die Sie zur Umsetzung Ihrer Idee benötigen? Ein Fragen oder Anmerkungen zum Sicherheitsmodell haben? Spezifikationsproblem melden auf der entsprechendes GitHub-Repository oder fügen Sie Ihre Gedanken zu einem bestehenden Problem.

Problem mit der Implementierung melden

Haben Sie bei der Implementierung von Chrome einen Fehler gefunden? Oder ist die Implementierung von der Spezifikation abweichen? Melde einen Fehler unter new.crbug.com. Geben Sie so viele Details wie möglich an. und geben Sie Blink>Media>WebCodecs in das Feld Components (Komponenten) ein. Glitch eignet sich hervorragend, um schnelle und einfache Reproduktionen zu teilen.

Unterstützung für die API anzeigen

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

Senden Sie E-Mails an media-dev@chromium.org oder einen Tweet. an @ChromiumDev mit dem Hashtag #WebCodecs und teilen Sie uns mit, wo und wie Sie sie nutzen.

Hero-Image von Denise Jans bei Unsplash