Manipulation des composants du flux vidéo
Les technologies Web modernes offrent de nombreuses possibilités pour travailler avec la vidéo. API Media Stream API Media Recording, API Media Source, et l'API WebRTC à un ensemble complet d'outils permettant d'enregistrer, de transférer et de lire des flux vidéo. Bien qu'elles résolvent certaines tâches de haut niveau, ces API ne permettent pas les programmeurs travaillent avec des composants individuels d'un flux vidéo, tels que des images et des fragments de vidéo ou d'audio non muxés. Pour obtenir un accès de bas niveau à ces composants de base, les développeurs utilisent WebAssembly pour intégrer les codecs vidéo et audio dans le navigateur. Mais étant donné que les navigateurs récents sont déjà fournis avec divers codecs (qui sont souvent accéléré par le matériel), les reconditionner, car WebAssembly semble être un gaspillage les 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 le mode de traitement du contenu multimédia : montage vidéo, visioconférence, le streaming, etc.
Workflow de traitement de vidéos
Les images sont la pièce maîtresse du traitement vidéo. Dans WebCodecs, la plupart des classes de consommer ou de produire des frames. Les encodeurs vidéo convertissent les images en images fragments. Les décodeurs vidéo font le contraire.
De plus, VideoFrame
fonctionne bien avec les autres API Web, car il est 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 créé à 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 les WebCodecs à des pistes de flux multimédia.
MediaStreamTrackProcessor
divise les pistes multimédias dans des images individuelles.MediaStreamTrackGenerator
crée une piste multimédia à partir d'un flux d'images.
WebCodecs et Web workers
Par défaut, l'API WebCodecs effectue le plus gros du travail de manière asynchrone et en dehors du thread principal. Mais comme les rappels de frame et de fragment peuvent souvent être appelés plusieurs fois par seconde, elles pourraient encombrer le thread principal et rendre ainsi le site web moins réactif. Par conséquent, il est préférable de déplacer le traitement des frames individuels et des fragments codés dans un Web Worker.
Pour vous aider, ReadableStream
offre un moyen pratique de transférer automatiquement toutes les images provenant d'un média
vers le nœud de calcul. Par exemple, MediaStreamTrackProcessor
peut être utilisé pour obtenir
ReadableStream
pour un titre de flux multimédia provenant de la webcam Après cela
Le flux est transféré vers un nœud de calcul Web où les trames sont lues une par une et mises en file d'attente
en VideoEncoder
.
Avec HTMLCanvasElement.transferControlToOffscreen
, le rendu peut même être effectué en dehors du thread principal. Mais si tous les outils
de haut niveau sont devenus
gênante, la VideoFrame
elle-même est transférable et peut être
déplacées entre les nœuds de calcul.
WebCodecs en action
Encodage
<ph type="x-smartling-placeholder">Tout commence par un VideoFrame
.
Il existe trois façons de construire des images vidéo.
À partir d'une source d'image (canevas, bitmap d'une image ou é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 cadres 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 une image à partir de sa représentation de pixels binaire dans un élément
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 images peuvent être encodées dans
Objets EncodedVideoChunk
avec un VideoEncoder
.
Avant l'encodage, vous devez attribuer deux objets JavaScript à VideoEncoder
:
- un dictionnaire Init avec deux fonctions pour gérer les fragments encodés et
les erreurs. Ces fonctions sont définies par le développeur et ne peuvent plus être modifiées
elles sont transmises au constructeur
VideoEncoder
. - Objet de configuration d'encodeur, qui contient les paramètres de la sortie
flux vidéo. Vous pouvez modifier ces paramètres ultérieurement en appelant
configure()
.
La méthode configure()
génère NotSupportedError
si la configuration n'est pas
compatibles avec le navigateur. Nous vous conseillons d'appeler la méthode statique
VideoEncoder.isConfigSupported()
par la configuration pour vérifier au préalable si
la configuration est prise en charge
et attendre ce que vous avez promis.
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 tous deux immédiatement un résultat, sans attendre l'événement
le travail réel à accomplir. Il permet de mettre plusieurs trames en file d'attente pour l'encodage au niveau
simultanément, tandis que encodeQueueSize
indique le nombre de requêtes en attente dans la file d'attente
pour terminer l'encodage précédent.
Les erreurs sont signalées en insérant immédiatement une exception, au cas où les arguments
ou si l'ordre des appels de méthode enfreint le contrat d'API, ou en appelant error()
rappel pour les problèmes rencontrés dans l'implémentation du codec.
Si l'encodage aboutit, output()
est appelé avec un nouveau bloc encodé en tant qu'argument.
Un autre détail important ici est que les images
doivent être communiquées lorsqu’elles ne sont pas
n'est plus nécessaire 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 l'encodage du code en écrivant une fonction qui gère morceaux de vidéo encodées à mesure qu'ils sortent de l'encodeur. Habituellement, cette fonction envoie des blocs de données sur le réseau ou les multiple sur un média à des fins de 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
terminée, vous pouvez appeler flush()
et attendre la promesse.
await encoder.flush();
Décodage
<ph type="x-smartling-placeholder">La configuration d'un VideoDecoder
est semblable à ce qui a été fait pour
VideoEncoder
: deux fonctions sont transmises lors de la création du décodeur, et le codec
sont transmis à configure()
.
L'ensemble des paramètres du codec varie d'un codec à l'autre. Par exemple, le codec H.264
peut avoir besoin d'un blob binaire.
de l'AVCC, sauf s'il est encodé au format de l'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 fragment, vous avez besoin des éléments suivants:
BufferSource
de données vidéo encodées- L'horodatage de début du fragment en microsecondes (heure multimédia de la première trame encodée dans le fragment)
- du type de segment, l'un des suivants:
<ph type="x-smartling-placeholder">
- </ph>
key
si le fragment peut être décodé indépendamment des fragments précédentsdelta
si le fragment ne peut être décodé qu'après qu'un ou plusieurs fragments précédents ont été décodés
De plus, tous les fragments émis par l'encodeur sont prêts pour le décodeur en l'état. Toutes les informations ci-dessus concernant Error Reporting et la nature asynchrone des méthodes d'encodeur sont également valables 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 cadre fraîchement décodé peut s'afficher sur la page. Il est
il est préférable de s'assurer que le rappel de sortie du décodeur (handleFrame()
)
revient rapidement. Dans l'exemple ci-dessous, elle n'ajoute qu'une image à la file d'attente de
images prêtes pour le rendu.
L'affichage se déroule séparément et comprend deux étapes:
- En attente du bon moment pour afficher le frame.
- Dessiner le cadre sur le canevas.
Lorsqu'un frame n'est plus nécessaire, appelez close()
pour libérer la mémoire sous-jacente
avant que le récupérateur de mémoire n'y parvienne, cela réduira la quantité moyenne
la 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 de développement
Utiliser le panneau multimédia dans les outils pour les développeurs Chrome afin d'afficher les journaux multimédias et de déboguer les WebCodecs.
<ph type="x-smartling-placeholder">Démo
La démonstration ci-dessous montre le rendu des images d'animation à partir d'un canevas:
- filmée à 25 images par seconde dans une
ReadableStream
parMediaStreamTrackProcessor
- transférée à un nœud de calcul Web
- codé au format vidéo H.264
- décodée en une séquence d'images vidéo
- et affichés sur le deuxième canevas en utilisant
transferControlToOffscreen()
Autres démonstrations
Consultez également nos autres démonstrations:
- Décoder les GIF avec ImageDecoder
- Enregistrer l'entrée de l'appareil photo dans un fichier
- Lecture MP4
- Autres exemples
Utiliser l'API WebCodecs
Détection de caractéristiques
Pour vérifier la compatibilité avec les 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 échoue donc si la valeur de self.isSecureContext
est "false".
Commentaires
L'équipe Chrome aimerait connaître votre avis sur l'API WebCodecs.
Présentez-nous la conception de l'API
Y a-t-il un aspect de l'API qui ne fonctionne pas comme prévu ? Ou sont s'il manque des méthodes ou des propriétés dont vous avez besoin pour mettre en œuvre votre idée ? Avoir un une question ou un commentaire sur le modèle de sécurité ? Signaler un problème de spécification sur le dépôt GitHub correspondant, ou ajoutez vos réflexions sur un problème existant.
Signaler un problème d'implémentation
Avez-vous détecté un bug dans l'implémentation de Chrome ? Ou l'implémentation
différent des spécifications ? Signalez un bug sur new.crbug.com.
Veillez à inclure autant de détails que possible, des instructions simples pour
reproduire, puis saisissez Blink>Media>WebCodecs
dans la zone Composants.
Glitch est idéal pour partager des répétitions rapidement et facilement.
Apportez votre soutien à l'API
Prévoyez-vous d'utiliser l'API WebCodecs ? Votre soutien public aide le l'équipe Chrome à prioriser les fonctionnalités et à montrer aux autres fournisseurs de navigateurs à quel point est de les soutenir.
Envoyez des e-mails à media-dev@chromium.org ou un tweet.
à @ChromiumDev en utilisant le hashtag
#WebCodecs
et n'hésitez pas à nous dire où et comment vous l'utilisez.
Image héros de Denise Jan dans Unsplash.