Traitement des vidéos avec WebCodecs

Manipulation des composants d'un flux vidéo

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

Les technologies Web modernes offrent de nombreuses façons de travailler avec la vidéo. L'API Media Stream, l'API Media Recording, l'API Media Source et l'API WebRTC s'ajoutent à un ensemble d'outils complet permettant d'enregistrer, de transférer et de lire des flux vidéo. Lors de la résolution de 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 fragments non multiplex de vidéo ou d'audio encodés. Pour obtenir un accès de bas niveau à ces composants de base, les développeurs utilisent WebAssembly afin d'intégrer des codecs vidéo et audio dans le navigateur. Toutefois, étant donné que les navigateurs modernes sont déjà fournis avec divers codecs (souvent accélérés par le matériel), les reconditionnement au fur et à mesure 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. Notamment :

  • 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 traitement du contenu multimédia, comme les éditeurs vidéo, la visioconférence, le streaming vidéo, etc.

Workflow de traitement des vidéos

Les images sont la pièce maîtresse du traitement d'une vidéo. Ainsi, dans WebCodecs, la plupart des classes consomment ou produisent des frames. Les encodeurs vidéo convertissent les images en fragments encodés. Les décodeurs vidéo font le contraire.

De plus, VideoFrame fonctionne parfaitement avec les autres API Web, car il est un CanvasImageSource et possède un constructeur qui accepte CanvasImageSource. Elle peut donc être utilisée 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 binôme avec les classes de l'API Insertable Streams qui associent les WebCodecs aux pistes de flux multimédia.

  • MediaStreamTrackProcessor divise les pistes multimédias en frames individuels.
  • MediaStreamTrackGenerator crée une piste multimédia à partir d'un flux d'images.

Codecs Web et workers Web

De par sa conception, l'API WebCodecs effectue toutes les tâches complexes de manière asynchrone et en dehors du thread principal. Toutefois, comme les rappels de frame et de fragment peuvent souvent être appelés plusieurs fois par seconde, ils peuvent encombrer le thread principal et rendre le site Web moins réactif. Par conséquent, il est préférable de transférer la gestion des frames individuels et des fragments encodés dans un nœud de calcul Web.

Pour vous aider, ReadableStream offre un moyen pratique de transférer automatiquement toutes les trames provenant d'une piste multimédia vers le nœud de calcul. Par exemple, MediaStreamTrackProcessor permet d'obtenir un ReadableStream pour une piste de flux multimédia provenant de la webcam. Ensuite, le flux est transféré à un nœud de calcul Web, où les trames sont lues une par une et placées en file d'attente dans un VideoEncoder.

Avec HTMLCanvasElement.transferControlToOffscreen, il est possible d'effectuer un rendu uniforme en dehors du thread principal. Toutefois, si tous les outils de haut niveau se révèlent gênants, VideoFrame lui-même est transférable et peut être déplacé d'un nœud de calcul à un autre.

WebCodecs en action

Encodage

Chemin d'un canevas ou d'un ImageBitmap vers le réseau ou l'espace de stockage
Chemin d'accès d'un Canvas ou d'un ImageBitmap au réseau ou à l'espace de stockage

Tout commence par 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 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 binaire en pixels 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 trames peuvent être encodées en objets EncodedVideoChunk avec un élément VideoEncoder.

Avant l'encodage, VideoEncoder doit recevoir deux objets JavaScript:

  • Dictionnaire d'initialisation avec deux fonctions pour la gestion des fragments encodés et des 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 exception 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 trames via la méthode encode(). configure() et encode() renvoient immédiatement un résultat sans attendre la fin du travail réel. Elle permet de mettre plusieurs images en file d'attente pour encodage, tandis que encodeQueueSize indique le nombre de requêtes en attente dans la file d'attente des encodages précédents. 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 d'API, ou en appelant le rappel error() en cas de problèmes rencontrés dans l'implémentation du codec. Si l'encodage aboutit, le rappel output() est appelé avec un nouveau fragment encodé en tant qu'argument. Autre détail important : lorsque vous n'avez plus besoin des frames, vous devez appeler 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 les fragments de vidéo encodées lorsqu'ils sortent de l'encodeur. Généralement, cette fonction envoie des fragments de données sur le réseau ou les muxe 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 que 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

Chemin du réseau ou de l&#39;espace de stockage vers un canevas ou un ImageBitmap.
Chemin d'accès du réseau ou de 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 les paramètres de codec sont transmis à configure().

L'ensemble des paramètres du codec varie d'un codec à un autre. Par exemple, le codec H.264 peut nécessiter un blob binaire AVCC, à moins qu'il ne soit 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
  • Code temporel de début du fragment en microsecondes (temps média de la première image encodée du fragment)
  • le type de fragment, l'un des suivants :
    • 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 le décodage d'un ou de plusieurs fragments précédents

De plus, tous les blocs émis par l'encodeur sont prêts pour le décodeur en l'état. Tout ce qui a été dit ci-dessus concernant les rapports d'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();

Voyons maintenant comment afficher un frame fraîchement décodé sur la page. Il est préférable de s'assurer que le rappel de sortie du décodeur (handleFrame()) est rapidement renvoyé. Dans l'exemple ci-dessous, elle n'ajoute qu'une image à la file d'attente des images prêtes pour le rendu. Le rendu s'effectue séparément et se déroule en deux étapes:

  1. Attendre le bon moment pour afficher le cadre.
  2. Dessin du cadre sur la toile

Une fois qu'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 ait accès. 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 des outils pour les développeurs Chrome pour afficher les journaux multimédias et déboguer les WebCodecs.

Capture d&#39;écran du panneau &quot;Media&quot; (Médias) pour le débogage de WebCodecs
Panneau multimédia dans les outils pour les développeurs Chrome pour le débogage des codecs Web.

Démonstration

La démonstration ci-dessous montre comment fonctionnent les frames d'animation à partir d'un canevas:

  • capture à 25 ips en ReadableStream par MediaStreamTrackProcessor
  • transféré à un collaborateur Web
  • codé au format vidéo H.264
  • décodées à nouveau en une séquence d'images vidéo
  • et affichée sur le deuxième canevas à l'aide de transferControlToOffscreen()

Autres démonstrations

Consultez également nos autres démonstrations:

Utiliser l'API WebCodecs

Détection de fonctionnalités

Pour vérifier la compatibilité de 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".

Commentaires

L'équipe Chrome souhaite connaître votre avis sur votre expérience avec l'API WebCodecs.

Décrivez-nous la conception de l'API.

Y a-t-il quelque chose dans l'API qui ne fonctionne 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 d'implémentation

Avez-vous détecté un bug dans l'implémentation de Chrome ? Ou l'implémentation est-elle différente des spécifications ? Signalez un bug sur new.crbug.com. Veillez à inclure autant de détails que possible, ainsi que des instructions simples pour reproduire le bug, puis saisissez Blink>Media>WebCodecs dans la zone Composants. Glitch est idéal pour partager des reproductions simples et rapides.

Afficher la compatibilité avec l'API

Comptez-vous utiliser l'API WebCodecs ? Votre assistance publique 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 des e-mails à media-dev@chromium.org ou envoyez un tweet à @ChromiumDev avec le hashtag #WebCodecs, et indiquez-nous où et comment vous l'utilisez.

Hero image (Image héros) de Denise Jans sur Unsplash.