Manipulowanie komponentami strumienia wideo.
Nowoczesne technologie internetowe oferują wiele możliwości pracy z filmami. Interfejsów Media Stream API, Media record API, Media Source API i WebRTC API tworzy rozbudowany zestaw narzędzi do nagrywania, przesyłania i odtwarzania strumieni wideo. Podczas rozwiązywania niektórych ogólnych zadań, te interfejsy API nie pozwalają programistom internetowym na pracę z poszczególnymi komponentami strumienia wideo, takimi jak ramki i niezmodyfikowane fragmenty zakodowanego obrazu lub dźwięku. Aby uzyskać niskopoziomowy dostęp do tych podstawowych komponentów, programiści korzystają z WebAssembly, by zainstalować kodeki wideo i audio do przeglądarki. Nowoczesne przeglądarki zawierają jednak różne kodeki (często przyspieszane przez sprzęt), więc przepakowanie ich w taki sposób, że standard WebAssembly wydaje się marnować zasoby ludzkie i komputerowe.
Interfejs WebCodecs API eliminuje tę nieefektywność, dając programistom możliwość korzystania z komponentów multimedialnych, które są już w przeglądarce. Oto najważniejsze kwestie:
- Dekodery audio i wideo
- Kodery audio i wideo
- Nieprzetworzone klatki wideo
- Dekodery obrazów
Interfejs WebCodecs API jest przydatny w aplikacjach internetowych, które wymagają pełnej kontroli nad sposobem przetwarzania treści multimedialnych, takich jak edytory wideo, konferencje wideo, strumieniowanie wideo itp.
Proces przetwarzania filmu
Klatki stanowią podstawę przetwarzania filmu. Dlatego w WebCodecs większość klas wykorzystuje lub tworzy ramki. Kodery wideo konwertują klatki na zakodowane fragmenty. Dekodery wideo działają odwrotnie.
Poza tym VideoFrame
dobrze współpracuje z innymi interfejsami API – jest CanvasImageSource
i ma konstruktor, który akceptuje CanvasImageSource
.
Można go więc używać w takich funkcjach jak drawImage()
i texImage2D()
. Można go również tworzyć z kanw, map bitowych, elementów wideo i innych klatek wideo.
Interfejs WebCodecs API działa dobrze w połączeniu z klasami z Insertable Streams API, które łączą WebCodecs ze ścieżkami strumienia multimediów.
MediaStreamTrackProcessor
dzieli ścieżki multimediów na osobne klatki.MediaStreamTrackGenerator
tworzy ścieżkę multimediów ze strumienia ramek.
Kodeki internetowe i mechanizmy robocze
Interfejs WebCodecs API z założenia wykonuje całą pracę asynchronicznie i poza głównym wątkiem. Wywołania zwrotne ramek i fragmentów mogą być często wywoływane kilka razy na sekundę, co może powodować zaśmiecanie głównego wątku i sprawić, że witryna będzie mniej responsywna. Dlatego zaleca się przeniesienie obsługi poszczególnych klatek i zakodowanych fragmentów do mechanizmu Web Worker.
W tym celu dostępny jest ReadableStream, który umożliwia automatyczne przenoszenie wszystkich klatek pochodzących ze ścieżki multimediów do instancji roboczej. Za pomocą usługi MediaStreamTrackProcessor
można na przykład uzyskać wartość ReadableStream
dla ścieżki strumienia multimediów pochodzącej z kamery internetowej. Następnie strumień jest przekazywany do instancji roboczej internetowego, gdzie klatki są odczytywane jedna po drugiej i umieszczone w kolejce do elementu VideoEncoder
.
HTMLCanvasElement.transferControlToOffscreen
umożliwia renderowanie nawet poza wątkiem głównym. Jeśli jednak wszystkie ogólne narzędzia okazały się niewygodne, usługę VideoFrame
można przenieść i może zostać przeniesiona między pracownikami.
Kodeki internetowe w praktyce
Kodowanie
Wszystko zaczyna się od VideoFrame
.
Istnieją 3 sposoby tworzenia klatek wideo.
Ze źródła obrazu, takiego jak obiekt 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 narzędzia
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; }
Tworzenie ramki na podstawie jej reprezentacji piksela binarnego 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 można kodować do obiektów EncodedVideoChunk
za pomocą VideoEncoder
.
Przed kodowaniem VideoEncoder
musi otrzymać 2 obiekty JavaScript:
- Słownik inicjujący z 2 funkcjami do obsługi zakodowanego fragmentu i błędów. Te funkcje są definiowane przez programistę i nie można ich zmienić po ich przekazaniu do konstruktora
VideoEncoder
. - Obiekt konfiguracji kodera, który zawiera parametry wyjściowego strumienia wideo. Możesz zmienić te parametry później, wywołując
configure()
.
Metoda configure()
zgłosi NotSupportedError
, jeśli konfiguracja nie jest obsługiwana przez przeglądarkę. Zachęcamy do wywołania metody statycznej VideoEncoder.isConfigSupported()
z konfiguracją w celu wcześniejszego sprawdzenia, czy konfiguracja jest obsługiwana, i poczekania 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 koder jest gotowy do przyjmowania klatek za pomocą metody encode()
.
Zarówno configure()
, jak i encode()
wracają natychmiast, bez czekania na zakończenie pierwotnej pracy. Pozwala na dodanie kilku klatek do kolejki kodowania jednocześnie, a encodeQueueSize
pokazuje, ile żądań czeka w kolejce na zakończenie poprzedniego kodowania.
Błędy są zgłaszane przez natychmiastowe zgłoszenie wyjątku (jeśli argumenty lub kolejność wywołań metody naruszają umowę interfejsu API), albo przez wywołanie wywołania zwrotnego error()
w przypadku problemów, które wystąpiły w implementacji kodeka.
Jeśli kodowanie zakończy się powodzeniem, wywołanie zwrotne output()
zostanie wykonane z nowym zakodowanym fragmentem jako argumentem.
Kolejną ważną wskazówką jest to, że ramki trzeba poinformować za pomocą wywołania close()
, gdy nie są już potrzebne.
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 zakończyć kodowanie, tworząc funkcję, która obsługuje fragmenty zakodowanego filmu wysyłane z kodera. Zwykle ta funkcja służy do wysyłania fragmentów danych przez sieć lub miksowania ich do kontenera 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 którymś momencie zechcesz sprawdzić, czy wszystkie oczekujące żądania kodowania zostały zrealizowane, możesz wywołać metodę flush()
i poczekać na jej obietnicę.
await encoder.flush();
Dekodowanie
Konfigurowanie VideoDecoder
przebiega podobnie jak w przypadku VideoEncoder
: 2 funkcje są przekazywane podczas tworzenia dekodera, a parametry kodeka są przekazywane funkcji 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ąć dostarczać do niego obiekty EncodedVideoChunk
.
Aby utworzyć fragment, potrzebujesz:
BufferSource
zakodowanych danych wideo- sygnatura czasowa rozpoczęcia fragmentu w mikrosekundach (czas multimediów pierwszej zakodowanej klatki we fragmencie).
- jako typ fragmentu, a jedną z tych wartości:
key
, jeśli fragment można odkodować niezależnie od poprzednich fragmentówdelta
, jeśli fragment można zdekodować dopiero po odkodowaniu co najmniej 1 poprzedniego fragmentu
Poza tym wszystkie fragmenty wysyłane przez koder są gotowe do działania dekodera w niezmienionej postaci. Wszystkie powyższe informacje o raportowaniu błędów i asynchronicznym charakterze metod kodera są równie istotne 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 możesz zobaczyć, jak może wyświetlić się na stronie świeżo zdekodowana ramka. Lepiej mieć pewność, że wyjściowe wywołanie zwrotne dekodera (handleFrame()
) będzie szybko zwracane. W poniższym przykładzie dodaje ona tylko klatkę do kolejki ramek gotowych do renderowania.
Renderowanie odbywa się oddzielnie i obejmuje 2 kroki:
- Czekam na odpowiedni moment, aby wyświetlić klatkę.
- Rysowanie ramki na płótnie.
Gdy ramka nie jest już potrzebna, wywołaj close()
, aby zwolnić pamięć bazową, zanim trafi do niej moduł odśmiecania pamięci. Pozwoli to zmniejszyć ilość pamięci wykorzystywanej 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świetlić dzienniki multimediów i debugować kod WebCodecs.
Wersja demonstracyjna
Poniższy przykład pokazuje, jak wyglądają klatki animacji z obszaru roboczego:
- zarejestrowany przy 25 kl./s w
ReadableStream
przezMediaStreamTrackProcessor
- przeniesiono do instancji roboczej
- zakodowane w formacie H.264
- ponownie zakodowany w sekwencji klatek wideo
- i wyrenderowano w drugim obszarze roboczym za pomocą
transferControlToOffscreen()
Inne wersje demonstracyjne
Sprawdź też inne wersje demonstracyjne:
- Dekodowanie GIF-ów za pomocą ImageDecoder
- Zapisywanie danych wejściowych z aparatu do pliku
- Odtwarzanie MP4
- Inne przykłady
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 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ę o korzystaniu z interfejsu WebCodecs API.
Opowiedz nam o projekcie interfejsu API
Czy jest coś, co nie działa w interfejsie API zgodnie z oczekiwaniami? A może brakuje metod lub właściwości, których potrzebujesz do realizacji swojego pomysłu? Masz pytanie lub komentarz na temat modelu zabezpieczeń? Zgłoś problem ze specyfikacją w odpowiednim repozytorium GitHub lub dodaj swoje uwagi do istniejącego problemu.
Zgłoś problem z implementacją
Czy wystąpił błąd związany z implementacją przeglądarki Chrome? A może implementacja różni się od specyfikacji? Zgłoś błąd na stronie new.crbug.com. Podaj jak najwięcej szczegółów, proste instrukcje odtwarzania i wpisz Blink>Media>WebCodecs
w polu Komponenty.
Usterki to świetny sposób na udostępnianie szybkich i łatwych replik.
Pokaż obsługę interfejsu API
Czy zamierzasz używać interfejsu WebCodecs API? Twoja publiczna pomoc pomaga zespołowi Chrome priorytetowo traktować funkcje i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.
Wyślij e-maile na adres media-dev@chromium.org lub wyślij tweeta na adres @ChromiumDev, używając hashtagu #WebCodecs
, i daj nam znać, gdzie i jak używasz tego elementu.
Baner powitalny autorstwa Denise Jans w aplikacji Unsplash.