Przetwarzanie wideo za pomocą kodeków WebCodecs

Manipulowanie komponentami strumienia wideo.

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

Nowoczesne technologie internetowe umożliwiają pracę z filmami na wiele sposobów. Media Stream API, MediaRecord API Interfejs API Media Source i WebRTC API sumują się po rozbudowany zestaw narzędzi do nagrywania, przesyłania i odtwarzania strumieni wideo. Te interfejsy API nie pozwalają, aby internet był w stanie programiści pracują z poszczególnymi elementami strumienia wideo, takimi jak ramki; i fragmenty zakodowanego pliku wideo lub audio. Aby uzyskać niskopoziomowy dostęp do tych podstawowych komponentów, deweloperzy używają WebAssembly, aby wczytać kodeki audio i wideo do przeglądarki. Ale biorąc pod uwagę, które nowoczesne przeglądarki obsługują już różne kodeki (które często są jest obecnie przyspieszona przez sprzęt), przepakowywanie ich w formacie WebAssembly wydaje się marnotrawstwem zasobów ludzkich i komputerowych.

Interfejs WebCodecs API eliminuje tę nieefektywność. Dzięki temu programiści mogą używać komponentów multimedialnych, które są już dostępne z przeglądarki. Oto najważniejsze kwestie:

  • Dekodery audio i wideo
  • Kodery audio i wideo
  • Nieprzetworzone klatki wideo
  • Dekodery obrazów

Interfejs API WebCodecs jest przydatny w przypadku aplikacji internetowych, które wymagają pełnej kontroli nad sposobu przetwarzania treści multimedialnych, na przykład w edytorach wideo, rozmowach wideo strumieniowanie itp.

Proces przetwarzania filmu

Klatki to najważniejszy element przetwarzania wideo. Dlatego w WebCodecs większość klas oraz konsumpcji lub tworzenia ramek. Kodery wideo konwertują klatki na zakodowane fragmentami. Dekodery wideo działają w odwrotny sposób.

Poza tym VideoFrame dobrze współgra z innymi internetowymi interfejsami API, ponieważ jest CanvasImageSource i ma konstruktor akceptujący CanvasImageSource. Można go więc używać w funkcjach takich jak drawImage() i texImage2D(). Można go także skonstruować z obiektów canvas, map bitowych, elementów wideo i innych klatek wideo.

Interfejs WebCodecs API dobrze działa w połączeniu z klasami z interfejsu Insertable Streams API. łączących WebCodecs ze ścieżkami strumienia multimediów.

  • Funkcja MediaStreamTrackProcessor dzieli ścieżki multimedialne na pojedyncze klatki.
  • MediaStreamTrackGenerator tworzy ścieżkę multimediów na podstawie strumienia klatek.

Kodeki internetowe i procesory internetowe

Z założenia interfejs WebCodecs API wykonuje te zadania w sposób asynchronicznie i z głównego wątku. Wywołania zwrotne ramek i fragmentów mogą być jednak często wywoływane wiele razy na sekundę, mogą zaśmiecać główny wątek i tym samym pogarszać responsywność strony. Dlatego lepiej przenieść obsługę pojedynczych klatek i zakodowanych fragmentów do Web Worker.

Aby Ci w tym pomóc, skorzystaj z usługi ReadableStream. zapewnia wygodny sposób automatycznego przenoszenia wszystkich klatek pochodzących z multimediów do instancji roboczej. Za pomocą MediaStreamTrackProcessor można na przykład uzyskać ReadableStream w przypadku ścieżki strumienia multimediów pochodzącej z kamery internetowej. Później strumień jest przesyłany do mechanizmu WWW, gdzie klatki są odczytywane po kolei i umieszczane w kolejce w: VideoEncoder.

Dzięki HTMLCanvasElement.transferControlToOffscreen renderowanie można przeprowadzać poza wątkiem głównym. Ale gdyby wszystkie narzędzia wysokiego poziomu jako niewygodne, ale sama usługa VideoFrame można przenieść i zostały przeniesione między instancjami roboczymi.

Kodeki internetowe w praktyce

Kodowanie

Ścieżka z Canvas lub ImageBitmap do sieci lub miejsca na dane
Ścieżka z Canvas lub ImageBitmap do sieci lub miejsca na dane

Wszystko zaczyna się od VideoFrame. Klatki wideo można tworzyć na 3 sposoby.

  • Ze źródła obrazu, takiego jak obiekt canvas, obraz bitmapowa lub element wideo.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Użyj narzędzia MediaStreamTrackProcessor, aby pobrać klatki z instancji MediaStreamTrack

    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;
    }
    
  • Utwórz ramkę na podstawie jej binarnej reprezentacji w 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);
    

Niezależnie od tego, skąd pochodzą, ramki mogą być zakodowane w EncodedVideoChunk obiektów z VideoEncoder.

Przed zakodowaniem VideoEncoder musi otrzymać 2 obiekty JavaScript:

  • Uruchom słownik z 2 funkcjami do obsługi zakodowanych fragmentów . Te funkcje są definiowane przez programistę i nie można ich zmienić po są przekazywane do konstruktora VideoEncoder.
  • Obiekt konfiguracji kodera, który zawiera parametry dla danych wyjściowych strumienia wideo. Możesz później zmienić te parametry, wywołując funkcję configure().

Metoda configure() wywołuje NotSupportedError, jeśli konfiguracja jest inna obsługiwane przez przeglądarkę. Zachęcamy do wywołania metody statycznej VideoEncoder.isConfigSupported() z konfiguracją, aby wcześniej sprawdzić, czy konfiguracja jest obsługiwana i poczekaj na jej obietnicę.

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.
}

Po skonfigurowaniu kodera może on akceptować klatki za pomocą metody encode(). Zarówno configure(), jak i encode() wracają natychmiast bez czekania na do ich wykonania. Pozwala to umieścić kilka klatek w kolejce do kodowania w tym samym czasie, a encodeQueueSize pokazuje, ile żądań oczekuje w kolejce , aby dokończyć kodowanie. Błędy są zgłaszane przez natychmiastowe zgłoszenie wyjątku, w przypadku gdy argumenty lub kolejność wywołań metody narusza umowę interfejsu API albo przez wywołanie metody error() w razie problemów z implementacją kodeka. Jeśli kodowanie się zakończy, output() wywołanie zwrotne jest wywoływane z nowym zakodowanym fragmentem jako argumentem. Inną ważną kwestią jest to, że ramki muszą być informowane, gdy może być jeszcze potrzebny, dzwoniąc pod numer close().

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

Czas skończyć kodowanie przez napisanie funkcji, która obsługuje fragmentami zakodowanego filmu, które wydobywa się z kodera. Zwykle funkcja ta służy do wysyłania fragmentów danych przez sieć lub muksowania ich do nośnika kontener do przechowywania danych.

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

Jeśli w którymś momencie trzeba będzie sprawdzić, czy wszystkie oczekujące żądania kodowania zostały została zakończona, możesz zadzwonić do firmy flush() i zaczekać na jej obietnicę.

await encoder.flush();

Dekodowanie

Ścieżka z sieci lub pamięci masowej do Canvas lub ImageBitmap.
Ścieżka z sieci lub pamięci masowej do Canvas lub ImageBitmap.

Konfigurowanie usługi VideoDecoder wygląda podobnie do tego, VideoEncoder: przy tworzeniu dekodera przekazywane są 2 funkcje, a kodek dla parametru configure().

Zestaw parametrów kodeka różni się w zależności od kodeka. Na przykład kodek H.264 może być potrzebny binarny obiekt blob AVCC, chyba że jest zakodowany w tak zwanym formacie Annex B (encoderConfig.avc = { format: "annexb" }).

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.
}

Po zainicjowaniu dekodera możesz zacząć dostarczać go za pomocą obiektów EncodedVideoChunk. Aby utworzyć fragment, będziesz potrzebować:

  • BufferSource zakodowanych danych wideo
  • sygnatura czasowa rozpoczęcia fragmentu w mikrosekundach (czas multimediów pierwszej zakodowanej klatki we fragmencie);
  • typ fragmentu, jeden z:
    • key, jeśli fragment można zdekodować niezależnie od poprzednich
    • delta, jeśli fragment można zdekodować tylko po odkodowaniu co najmniej jednego poprzedniego fragmentu

Poza tym wszystkie fragmenty emitowane przez koder są gotowe do dekodera w niezmienionej postaci. Wszystkie powyższe informacje o raportowaniu błędów i asynchronicznym charakterze metod kodera są równie prawdziwe dla dekoderów.

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

Czas pokazać, jak można wyświetlić na stronie niedawno zdekodowaną ramkę. Jest lepiej jest mieć pewność, że dekoder generuje wywołanie zwrotne (handleFrame()) szybko zwraca. W poniższym przykładzie dodaje tylko ramkę do kolejki ramki gotowe do renderowania. Renderowanie odbywa się oddzielnie i składa się z 2 etapów:

  1. Oczekiwanie na odpowiedni moment na wyświetlenie klatki.
  2. Rysując ramkę na obszarze roboczym.

Gdy klatka nie jest już potrzebna, wywołaj close(), aby zwolnić pamięć bazową. zanim dotrze do śmieci, zmniejsza to średnią ilość pamięci używanej przez aplikację internetową.

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

Wskazówki dla programistów

Korzystanie z panelu multimediów w Narzędziach deweloperskich w Chrome.

Zrzut ekranu przedstawiający panel multimediów do debugowania kodeków internetowych
Panel multimedialny w Narzędziach deweloperskich w Chrome do debugowania kodeków internetowych.

Prezentacja

Poniższy przykład pokazuje, jak wyglądają klatki animacji z obiektu canvas:

  • przechwycony z szybkością 25 kl./s w formacie ReadableStream przez MediaStreamTrackProcessor
  • przeniesione do Web Worker
  • zakodowany w formacie wideo H.264
  • są ponownie zdekodowane w sekwencję klatek filmu.
  • i wyrenderowano na drugim obszarze roboczym przy użyciu transferControlToOffscreen()

Inne wersje demonstracyjne

Zobacz też inne wersje demonstracyjne:

Korzystanie z interfejsu WebCodecs API

Wykrywanie cech

Aby sprawdzić dostępność kodeków internetowych:

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

Pamiętaj, że interfejs WebCodecs API jest dostępny tylko w bezpiecznych kontekstach, więc wykrywanie nie powiedzie się, jeśli self.isSecureContext ma wartość false (fałsz).

Prześlij opinię

Zespół Chrome chce poznać Twoją opinię na temat interfejsu WebCodecs API.

Opowiedz nam o konstrukcji interfejsu API

Czy jest coś, co nie działa w interfejsie API zgodnie z oczekiwaniami? są lub są brakuje metod lub właściwości potrzebnych do realizacji pomysłu? Musisz pytanie lub komentarz na temat modelu zabezpieczeń? Zgłoś problem ze specyfikacją w odpowiednie repozytorium GitHub lub dodaj swoje przemyślenia na temat istniejącego problemu.

Zgłoś problem z implementacją

Czy wystąpił błąd z implementacją Chrome? Czy wdrożenie różni się od specyfikacji? Zgłoś błąd na new.crbug.com. Podaj jak najwięcej szczegółów, proste instrukcje. i w polu Komponenty wpisz Blink>Media>WebCodecs. Usługa Glitch świetnie nadaje się do szybkiego i łatwego udostępniania poprawek.

Pokaż wsparcie dla interfejsu API

Czy planujesz użycie interfejsu API WebCodecs? Twoje publiczne wsparcie pomaga Zespół Chrome nadaje priorytet funkcjom i pokazuje innym dostawcom przeglądarek, jest wspieranie ich.

Wysyłaj e-maile na adres [email protected] lub tweeta do @ChromiumDev za pomocą hashtagu #WebCodecs i daj nam znać, gdzie i jak go używasz.

Baner powitalny – autor: Denise Jans w sekcji Unsplash.