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;
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 atrybutemSourceBuffer
do przesyłania elementu multimedialnego. - wywołanie
fetch()
lub XHR w celu pobrania danych multimedialnych w obiekcieResponse
. - Wywołanie aplikacji
Response.arrayBuffer()
dotyczące źródła treściMediaSource.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>
i<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.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 prawdziwa przed wywołaniem funkcjiMediaSource.endOfStream()
lub zaktualizowaniem elementuMediaSource.duration
. - Jeśli
MediaSource.readyState
ma wartośćended
, wywołania takie jakappendBuffer()
iremove()
albo ustawienieSourceBuffer.mode
lubSourceBuffer.timestampOffset
spowodują przeniesienie 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 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.');
}
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
– instancjaMediaSource
nie jest przypięta do elementu multimedialnego.open
– instancjaMediaSource
jest dołączona do elementu multimedialnego i jest gotowa do odbierania danych lub właśnie je odbiera.ended
– instancjaMediaSource
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);
});
}