Traitement des vidéos avec WebCodecs

Manipulation des composants du flux vidéo

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

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">
</ph> Chemin d&#39;accès depuis un canevas ou un ImageBitmap vers le réseau ou l&#39;espace de stockage
Chemin d'un Canvas ou d'un ImageBitmap vers le réseau ou l'espace de stockage

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'un MediaStreamTrack

    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">
</ph> Chemin d&#39;accès depuis le réseau ou l&#39;espace de stockage vers un canevas ou un ImageBitmap.
Chemin d'accès depuis le réseau ou l'espace de stockage vers un Canvas ou un ImageBitmap.

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édents
    • delta 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:

  1. En attente du bon moment pour afficher le frame.
  2. 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">
</ph> Capture d&#39;écran du panneau &quot;Multimédia&quot; pour le débogage des WebCodecs
Media Panel dans les outils pour les développeurs Chrome pour déboguer les WebCodecs.

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 par MediaStreamTrackProcessor
  • 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:

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.