Manipuler les composants du flux vidéo.
Les technologies Web modernes offrent de nombreuses façons de travailler avec des vidéos. Les API Media Stream, Media Recording, Media Source et WebRTC constituent un ensemble d'outils complets pour enregistrer, transférer et lire des flux vidéo. Bien qu'elles permettent de résoudre certaines tâches de haut niveau, ces API ne permettent pas aux programmeurs Web de travailler avec des composants individuels d'un flux vidéo, tels que des frames et des blocs non multiplexés de vidéo ou d'audio encodés. Pour accéder à ces composants de base, les développeurs utilisent WebAssembly pour intégrer des codecs vidéo et audio dans le navigateur. Toutefois, étant donné que les navigateurs modernes sont déjà fournis avec une variété de codecs (souvent accélérés par le matériel), les reconditionner en tant que WebAssembly semble être un gaspillage de ressources humaines et informatiques.
L'API WebCodecs élimine cette inefficacité en permettant aux programmeurs d'utiliser des composants multimédias déjà présents dans le navigateur. Plus spécifiquement :
- Décodeurs vidéo et audio
- Encodeurs vidéo et audio
- Images vidéo brutes
- Décodeurs d'images
L'API WebCodecs est utile pour les applications Web qui nécessitent un contrôle total sur la façon dont le contenu multimédia est traité, comme les éditeurs vidéo, les applications de visioconférence, le streaming vidéo, etc.
Workflow de traitement des vidéos
Les frames sont au cœur du traitement vidéo. Ainsi, dans WebCodecs, la plupart des classes consomment ou produisent des frames. Les encodeurs vidéo convertissent les images en blocs encodés. Les décodeurs vidéo font l'inverse.
De plus, VideoFrame
fonctionne bien avec d'autres API Web en étant un CanvasImageSource
et en ayant un constructeur qui accepte CanvasImageSource
.
Elle peut donc être utilisée dans des fonctions telles que drawImage()
et texImage2D()
. Il peut également être construit à partir de canevas, de bitmaps, d'éléments vidéo et d'autres images vidéo.
L'API WebCodecs fonctionne bien en tandem avec les classes de l'API Insertable Streams, qui connectent WebCodecs aux pistes de flux multimédias.
MediaStreamTrackProcessor
divise les pistes multimédias en images individuelles.MediaStreamTrackGenerator
crée une piste multimédia à partir d'un flux d'images.
WebCodecs et Web Workers
Par conception, l'API WebCodecs effectue tout le travail lourd de manière asynchrone et en dehors du thread principal. Toutefois, comme les rappels de frame et de bloc peuvent souvent être appelés plusieurs fois par seconde, ils peuvent encombrer le thread principal et ainsi rendre le site Web moins réactif. Il est donc préférable de déplacer la gestion des frames individuels et des blocs encodés dans un Web Worker.
Pour vous aider, ReadableStream offre un moyen pratique de transférer automatiquement tous les frames provenant d'une piste multimédia vers le worker. Par exemple, MediaStreamTrackProcessor
peut être utilisé pour obtenir un ReadableStream
pour une piste de flux multimédia provenant de la webcam. Le flux est ensuite transféré à un nœud de calcul Web, où les frames sont lus un par un et mis en file d'attente dans un VideoEncoder
.
Avec HTMLCanvasElement.transferControlToOffscreen
, même le rendu peut être effectué en dehors du thread principal. Toutefois, si tous les outils de haut niveau s'avèrent peu pratiques, VideoFrame
lui-même est transférable et peut être déplacé entre les workers.
WebCodecs en action
Encodage

Canvas
ou d'un ImageBitmap
au réseau ou au stockageTout commence par un VideoFrame
.
Il existe trois façons de construire des images vidéo.
À partir d'une source d'image telle qu'un canevas, un bitmap d'image ou un élément vidéo.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Utiliser
MediaStreamTrackProcessor
pour extraire des frames d'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; }
Créez un frame à partir de sa représentation binaire en pixels dans 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);
Quelle que soit leur origine, les frames peuvent être encodés dans des objets EncodedVideoChunk
avec un VideoEncoder
.
Avant l'encodage, VideoEncoder
doit recevoir deux objets JavaScript :
- Initialisez le dictionnaire avec deux fonctions pour gérer les blocs encodés et les erreurs. Ces fonctions sont définies par le développeur et ne peuvent pas être modifiées après avoir été transmises au constructeur
VideoEncoder
. - Objet de configuration de l'encodeur, qui contient les paramètres du flux vidéo de sortie. Vous pourrez modifier ces paramètres ultérieurement en appelant
configure()
.
La méthode configure()
génère NotSupportedError
si la configuration n'est pas compatible avec le navigateur. Nous vous encourageons à appeler la méthode statique VideoEncoder.isConfigSupported()
avec la configuration pour vérifier au préalable si la configuration est compatible et attendre sa promesse.
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.
}
Une fois l'encodeur configuré, il est prêt à accepter les frames via la méthode encode()
.
configure()
et encode()
renvoient immédiatement une réponse, sans attendre la fin de l'opération. Il permet de mettre en file d'attente plusieurs frames à encoder en même temps, tandis que encodeQueueSize
indique le nombre de requêtes en attente dans la file d'attente pour que les encodages précédents se terminent.
Les erreurs sont signalées soit en générant immédiatement une exception, si les arguments ou l'ordre des appels de méthode enfreignent le contrat d'API, soit en appelant le rappel error()
pour les problèmes rencontrés dans l'implémentation du codec.
Si l'encodage se termine correctement, le rappel output()
est appelé avec un nouveau bloc encodé comme argument.
Autre détail important : les frames doivent être informés lorsqu'ils ne sont plus nécessaires en appelant 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();
}
}
Enfin, il est temps de terminer le code d'encodage en écrivant une fonction qui gère les blocs de vidéo encodée lorsqu'ils sortent de l'encodeur. En règle générale, cette fonction envoie des blocs de données sur le réseau ou les multiplexe dans un conteneur multimédia pour le stockage.
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,
});
}
Si, à un moment donné, vous devez vous assurer que toutes les demandes d'encodage en attente ont été traitées, vous pouvez appeler flush()
et attendre sa promesse.
await encoder.flush();
Décodage

Canvas
ou un ImageBitmap
.La configuration d'un VideoDecoder
est semblable à celle effectuée pour VideoEncoder
: deux fonctions sont transmises lors de la création du décodeur, et les paramètres du codec sont fournis à configure()
.
L'ensemble des paramètres de codec varie d'un codec à l'autre. Par exemple, le codec H.264 peut nécessiter un blob binaire d'AVCC, sauf s'il est encodé au format dit Annexe 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.
}
Une fois le décodeur initialisé, vous pouvez commencer à l'alimenter avec des objets EncodedVideoChunk
.
Pour créer un bloc, vous aurez besoin des éléments suivants :
BufferSource
de données vidéo encodées- Code temporel de début du bloc en microsecondes (temps média du premier frame encodé du bloc)
- Type du bloc, l'un des suivants :
key
si le bloc peut être décodé indépendamment des blocs précédentsdelta
si le bloc ne peut être décodé qu'après le décodage d'un ou plusieurs blocs précédents
De plus, tous les blocs émis par l'encodeur sont prêts pour le décodeur tels quels. Tout ce qui a été dit ci-dessus sur le signalement des erreurs et la nature asynchrone des méthodes de l'encodeur s'applique également aux décodeurs.
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();
Il est maintenant temps de montrer comment un frame fraîchement décodé peut être affiché sur la page. Il est préférable de s'assurer que le rappel de sortie du décodeur (handleFrame()
) renvoie rapidement une réponse. Dans l'exemple ci-dessous, il ajoute uniquement un frame à la file d'attente des frames prêts à être rendus.
Le rendu se fait séparément et comprend deux étapes :
- En attente du bon moment pour afficher le frame.
- Dessin du cadre sur le canevas.
Une fois qu'un frame n'est plus nécessaire, appelez close()
pour libérer la mémoire sous-jacente avant que le garbage collector ne s'en charge. Cela réduira la quantité moyenne de mémoire utilisée par l'application 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);
}
Conseils pour les développeurs
Utilisez le panneau "Média" des outils pour les développeurs Chrome afin d'afficher les journaux multimédias et de déboguer WebCodecs.

Démo
La démonstration montre comment les frames d'animation d'un canevas sont :
- enregistré à 25 FPS dans un
ReadableStream
parMediaStreamTrackProcessor
- transféré à un Web Worker
- encodée au format vidéo H.264
- décodée à nouveau en une séquence d'images vidéo.
- et affiché sur le deuxième canevas à l'aide de
transferControlToOffscreen()
.
Autres démonstrations
Regardez aussi nos autres démos :
- Décoder des GIF avec ImageDecoder
- Capturer la vidéo de la caméra dans un fichier
- Lecture MP4
- Autres exemples
Utiliser l'API WebCodecs
Détection de caractéristiques
Pour vérifier la compatibilité avec WebCodecs :
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
N'oubliez pas que l'API WebCodecs n'est disponible que dans les contextes sécurisés. La détection échouera donc si self.isSecureContext
est défini sur "false".
Commentaires
L'équipe Chrome souhaite connaître votre avis sur l'API WebCodecs.
Parlez-nous de la conception de l'API
Y a-t-il quelque chose dans l'API qui ne fonctionne pas comme prévu ? Ou bien manquent-ils des méthodes ou des propriétés dont vous avez besoin pour mettre en œuvre votre idée ? Vous avez une question ou un commentaire sur le modèle de sécurité ? Signalez un problème lié aux spécifications dans le dépôt GitHub correspondant ou ajoutez vos commentaires à un problème existant.
Signaler un problème d'implémentation
Avez-vous trouvé un bug dans l'implémentation de Chrome ? Ou l'implémentation est-elle différente de la spécification ? Signalez un bug sur new.crbug.com. Veillez à inclure autant de détails que possible, des instructions simples pour le reproduire et saisissez Blink>Media>WebCodecs
dans la zone Composants.
Soutenir l'API
Comptez-vous utiliser l'API WebCodecs ? Votre soutien public aide l'équipe Chrome à hiérarchiser les fonctionnalités et montre aux autres fournisseurs de navigateurs à quel point il est essentiel de les prendre en charge.
Envoyez un e-mail à media-dev@chromium.org ou un tweet à @ChromiumDev avec le hashtag #WebCodecs
pour nous indiquer où et comment vous l'utilisez.
Image héros par Denise Jans sur Unsplash.