Manipolare i componenti dello stream video.
Le moderne tecnologie web offrono numerosi modi per lavorare con i video. L'API Media Stream, l'API Media Recording, l'API Media Source e l'API WebRTC formano un ricco set di strumenti per la registrazione, il trasferimento e la riproduzione di stream video. Sebbene risolvano determinate attività di alto livello, queste API non consentono ai programmatori web di lavorare con i singoli componenti di uno stream video, come i frame e i chunk non muxati di video o audio codificati. Per ottenere l'accesso a basso livello a questi componenti di base, gli sviluppatori utilizzano WebAssembly per integrare i codec video e audio nel browser. Tuttavia, dato che i browser moderni sono già dotati di una serie di codec (spesso accelerati dall'hardware), il loro nuovo confezionamento come WebAssembly sembra uno spreco di risorse umane e informatiche.
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
- Fotogrammi video non elaborati
- Decodificatori di immagini
L'API WebCodecs è utile per le applicazioni web che richiedono il controllo completo sul modo in cui vengono elaborati i contenuti multimediali, ad esempio editor video, videoconferenze, streaming video e così via.
Flusso di lavoro di elaborazione dei video
I fotogrammi sono il fulcro dell'elaborazione video. Pertanto, in WebCodecs la maggior parte delle classi consumerà o produrrà frame. I codificatori video convertono i frame in chunk codificati. I decodificatori video fanno il contrario.
Inoltre, VideoFrame
è compatibile con altre API web perché è un CanvasImageSource
e ha un costruttore che accetta CanvasImageSource
.
Pertanto, può essere utilizzato in funzioni come drawImage()
e texImage2D()
. Inoltre, può essere creato da canvas, bitmap, elementi video e altri frame video.
L'API WebCodecs funziona bene in combinazione con le classi dell'API Insertable Streams che collegano WebCodecs alle tracce dello stream multimediale.
MediaStreamTrackProcessor
suddivide le tracce multimediali in singoli frame.MediaStreamTrackGenerator
crea una traccia multimediale da uno stream di frame.
WebCodec e web worker
Per impostazione predefinita, l'API WebCodecs esegue tutte le operazioni più complesse in modo asincrono e al di fuori del thread principale. Tuttavia, poiché i callback di frame e chunk possono essere chiamati spesso più volte al secondo, potrebbero ingombrare il thread principale e rendere il sito web meno reattivo. Pertanto, è preferibile spostare la gestione dei singoli frame e dei chunk codificati in un worker web.
Per aiutarti, ReadableStream
offre un modo pratico per trasferire automaticamente tutti i frame provenienti da una traccia media al worker. Ad esempio, MediaStreamTrackProcessor
può essere utilizzato per ottenere un
ReadableStream
per una traccia dello stream multimediale proveniente dalla webcam. Successivamente,
lo stream viene trasferito a un web worker in cui i frame vengono letti uno alla volta e messi in coda
in un VideoEncoder
.
Con HTMLCanvasElement.transferControlToOffscreen
è possibile eseguire anche il rendering al di fuori del thread principale. Tuttavia, se tutti gli strumenti di alto livello risultano scomodi, VideoFrame
stesso è trasferibile e può essere spostato da un utente all'altro.
WebCodecs in azione
Codifica
Tutto inizia con un VideoFrame
.
Esistono tre modi per creare frame video.
Da un'origine immagine come una tela, una bitmap immagine o un elemento video.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Utilizza
MediaStreamTrackProcessor
per estrarre frame da unMediaStreamTrack
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; }
Creare un frame dalla sua rappresentazione in pixel binari in un
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 loro origine, i frame possono essere codificati in oggetti EncodedVideoChunk
con un VideoEncoder
.
Prima della codifica, a VideoEncoder
devono essere assegnati due oggetti JavaScript:
- Inizializza il dizionario con due funzioni per gestire i chunk codificati e gli errori. Queste funzioni sono definite dallo sviluppatore e non possono essere modificate dopo essere state 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()
restituirà NotSupportedError
se la configurazione non è supportata dal browser. Ti invitiamo a chiamare il metodo statico
VideoEncoder.isConfigSupported()
con la configurazione per verificare in anticipo se
la configurazione è supportata e attendere la relativa 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 i frame tramite il metodo encode()
.
Sia configure()
che encode()
restituiscono immediatamente senza attendere il completamento del lavoro effettivo. Consente di mettere in coda più frame per la codifica contemporaneamente, mentre encodeQueueSize
mostra quante richieste sono in attesa in coda per il completamento delle codifiche precedenti.
Gli errori vengono segnalati lanciando immediatamente un'eccezione, nel caso in cui gli argomenti o l'ordine delle chiamate ai metodi violino il contratto dell'API, oppure chiamando 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 i frame devono essere avvisati 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();
}
}
Finalmente è il momento di completare il codice di codifica scrivendo una funzione che gestisca i blocchi di video codificati quando escono dal codificatore. In genere, questa funzione invia blocchi di dati sulla rete o li muxa in un contenitore 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 la relativa promessa.
await encoder.flush();
Decodifica
La configurazione di un VideoDecoder
è simile a quella eseguita per il VideoEncoder
: quando viene creato il decodificatore, vengono passate due funzioni e i parametri del codec vengono assegnati a configure()
.
L'insieme di parametri del codec varia da un codec all'altro. Ad esempio, il codec H.264 potrebbe richiedere un blob binario di AVCC, a meno che non sia codificato nel cosiddetto formato allegato 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 decodificatore, puoi iniziare a fornirgli oggetti EncodedVideoChunk
.
Per creare un chunk, ti serviranno:
- Un
BufferSource
di dati video codificati - il timestamp di inizio del chunk in microsecondi (tempo medio del primo frame codificato nel chunk)
- il tipo di chunk, uno dei seguenti:
key
se il chunk può essere decodificato indipendentemente dai chunk precedentidelta
se il chunk può essere decodificato solo dopo la decodifica di uno o più chunk precedenti
Inoltre, tutti i chunk emessi dall'encoder sono pronti per il decoder così come sono. Tutto ciò che è stato detto sopra sulla generazione di report sugli errori e sulla natura asincrona degli metodi dell'encoder vale 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 dell'output del decodificatore (handleFrame()
) ritorni rapidamente. Nell'esempio riportato di seguito, viene aggiunto un solo frame alla coda di frame pronti per il rendering.
Il rendering viene eseguito separatamente e prevede due passaggi:
- In attesa del momento giusto per mostrare il frame.
- Disegno del frame sul canvas.
Quando un frame non è più necessario, chiama close()
per rilasciare la memoria sottostante
prima che il garbage collector la raggiunga. In questo modo, verrà ridotta 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 gli sviluppatori
Utilizza il riquadro Media in Chrome DevTools per visualizzare i log multimediali e eseguire il debug di WebCodecs.
Demo
La demo seguente mostra come sono i frame di animazione di una tela:
- acquisito a 25 fps in un
ReadableStream
daMediaStreamTrackProcessor
- trasferito a un worker web
- codificati in formato video H.264
- decodificato di nuovo in una sequenza di fotogrammi video
- e visualizzato sulla seconda tela utilizzando
transferControlToOffscreen()
Altre demo
Dai un'occhiata anche alle altre nostre demo:
- Decodificare le GIF con ImageDecoder
- Acquisire l'input della videocamera in un file
- Riproduzione di file MP4
- Altri esempi
Utilizzo dell'API WebCodecs
Rilevamento di funzionalità
Per verificare il supporto di 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
è falso.
Feedback
Il team di Chrome vuole conoscere la tua esperienza con l'API WebCodecs.
Fornisci informazioni sul design dell'API
C'è qualcosa nell'API che non funziona come previsto? Oppure mancano metodi o proprietà necessari per implementare la tua idea? Hai una domanda o un commento sul modello di sicurezza? Invia una segnalazione relativa alle specifiche nel corrispondente repository GitHub o aggiungi il tuo parere a una segnalazione esistente.
Segnalare un problema con l'implementazione
Hai trovato un bug nell'implementazione di Chrome? Oppure l'implementazione è diversa dalla specifica? Segnala un bug all'indirizzo new.crbug.com.
Assicurati di includere il maggior numero di dettagli possibile, istruzioni semplici per la
riproduzione e inserisci Blink>Media>WebCodecs
nella casella Componenti.
Glitch è ideale per condividere riproduzioni rapide e semplici.
Mostra il supporto per l'API
Intendi 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 quanto sia importante supportarle.
Invia email all'indirizzo media-dev@chromium.org o un tweet
a @ChromiumDev utilizzando l'hashtag
#WebCodecs
e facci sapere dove e come lo utilizzi.
Immagine hero di Denise Jans su Unsplash.