Medienquellenerweiterungen

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

Media Source Extensions (MSE) sind eine JavaScript API, mit der Sie Streams für die Wiedergabe von Audio- oder Videosegmenten erstellen können. Auch wenn es in diesem Artikel nicht behandelt wird, ist es erforderlich, dass du mit MSE vertraut bist, wenn du Videos in deine Website einbetten möchtest, die Folgendes tun:

  • Adaptives Streaming, also die Anpassung an Gerätefunktionen und Netzwerkbedingungen,
  • Adaptive Teilung, z. B. Anzeigenbereitstellung
  • Zeitverschiebung
  • Kontrolle über Leistung und Downloadgröße
Grundlegender MSE-Datenfluss
Abbildung 1: Grundlegender MSE-Datenfluss

Sie können sich MSE quasi als eine Kette vorstellen. Wie in der Abbildung dargestellt, befinden sich zwischen der heruntergeladenen Datei und den Medienelementen mehrere Ebenen.

  • Ein <audio>- oder <video>-Element zum Abspielen der Medien.
  • Eine MediaSource-Instanz mit einem SourceBuffer zum Einspeisen des Medienelements.
  • Ein fetch()- oder XHR-Aufruf zum Abrufen von Mediendaten in einem Response-Objekt.
  • Ein Aufruf von Response.arrayBuffer(), um MediaSource.SourceBuffer zu erfassen.

In der Praxis sieht die Kette so aus:

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

Wenn Sie die Dinge aus den bisherigen Erläuterungen verstehen können, können Sie jetzt aufhören zu lesen. Eine ausführlichere Erläuterung erhaltet ihr im Folgenden. Ich werde diese Kette anhand eines einfachen MSE-Beispiels durchgehen. Mit jedem Build-Schritt wird Code zum vorherigen Schritt hinzugefügt.

Hinweis zur Übersichtlichkeit

Enthält dieser Artikel alle wichtigen Informationen zum Abspielen von Medien auf einer Webseite? Nein, sie soll Ihnen lediglich helfen, komplizierteren Code zu verstehen, den Sie an anderer Stelle finden könnten. Zum besseren Verständnis wird in diesem Dokument vieles vereinfacht und ausgeschlossen. Wir denken, dass wir damit durchkommen können, da wir auch die Verwendung einer Bibliothek wie dem Shaka-Player von Google empfehlen. Ich werde immer wieder darauf hinweisen, wo ich das Ganze absichtlich vereinfachen werde.

Einige Dinge, die nicht behandelt werden

Ich möchte hier ein paar Dinge, die ich nicht behandeln werde, - und zwar in keiner bestimmten Reihenfolge -.

  • Wiedergabesteuerung. Sie erhalten diese kostenlos, da wir die HTML5-Elemente <audio> und <video> verwenden.
  • Fehlerbehandlung –

Zur Verwendung in Produktionsumgebungen

Bei der Verwendung von MSE-bezogenen APIs in der Produktionsumgebung würde ich Folgendes empfehlen:

  • Bevor Sie diese APIs aufrufen, sollten Sie alle Fehlerereignisse oder API-Ausnahmen bearbeiten und HTMLMediaElement.readyState und MediaSource.readyState prüfen. Diese Werte können sich ändern, bevor verknüpfte Ereignisse gesendet werden.
  • Achten Sie darauf, dass die vorherigen appendBuffer()- und remove()-Aufrufe nicht noch ausgeführt werden. Prüfen Sie dazu den booleschen Wert SourceBuffer.updating, bevor Sie mode, timestampOffset, appendWindowStart oder appendWindowEnd von SourceBuffer aktualisieren oder appendBuffer() oder remove() in SourceBuffer aufrufen.
  • Achten Sie bei allen SourceBuffer-Instanzen, die zu MediaSource hinzugefügt wurden, darauf, dass keiner der updating-Werte wahr ist, bevor Sie MediaSource.endOfStream() aufrufen oder MediaSource.duration aktualisieren.
  • Wenn der Wert von MediaSource.readyState ended ist, führen Aufrufe wie appendBuffer() und remove() oder das Festlegen von SourceBuffer.mode oder SourceBuffer.timestampOffset dazu, dass dieser Wert in open übergeht. Sie sollten also auf die Verarbeitung mehrerer sourceopen-Ereignisse vorbereitet sein.
  • Bei der Verarbeitung von HTMLMediaElement error-Ereignissen kann der Inhalt von MediaError.message nützlich sein, um die Ursache des Fehlers zu ermitteln. Dies gilt insbesondere für Fehler, die in Testumgebungen schwer reproduziert werden können.

MediaSource-Instanz an Medienelement anhängen

Wie bei vielen Dingen in der Webentwicklung heutzutage beginnen Sie mit der Funktionserkennung. Rufen Sie als Nächstes ein Medienelement ab, entweder ein <audio>- oder <video>-Element. Erstellen Sie abschließend eine Instanz von MediaSource. Es wird in eine URL umgewandelt und an das Attribut „source“ des Medienelements übergeben.

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.');
}
Ein Quellattribut als Blob
Abbildung 1: Ein Quellattribut als Blob

Es mag etwas seltsam erscheinen, dass ein MediaSource-Objekt an ein src-Attribut übergeben werden kann. Normalerweise sind es Strings, können aber auch Blobs sein. Wenn Sie sich eine Seite mit eingebetteten Medien ansehen und deren Medienelement untersuchen, werden Sie verstehen, was ich meine.

Ist die MediaSource-Instanz bereit?

URL.createObjectURL() ist selbst synchron, verarbeitet den Anhang jedoch asynchron. Dies führt zu einer geringfügigen Verzögerung, bevor Sie mit der MediaSource-Instanz etwas unternehmen können. Glücklicherweise gibt es Möglichkeiten, dies zu testen. Die einfachste Möglichkeit ist die Verwendung einer MediaSource-Eigenschaft namens readyState. Das Attribut readyState beschreibt die Beziehung zwischen einer MediaSource-Instanz und einem Medienelement. Sie kann einen der folgenden Werte haben:

  • closed: Die Instanz MediaSource ist mit keinem Medienelement verknüpft.
  • open: Die Instanz MediaSource ist an ein Medienelement angehängt und kann Daten empfangen oder empfangen.
  • ended: Die Instanz MediaSource ist an ein Medienelement angehängt und alle Daten wurden an dieses Element übergeben.

Das direkte Abfragen dieser Optionen kann sich negativ auf die Leistung auswirken. Glücklicherweise löst MediaSource auch Ereignisse aus, wenn sich readyState ändert, insbesondere sourceopen, sourceclosed, sourceended. In meinem Beispiel verwende ich das sourceopen-Ereignis, um mir mitzuteilen, wann das Video abgerufen und gepuffert werden soll.

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>

Wie Sie sehen, habe ich auch revokeObjectURL() genannt. Ich weiß, dass dies zu früh erscheint, aber ich kann dies jederzeit tun, nachdem das Attribut src des Medienelements mit einer MediaSource-Instanz verbunden ist. Durch den Aufruf dieser Methode werden keine Objekte gelöscht. Er ermöglicht der Plattform, die automatische Speicherbereinigung zu einem geeigneten Zeitpunkt auszuführen, weswegen ich sie sofort aufrufe.

SourceBuffer erstellen

Jetzt ist es an der Zeit, das SourceBuffer zu erstellen. Dies ist das Objekt, das die Daten zwischen Medienquellen und Medienelementen verbindet. Ein SourceBuffer muss spezifisch für den Typ der Mediendatei sein, die Sie laden.

In der Praxis erreichen Sie dies, indem Sie addSourceBuffer() mit dem entsprechenden Wert aufrufen. Beachten Sie, dass der String des MIME-Typs im Beispiel unten einen MIME-Typ und zwei Codecs enthält. Dies ist ein MIME-String für eine Videodatei, der jedoch separate Codecs für die Video- und Audioteile der Datei verwendet.

In Version 1 der MSE-Spezifikation können User-Agents unterscheiden, ob sowohl ein MIME-Typ als auch ein Codec erforderlich sind. Einige User-Agents benötigen zwar keine Voraussetzung, lassen aber nur den MIME-Typ zu. Einige User-Agents, beispielsweise Chrome, benötigen einen Codec für MIME-Typen, die ihre Codecs nicht selbst beschreiben. Anstatt zu versuchen, alles zu klären, ist es besser, einfach beide zu berücksichtigen.

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

Mediendatei abrufen

Wenn Sie im Internet nach MSE-Beispielen suchen, werden Sie viele finden, die Mediendateien mit XHR abrufen. Ich verwende die Fetch API und das zurückgegebene Promise-Objekt, um noch innovativer zu werden. In Safari funktioniert das aber nicht ohne fetch()-Polyfill.

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

Ein Player mit Produktionsqualität verfügt über dieselbe Datei in mehreren Versionen, um verschiedene Browser zu unterstützen. Es könnten separate Dateien für Audio und Video verwendet werden, damit Audio je nach Spracheinstellungen ausgewählt werden kann.

Realer Code hätte außerdem mehrere Kopien von Mediendateien mit unterschiedlichen Auflösungen, sodass er an unterschiedliche Gerätefunktionen und Netzwerkbedingungen angepasst werden könnte. Eine solche Anwendung ist in der Lage, Videos mithilfe von Bereichsanfragen oder Segmenten in Blöcken zu laden und abzuspielen. Dies ermöglicht die Anpassung an die Netzwerkbedingungen, während Medien wiedergegeben werden. Vielleicht haben Sie schon die Begriffe DASH oder HLS gehört. Eine ausführliche Diskussion dieses Themas wird in dieser Einführung nicht behandelt.

Antwortobjekt verarbeiten

Der Code sieht fast fertig aus, aber die Medien werden nicht abgespielt. Wir müssen Mediendaten aus dem Response-Objekt in das SourceBuffer-Objekt abrufen.

In der Regel werden Daten vom Antwortobjekt an die MediaSource-Instanz übergeben, indem ein ArrayBuffer-Objekt aus dem Antwortobjekt abgerufen und an das SourceBuffer-Objekt übergeben wird. Rufen Sie zuerst response.arrayBuffer() auf, das ein Promise an den Puffer zurückgibt. In meinem Code habe ich dieses Promise an eine zweite then()-Klausel übergeben und an die SourceBuffer angehängt.

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

endOfStream() aufrufen

Nachdem alle ArrayBuffers angehängt wurden und keine weiteren Mediendaten erwartet werden, rufen Sie MediaSource.endOfStream() auf. Dadurch wird MediaSource.readyState in ended geändert und das Ereignis sourceended ausgelöst.

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

Die endgültige Version

Hier ist das vollständige Codebeispiel. Ich hoffe, Sie haben etwas über Media Source Extensions erfahren.

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

Feedback