Manipuler les composants d'un flux vidéo
Les technologies Web modernes offrent de nombreuses façons de travailler avec la vidéo. Les API Media Stream, Media Recording, Media Source, et WebRTC constituent un ensemble d'outils complet pour enregistrer, transférer et lire des flux vidéo. Bien qu'elles résolvent 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 images et des blocs non multiplexés de vidéo ou d'audio encodés. Pour accéder à ces composants de base à bas niveau, les développeurs utilisent WebAssembly pour intégrer des codecs vidéo et audio au navigateur. Toutefois, étant donné que les navigateurs modernes sont déjà fournis avec différents 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 visioconférences, le streaming vidéo, etc.
Workflow de traitement vidéo
Les images sont au cœur du traitement vidéo. Ainsi, dans WebCodecs, la plupart des classes consomment ou produisent des images. Les encodeurs vidéo convertissent les images en blocs encodés. Les décodeurs vidéo font le contraire.
VideoFrame fonctionne également bien avec d'autres API Web en étant un CanvasImageSource et en ayant un constructeur qui accepte CanvasImageSource.
Il peut donc être utilisé dans des fonctions telles que drawImage() ettexImage2D(). 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édia.
MediaStreamTrackProcessordivise les pistes multimédias en images individuelles.MediaStreamTrackGeneratorcrée une piste multimédia à partir d'un flux d'images.
WebCodecs et nœuds de calcul Web
Par conception, l'API WebCodecs effectue toutes les tâches lourdes de manière asynchrone et en dehors du thread principal. Toutefois, étant donné que les rappels d'images et de blocs peuvent souvent être appelés plusieurs fois par seconde, ils peuvent encombrer le thread principal et rendre ainsi le site Web moins réactif. Il est donc préférable de déplacer la gestion des images individuelles et des blocs encodés vers un nœud de calcul Web.
Pour ce faire, ReadableStream
offre un moyen pratique de transférer automatiquement toutes les images provenant d'une piste multimédia
vers le nœud de calcul. 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é vers un nœud de calcul Web où les images sont lues une par une et mises en file d'attente dans un VideoEncoder.
Avec HTMLCanvasElement.transferControlToOffscreen, le rendu peut même ê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 nœuds de calcul.
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 });Utilisez
MediaStreamTrackProcessorpour extraire des images d'unMediaStreamTrackconst 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 une image à partir de sa représentation binaire de pixels dans un
BufferSourceconst 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 images peuvent être encodées dans des objets EncodedVideoChunk avec un VideoEncoder.
Avant l'encodage, VideoEncoder doit recevoir deux objets JavaScript :
- Dictionnaire d'initialisation 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 pouvez modifier ces paramètres ultérieurement en appelant
configure().
La méthode configure() génère une erreur NotSupportedError si la configuration n'est pas compatible avec le navigateur. Nous vous recommandons d'appeler la méthode statique VideoEncoder.isConfigSupported() avec la configuration pour vérifier au préalable si elle 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 images via la méthode encode().
configure() et encode() renvoient immédiatement une réponse, sans attendre la fin de l'opération en cours. Cela permet de mettre en file d'attente plusieurs images pour l'encodage 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.
Un autre détail important est que les images doivent être informées lorsqu'elles 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 requêtes d'encodage en attente ont été effectué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 le VideoEncoder : deux fonctions sont transmises lors de la création du décodeur, et les paramètres du codec sont transmis à configure().
L'ensemble des paramètres du 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 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.
}
Une fois le décodeur initialisé, vous pouvez commencer à l'alimenter avec des objets EncodedVideoChunk.
Pour créer un bloc, vous devez disposer des éléments suivants :
- Un
BufferSourcede données vidéo encodées - L'horodatage de début du bloc en microsecondes (temps multimédia de la première image encodée du bloc)
- Le type de bloc, l'un des suivants :
keysi le bloc peut être décodé indépendamment des blocs précédentsdeltasi le bloc ne peut être décodé qu'après le décodage d'un ou de 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 concernant le signalement des erreurs et la nature asynchrone des méthodes de l'encodeur est également vrai pour les 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 une image fraîchement décodée peut être affichée 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 n'ajoute qu'une image à la file d'attente des images prêtes pour le rendu.
Le rendu se produit séparément et comporte deux étapes :
- Attendre le bon moment pour afficher l'image.
- Dessiner l'image sur le canevas.
Une fois qu'une image n'est plus nécessaire, appelez close() pour libérer la mémoire sous-jacente avant que le récupérateur de mémoire 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 "Media" des outils pour les développeurs Chrome pour afficher les journaux multimédias et déboguer WebCodecs.
Démo
La démo montre comment les images d'animation d'un canevas sont :
- capturées à 25 images par seconde dans un
ReadableStreamparMediaStreamTrackProcessor; - transférées vers un nœud de calcul Web ;
- encodées au format vidéo H.264 ;
- décodées à nouveau en une séquence d'images vidéo ;
- et affichées sur le deuxième canevas à l'aide de
transferControlToOffscreen().
Autres démonstrations
Consultez également nos autres démos :
- Décodage de GIF avec ImageDecoder
- Capture de l'entrée de la caméra dans un fichier
- Lecture MP4
- Autres exemples
Utiliser l'API WebCodecs
Détection de fonctionnalités
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 des contextes sécurisés,
la détection échouera donc si self.isSecureContext est défini sur "false".
En savoir plus
Si vous débutez avec WebCodecs, Principes de base de WebCodecs propose des articles détaillés avec de nombreux exemples pour vous aider à en savoir plus.
Commentaires
L'équipe Chrome souhaite connaître votre expérience avec l'API WebCodecs.
Parlez-nous de la conception de l'API
Un élément de l'API ne fonctionne-t-il pas comme prévu ? Ou manque-t-il 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 de spécification dans le dépôt GitHub correspondant ou ajoutez vos commentaires à un problème existant.
Signaler un problème lié à l'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
la reproduction et saisissez Blink>Media>WebCodecs dans la zone Components (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.