manipulowanie komponentami strumienia wideo;
Nowoczesne technologie internetowe oferują wiele sposobów pracy z filmami. Media Stream API, Media Recording API, Media Source API i WebRTC API tworzą bogaty zestaw narzędzi do nagrywania, przesyłania i odtwarzania strumieni wideo. Podczas rozwiązywania niektórych zadań na wysokim poziomie te interfejsy API nie pozwalają programistom pracować z poszczególnymi komponentami strumienia wideo, takimi jak klatki i niezdemuxowane fragmenty zakodowanego wideo lub dźwięku. Aby uzyskać dostęp do tych podstawowych komponentów na niskim poziomie, deweloperzy używali WebAssembly do wprowadzania kodków wideo i dźwięku do przeglądarki. Biorąc jednak pod uwagę, że nowoczesne przeglądarki są już wyposażone w różne kodeki (często przyspieszane przez sprzęt), ich przepakowanie do formatu WebAssembly wydaje się marnotrawstwem zasobów ludzkich i komputerowych.
WebCodecs API eliminuje tę nieefektywność, dając programistom możliwość korzystania z komponentów multimedialnych, które są już obecne w przeglądarce. Więcej szczegółów:
- Dekodery wideo i dźwięku
- Kodeki wideo i audio
- Nieedytowane klatki wideo
- Dekodery obrazu
Interfejs WebCodecs API jest przydatny w przypadku aplikacji internetowych, które wymagają pełnej kontroli nad sposobem przetwarzania treści multimedialnych, np. edytorów wideo, programów do wideokonferencji czy strumieniowania wideo.
Proces przetwarzania filmów
Ramki są kluczowym elementem przetwarzania wideo. W związku z tym w WebCodecs większość klas zużywa lub generuje klatki. Kodery wideo przekształcają klatki w zakodowane segmenty. Dekodery wideo działają odwrotnie.
Ponadto VideoFrame
współpracuje z innymi interfejsami API w internecie, ponieważ jest to obiekt typu CanvasImageSource
i ma konstruktor, który akceptuje CanvasImageSource
.
Można go używać w funkcjach takich jak drawImage()
i texImage2D()
. Możesz też tworzyć je na podstawie obrazów, bitmap, elementów wideo i innych klatek wideo.
Interfejs WebCodecs API dobrze współpracuje z klasami z interfejsu Insertable Streams API, które łączą WebCodecs z ścieżkami strumienia danych multimedialnych.
MediaStreamTrackProcessor
dzieli ścieżki multimedialne na poszczególne klatki.MediaStreamTrackGenerator
tworzy ścieżkę multimedialną na podstawie strumienia klatek.
WebCodecs i procesy internetowe
Zgodnie z projektem interfejs WebCodecs API wykonuje wszystkie ciężkie zadania asynchronicznie i poza wątkiem głównym. Jednak ponieważ funkcje wywoływane po utworzeniu ramki lub fragmentu mogą być wywoływane wielokrotnie na sekundę, mogą one zaśmiecać główny wątek i w ten sposób zmniejszać responsywność witryny. Dlatego lepiej jest przekazać obsługę poszczególnych klatek i zakodowanych fragmentów do instancji roboczej przeglądarki.
Aby ułatwić to zadanie, ReadableStream zapewnia wygodny sposób automatycznego przenoszenia wszystkich klatek z ścieżki medialnej do wątku. Na przykład za pomocą MediaStreamTrackProcessor
można uzyskać ReadableStream
dla ścieżki strumienia multimediów pochodzącego z kamery internetowej. Następnie strumień jest przekazywany do web workera, gdzie ramki są odczytywane pojedynczo i dodawane do kolejki VideoEncoder
.
Dzięki HTMLCanvasElement.transferControlToOffscreen
nawet renderowanie może odbywać się poza głównym wątkiem. Jeśli jednak okaże się, że korzystanie z tych narzędzi wysokiego poziomu jest niewygodne, możesz przekazać VideoFrame
innym pracownikom.
WebCodecs w praktyce
Kodowanie

Canvas
lub ImageBitmap
do sieci lub miejsca na daneWszystko zaczyna się od VideoFrame
.
Ramki wideo można tworzyć na 3 sposoby.
z źródła obrazu, takiego jak kanwa, bitmapa lub element wideo;
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Użyj
MediaStreamTrackProcessor
, aby pobrać ramki zMediaStreamTrack
.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 binarnej reprezentacji pikseli 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ć kodowane w obiektach EncodedVideoChunk
za pomocą VideoEncoder
.
Przed kodowaniem obiektowi VideoEncoder
należy przekazać 2 obiekty JavaScript:
- Inicjalizacja słownika z 2 funkcjami do obsługi zakodowanych fragmentów i błędów. Te funkcje są definiowane przez dewelopera i nie można ich zmienić po przekazaniu do konstruktora
VideoEncoder
. - Obiekt konfiguracji kodera, który zawiera parametry wyjściowego strumienia wideo. Te parametry możesz później zmienić, wywołując funkcję
configure()
.
Jeśli przeglądarka nie obsługuje konfiguracji, metoda configure()
zwróci wartość NotSupportedError
. Zalecamy wywołanie metody statycznej VideoEncoder.isConfigSupported()
z konfiguracją, aby sprawdzić, czy jest ona obsługiwana, i odczekać na 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 koder jest gotowy do przyjmowania klatek za pomocą metody encode()
.
Zarówno configure()
, jak i encode()
zwracają wartości natychmiast, bez oczekiwania na zakończenie rzeczywistej pracy. Umożliwia to umieszczenie w kolejce kilku klatek do zakodowania w tym samym czasie, a encodeQueueSize
pokazuje, ile żądań oczekuje w kolejce na zakończenie poprzednich kodowań.
Błędy są zgłaszane albo przez natychmiastowe wyrzucenie wyjątku, jeśli argumenty lub kolejność wywołań metody naruszają kontrakt interfejsu API, albo przez wywołanie funkcji zwracającej wywołanie zwrotne error()
w przypadku problemów napotkanych podczas implementacji kodeka.
Jeśli kodowanie zostanie zakończone pomyślnie, wywoływana jest funkcja wywołania zwrotnego output()
z nowym zakodowanym fragmentem jako argumentem.
Kolejną ważną kwestią jest to, że ramki muszą być informowane, kiedy nie są już potrzebne, przez wywołanie funkcji 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();
}
}
Na koniec należy dokończyć kodowanie, pisząc funkcję, która będzie obsługiwać fragmenty zakodowanego filmu po ich wyjściu z kodera. Zwykle ta funkcja polega na wysyłaniu fragmentów danych przez sieć lub zmiatania ich w kontenerze multimediów na potrzeby przechowywania.
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 jakimś momencie chcesz się upewnić, że wszystkie oczekujące prośby o kodowanie zostały zrealizowane, możesz zadzwonić pod numer flush()
i poczekać na spełnienie obietnicy.
await encoder.flush();
Dekodowanie

Canvas
lub ImageBitmap
.Konfigurowanie VideoDecoder
jest podobne do konfigurowania VideoEncoder
: podczas tworzenia dekodera są przekazywane 2 funkcje, a parametry kodeka są przekazywane do configure()
.
Zestaw parametrów kodeka różni się w zależności od kodeka. Na przykład kodek H.264 może wymagać bloba binarnego AVCC, chyba że jest zakodowany w tak zwanym formacie załącznika 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ąć podawać mu obiekty EncodedVideoChunk
.
Aby utworzyć fragment, musisz mieć:
BufferSource
zakodowanych danych wideo- sygnatura czasowa początku fragmentu w mikrosekundach (czas trwania pierwszej zakodowanej klatki w danym fragmencie)
- typ fragmentu:
key
jeśli fragment może być dekodowany niezależnie od poprzednich fragmentów.delta
jeśli fragment może zostać zdekodowany dopiero po dekodowaniu co najmniej jednego poprzedniego fragmentu.
Wszystkie informacje o raportowaniu błędów i niesynchronizowanym charakterze metod kodera są również prawdziwe w przypadku dekodera.
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();
Teraz pokażę, jak można wyświetlić na stronie świeżo zdekodowaną ramkę. Lepiej jest zadbać o to, aby wywołanie zwrotne wyjścia dekodera (handleFrame()
) było szybkie. W przykładzie poniżej dodaje on tylko jeden kadr do kolejki klatek gotowych do renderowania.
Renderowanie odbywa się osobno i składa się z 2 etapów:
- Czekam na odpowiedni moment, aby wyświetlić kadr.
- Rysowanie ramki na obszarze roboczym.
Gdy ramka nie jest już potrzebna, wywołaj funkcję close()
, aby zwolnić pamięć, zanim zrobi to zbieracz. Spowoduje to zmniejszenie średniej ilości 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
Aby wyświetlać dzienniki multimediów i debugować WebCodecs, użyj panelu multimediów w Narzędziach deweloperskich w Chrome.

Prezentacja
Demonstracja poniżej pokazuje, jak wyglądają klatki animacji z płótna:
MediaStreamTrackProcessor
zarejestrował plikReadableStream
z szybkością 25 FPS- przeniesiono do elementu web worker.
- zakodowany w formacie wideo H.264;
- dekodowany ponownie w sekwencję klatek wideo.
- i wyrenderowany na drugim płótnie za pomocą
transferControlToOffscreen()
Inne wersje demonstracyjne
Zapoznaj się też z naszą inną wersją demonstracyjną:
- Dekodowanie GIF-ów za pomocą ImageDecoder
- Nagrywanie sygnału z kamery w pliku
- Odtwarzanie MP4
- Inne próbki
Korzystanie z interfejsu WebCodecs API
Wykrywanie funkcji
Aby sprawdzić obsługę WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Pamiętaj, że interfejs WebCodecs API jest dostępny tylko w zabezpieczonym kontekście, więc wykrywanie nie powiedzie się, jeśli self.isSecureContext
ma wartość fałsz.
Prześlij opinię
Zespół Chrome chce poznać Twoje wrażenia z korzystania z interfejsu WebCodecs API.
Poinformuj nas o projektowaniu interfejsu API
Czy coś w interfejsie API nie działa zgodnie z oczekiwaniami? A może brakuje metod lub właściwości, których potrzebujesz do wdrożenia swojego pomysłu? Masz pytanie lub komentarz dotyczący modelu zabezpieczeń? Zgłoś problem ze specyfikacją w odpowiednim repozytorium GitHub lub dodaj swoje uwagi do istniejącego problemu.
Zgłaszanie problemów z implementacją
Czy znalazłeś/znalazłaś błąd w implementacji Chrome? A może implementacja różni się od specyfikacji? Zgłoś błąd na stronie new.crbug.com. Pamiętaj, aby podać jak najwięcej szczegółów i proste instrukcje odtworzenia błędu. W polu Składniki wpisz Blink>Media>WebCodecs
.
Pokaż pomoc dotyczącą interfejsu API
Zamierzasz używać interfejsu WebCodecs API? Twoja publiczna pomoc pomaga zespołowi Chrome ustalać priorytety funkcji i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.
Wyślij e-maila na adres [email protected] lub wyślij tweeta do @ChromiumDev z użyciem hashtaga #WebCodecs
i poinformuj nas, gdzie i jak go używasz.
Baner powitalny autorstwa Denise Jans z Unsplash.