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 pour la lecture de segments audio ou vidéo. Bien que cet article ne traite pas de la MSE, vous devez comprendre ce concept si vous souhaitez intégrer des vidéos sur votre site qui effectuent des actions telles que :

  • Le streaming adaptatif, qui consiste à s'adapter aux fonctionnalités de l'appareil et aux conditions du réseau
  • L'assemblage adaptatif, comme 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

Vous pouvez presque considérer 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 élément 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 de Response.arrayBuffer() pour alimenter MediaSource.SourceBuffer.

En pratique, la chaîne se présente comme suit:

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 comprendre les choses à partir des explications fournies jusqu'à présent, n'hésitez pas à arrêter de lire. Pour une explication plus détaillée, lisez la suite. Je vais vous présenter 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 indique-t-il tout ce que vous devez savoir sur la lecture de contenus multimédias sur une page Web ? Non, il n'a pour but que de vous aider à comprendre le code plus complexe que vous pourriez trouver ailleurs. Par souci de clarté, ce document simplifie et exclut de nombreux éléments. Pour éviter cela, nous vous recommandons également d'utiliser une bibliothèque telle que Google's Shaka Player. Je noterai tout au long de cet article les simplifications que j'ai délibérément apportées.

Quelques points non couverts

Voici quelques points que je ne traiterai pas, sans ordre particulier.

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

À utiliser dans les environnements de production

Voici quelques recommandations pour utiliser les API liées à MSE en production :

  • Avant d'appeler 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 toujours en cours en vérifiant la valeur booléenne SourceBuffer.updating avant de mettre à jour les mode, timestampOffset, appendWindowStart et appendWindowEnd du SourceBuffer, ou d'appeler appendBuffer() ou remove() sur le 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 MediaSource.duration.
  • Si la valeur MediaSource.readyState est ended, les appels tels que appendBuffer() et remove(), ou le paramétrage de SourceBuffer.mode ou SourceBuffer.timestampOffset, entraînent la transition de cette valeur vers open. Vous devez donc ê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 l'origine du problème, en particulier pour les erreurs difficiles à reproduire dans les environnements de test.

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

Comme pour de nombreuses choses dans le développement Web de nos jours, vous commencez par la détection des fonctionnalités. Obtenez ensuite un élément multimédia, un élément <audio> ou <video>. Enfin, créez une instance de MediaSource. Il est transformé en URL et transmis à l&#39;attribut source de l&#39;é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 au format blob
Figure 1: Un attribut source sous forme de blob

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

L'instance MediaSource est-elle prête ?

URL.createObjectURL() est lui-même synchrone. Toutefois, il traite la pièce jointe de manière asynchrone. Cela entraîne un léger retard avant que vous puissiez effectuer une action avec l'instance MediaSource. Heureusement, il existe des moyens de le vérifier. Le moyen le 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 avoir 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 est prête à recevoir des données ou en reçoit.
  • 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 m'indiquer 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 après la connexion de l'attribut src de l'élément multimédia à une instance MediaSource. L'appel de cette méthode ne détruit aucun objet. Cela permet à la plate-forme de gérer le nettoyage de la mémoire à un 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 le transfert de 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 différer sur la nécessité de demander à la fois un type mime et un codec. Certains user-agents n'exigent pas, mais n'autorisent que le type mime. Certains agents utilisateur, comme Chrome, nécessitent un codec pour les types mime qui ne se décrivent pas eux-mêmes. Plutôt que d'essayer de trier tout cela, 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 effectuez une recherche sur Internet d'exemples de MSE, vous en trouverez de nombreux qui récupèrent des fichiers multimédias à l'aide de XHR. Pour être plus à la pointe, 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 un 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 de qualité de production dispose du même fichier dans plusieurs versions pour prendre en charge différents navigateurs. Il peut utiliser des fichiers distincts pour l'audio et la vidéo afin de permettre de sélectionner l'audio en fonction des paramètres de langue.

Le code réel comporte également plusieurs copies de fichiers multimédias de différentes résolutions, afin de pouvoir s'adapter à différentes capacités de l'appareil et conditions de réseau. Une telle application peut charger et 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 lorsque des contenus multimédias sont lus. Vous avez peut-être entendu les termes DASH ou HLS, qui sont deux méthodes permettant d'y parvenir. Une discussion complète de ce sujet dépasse le cadre 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.

Le moyen classique de transmettre des données de l'objet de réponse à l'instance MediaSource consiste à obtenir une valeur ArrayBuffer à partir de l'objet de réponse et à la transmettre à SourceBuffer. Commencez par appeler response.arrayBuffer(), qui renvoie une promesse dans le tampon. Dans mon code, j'ai transmis cette promesse à une deuxième clause then(), où je l'ai ajoutée à 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 est remplacé par ended et l'événement sourceended est déclenché.

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);
    });
}

Version finale

Voici l'exemple de code complet. J'espère que vous en savez plus sur les extensions de source multimé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