Extensions de source multimédia

François Beaufort
François Beaufort
Joe Medley
Joe Medley

Media Source Extensions (MSE) est une API JavaScript qui vous permet de créer des flux à lire à partir de segments audio ou vidéo. Bien que cet article ne soit pas abordé dans cet article, il est nécessaire de comprendre la MSE si vous souhaitez intégrer à votre site des vidéos qui:

  • Le streaming adaptatif, c'est-à-dire l'adaptation aux fonctionnalités de l'appareil et aux conditions du réseau
  • Insertions adaptatives, par exemple l'insertion d'annonces
  • Décalage temporel
  • Contrôle des performances et de la taille de téléchargement
Flux de données MSE de base
Figure 1: Flux de données MSE de base

On peut presque considérer les MSE comme une chaîne. Comme le montre la figure, plusieurs couches se trouvent entre le fichier téléchargé et les éléments multimédias.

  • Un élément <audio> ou <video> pour lire le contenu multimédia
  • Une instance MediaSource avec un SourceBuffer pour alimenter l'élément multimédia.
  • Un appel fetch() ou XHR pour récupérer des données multimédias dans un objet Response.
  • Appel à Response.arrayBuffer() pour nourrir MediaSource.SourceBuffer.

En pratique, la chaîne ressemble à ceci:

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Si vous pouvez trier les explications jusqu'à présent, n'hésitez pas à arrêter de lire maintenant. Si vous souhaitez une explication plus détaillée, lisez la suite. Je vais parcourir cette chaîne en créant un exemple MSE de base. Chacune des étapes de compilation ajoute du code à l'étape précédente.

Remarque concernant la clarté

Cet article vous dira-t-il tout ce que vous devez savoir sur la lecture de contenus multimédias sur une page Web ? Non. Il est uniquement destiné à vous aider à comprendre un code plus compliqué que vous pourriez trouver ailleurs. Par souci de clarté, ce document simplifie et exclut de nombreuses choses. Nous pensons que nous pouvons nous en sortir, car nous vous recommandons également d'utiliser une bibliothèque telle que Google Shaka Player. Je le simplifiais délibérément tout au long de ce document.

Quelques points non abordés

Ici, sans ordre particulier, voici quelques points que nous n'aborderons pas.

  • Commandes de lecture Elles sont sans frais grâce à l'utilisation des éléments HTML5 <audio> et <video>.
  • Traiter les erreurs :

À utiliser dans des environnements de production

Voici quelques recommandations pour une utilisation en production d'API liées à MSE:

  • Avant d'effectuer des appels sur ces API, gérez les événements d'erreur ou les exceptions d'API, puis vérifiez HTMLMediaElement.readyState et MediaSource.readyState. Ces valeurs peuvent changer avant la diffusion des événements associés.
  • Assurez-vous que les appels appendBuffer() et remove() précédents ne sont pas encore en cours en vérifiant la valeur booléenne SourceBuffer.updating avant de mettre à jour les mode, timestampOffset, appendWindowStart et appendWindowEnd de SourceBuffer, ou d'appeler appendBuffer() ou remove() sur SourceBuffer.
  • Pour toutes les instances SourceBuffer ajoutées à votre MediaSource, assurez-vous qu'aucune de leurs valeurs updating n'est vraie avant d'appeler MediaSource.endOfStream() ou de mettre à jour le MediaSource.duration.
  • Si la valeur MediaSource.readyState est définie sur ended, les appels tels que appendBuffer() et remove(), ou la définition de SourceBuffer.mode ou SourceBuffer.timestampOffset font passer cette valeur à open. Cela signifie que vous devez être prêt à gérer plusieurs événements sourceopen.
  • Lors de la gestion des événements HTMLMediaElement error, le contenu de MediaError.message peut être utile pour déterminer la cause première de la défaillance, en particulier pour les erreurs difficiles à reproduire dans les environnements de test.

Associer une instance MediaSource à un élément multimédia

À l'heure actuelle, comme pour beaucoup d'éléments dans le développement Web, vous commencez par la détection des caractéristiques. Ensuite, récupérez un élément multimédia, à savoir un élément <audio> ou <video>. Enfin, créez une instance de MediaSource. Il est transformé en URL et transmis à l'attribut source de l'élément multimédia.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  // Is the MediaSource instance ready?
} else {
  console.log('The Media Source Extensions API is not supported.');
}
Attribut source sous forme de blob
Figure 1: Attribut source sous forme de blob

Le fait qu'un objet MediaSource puisse être transmis à un attribut src peut sembler un peu bizarre. Il s'agit généralement de chaînes, mais il peut également s'agir de blobs. Si vous inspectez une page contenant des éléments multimédias intégrés et examinez son élément multimédia, vous verrez ce que je veux dire.

L'instance MediaSource est-elle prête ?

URL.createObjectURL() est lui-même synchrone. Cependant, il traite le rattachement de manière asynchrone. Cela entraîne un léger retard avant que vous ne puissiez faire quoi que ce soit avec l'instance MediaSource. Heureusement, il existe des moyens de le vérifier. La méthode la plus simple consiste à utiliser une propriété MediaSource appelée readyState. La propriété readyState décrit la relation entre une instance MediaSource et un élément multimédia. Il peut prendre l'une des valeurs suivantes:

  • closed : l'instance MediaSource n'est pas associée à un élément multimédia.
  • open : l'instance MediaSource est associée à un élément multimédia et peut recevoir des données ou en recevoir.
  • ended : l'instance MediaSource est associée à un élément multimédia et toutes ses données ont été transmises à cet élément.

Interroger directement ces options peut avoir un impact négatif sur les performances. Heureusement, MediaSource déclenche également des événements lorsque readyState change, en particulier sourceopen, sourceclosed et sourceended. Pour l'exemple que je crée, je vais utiliser l'événement sourceopen pour me dire quand extraire et mettre en mémoire tampon la vidéo.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  <strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
  console.log("The Media Source Extensions API is not supported.")
}

<strong>function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  // Create a SourceBuffer and get the media file.
}</strong>

Notez que j'ai également appelé revokeObjectURL(). Je sais que cela semble prématuré, mais je peux le faire à tout moment une fois que l'attribut src de l'élément multimédia est connecté à une instance MediaSource. L'appel de cette méthode ne détruit aucun objet. Il permet à la plate-forme de gérer la récupération de mémoire au moment opportun. C'est pourquoi je l'appelle immédiatement.

Créer un SourceBuffer

Il est maintenant temps de créer SourceBuffer, qui est l'objet qui assure la séparation des données entre les sources multimédias et les éléments multimédias. Un SourceBuffer doit être spécifique au type de fichier multimédia que vous chargez.

En pratique, vous pouvez le faire en appelant addSourceBuffer() avec la valeur appropriée. Notez que dans l'exemple ci-dessous, la chaîne de type MIME contient un type MIME et deux codecs. Il s'agit d'une chaîne MIME pour un fichier vidéo, mais elle utilise des codecs distincts pour les parties vidéo et audio du fichier.

La version 1 de la spécification MSE permet aux user-agents de choisir une autre méthode pour exiger à la fois un type MIME et un codec. Certains user-agents ne l'exigent pas, mais n'autorisent que le type MIME. Certains user-agents, Chrome par exemple, ont besoin d'un codec pour les types MIME qui ne décrivent pas eux-mêmes leurs codecs. Plutôt que d'essayer de tout trier, il est préférable d'inclure les deux.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  <strong>
    var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
    the mediaSource instance. // Store it in a variable so it can be used in a
    closure. var mediaSource = e.target; var sourceBuffer =
    mediaSource.addSourceBuffer(mime); // Fetch and process the video.
  </strong>;
}

Obtenir le fichier multimédia

Si vous recherchez des exemples de MSE sur Internet, vous trouverez de nombreux fichiers multimédias qui récupèrent des fichiers multimédias à l'aide de XHR. Pour être à la pointe de la technologie, je vais utiliser l'API Fetch et la Promise qu'elle renvoie. Si vous essayez de le faire dans Safari, cela ne fonctionnera pas sans polyfill fetch().

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  <strong>
    fetch(videoUrl) .then(function(response){' '}
    {
      // Process the response object.
    }
    );
  </strong>;
}

Un lecteur en qualité production peut disposer du même fichier dans plusieurs versions pour prendre en charge différents navigateurs. Elle peut utiliser des fichiers distincts pour l'audio et la vidéo afin de permettre la sélection de l'audio en fonction des paramètres de langue.

Le code réel dispose également de plusieurs copies de fichiers multimédias de différentes résolutions, de sorte qu'il puisse s'adapter à différentes capacités d'appareil et conditions réseau. Une telle application est capable de charger et de lire des vidéos par fragments à l'aide de requêtes de plage ou de segments. Cela permet de s'adapter aux conditions du réseau pendant la lecture de contenus multimédias. Vous avez peut-être entendu les termes DASH ou HLS, qui sont deux méthodes pour y parvenir. Une discussion complète sur ce sujet ne relève pas de cette introduction.

Traiter l'objet de réponse

Le code semble presque terminé, mais la lecture du contenu multimédia n'est pas lancée. Nous devons obtenir les données multimédias de l'objet Response vers SourceBuffer.

La méthode classique pour transmettre les données de l'objet de réponse à l'instance MediaSource consiste à obtenir un ArrayBuffer à partir de l'objet de réponse et à le transmettre à SourceBuffer. Commencez par appeler response.arrayBuffer(), qui renvoie une promesse au tampon. Dans mon code, j'ai transmis cette promesse à une deuxième clause then(), où je l'ajoute à SourceBuffer.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      <strong>return response.arrayBuffer();</strong>
    })
    <strong>.then(function(arrayBuffer) {
      sourceBuffer.appendBuffer(arrayBuffer);
    });</strong>
}

Appeler endOfStream()

Une fois que tous les ArrayBuffers ont été ajoutés et qu'aucune autre donnée multimédia n'est attendue, appelez MediaSource.endOfStream(). MediaSource.readyState devient ended et déclenchera l'événement sourceended.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      <strong>sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });</strong>
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

La version finale

Voici l'exemple de code complet. J'espère que vous avez appris des choses sur les extensions de source média.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Commentaires