Medienquellenerweiterungen

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

Media Source Extensions (MSE) ist eine JavaScript API, mit der du Streams für die Wiedergabe aus Audio- oder Videosegmenten erstellen kannst. Auch wenn es in diesem Artikel nicht behandelt wird, ist es wichtig, MSE zu verstehen, wenn Sie Videos auf Ihrer Website einbetten möchten, die Folgendes ermöglichen:

  • Adaptives Streaming, d. h. Anpassung an die Gerätefunktionen und Netzwerkbedingungen
  • Adaptives Splicing, z. B. Anzeigeneinblendung
  • Zeitverschiebung
  • Leistung und Downloadgröße steuern
Grundlegender MSE-Datenfluss
Abbildung 1: Grundlegender MSE-Datenfluss

Sie können sich MSE fast als 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 für das Medienelement.
  • Ein fetch()- oder XHR-Aufruf zum Abrufen von Mediendaten in einem Response-Objekt.
  • Ein Aufruf an Response.arrayBuffer(), MediaSource.SourceBuffer zu füttern.

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 bisherigen Erklärungen verstanden haben, können Sie mit dem Lesen aufhören. Eine ausführlichere Erklärung finden Sie weiter unten. Ich werde diese Kette durchgehen, indem ich ein einfaches MSE-Beispiel erstelle. Mit jedem Build-Schritt wird Code zum vorherigen Schritt hinzugefügt.

Hinweis zur Klarheit

Wird in diesem Artikel alles Wissenswerte zum Abspielen von Medien auf einer Webseite beschrieben? Nein, es soll Ihnen nur dabei helfen, komplizierteren Code zu verstehen, den Sie möglicherweise an anderer Stelle finden. Zur Verdeutlichung werden in diesem Dokument viele Dinge vereinfacht und ausgeschlossen. Wir gehen davon aus, dass dies in Ordnung ist, da wir auch die Verwendung einer Bibliothek wie dem Shaka Player von Google empfehlen. Ich werde immer darauf hinweisen, wenn ich absichtlich vereinfache.

Nicht abgedeckte Fälle

Hier sind einige Dinge, die ich nicht behandeln werde, in keiner bestimmten Reihenfolge:

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

Für die Verwendung in Produktionsumgebungen

Im Folgenden findest du einige Empfehlungen für die Produktionsnutzung von MSE-bezogenen APIs:

  • Bevor Sie Aufrufe an diese APIs senden, prüfen Sie alle Fehlerereignisse oder API-Ausnahmen und prüfen Sie HTMLMediaElement.readyState und MediaSource.readyState. Diese Werte können sich ändern, bevor die zugehörigen Ereignisse gesendet werden.
  • Prüfe den booleschen Wert SourceBuffer.updating, bevor du mode, timestampOffset, appendWindowStart oder appendWindowEnd von SourceBuffer aktualisierst oder appendBuffer() oder remove() auf SourceBuffer aufrufst, um sicherzustellen, dass keine vorherigen appendBuffer()- und remove()-Aufrufe noch laufen.
  • Achten Sie darauf, dass für alle SourceBuffer-Instanzen, die Ihrer MediaSource hinzugefügt wurden, keiner der updating-Werte „wahr“ ist, bevor Sie MediaSource.endOfStream() aufrufen oder die MediaSource.duration aktualisieren.
  • Wenn der Wert von MediaSource.readyState ended ist, wird durch Aufrufe wie appendBuffer() und remove() oder durch Festlegen von SourceBuffer.mode oder SourceBuffer.timestampOffset der Wert in open geändert. Sie sollten also darauf vorbereitet sein, mehrere sourceopen-Ereignisse zu verarbeiten.
  • Beim Umgang mit HTMLMediaElement error-Ereignissen kann der Inhalt von MediaError.message hilfreich sein, um die Ursache des Fehlers zu ermitteln, insbesondere bei Fehlern, die in Testumgebungen schwer zu reproduzieren sind.

MediaSource-Instanz an ein Medienelement anhängen

Wie bei vielen anderen Dingen in der Webentwicklung, die heutzutage auch in der Webentwicklung tätig sind, beginnen Sie mit der Funktionserkennung. Rufe als Nächstes ein Medienelement ab, entweder ein <audio>- oder ein <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.');
}
Quellattribut als Blob
Abbildung 1: Quellattribut als Blob

Dass ein MediaSource-Objekt an ein src-Attribut übergeben werden kann, mag etwas seltsam erscheinen. In der Regel sind es Strings, aber es können auch Blobs sein. Wenn Sie eine Seite mit eingebetteten Medien und deren Medienelement untersuchen, sehen Sie, was ich meine.

Ist die MediaSource-Instanz bereit?

URL.createObjectURL() ist selbst synchron, verarbeitet den Anhang jedoch asynchron. Dadurch kann es etwas dauern, bis Sie etwas mit der MediaSource-Instanz tun 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. Es kann einen der folgenden Werte haben:

  • closed: Die MediaSource-Instanz ist an kein Medienelement angehängt.
  • open: Die MediaSource-Instanz ist mit einem Medienelement verknüpft und empfängt Daten oder ist bereit, Daten zu empfangen.
  • ended: Die MediaSource-Instanz ist an ein Medienelement angehängt und alle zugehörigen Daten wurden an dieses Element übergeben.

Wenn Sie diese Optionen direkt abfragen, kann sich das negativ auf die Leistung auswirken. Glücklicherweise löst MediaSource auch Ereignisse aus, wenn sich readyState ändert, insbesondere sourceopen, sourceclosed und sourceended. In meinem Beispiel verwende ich das Ereignis sourceopen, um anzugeben, wann das Video abgerufen und zwischengespeichert 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>

Beachte, dass ich auch revokeObjectURL() genannt habe. Ich weiß, dass das voreilig erscheint, aber ich kann das jederzeit tun, nachdem das src-Attribut des Medienelements mit einer MediaSource-Instanz verbunden ist. Durch den Aufruf dieser Methode werden keine Objekte zerstört. Es ermöglicht der Plattform jedoch, die Garbage Collection zum richtigen Zeitpunkt auszuführen. Deshalb rufe ich sie sofort auf.

SourceBuffer erstellen

Jetzt ist es an der Zeit, die SourceBuffer zu erstellen. Dieses Objekt ist für die Übertragung von Daten zwischen Medienquellen und Medienelementen verantwortlich. Ein SourceBuffer muss für den Typ der geladenen Mediendatei spezifisch sein.

In der Praxis können Sie dazu addSourceBuffer() mit dem entsprechenden Wert aufrufen. Beachte, dass der MIME-Typ-String im folgenden Beispiel einen MIME-Typ und zwei Codecs enthält. Dies ist ein MIME-String für eine Videodatei, verwendet jedoch separate Codecs für die Video- und Audioteile der Datei.

Version 1 der MSE-Spezifikation ermöglicht es, dass User-Agents sich darin unterscheiden, ob sowohl ein MIME-Typ als auch ein Codec erforderlich sind. Einige User-Agents erfordern den MIME-Typ nicht, erlauben ihn aber. Einige User-Agents, z. B. Chrome, benötigen einen Codec für MIME-Typen, die ihre Codecs nicht selbst beschreiben. Anstatt zu versuchen, das alles zu sortieren, ist es besser, beides 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, finden Sie viele, die Mediendateien mit XHR abrufen. Für ein besseres Ergebnis verwende ich die Fetch API und das Promise, das sie zurückgibt. Wenn Sie dies in Safari versuchen, funktioniert es ohne eine fetch()-Polyfill nicht.

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 in Produktionsqualität sollte dieselbe Datei in mehreren Versionen haben, um verschiedene Browser zu unterstützen. Es könnten separate Dateien für Audio und Video verwendet werden, damit Audio basierend auf den Spracheinstellungen ausgewählt werden kann.

Der tatsächliche Code hätte außerdem mehrere Kopien von Mediendateien mit unterschiedlichen Auflösungen, sodass er sich an unterschiedliche Gerätefunktionen und Netzwerkbedingungen anpassen könnte. Eine solche Anwendung kann Videos entweder mithilfe von Bereichsanfragen oder Segmenten in Teilen laden und abspielen. So können sich die Netzwerkbedingungen während der Medienwiedergabe anpassen. Möglicherweise haben Sie die Begriffe DASH oder HLS gehört, zwei Methoden, um dies zu erreichen. Eine vollständige Erläuterung dieses Themas würde den Rahmen dieser Einführung sprengen.

Antwortobjekt verarbeiten

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

Normalerweise werden Daten vom Antwortobjekt an die MediaSource-Instanz übergeben, indem ein ArrayBuffer aus dem Antwortobjekt abgerufen und an die SourceBuffer übergeben wird. Rufen Sie zuerst response.arrayBuffer() auf, um ein Versprechen für den Puffer zurückzugeben. In meinem Code habe ich dieses Promise an eine zweite then()-Klausel übergeben, wo ich es an SourceBuffer anfüge.

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, rufe MediaSource.endOfStream() auf. Dadurch wird MediaSource.readyState in ended geändert und das sourceended-Ereignis 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, dass Sie etwas über Erweiterungen für Medienquellen gelernt haben.

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