manipulowanie komponentami strumienia wideo;
Nowoczesne technologie internetowe oferują wiele sposobów na pracę 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 kodek obrazu i dźwięku do przeglądarki. Jednak biorąc pod uwagę, że nowoczesne przeglądarki są już wyposażone w różne kodeki (które są często przyspieszane przez sprzęt), przepakowanie ich jako 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. Oto najważniejsze kwestie:
- 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, takich jak edytory wideo, konferencje wideo, strumieniowanie wideo itp.
Proces przetwarzania filmu
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 sieci, 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że też być zbudowany z płócien, 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 przenieść 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 ramek 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 wstawiane do kolejki VideoEncoder
.
Dzięki HTMLCanvasElement.transferControlToOffscreen
nawet renderowanie może być wykonywane poza wątkiem głównym. Jeśli jednak okaże się, że żadne z dostępnych narzędzi nie jest wygodne w użyciu, możesz przekazać VideoFrame
innym pracownikom.
WebCodecs w praktyce
Kodowanie
Wszystko zaczyna się od VideoFrame
.
Ramki wideo można tworzyć na 3 sposoby.
Z źródła obrazu, takiego jak płótno, 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ę z binarnej reprezentacji piksela 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 obiekt VideoEncoder
musi mieć 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 rzucenie wyjątku, jeśli argumenty lub kolejność wywołań metody naruszają kontrakt interfejsu API, albo przez wywołanie funkcji zwracającej wartością error()
w przypadku problemów napotkanych podczas implementacji kodeka.
Jeśli kodowanie zakończy się pomyślnie, wywoływana jest funkcja wywołania zwrotnego output()
z nowym zakodowanym fragmentem jako argumentem.
Kolejną ważną rzeczą 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();
}
}
Nadszedł czas, aby zakończyć kodowanie, pisząc funkcję, która będzie obsługiwać fragmenty zakodowanego filmu po ich wyjściu z enkodera. Zwykle ta funkcja polega na wysyłaniu fragmentów danych przez sieć lub zapisywaniu ich w kontenerze multimediów.
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
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ć binarnego bloba 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ąć 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żna zdekodować 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 element nie jest już potrzebny, wywołaj funkcję close()
, aby zwolnić pamięć podrzędną, 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ł film w standardzieReadableStream
z częstotliwością 25 FPS- przeniesiono do zadania internetowego.
- 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 cech
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 związane z interfejsem 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 na temat 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? Czy 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
.
Glitch to świetne narzędzie do szybkiego i łatwego udostępniania informacji o powtarzających się problemach.
Pokaż informacje o pomocy dotyczącej 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 media-dev@chromium.org 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.