Manipolazione dei componenti del video stream.
Le moderne tecnologie web offrono ampi modi per lavorare con i video. L'API Media Stream, l'API Media Recording, l'API Media Source e l'API WebRTC si aggiungono a un ricco set di strumenti per la registrazione, il trasferimento e la riproduzione di stream video. Per risolvere determinate attività di alto livello, queste API non consentono ai programmatori web di lavorare con i singoli componenti di uno stream video, come frame e blocchi non combinati di video o audio codificati. Per ottenere l'accesso di basso livello a questi componenti di base, gli sviluppatori hanno utilizzato WebAssembly per importare codec video e audio nel browser. Tuttavia, dal momento che i browser moderni sono già dotati di una varietà di codec (spesso accelerati dall'hardware), la loro ripacchettizzazione in WebAssembly sembra uno spreco di risorse umane e computer.
L'API WebCodecs elimina questa inefficienza offrendo ai programmatori un modo per utilizzare i componenti multimediali già presenti nel browser. In particolare:
- Decodificatori video e audio
- Codificatori video e audio
- Frame video non elaborati
- Decodificatori di immagini
L'API WebCodecs è utile per le applicazioni web che richiedono il controllo completo sulle modalità di elaborazione dei contenuti multimediali, come editor video, videoconferenze, streaming video e così via.
Flusso di lavoro dell'elaborazione video
I fotogrammi sono il pezzo forte dell'elaborazione video. Quindi in WebCodecs la maggior parte delle classi consuma o produce frame. I codificatori video convertono i fotogrammi in blocchi codificati. I decoder video fanno il contrario.
Inoltre VideoFrame
funziona bene con altre API web in quanto CanvasImageSource
e con un costruttore che accetta CanvasImageSource
.
Quindi può essere utilizzato in funzioni come drawImage()
etexImage2D()
. Inoltre, può essere creato a partire da canvas, bitmap, elementi video e altri frame video.
L'API WebCodecs funziona bene in coppia con le classi dell'API Insertable Streams, che collegano WebCodecs alle tracce degli stream multimediali.
MediaStreamTrackProcessor
suddivide le tracce multimediali in singoli frame.MediaStreamTrackGenerator
crea una traccia multimediale a partire da uno stream di frame.
WebCodec e web worker
L'API WebCodecs è stata progettata per svolgere le attività più complesse in modo asincrono e fuori dal thread principale. Tuttavia, poiché i callback di frame e blocchi possono essere spesso chiamati più volte al secondo, potrebbero ingombrare il thread principale e quindi rendere meno reattivo il sito web. Pertanto è preferibile spostare la gestione dei singoli frame e dei blocchi codificati in un web worker.
Per aiutarti, ReadableStream
fornisce un modo pratico per trasferire automaticamente tutti i frame provenienti da una traccia multimediale
al worker. Ad esempio, MediaStreamTrackProcessor
può essere utilizzato per ottenere un ReadableStream
per una traccia di streaming multimediale proveniente dalla webcam. Successivamente, lo stream viene trasferito a un web worker, in cui i frame vengono letti uno alla volta e inseriti in coda in un VideoEncoder
.
Con HTMLCanvasElement.transferControlToOffscreen
è possibile eseguire il rendering anche al di fuori del thread principale. Tuttavia, nel caso in cui tutti gli strumenti di alto livello si rivelassero scomodi, VideoFrame
stesso è trasferibile e potrebbe essere spostato da un worker a un altro.
WebCodec in azione
Codifica
Tutto inizia con un VideoFrame
.
Esistono tre modi per creare i fotogrammi video.
Da un'origine immagine come un canvas, una bitmap immagine o un elemento video.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Usa
MediaStreamTrackProcessor
per estrarre frame da unaMediaStreamTrack
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; }
Crea un frame dalla sua rappresentazione di pixel binari in una
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);
Indipendentemente dalla provenienza, i frame possono essere codificati in oggetti EncodedVideoChunk
con un VideoEncoder
.
Prima della codifica, a VideoEncoder
devono essere assegnati due oggetti JavaScript:
- Dizionario init con due funzioni per la gestione di blocchi ed errori
codificati. Queste funzioni sono definite dallo sviluppatore e non possono essere modificate dopo essere passate al costruttore
VideoEncoder
. - Oggetto di configurazione del codificatore, che contiene i parametri per lo stream video di output. Puoi modificare questi parametri in un secondo momento chiamando
configure()
.
Il metodo configure()
restituisce NotSupportedError
se la configurazione non è supportata dal browser. Ti consigliamo di chiamare il metodo statico VideoEncoder.isConfigSupported()
con la configurazione per verificare in anticipo se la configurazione è supportata e attendere la promessa.
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.
}
Una volta configurato, il codificatore è pronto ad accettare frame tramite il metodo encode()
.
Sia configure()
che encode()
ritornano immediatamente senza dover attendere il
completamento del lavoro effettivo. Consente a diversi frame di mettere in coda per la codifica contemporaneamente, mentre encodeQueueSize
mostra quante richieste sono in coda per completare le codifiche precedenti.
Gli errori vengono segnalati generando immediatamente un'eccezione, nel caso in cui gli argomenti o l'ordine delle chiamate al metodo violino il contratto dell'API oppure richiamando il callback error()
per i problemi riscontrati nell'implementazione del codec.
Se la codifica viene completata correttamente, il callback output()
viene chiamato con un nuovo blocco codificato come argomento.
Un altro dettaglio importante è che è necessario avvisare i frame quando non sono più necessari chiamando 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();
}
}
Infine, si completa il codice di codifica scrivendo una funzione che gestisca i blocchi di video codificati che escono dal codificatore. Di solito, questa funzione inviava blocchi di dati sulla rete o li mux in un container multimediale per l'archiviazione.
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,
});
}
Se a un certo punto dovessi assicurarti che tutte le richieste di codifica in attesa siano state completate, puoi chiamare flush()
e attendere l'esito.
await encoder.flush();
Decodifica
La configurazione di VideoDecoder
è simile a quella di VideoEncoder
: vengono passate due funzioni quando viene creato il decoder e i parametri del codec vengono assegnati a configure()
.
L'insieme dei parametri del codec varia da codec a codec. Ad esempio, il codec H.264 potrebbe richiedere un blob binario di AVCC, a meno che non sia codificato nel cosiddetto formato 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.
}
Una volta inizializzato il decoder, puoi iniziare a fornirlo con EncodedVideoChunk
oggetti.
Per creare un blocco, avrai bisogno di:
- Un
BufferSource
di dati video codificati - timestamp di inizio del blocco in microsecondi (tempo multimediale del primo frame codificato nel blocco)
- il tipo di blocco, uno dei seguenti:
key
se il blocco può essere decodificato indipendentemente dai blocchi precedenti.delta
se il blocco può essere decodificato solo dopo che uno o più blocchi precedenti sono stati decodificati
Inoltre, tutti i blocchi emessi dall'encoder sono pronti per il decoder così come sono. Tutto quanto detto sopra sulla segnalazione degli errori e sulla natura asincrona dei metodi dell'encoder è altrettanto valido anche per i decoder.
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();
Ora è il momento di mostrare come un frame appena decodificato può essere visualizzato nella pagina. È
meglio assicurarsi che il callback di output del decoder (handleFrame()
)
restituisca rapidamente. Nell'esempio seguente, aggiunge solo un frame alla coda dei frame pronti per il rendering.
Il rendering avviene separatamente ed è costituito da due passaggi:
- In attesa del momento giusto per mostrare l'inquadratura.
- Disegnamo il frame sulla tela.
Quando un frame non è più necessario, chiama close()
per rilasciare la memoria sottostante
prima che il garbage collector arrivi. In questo modo si riduce la quantità media di
memoria utilizzata dall'applicazione web.
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);
}
Suggerimenti per sviluppatori
Usa il Pannello multimediale in Chrome DevTools per visualizzare i log multimediali ed eseguire il debug dei WebCodecs.
Demo
La demo riportata di seguito mostra come sono i frame dell'animazione di una tela:
- acquisiti a 25 f/s in
ReadableStream
daMediaStreamTrackProcessor
- viene trasferito a un web worker
- Codificato nel formato video H.264
- decodificato di nuovo in una sequenza di fotogrammi video
- ed eseguito il rendering sul secondo canvas utilizzando
transferControlToOffscreen()
Altre demo
Guarda anche le altre nostre demo:
- Decodifica di GIF con ImageDecoder
- Acquisizione dell'input della fotocamera in un file
- Riproduzione MP4
- Altri esempi
Utilizzo dell'API WebCodecs
Rilevamento delle funzionalità
Per verificare il supporto per WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Tieni presente che l'API WebCodecs è disponibile solo in contesti sicuri, quindi il rilevamento non andrà a buon fine se self.isSecureContext
è false.
Feedback
Il team di Chrome vuole conoscere la tua esperienza con l'API WebCodecs.
Parlaci della progettazione dell'API
C'è qualcosa nell'API che non funziona come previsto? Oppure mancano metodi o proprietà per implementare la tua idea? Hai domande o commenti sul modello di sicurezza? Segnala un problema relativo alle specifiche sul repository GitHub corrispondente o aggiungi le tue opinioni su un problema esistente.
Segnala un problema con l'implementazione
Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione è
diversa dalle specifiche? Segnala un bug all'indirizzo new.crbug.com.
Assicurati di includere il maggior numero di dettagli possibile e di semplici istruzioni per la riproduzione e di inserire Blink>Media>WebCodecs
nella casella Componenti.
Glitch funziona benissimo per condividere riproduzioni rapide e semplici.
Mostra il supporto dell'API
Hai intenzione di utilizzare l'API WebCodecs? Il tuo supporto pubblico aiuta il team di Chrome a dare la priorità alle funzionalità e mostra ad altri fornitori di browser l'importanza di supportarle.
Invia email a media-dev@chromium.org o un tweet
a @ChromiumDev utilizzando l'hashtag
#WebCodecs
e facci sapere dove e come lo stai usando.
Immagine hero di Denise Jans su Unsplash.