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ń wysokiego poziomu te interfejsy API nie pozwalają programistom internetowym pracować z poszczególnymi komponentami strumienia wideo, takimi jak klatki i niezmultipleksowane fragmenty zakodowanego wideo lub dźwięku. Aby uzyskać dostęp niskiego poziomu do tych podstawowych komponentów, deweloperzy używają WebAssembly do przenoszenia kodeków wideo i audio do przeglądarki. Jednak biorąc pod uwagę, że nowoczesne przeglądarki są już wyposażone w różne kodeki (często przyspieszane przez sprzęt), ponowne pakowanie ich jako WebAssembly wydaje się marnotrawstwem zasobów ludzkich i komputerowych.
WebCodecs API eliminuje tę nieefektywność, umożliwiając programistom korzystanie z komponentów multimedialnych, które są już dostępne w przeglądarce. Więcej szczegółów:
- Dekodery audio i wideo
- Enkodery audio i wideo
- nieprzetworzone klatki wideo,
- Dekodery obrazów
Interfejs WebCodecs API jest przydatny w przypadku aplikacji internetowych, które wymagają pełnej kontroli nad sposobem przetwarzania treści multimedialnych, takich jak edytory wideo, aplikacje do wideokonferencji, strumieniowanie wideo itp.
Proces przetwarzania filmu
Klatki są najważniejszym elementem przetwarzania wideo. Dlatego w WebCodecs większość klas przetwarza lub generuje klatki. Enkodery wideo konwertują klatki na zakodowane fragmenty. Dekodery wideo działają odwrotnie.
VideoFrame
dobrze współpracuje z innymi interfejsami API sieci, ponieważ jest CanvasImageSource
i ma konstruktor, który akceptuje CanvasImageSource
. Można jej więc używać w funkcjach takich jak drawImage()
i texImage2D()
. Może też składać się z obszarów roboczych, bitmap, elementów wideo i innych klatek wideo.
Interfejs WebCodecs API dobrze współpracuje z klasami z Insertable Streams API, które łączą WebCodecs z ścieżkami strumieni multimediów.
MediaStreamTrackProcessor
dzieli ścieżki multimediów na poszczególne klatki.MediaStreamTrackGenerator
tworzy ścieżkę multimedialną ze strumienia klatek.
WebCodecs i procesy internetowe
Zgodnie z założeniami interfejs WebCodecs API wykonuje wszystkie złożone operacje asynchronicznie i poza wątkiem głównym. Jednak wywołania zwrotne ramek i fragmentów mogą być wywoływane kilka razy na sekundę, co może powodować przepełnienie wątku głównego i zmniejszać responsywność witryny. Dlatego lepiej jest przenieść obsługę poszczególnych klatek i zakodowanych fragmentów do wątku roboczego.
Aby to ułatwić, interfejs ReadableStream zapewnia wygodny sposób automatycznego przesyłania wszystkich klatek pochodzących ze ścieżki multimedialnej do procesu roboczego. Na przykład MediaStreamTrackProcessor
może służyć do uzyskania ReadableStream
dla ścieżki strumienia multimediów pochodzącej z kamery internetowej. Następnie strumień jest przekazywany do procesu roboczego, w którym ramki są odczytywane pojedynczo i umieszczane w kolejce w VideoEncoder
.
Dzięki HTMLCanvasElement.transferControlToOffscreen
renderowanie może odbywać się poza głównym wątkiem. Jeśli jednak wszystkie narzędzia wyższego poziomu okażą się niewygodne, VideoFrame
jest przenośny i może być przekazywany między pracownikami.
WebCodecs w praktyce
Kodowanie

Canvas
lub ImageBitmap
do sieci lub miejsca na daneWszystko zaczyna się od VideoFrame
. Istnieją 3 sposoby tworzenia klatek wideo.
ze źródła obrazu, takiego jak element canvas, bitmapa obrazu 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ć klatki 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 klatkę na podstawie jej 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 źródła klatki można kodować w EncodedVideoChunk
obiektach za pomocą VideoEncoder
.
Przed zakodowaniem VideoEncoder
musi otrzymać 2 obiekty JavaScript:
- Inicjowanie słownika za pomocą 2 funkcji do obsługi zakodowanych fragmentów i błędów. Te funkcje są zdefiniowane 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()
.
Metoda configure()
zgłosi błąd NotSupportedError
, jeśli konfiguracja nie jest obsługiwana przez przeglądarkę. Zalecamy wywołanie metody statycznej VideoEncoder.isConfigSupported()
z konfiguracją, aby wcześniej sprawdzić, czy jest ona obsługiwana, i poczekać 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 odbierania klatek za pomocą metody encode()
. Zarówno configure()
, jak i encode()
zwracają wynik natychmiast, bez czekania na zakończenie rzeczywistej pracy. Umożliwia to umieszczenie w kolejce kilku klatek do zakodowania w tym samym czasie, a encodeQueueSize
pokazuje, ile żądań czeka w kolejce na zakończenie poprzednich procesów kodowania. Błędy są zgłaszane przez natychmiastowe zgłoszenie wyjątku w przypadku, gdy argumenty lub kolejność wywołań metod naruszają umowę API, lub przez wywołanie funkcji zwrotnej error()
w przypadku problemów napotkanych podczas implementacji kodeka. Jeśli kodowanie zakończy się pomyślnie, wywoływane jest wywołanie zwrotne output()
z nowym zakodowanym fragmentem jako argumentem. Kolejnym ważnym szczegółem jest to, że ramki muszą być informowane o tym, 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 dokończ kodowanie, pisząc funkcję, która obsługuje fragmenty zakodowanego filmu, gdy wychodzą z kodera. Zwykle ta funkcja wysyła fragmenty danych przez sieć lub multipleksuje je do kontenera multimedialnego w celu 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 pewnym momencie chcesz mieć pewność, że wszystkie oczekujące żądania kodowania zostały zrealizowane, możesz wywołać funkcję flush()
i poczekać na jej obietnicę.
await encoder.flush();
Dekodowanie

Canvas
lub ImageBitmap
.Konfigurowanie VideoDecoder
jest podobne do tego, co zostało zrobione w przypadku VideoEncoder
: podczas tworzenia dekodera przekazywane są 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ć binarnego obiektu 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ąć przekazywać do niego obiekty EncodedVideoChunk
. Aby utworzyć fragment, musisz mieć:
BufferSource
zakodowanych danych wideo,- sygnatura czasowa początku fragmentu w mikrosekundach (czas multimediów pierwszej zakodowanej klatki w fragmencie);
- typ fragmentu, jeden z tych:
key
, jeśli fragment może być dekodowany niezależnie od poprzednich fragmentów.delta
jeśli fragment można zdekodować tylko po zdekodowaniu co najmniej 1 poprzedniego fragmentu.
Wszystkie fragmenty wygenerowane przez koder są gotowe do użycia przez dekoder w takiej postaci, w jakiej zostały wygenerowane. Wszystkie powyższe informacje o raportowaniu błędów i asynchronicznym charakterze metod kodera są równie prawdziwe w przypadku 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();
Teraz pokażemy, jak wyświetlić na stronie świeżo zdekodowaną ramkę. Warto zadbać o to, aby wywołanie zwrotne danych wyjściowych dekodera (handleFrame()
) szybko zwracało wynik. W podanym niżej przykładzie dodaje on tylko ramkę do kolejki ramek gotowych do renderowania. Renderowanie odbywa się osobno i składa się z 2 etapów:
- Czekanie na odpowiedni moment, aby wyświetlić klatkę.
- Rysowanie ramki na obszarze roboczym.
Gdy ramka nie jest już potrzebna, wywołaj funkcję close()
, aby zwolnić pamięć bazową, zanim zrobi to moduł odśmiecania. Zmniejszy 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
Użyj panelu multimediów w Narzędziach deweloperskich w Chrome, aby wyświetlać dzienniki multimediów i debugować WebCodecs.

Prezentacja
Wersja demonstracyjna pokazuje, jak klatki animacji z obiektu canvas są:
- nagrany w 25 kl./s na urządzeniu
ReadableStream
przezMediaStreamTrackProcessor
- przekazane do procesu roboczego w internecie.
- zakodowany w formacie wideo H.264,
- ponownie dekodowany do sekwencji klatek wideo.
- i wyrenderowany na drugim obszarze roboczym za pomocą
transferControlToOffscreen()
.
Inne wersje demonstracyjne
Zapoznaj się też z innymi wersjami demonstracyjnymi:
- Dekodowanie GIF-ów za pomocą ImageDecoder
- Zapisywanie obrazu z kamery w pliku
- Odtwarzanie MP4
- Inne próbki
Korzystanie z interfejsu WebCodecs API
Wykrywanie cech
Aby sprawdzić, czy przeglądarka obsługuje WebCodecs:
if ('VideoEncoder' in window) { // WebCodecs API is supported. }
Pamiętaj, że interfejs WebCodecs API jest dostępny tylko w zabezpieczonych kontekstach, więc wykrywanie zakończy się niepowodzeniem, jeśli wartość self.isSecureContext
będzie fałszywa.
Prześlij opinię
Zespół Chrome chce poznać Twoje wrażenia związane z korzystaniem z interfejsu WebCodecs API.
Opisz projekt interfejsu API
Czy w API jest coś, co nie działa tak, jak oczekujesz? Czy brakuje metod lub właściwości, które są potrzebne do wdrożenia Twojego 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 problemu z implementacją
Czy w implementacji Chrome występuje błąd? Czy implementacja różni się od specyfikacji? Zgłoś błąd na stronie new.crbug.com. Podaj jak najwięcej szczegółów, proste instrukcje odtwarzania problemu i wpisz Blink>Media>WebCodecs
w polu Komponenty.
Wyrażanie poparcia dla interfejsu API
Czy planujesz używać interfejsu WebCodecs API? Twoje publiczne wsparcie pomaga zespołowi Chrome ustalać priorytety funkcji i pokazuje innym dostawcom przeglądarek, jak ważne jest ich obsługiwanie.
Wyślij e-maila na adres [email protected] lub tweeta na adres @ChromiumDev z hasztagiem #WebCodecs
i napisz, gdzie i jak korzystasz z tego interfejsu.
Baner powitalny autorstwa Denise Jans z Unsplash.