Rozszerzenia źródła multimediów

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

Media Source Extensions (MSE) to interfejs JavaScript API, który umożliwia tworzenie strumieni do odtwarzania z segmentów dźwięku lub obrazu wideo. Chociaż nie jest to omawiane w tym artykule, musisz znać MSE, jeśli chcesz osadzić w swojej witrynie filmy, które:

  • strumieniowanie adaptacyjne, czyli dostosowywanie się do możliwości urządzenia i warunków sieci;
  • adaptacyjne zszywanie, np. wstawianie reklam;
  • Przesuwanie w czasie
  • kontrolowanie wydajności i rozmiaru pliku do pobrania;
Podstawowy przepływ danych MSE
Rys. 1: Podstawowy przepływ danych MSE

Można powiedzieć, że MSE jest łańcuchem. Jak widać na rysunku, między pobranym plikiem a elementami multimedialnymi znajduje się kilka warstw.

  • Element <audio> lub <video> do odtwarzania multimediów.
  • Instancja MediaSource z atrybutem SourceBuffer do przesyłania elementu multimedialnego.
  • wywołanie fetch() lub XHR w celu pobrania danych multimedialnych w obiekcie Response.
  • Wywołanie aplikacji Response.arrayBuffer() dotyczące źródła treści MediaSource.SourceBuffer.

W praktyce łańcuch wygląda tak:

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

Jeśli na podstawie dotychczasowych wyjaśnień udało Ci się już wszystko zrozumieć, możesz przestać czytać. Jeśli potrzebujesz bardziej szczegółowych wyjaśnień, czytaj dalej. Aby to wyjaśnić, pokażę, jak utworzyć podstawowy przykład MSE. Każdy z etapów kompilacji dodaje kod do poprzedniego etapu.

Uwaga dotycząca przejrzystości

Czy w tym artykule znajdziesz wszystkie informacje o odtwarzaniu multimediów na stronie internetowej? Nie. Jego zadaniem jest pomóc zrozumieć bardziej złożony kod, który można znaleźć gdzie indziej. Ze względu na przejrzystość ten dokument upraszcza i wyklucza wiele kwestii. Uważamy, że możemy to zrobić, ponieważ zalecamy też używanie biblioteki takiej jak odtwarzacz Shaka firmy Google. W całym tekście zaznaczam, że celowo je upraszczam.

Co nie jest objęte gwarancją

Oto kilka kwestii, których nie będę omawiać (w dowolnej kolejności).

  • Elementy sterujące odtwarzaniem. Otrzymujemy je bezpłatnie dzięki elementom HTML5 <audio><video>.
  • Obsługa błędów –

Do użytku w środowiskach produkcyjnych

Oto kilka zaleceń dotyczących korzystania z interfejsów API związanych z MSE w wersji produkcyjnej:

  • Zanim wykonasz wywołania tych interfejsów API, obsłuż wszystkie zdarzenia błędów lub wyjątki interfejsu API oraz sprawdź HTMLMediaElement.readyStateMediaSource.readyState. Te wartości mogą się zmienić przed dostarczeniem powiązanych zdarzeń.
  • Przed zaktualizowaniem wartości mode, timestampOffset, appendWindowStart, appendWindowEndSourceBuffer lub wywołaniem funkcji appendBuffer() lub remove()SourceBuffer sprawdź, czy poprzednie wywołania appendBuffer()remove() nie są nadal w toku, sprawdzając wartość logiczną SourceBuffer.updating.
  • W przypadku wszystkich instancji SourceBuffer dodanych do MediaSource sprawdź, czy żadna z wartości updating nie jest prawdziwa przed wywołaniem funkcji MediaSource.endOfStream() lub zaktualizowaniem elementu MediaSource.duration.
  • Jeśli MediaSource.readyState ma wartość ended, wywołania takie jak appendBuffer() i remove() albo ustawienie SourceBuffer.mode lub SourceBuffer.timestampOffset spowodują przeniesienie tej wartości do open. Oznacza to, że musisz być przygotowany na obsługę wielu zdarzeń sourceopen.
  • Podczas obsługi zdarzeń HTMLMediaElement error zawartość pliku MediaError.message może być przydatna do określenia głównej przyczyny błędu, zwłaszcza w przypadku błędów, które trudno odtworzyć w środowiskach testowych.

Dołączanie instancji MediaSource do elementu multimedialnego

Podobnie jak w przypadku wielu innych rzeczy związanych z programowaniem stron internetowych, zaczyna się od wykrywania cech. Następnie pobierz element multimedialny: <audio> lub <video>. Na koniec utwórz instancję MediaSource. Zostanie on zamieniony w adres URL i przekazany do atrybutu źródła elementu multimedialnego.

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.');
}
Atrybut źródłowy w postaci obiektu blob
Ilustracja 1. Atrybut źródła jako zbiór danych

Fakt, że obiekt MediaSource może zostać przekazany do atrybutu src, może wydawać się nieco dziwny. Zwykle są to ciągi strunowe, ale mogą też być blobami. Jeśli sprawdzisz stronę z osadzonym materiałem multimedialnym i jego element, zrozumiesz, o co chodzi.

Czy instancja MediaSource jest gotowa?

URL.createObjectURL() jest synchroniczny, ale przetwarza załącznik asynchronicznie. Powoduje to niewielkie opóźnienie, zanim będzie można wykonać jakiekolwiek czynności z instancją MediaSource. Na szczęście istnieją sposoby na sprawdzenie, czy tak jest. Najprostszym sposobem jest użycie właściwości MediaSource o nazwie readyState. Właściwość readyState opisuje relację między wystąpieniem MediaSource a elementem multimedialnym. Może mieć jedną z tych wartości:

  • closed – instancja MediaSource nie jest przypięta do elementu multimedialnego.
  • open – instancja MediaSource jest dołączona do elementu multimedialnego i jest gotowa do odbierania danych lub właśnie je odbiera.
  • ended – instancja MediaSource jest dołączana do elementu multimedialnego, a wszystkie jego dane zostały do niego przekazane.

Wybieranie tych opcji bezpośrednio może negatywnie wpłynąć na wydajność. Na szczęście MediaSource wywołuje też zdarzenia, gdy zmieni się zasada readyState, a w szczególności sourceopen, sourceclosed i sourceended. W tym przykładzie użyję zdarzenia sourceopen, aby określić, kiedy pobrać i zbuferować film.

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>

Zwróć uwagę, że dzwonię też pod numer revokeObjectURL(). Wiem, że to wydaje się przedwczesne, ale mogę to zrobić w dowolnym momencie po połączeniu atrybutu src elementu multimedialnego z instancją MediaSource. Wywołanie tej metody nie powoduje zniszczenia żadnych obiektów. Umożliwia platformie zarządzanie zbieraniem odpadów w odpowiednim czasie, dlatego wywołuję je natychmiast.

Tworzenie SourceBuffer

Czas utworzyć obiekt SourceBuffer, który służy do przenoszenia danych między źródłami multimediów a elementami multimedialnymi. Atrybut SourceBuffer musi być dostosowany do typu wczytywanego pliku multimedialnego.

W praktyce możesz to zrobić, wywołując funkcję addSourceBuffer() z odpowiednią wartością. Zwróć uwagę, że w przykładzie poniżej ciąg znaków typu mime zawiera typ mime i dwa kodeki. To jest ciąg MIME pliku wideo, ale dla jego części audio i wideo są używane różne kodeki.

Wersja 1 specyfikacji MSE pozwala przeglądarkom użytkownika na określenie, czy wymagają one zarówno typu MIME, jak i kodeki. Niektóre przeglądarki nie wymagają, ale zezwalają na typ MIME. Niektóre przeglądarki, np. Chrome, wymagają kodeka dla typów mime, które nie opisują swoich kodeków. Zamiast próbować odfiltrować te dane, lepiej uwzględnić je obie.

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

Pobierz plik multimedialny

Po wyszukaniu w internecie przykładów z MSE znajdziesz wiele plików multimedialnych, które można pobrać przy użyciu XHR. Aby stworzyć bardziej zaawansowane rozwiązanie, użyję interfejsu Fetch API i zwróconego przez niego obietnika. Jeśli spróbujesz to zrobić w Safari, nie będzie on działał bez kodu 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>;
}

Odtwarzacz o jakości produkcyjnej miałby ten sam plik w różnych wersjach, aby obsługiwać różne przeglądarki. Mogą one używać osobnych plików audio i wideo, aby umożliwić wybór ścieżki audio na podstawie ustawień języka.

Kod w rzeczywistych warunkach zawierałby też wiele kopii plików multimedialnych w różnych rozdzielczościach, aby można było dostosować go do różnych funkcji urządzenia i warunków sieci. Taka aplikacja może wczytywać i odtwarzać filmy w kawałkach za pomocą żądań zakresu lub segmentów. Umożliwia to dostosowanie się do warunków sieci podczas odtwarzania multimediów. Istnieją nazwy DASH i HLS, które pozwalają osiągnąć ten cel. Pełna omówienia tej kwestii wykracza poza zakres tego wprowadzenia.

Przetwarzanie obiektu odpowiedzi

Kod wygląda na prawie gotowy, ale multimedia nie działają. Musimy przesłać dane multimediów z obiektu Response do obiektu SourceBuffer.

Typowy sposób przekazywania danych z obiektu odpowiedzi do instancji MediaSource polega na pobraniu obiektu ArrayBuffer z obiektu odpowiedzi i przekazaniu go do obiektu SourceBuffer. Najpierw wywołaj funkcję response.arrayBuffer(), która zwraca obietnicę do bufora. W moim kodzie to obietnica została przekazana do drugiej klauzuli then(), gdzie dołączam ją do 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>
}

Wywołaj endOfStream()

Gdy wszystkie ArrayBuffers zostaną dołączone i nie będzie już więcej danych multimedialnych, wywołaj funkcję MediaSource.endOfStream(). Spowoduje to zmianę wartości MediaSource.readyState na ended i wywołanie zdarzenia 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);
    });
}

Ostateczna wersja

Oto przykładowy pełny kod. Mam nadzieję, że udało Ci się dowiedzieć czegoś o rozszerzeniach źródła multimediów.

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

Prześlij opinię