Rozszerzenia źródła multimediów

François Beaufort
François Beaufort
Jan Kowalski
Joe Medley

Rozszerzenia źródeł multimediów (MSE) to interfejs API JavaScript, który umożliwia tworzenie strumieni do odtwarzania z segmentów audio lub wideo. Chociaż nie zostało to opisane w tym artykule, zrozumienie MSE jest niezbędne, jeśli chcesz umieszczać w swojej witrynie filmy, które umożliwiają:

  • Adaptacyjne przesyłanie strumieniowe, które oznacza dostosowywanie się do możliwości urządzenia i warunków sieci.
  • Łączenie adaptacyjne, np. wstawianie reklam
  • Przesunięcie w czasie
  • Kontrola wydajności i rozmiaru pobierania
Podstawowy przepływ danych MSE
Rysunek 1. Podstawowy przepływ danych MSE

MSE można porównać do łańcucha. Jak pokazano na ilustracji, między pobranym plikiem a elementami multimedialnymi znajduje się kilka warstw.

  • Element <audio> lub <video> służący do odtwarzania multimediów.
  • Wystąpienie MediaSource z atrybutem SourceBuffer do przesyłania elementu multimedialnego.
  • Wywołanie fetch() lub XHR służące do pobierania danych multimedialnych w obiekcie Response.
  • Wywołanie metody Response.arrayBuffer() w celu pliku danych 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 potrafisz rozwiązać ten problem, nie musisz już czytać. Jeśli potrzebujesz bardziej szczegółowych wyjaśnień, czytaj dalej. Przejdę do tego łańcucha, tworząc podstawowy przykład MSE. Każdy krok kompilacji spowoduje dodanie kodu do poprzedniego kroku.

Uwaga dotycząca przejrzystości

Czy ten artykuł zawiera wszystko, co musisz wiedzieć o odtwarzaniu multimediów na stronie internetowej? Nie. Ma na celu pomoc w zrozumieniu bardziej złożonego kodu, który można znaleźć w innym miejscu. Aby rozwiać wątpliwości: ten dokument upraszcza wiele aspektów i je wyklucza. Naszym zdaniem możemy temu zapobiec, ponieważ zalecamy korzystanie z biblioteki takiej jak Google Shaka Player. Będę zaznaczać, gdzie celowo je upraszczam.

Kilka kwestii

Oto kilka spraw, których nie będę uwzględniać, w kolejności ustalonej.

  • Sterowanie odtwarzaniem. Są one udostępniane bezpłatnie, ponieważ użyto elementów <audio> HTML5 i <video>.
  • Obsługa błędów –

Do użytku w środowiskach produkcyjnych

Oto kilka rzeczy, które polecam w środowisku produkcyjnym interfejsów API związanych z MSE:

  • Przed wywołaniem tych interfejsów API zajmij się wszelkimi zdarzeniami błędów lub wyjątkami od interfejsów API i sprawdź HTMLMediaElement.readyState oraz MediaSource.readyState. Te wartości mogą się zmienić przed dostarczeniem powiązanych zdarzeń.
  • Aby upewnić się, że poprzednie wywołania appendBuffer() i remove() nie są nadal wykonywane, sprawdź wartość logiczną SourceBuffer.updating przed zaktualizowaniem parametrów mode, timestampOffset, appendWindowStart lub appendWindowEnd SourceBuffer albo wywołaniu funkcji appendBuffer() lub remove() w interfejsie SourceBuffer.
  • Przed wywołaniem metody MediaSource.endOfStream() lub zaktualizowaniem metody MediaSource.duration w przypadku wszystkich instancji SourceBuffer dodanych do MediaSource upewnij się, że żadna z ich wartości updating nie jest spełniony.
  • Jeśli MediaSource.readyState ma wartość ended, wywołania takie jak appendBuffer() lub remove() albo ustawienie SourceBuffer.mode lub SourceBuffer.timestampOffset spowoduje zmianę tej wartości na open. Oznacza to, że musisz przygotować się na obsługę wielu zdarzeń sourceopen.
  • W przypadku obsługi zdarzeń HTMLMediaElement error zawartość MediaError.message może być przydatna do określania głównej przyczyny błędu, szczególnie w przypadku błędów, które trudno jest odtworzyć w środowiskach testowych.

Dołączanie wystąpienia MediaSource do elementu multimedialnego

Podobnie jak w przypadku tworzenia stron internetowych, zaczynamy od wykrywania funkcji. Następnie pobierz element multimedialny, <audio> lub <video>. Na koniec utwórz instancję MediaSource. Jest on przekształcany w adres URL i przekazywany 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ła jako obiekt blob
Rysunek 1. Atrybut źródła w postaci obiektu blob

To, że obiekt MediaSource można przekazać do atrybutu src, może wydawać się nieco niepasujący. Zwykle są to ciągi tekstowe, ale mogą być też blobami. Jeśli przyjrzysz się stronie z osadzonymi elementami multimedialnymi i przyjrzysz się jej elementom multimedialnym, zrozumiesz, o co mi chodzi.

Czy instancja MediaSource jest gotowa?

Komponent URL.createObjectURL() jest synchroniczny, ale przetwarza załącznik asynchronicznie. Powoduje to niewielkie opóźnienie w wykonaniu jakichkolwiek czynności z instancją MediaSource. Na szczęście można to sprawdzić na kilka sposobów. Najprostszym sposobem jest użycie właściwości MediaSource o nazwie readyState. Właściwość readyState opisuje relację między instancją MediaSource a elementem multimedialnym. Może mieć jedną z tych wartości:

  • closed – wystąpienie MediaSource nie jest podłączone do elementu multimedialnego.
  • open – instancja MediaSource jest dołączona do elementu multimedialnego i jest gotowa do odbierania danych lub je odbiera.
  • ended – instancja MediaSource jest dołączona do elementu multimedialnego i wszystkie jej dane zostały przekazane do tego elementu.

Bezpośrednie zapytania dotyczące tych opcji mogą negatywnie wpłynąć na wydajność. Na szczęście MediaSource uruchamia też zdarzenia po zmianie parametru readyState, w szczególności sourceopen, sourceclosed, sourceended. W tym przykładzie użyję zdarzenia sourceopen, aby określić, kiedy pobrać i buforować 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ż do: revokeObjectURL(). Wiem, że to przedwcześnie, ale mogę to zrobić w każdej chwili, gdy atrybut src elementu multimedialnego zostanie połączony z wystąpieniem MediaSource. Wywołanie tej metody nie powoduje zniszczenia żadnych obiektów. Umożliwia platformie obsługę procesu usuwania odpadów w odpowiednim czasie, dlatego od razu to robię.

Tworzenie obiektu SourceBuffer

Pora utworzyć SourceBuffer, czyli obiekt, który tak naprawdę rozdziela dane między źródłami multimediów i elementami multimedialnymi. Wartość SourceBuffer musi odpowiadać typowi wczytywanego pliku multimedialnego.

W praktyce możesz to zrobić, wywołując addSourceBuffer() z odpowiednią wartością. Zwróć uwagę, że w przykładzie poniżej ciąg znaków typu MIME zawiera typ MIME i 2 kodeki. Jest to ciąg MIME pliku wideo, ale dla fragmentów wideo i audio do tych plików wykorzystywane są osobne kodeki.

Wersja 1 specyfikacji MSE pozwala klientom użytkownika różnicować, czy wymagają zarówno typu MIME, jak i kodeka. Niektóre klienty użytkownika nie wymagają, ale zezwalają na stosowanie tylko typu MIME. Na przykład niektóre klienty użytkownika (Chrome) wymagają kodeka w przypadku typów MIME, które nie opisują swoich kodeków samodzielnie. Zamiast segregować wszystko od siebie, lepiej uwzględnić oba te elementy.

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 MSE znajdziesz sporo plików multimedialnych za pomocą XHR. Aby to zrobić, użyję interfejsu Fetch API i zwracanego przez niego Promise. Jeśli spróbujesz zrobić to w Safari, nie zadziała bez użycia 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 jakości produkcyjnej może zawierać ten sam plik w wielu wersjach dla różnych przeglądarek. Może użyć osobnych plików audio i wideo, aby umożliwić wybieranie dźwięku na podstawie ustawień języka.

Prawdziwy kod mógłby też mieć wiele kopii plików multimedialnych o różnej rozdzielczości, aby mógł dostosowywać się do różnych możliwości urządzenia i warunków sieci. Taka aplikacja może wczytywać i odtwarzać filmy fragmentami, korzystając z żądań zakresu lub segmentów. Dzięki temu można dostosować się do warunków sieciowych podczas odtwarzania multimediów. Być może znasz terminy DASH lub HLS, które to dwie metody. Pełny zakres dyskusji na ten temat wykracza poza zakres tego wprowadzenia.

Przetwarzanie obiektu odpowiedzi

Kod wygląda na prawie gotowy, ale multimedia się nie odtwarzają. Musimy pobrać dane mediów z obiektu Response do SourceBuffer.

Typowym sposobem na przekazanie danych z obiektu odpowiedzi do instancji MediaSource jest pobranie obiektu ArrayBuffer z obiektu odpowiedzi i przekazanie go do SourceBuffer. Zacznij od wywołania metody response.arrayBuffer(), która zwraca obietnicę do bufora. W kodzie przekazuję tę obietnicę do drugiej klauzuli then(), gdzie dołączam ją do sekcji 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łanie metody endOfStream()

Po dołączeniu wszystkich elementów ArrayBuffers i nie powinny się już pojawiać dane dotyczące multimediów, wywołaj metodę MediaSource.endOfStream(). Spowoduje to zmianę MediaSource.readyState na ended i wywoła zdarzenie 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 pełny przykład kodu. Mam nadzieję, że wiesz już coś 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ę