Manipulowanie komponentami strumienia wideo.
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
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 instancjiMediaStreamTrack
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
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 poprzednichdelta
, 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:
- Oczekiwanie na odpowiedni moment na wyświetlenie klatki.
- 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.
Prezentacja
Poniższy przykład pokazuje, jak wyglądają klatki animacji z obiektu canvas:
- przechwycony z szybkością 25 kl./s w formacie
ReadableStream
przezMediaStreamTrackProcessor
- 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:
- Dekodowanie GIF-ów za pomocą ImageDecoder
- Rejestrowanie danych z aparatu w pliku
- Odtwarzanie w formacie MP4
- Inne przykłady
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 media-dev@chromium.org 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.