Manipulation des composants du flux vidéo
Les technologies Web modernes offrent de nombreuses possibilités de travailler avec la vidéo. L'API Media Stream, l'API Media Recording, l'API Media Source et l'API 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 les composants individuels d'un flux vidéo, tels que les images et les segments de vidéo ou d'audio encodés non multiplexés. Pour obtenir un accès de bas niveau à ces composants de base, les développeurs ont utilisé WebAssembly pour intégrer des codecs vidéo et audio dans le navigateur. Toutefois, étant donné que les navigateurs modernes sont déjà livrés avec divers codecs (qui sont 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 les 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 des vidéos
Les images sont au cœur du traitement vidéo. Par conséquent, 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
est également compatible avec d'autres API Web en tant que CanvasImageSource
et dispose d'un constructeur qui accepte CanvasImageSource
.
Il peut donc être utilisé 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 trames individuelles.MediaStreamTrackGenerator
crée une piste multimédia à partir d'un flux d'images.
WebCodecs et Web Workers
Par conception, l'API WebCodecs effectue toutes les tâches lourdes de manière asynchrone et en dehors du thread principal. Toutefois, comme les rappels de frame et de chunk peuvent souvent être appelés plusieurs fois par seconde, ils peuvent encombrer le thread principal et rendre le site Web moins réactif. Il est donc préférable de déplacer la gestion des frames individuels et des segments encodés dans un nœud de calcul Web.
Pour vous aider, ReadableStream fournit un moyen pratique de transférer automatiquement tous les frames provenant d'un canal multimédia vers le worker. Par exemple, MediaStreamTrackProcessor
peut être utilisé pour obtenir un ReadableStream
pour un flux multimédia provenant de la webcam. Ensuite, le flux est transféré vers 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
, le rendu peut même être effectué en dehors du thread principal. Toutefois, si tous les outils de haut niveau se sont révélés peu pratiques, VideoFrame
lui-même est transférable et peut être déplacé entre les travailleurs.
WebCodecs en action
Encodage
Tout commence par un VideoFrame
.
Il existe trois façons de créer 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éer un frame à partir de sa représentation binaire des 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 cadres peuvent être encodés dans des objets EncodedVideoChunk
avec un VideoEncoder
.
Avant l'encodage, VideoEncoder
doit recevoir deux objets JavaScript:
- Initialisation du 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 une fois 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 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 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 des images via la méthode encode()
.
configure()
et encode()
renvoient immédiatement une réponse, sans attendre la fin du travail réel. Il permet à plusieurs frames d'être mis en file d'attente 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 soient terminés.
Les erreurs sont signalées en générant immédiatement une exception, si les arguments ou l'ordre des appels de méthode ne respectent pas le contrat de l'API, ou en appelant le rappel error()
en cas de problème rencontré lors de l'implémentation du codec.
Si l'encodage aboutit, le rappel output()
est appelé avec un nouveau bloc encodé en tant qu'argument.
Un autre détail important est que les frames doivent être informés de leur inutilité 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 segments de vidéo encodée à mesure qu'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 les stocker.
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 vous devez vous assurer qu'à un moment donné toutes les requêtes d'encodage en attente ont été traitées, vous pouvez appeler flush()
et attendre sa promesse.
await encoder.flush();
Décodage
La configuration d'un VideoDecoder
est semblable à celle du 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 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 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 à le nourrir avec des objets EncodedVideoChunk
.
Pour créer un bloc, vous avez besoin des éléments suivants:
BufferSource
de données vidéo encodées- Code temporel de début du segment en microsecondes (code temporel du premier frame encodé du segment)
- le type de bloc, l'un des suivants :
key
si le segment peut être décodé indépendamment des segments précédentsdelta
si le segment ne peut être décodé qu'après le décodage d'un ou de plusieurs segments précédents
De plus, tous les segments é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 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 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. Dans l'exemple ci-dessous, il n'ajoute qu'un seul frame à la file d'attente des frames prêts à être affichés.
Le rendu se produit séparément et comprend deux étapes:
- Attend que le moment soit opportun 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 occupe. 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 multimédia dans les outils pour les développeurs Chrome pour afficher les journaux multimédias et déboguer WebCodecs.
Démo
La démonstration ci-dessous montre comment les frames d'animation d'un canevas sont:
- capturé à 25 FPS dans un
ReadableStream
parMediaStreamTrackProcessor
- transféré à un worker Web ;
- encodé au format vidéo H.264 ;
- décodé à nouveau en séquence d'images vidéo ;
- et affiché sur le deuxième canevas à l'aide de
transferControlToOffscreen()
Autres démonstrations
Découvrez également nos autres démonstrations:
- Décoder des GIF avec ImageDecoder
- Capturer l'entrée de l'appareil photo 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 les contextes sécurisés. Par conséquent, la détection échouera si self.isSecureContext
est faux.
Commentaires
L'équipe Chrome souhaite connaître votre expérience avec l'API WebCodecs.
Parlez-nous de la conception de l'API
L'API ne fonctionne-t-elle pas comme prévu ? Ou manque-t-il des méthodes ou des propriétés dont vous avez besoin pour implémenter 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 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 reproduire le problème et saisissez Blink>Media>WebCodecs
dans le champ Composants.
Glitch est idéal pour partager des reproductions rapidement et facilement.
Afficher la compatibilité avec 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 en utilisant le hashtag #WebCodecs
et indiquez-nous où et comment vous l'utilisez.
Image héros par Denise Jans sur Unsplash.