Media Source Extensions (MSE) to interfejs API JavaScriptu, 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;

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. - Wystąpienie elementu
MediaSource
z elementemSourceBuffer
, który zasila element multimedialny. - wywołanie
fetch()
lub XHR w celu pobrania danych multimedialnych w obiekcieResponse
. - Prośba o
Response.arrayBuffer()
doMediaSource.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 chcesz dowiedzieć się więcej, czytaj dalej. Pokażę Ci ten łańcuch, tworząc prosty 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, służy ono tylko do pomocy w zrozumieniu bardziej skomplikowanego kodu, który możesz znaleźć gdzie indziej. W celu zachowania przejrzystości 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 tekście będę zaznaczać, gdzie celowo upraszczam opis.
Co nie jest objęte gwarancją
Oto kilka kwestii, których nie będę omawiać (w dowolnej kolejności).
- Elementy sterujące odtwarzaniem. Używamy ich bezpłatnie dzięki elementom HTML5
<audio>
i<video>
. - Obsługa błędów –
Do użytku w środowiskach produkcyjnych
Oto kilka kwestii, które zalecam w przypadku produkcyjnego korzystania z interfejsów API związanych z MSE:
- Zanim wykonasz wywołania tych interfejsów API, obsłuż wszystkie zdarzenia błędów lub wyjątki interfejsu API oraz sprawdź
HTMLMediaElement.readyState
iMediaSource.readyState
. Te wartości mogą się zmienić przed dostarczeniem powiązanych zdarzeń. - Przed zaktualizowaniem wartości
mode
,timestampOffset
,appendWindowStart
,appendWindowEnd
wSourceBuffer
lub wywołaniem funkcjiappendBuffer()
lubremove()
wSourceBuffer
sprawdź, czy poprzednie wywołaniaappendBuffer()
iremove()
nie są nadal w toku, sprawdzając wartość logicznąSourceBuffer.updating
. - W przypadku wszystkich instancji
SourceBuffer
dodanych doMediaSource
sprawdź, czy żadna z wartościupdating
nie jest równa „true” przed wywołaniem funkcjiMediaSource.endOfStream()
lub zaktualizowaniemMediaSource.duration
. - Jeśli wartość
MediaSource.readyState
toended
, wywołania takie jakappendBuffer()
iremove()
lub ustawienieSourceBuffer.mode
lubSourceBuffer.timestampOffset
spowodują przejście tej wartości doopen
. Oznacza to, że musisz być przygotowany na obsługę wielu zdarzeńsourceopen
. - Podczas obsługi zdarzeń
HTMLMediaElement error
zawartość plikuMediaError.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 wystąpienia MediaSource do elementu multimedialnego
Podobnie jak w przypadku wielu innych aspektów tworzenia stron internetowych, zaczynasz od wykrywania cech. 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.');
}

Fakt, że obiekt MediaSource
może być przekazywany do atrybutu src
, może wydawać się nieco dziwny. Zwykle są to ciągi znaków, ale mogą też być plamami danych.
Jeśli sprawdzisz stronę z osadzonym plikiem multimedialnym i jego element, zrozumiesz, o co chodzi.
Czy instancja MediaSource jest gotowa?
URL.createObjectURL()
jest synchroniczny, ale przetwarza załącznik asynchronicznie. Z tego powodu może minąć chwila, 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 ona mieć jedną z tych wartości:
closed
– instancjaMediaSource
nie jest przypięta do elementu multimedialnego.open
– instancjaMediaSource
jest dołączona do elementu multimedialnego i jest gotowa do odbioru danych lub odbiera dane.ended
– instancjaMediaSource
jest dołączona do elementu multimedialnego, a wszystkie jej dane zostały przekazane temu elementowi.
Wybieranie tych opcji bezpośrednio może negatywnie wpłynąć na wydajność. Na szczęście MediaSource
wywołuje też zdarzenia, gdy zmienia się 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 zadzwoniłeś/zadzwoniłaś też na numer revokeObjectURL()
. Wiem, że to za wcześnie, ale mogę to zrobić w każdej chwili po połączeniu atrybutu src
elementu multimedialnego z instancją MediaSource
. Wywołanie tej metody nie powoduje usunięcia żadnych obiektów. Umożliwia platformie zarządzanie zbieraniem odpadów w odpowiednim czasie, dlatego wywołuję je natychmiast.
Tworzenie SourceBuffer
Teraz czas utworzyć obiekt SourceBuffer
, który przenosi dane między źródłami multimediów a elementami multimediów. 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 ciąg mime dla pliku wideo, ale używa osobnych kodeków do części wideo i audio.
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
Jeśli wyszukasz w internecie przykłady MSE, znajdziesz wiele plików, które pobierają pliki multimedialne za pomocą XHR. Aby stworzyć bardziej zaawansowane rozwiązanie, użyję interfejsu Fetch API i zwróconego przez niego obietnika. Jeśli próbujesz to zrobić w Safari, nie uda Ci się to bez 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>;
}
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. Możesz znać terminy DASH lub HLS, które są dwoma metodami realizacji tego procesu. Pełna dyskusja na ten temat 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łanie 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);
});
}
Wersja ostateczna
Oto pełny przykład kodu. 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);
});
}