Wprowadzenie
Rozszerzenia źródeł multimedialnych (MSE) zapewniają rozszerzone buforowanie i kontrolę odtwarzania elementów HTML5 <audio>
i <video>
. Choć pierwotna wersja została opracowana w celu ułatwienia dynamicznego adaptacyjnego strumieniowego przesyłania przez HTTP (DASH), poniżej opisujemy, jak można ich używać w przypadku audio. zwłaszcza w przypadku odtwarzania bez przerw.
Pewnie zdarzyło Ci się posłuchać albumu, w którym utwory płynnie płynnie utrzymywały się między utworami. być może w tej chwili jej słuchasz. Wykonawcy tworzą takie odtwarzanie bez przerw, w ramach artystycznego wyboru, a także jako artefakt płyt winylowych i dysków CD, na których dźwięk został zapisany jako jedna ciągła transmisja. Niestety ze względu na sposób, w jaki działają nowoczesne kodeki audio, takie jak MP3 i AAC, brak płynności w odbiorze dźwięku.
Szczegółowe informacje na ten temat omówimy poniżej, ale na razie zaczniemy od demonstracji. Poniżej znajduje się pierwsze 30 sekund świetnego Sintel podzielonego na 5 osobnych plików MP3 i połączonych z użyciem MSE. Czerwone linie oznaczają luki powstałe podczas tworzenia (kodowania) każdego pliku MP3. w tych miejscach pojawią się błędy.
Ojej! Nie jest to przyjemne doświadczenie. co możemy poprawić. Przy odrobinie pracy, używaniu dokładnie tych samych plików MP3 z powyższej prezentacji, możemy użyć MSE, by usunąć te irytujące luki. Zielone linie w następnej wersji demonstracyjnej wskazują, gdzie pliki zostały połączone, a luki zostały usunięte. W Chrome w wersji 38 i nowszych odtwarzanie filmu będzie płynnie odtwarzane.
Istnieje różne sposoby tworzenia treści bez przerw. Na potrzeby tej prezentacji skupimy się na typach plików, które może znajdować zwykły użytkownik. Każdy plik został zakodowany oddzielnie, niezależnie od segmentów audio przed nim i po nim.
Konfiguracja podstawowa
Najpierw cofnijmy się i omówmy podstawową konfigurację instancji MediaSource
. Rozszerzenia źródeł multimediów, jak sama nazwa wskazuje, są tylko rozszerzeniami istniejących elementów multimedialnych. Poniżej przypiszemy wartość Object URL
, reprezentującą nasze wystąpienie MediaSource
, do atrybutu źródła elementu audio. tak samo jak ze standardowym adresem URL.
var audio = document.createElement('audio');
var mediaSource = new MediaSource();
var SEGMENTS = 5;
mediaSource.addEventListener('sourceopen', function() {
var sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
function onAudioLoaded(data, index) {
// Append the ArrayBuffer data into our new SourceBuffer.
sourceBuffer.appendBuffer(data);
}
// Retrieve an audio segment via XHR. For simplicity, we're retrieving the
// entire segment at once, but we could also retrieve it in chunks and append
// each chunk separately. MSE will take care of assembling the pieces.
GET('sintel/sintel_0.mp3', function(data) { onAudioLoaded(data, 0); } );
});
audio.src = URL.createObjectURL(mediaSource);
Po połączeniu obiekt MediaSource
przeprowadzi inicjalizację i na koniec wywoła zdarzenie sourceopen
. możemy wtedy utworzyć SourceBuffer
. W powyższym przykładzie tworzymy plik audio/mpeg
, który umożliwia analizowanie i dekodowanie segmentów MP3. dostępnych jest kilka innych typów.
Nietypowe fale
Za chwilę wrócimy do kodu, ale przyjrzyjmy się bliżej plikowi, który właśnie dodaliśmy, szczególnie na końcu. Poniżej znajduje się wykres przedstawiający uśrednione dane z ostatnich 3000 próbek z obu kanałów ze ścieżki sintel_0.mp3
. Każdy piksel na czerwonej linii jest próbką zmiennoprzecinkową w zakresie [-1.0, 1.0]
.
O co chodzi z tymi zero (cichymi) próbkami? Wynika to z artefaktów kompresji wprowadzonych podczas kodowania. Niemal każdy koder wprowadza jakiś rodzaj dopełnienia. W tym przypadku w tym przypadku funkcja LAME dodała na końcu pliku dokładnie 576 próbek dopełnienia.
Oprócz dopełnienia na końcu każdego pliku zostało też dodane dopełnienie na początku. Jeśli spojrzymy dalej na ścieżkę sintel_1.mp3
, zobaczymy, że z przodu znajduje się kolejnych 576 próbek dopełnienia. Ilość dopełnienia różni się w zależności od kodera i treści, ale znamy dokładne wartości na podstawie wartości metadata
zawartych w każdym pliku.
To właśnie fragmenty ciszy na początku i na końcu każdego pliku są przyczyną błędów między segmentami przedstawionymi w poprzedniej prezentacji. Aby zapewnić płynne odtwarzanie, musimy usunąć te sekcje ciszy. Na szczęście możesz to łatwo zrobić za pomocą usługi MediaSource
. Poniżej zmienimy naszą metodę onAudioLoaded()
tak, aby wykorzystywała okno dołączania i przesunięcie sygnatury czasowej, aby usunąć tę ciszę.
Przykładowy kod
function onAudioLoaded(data, index) {
// Parsing gapless metadata is unfortunately non trivial and a bit messy, so
// we'll glaze over it here; see the appendix for details.
// ParseGaplessData() will return a dictionary with two elements:
//
// audioDuration: Duration in seconds of all non-padding audio.
// frontPaddingDuration: Duration in seconds of the front padding.
//
var gaplessMetadata = ParseGaplessData(data);
// Each appended segment must be appended relative to the next. To avoid any
// overlaps, we'll use the end timestamp of the last append as the starting
// point for our next append or zero if we haven't appended anything yet.
var appendTime = index > 0 ? sourceBuffer.buffered.end(0) : 0;
// Simply put, an append window allows you to trim off audio (or video) frames
// which fall outside of a specified time range. Here, we'll use the end of
// our last append as the start of our append window and the end of the real
// audio data for this segment as the end of our append window.
sourceBuffer.appendWindowStart = appendTime;
sourceBuffer.appendWindowEnd = appendTime + gaplessMetadata.audioDuration;
// The timestampOffset field essentially tells MediaSource where in the media
// timeline the data given to appendBuffer() should be placed. I.e., if the
// timestampOffset is 1 second, the appended data will start 1 second into
// playback.
//
// MediaSource requires that the media timeline starts from time zero, so we
// need to ensure that the data left after filtering by the append window
// starts at time zero. We'll do this by shifting all of the padding we want
// to discard before our append time (and thus, before our append window).
sourceBuffer.timestampOffset =
appendTime - gaplessMetadata.frontPaddingDuration;
// When appendBuffer() completes, it will fire an updateend event signaling
// that it's okay to append another segment of media. Here, we'll chain the
// append for the next segment to the completion of our current append.
if (index == 0) {
sourceBuffer.addEventListener('updateend', function() {
if (++index < SEGMENTS) {
GET('sintel/sintel_' + index + '.mp3',
function(data) { onAudioLoaded(data, index); });
} else {
// We've loaded all available segments, so tell MediaSource there are no
// more buffers which will be appended.
mediaSource.endOfStream();
URL.revokeObjectURL(audio.src);
}
});
}
// appendBuffer() will now use the timestamp offset and append window settings
// to filter and timestamp the data we're appending.
//
// Note: While this demo uses very little memory, more complex use cases need
// to be careful about memory usage or garbage collection may remove ranges of
// media in unexpected places.
sourceBuffer.appendBuffer(data);
}
Płynna fala
Zobaczmy, co udało się osiągnąć dzięki naszemu nowemu kodowi. Przyjrzyjmy się jeszcze raz fali po zastosowaniu okien dołączonych. Poniżej widzimy, że sekcja cicha na końcu sintel_0.mp3
(w kolorze czerwonym) i sekcja cicha na początku sintel_1.mp3
(niebieska) została usunięta. Zapewnia to płynne przejście między segmentami.
Podsumowanie
W ten sposób zszyliśmy wszystkie 5 segmentów w jeden i dotarliśmy do końca prezentacji. Być może zauważyłeś jeszcze, że metoda onAudioLoaded()
nie uwzględnia kontenerów ani kodeków. Oznacza to, że wszystkie te metody będą działać niezależnie od rodzaju kontenera czy kodeka. Poniżej możesz ponownie odtworzyć oryginalną wersję demonstracyjną pliku w formacie DASH z fragmentem kodu w formacie DASH zamiast pliku MP3.
Jeśli chcesz dowiedzieć się więcej, zapoznaj się z dodatkami poniżej, aby dowiedzieć się więcej o tworzeniu treści bez przerw i analizowaniu metadanych. Możesz też zapoznać się z usługą gapless.js
, aby przyjrzeć się bliżej kodowi, na którym opiera się ta wersja demonstracyjna.
Dziękujemy za uwagę!
Załącznik A: Tworzenie treści bez przerw
Prawidłowe tworzenie treści bez przerw może być trudne. Poniżej omówimy proces tworzenia multimediów używanych w tej wersji demonstracyjnej Sintel. Na początek potrzebujesz kopii bezstratnej ścieżki dźwiękowej FLAC firmy Sintel. dla potomności jest dostępny poniżej SHA1. Narzędzia potrzebne są programy FFmpeg, MP4Box, LAME i instalacja OSX z funkcją afconvert.
unzip Jan_Morgenstern-Sintel-FLAC.zip
sha1sum 1-Snow_Fight.flac
# 0535ca207ccba70d538f7324916a3f1a3d550194 1-Snow_Fight.flac
Najpierw rozdzielimy pierwsze 31, 5 sekundy utworu 1-Snow_Fight.flac
. Chcemy też dodać 2,5-sekundowe zanikanie, które rozpoczyna się w 28 s, aby uniknąć kliknięć po zakończeniu odtwarzania. Możesz to zrobić, korzystając z poniższego wiersza poleceń FFmpeg, umieszczając wyniki w formacie sintel.flac
.
ffmpeg -i 1-Snow_Fight.flac -t 31.5 -af "afade=t=out:st=28:d=2.5" sintel.flac
Następnie dzielimy pliki na 5 plików wave każdy po 6, 5 sekundy. najłatwiej jest użyć wave'a, bo prawie każdy koder obsługuje jego przetwarzanie. Ponownie możemy to zrobić w FFmpeg. Po nim mamy: sintel_0.wav
, sintel_1.wav
, sintel_2.wav
, sintel_3.wav
i sintel_4.wav
.
ffmpeg -i sintel.flac -acodec pcm_f32le -map 0 -f segment \
-segment_list out.list -segment_time 6.5 sintel_%d.wav
Teraz utworzymy pliki MP3. LAME ma kilka opcji tworzenia treści bez przerw. Jeśli masz kontrolę nad treścią, rozważ użycie funkcji --nogap
z kodowaniem wsadowym wszystkich plików, aby całkowicie uniknąć dopełnienia między segmentami. Na potrzeby prezentacji chcemy jednak dodać takie dopełnienie, więc zostanie użyte standardowe, wysokiej jakości kodowanie VBR plików wave.
lame -V=2 sintel_0.wav sintel_0.mp3
lame -V=2 sintel_1.wav sintel_1.mp3
lame -V=2 sintel_2.wav sintel_2.mp3
lame -V=2 sintel_3.wav sintel_3.mp3
lame -V=2 sintel_4.wav sintel_4.mp3
To wszystko, co trzeba zrobić, aby utworzyć pliki MP3. Teraz omówimy tworzenie pofragmentowanych plików MP4. Postępujemy zgodnie z instrukcjami Apple dotyczącymi tworzenia multimediów dostosowanych do iTunes. Poniżej przekonwertujemy pliki wave na pośrednie pliki CAF zgodnie z instrukcjami przed zakodowaniem ich jako AAC w kontenerze MP4 z użyciem zalecanych parametrów.
afconvert sintel_0.wav sintel_0_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_1.wav sintel_1_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_2.wav sintel_2_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_3.wav sintel_3_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_4.wav sintel_4_intermediate.caf -d 0 -f caff \
--soundcheck-generate
afconvert sintel_0_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_0.m4a
afconvert sintel_1_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_1.m4a
afconvert sintel_2_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_2.m4a
afconvert sintel_3_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_3.m4a
afconvert sintel_4_intermediate.caf -d aac -f m4af -u pgcm 2 --soundcheck-read \
-b 256000 -q 127 -s 2 sintel_4.m4a
Mamy teraz kilka plików M4A, które musimy odpowiednio fragmentować, zanim będzie można ich używać z usługą MediaSource
. Do naszych celów użyjemy fragmentu o rozmiarze jednej sekundy. MP4Box zapisze każdy pofragmentowany plik MP4 w postaci pliku sintel_#_dashinit.mp4
wraz z plikiem manifestu MPEG-DASH (sintel_#_dash.mpd
), który możesz odrzucić.
MP4Box -dash 1000 sintel_0.m4a && mv sintel_0_dashinit.mp4 sintel_0.mp4
MP4Box -dash 1000 sintel_1.m4a && mv sintel_1_dashinit.mp4 sintel_1.mp4
MP4Box -dash 1000 sintel_2.m4a && mv sintel_2_dashinit.mp4 sintel_2.mp4
MP4Box -dash 1000 sintel_3.m4a && mv sintel_3_dashinit.mp4 sintel_3.mp4
MP4Box -dash 1000 sintel_4.m4a && mv sintel_4_dashinit.mp4 sintel_4.mp4
rm sintel_{0,1,2,3,4}_dash.mpd
Znakomicie. Obecnie mamy pofragmentowane pliki MP4 i MP3 z odpowiednimi metadanymi niezbędnymi do odtwarzania bez przerw. W Załączniku B znajdziesz więcej informacji na temat tego, jak wyglądają takie metadane.
Załącznik B. Analiza metadanych bez luk
Podobnie jak w przypadku tworzenia treści bez przerw, analizowanie metadanych bez przerw może być trudne, ponieważ nie istnieje żadna standardowa metoda przechowywania. Poniżej omówimy, w jaki sposób dwa najpopularniejsze kodery – LAME i iTunes – przechowują metadane bez odstępów. Zacznijmy od skonfigurowania kilku metod pomocniczych i opisu używanego powyżej narzędzia typu ParseGaplessData()
.
// Since most MP3 encoders store the gapless metadata in binary, we'll need a
// method for turning bytes into integers. Note: This doesn't work for values
// larger than 2^30 since we'll overflow the signed integer type when shifting.
function ReadInt(buffer) {
var result = buffer.charCodeAt(0);
for (var i = 1; i < buffer.length; ++i) {
result <<../= 8;
result += buffer.charCodeAt(i);
}
return result;
}
function ParseGaplessData(arrayBuffer) {
// Gapless data is generally within the first 512 bytes, so limit parsing.
var byteStr = new TextDecoder().decode(arrayBuffer.slice(0, 512));
var frontPadding = 0, endPadding = 0, realSamples = 0;
// ... we'll fill this in as we go below.
Zaczniemy od omówienia formatu metadanych Apple iTunes, ponieważ jest on najłatwiejszy do analizowania i objaśniania. W plikach MP3 i M4A iTunes (i afconvert) zapisz w ASCII krótką sekcję w następujący sposób:
iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00
Są one zapisywane w tagu ID3 w kontenerze MP3 oraz w atomie metadanych wewnątrz kontenera MP4. Do naszych celów możemy zignorować pierwszy token 0000000
. Następne 3 tokeny to dopełnienie z przodu, dopełnienie końcowe i całkowita liczba próbek bez dopełnienia. Dzieląc każdy z nich przez częstotliwość próbkowania dźwięku, uzyskujemy czas trwania każdej z nich.
// iTunes encodes the gapless data as hex strings like so:
//
// 'iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00'
// 'iTunSMPB[ 26 bytes ]####### frontpad endpad real samples'
//
// The approach here elides the complexity of actually parsing MP4 atoms. It
// may not work for all files without some tweaks.
var iTunesDataIndex = byteStr.indexOf('iTunSMPB');
if (iTunesDataIndex != -1) {
var frontPaddingIndex = iTunesDataIndex + 34;
frontPadding = parseInt(byteStr.substr(frontPaddingIndex, 8), 16);
var endPaddingIndex = frontPaddingIndex + 9;
endPadding = parseInt(byteStr.substr(endPaddingIndex, 8), 16);
var sampleCountIndex = endPaddingIndex + 9;
realSamples = parseInt(byteStr.substr(sampleCountIndex, 16), 16);
}
Z drugiej strony większość koderów MP3 typu open source przechowuje metadane bez przerw w specjalnym nagłówku Xing umieszczonym w cichej ramce MPEG (funkcja działa w tle, więc dekodery, które nie rozpoznają nagłówka Xing, odtwarzają dźwięk cichy). Ten tag nie zawsze jest dostępny i ma wiele pól opcjonalnych. Na potrzeby tej wersji demonstracyjnej mamy kontrolę nad multimediami, ale w praktyce wymagane są dodatkowe kontrole, aby określić, kiedy pełne metadane są dostępne.
Najpierw przeanalizujemy łączną liczbę próbek. Dla uproszczenia odczytamy go z nagłówka Xing, ale możemy go utworzyć na podstawie zwykłego nagłówka audio MPEG. Nagłówki Xing mogą być oznaczone tagiem Xing
lub Info
. Dokładnie 4 bajty po tagu są 32-bitowe elementy reprezentujące łączną liczbę klatek w pliku; pomnożenie tej wartości przez liczbę próbek w ramce daje nam łączną liczbę próbek w pliku.
// Xing padding is encoded as 24bits within the header. Note: This code will
// only work for Layer3 Version 1 and Layer2 MP3 files with XING frame counts
// and gapless information. See the following document for more details:
// http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
var xingDataIndex = byteStr.indexOf('Xing');
if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Info');
if (xingDataIndex != -1) {
// See section 2.3.1 in the link above for the specifics on parsing the Xing
// frame count.
var frameCountIndex = xingDataIndex + 8;
var frameCount = ReadInt(byteStr.substr(frameCountIndex, 4));
// For Layer3 Version 1 and Layer2 there are 1152 samples per frame. See
// section 2.1.5 in the link above for more details.
var paddedSamples = frameCount * 1152;
// ... we'll cover this below.
Skoro znamy już łączną liczbę próbek, możemy przejść do odczytywania liczby próbek dopełnienia. W zależności od kodera kod może być zapisany pod tagiem LAME lub Lavf umieszczonym w nagłówku Xing. Dokładnie 17 bajtów po tym nagłówku znajdują się 3 bajty reprezentujące dopełnienie z przodu i końca w 12-bitowym formacie każdy.
xingDataIndex = byteStr.indexOf('LAME');
if (xingDataIndex == -1) xingDataIndex = byteStr.indexOf('Lavf');
if (xingDataIndex != -1) {
// See http://gabriel.mp3-tech.org/mp3infotag.html#delays for details of
// how this information is encoded and parsed.
var gaplessDataIndex = xingDataIndex + 21;
var gaplessBits = ReadInt(byteStr.substr(gaplessDataIndex, 3));
// Upper 12 bits are the front padding, lower are the end padding.
frontPadding = gaplessBits >> 12;
endPadding = gaplessBits & 0xFFF;
}
realSamples = paddedSamples - (frontPadding + endPadding);
}
return {
audioDuration: realSamples * SECONDS_PER_SAMPLE,
frontPaddingDuration: frontPadding * SECONDS_PER_SAMPLE
};
}
W tym celu mamy do dyspozycji kompletną funkcję analizowania większości treści bez przerw. Przypadki z pewnością się zdarzają, dlatego zalecamy ostrożność przy korzystaniu z podobnego kodu w środowisku produkcyjnym.
Załącznik C. W przypadku czyszczenia pamięci
Pamięć należąca do instancji SourceBuffer
jest aktywnie wykorzystywana do czyszczenia pamięci zgodnie z typem treści, limitami dotyczącymi platformy i bieżącą pozycją odtwarzania. W Chrome pamięć zostanie najpierw odzyskana z odtwarzanych buforów. Jeśli jednak wykorzystanie pamięci przekroczy limity określone na platformie, zostanie usunięta z nieodtworzonych buforów.
Gdy odtwarzanie osiągnie lukę na osi czasu z powodu odzyskanej pamięci, może pojawić się błąd, jeśli przerwa jest wystarczająco mała, lub całkowicie przestać działać, jeśli przerwa będzie zbyt duża. Nie jest to też wygodne dla użytkowników, dlatego należy unikać dołączania zbyt wielu danych naraz i ręcznego usuwania zakresów z osi czasu multimediów, które nie są już potrzebne.
Zakresy można usuwać za pomocą metody remove()
dla każdej wartości SourceBuffer
; który w sekundach ma zakres [start, end]
. Podobnie jak w sytuacji appendBuffer()
, po zakończeniu każde zdarzenie remove()
wywoła zdarzenie updateend
. Inne usunięcia lub dołączenia nie powinny być wprowadzane do momentu uruchomienia zdarzenia.
W Chrome na komputery możesz jednocześnie przechowywać około 12 MB treści audio i 150 MB treści wideo. Nie należy polegać na tych wartościach w zależności od przeglądarki lub platformy. np. z pewnością nie są reprezentatywne dla urządzeń mobilnych.
Usuwanie czyszczenia ma wpływ tylko na dane dodane do SourceBuffers
. nie ma ograniczeń co do ilości danych, które można buforować w zmiennych JavaScriptu. W razie potrzeby możesz ponownie dołączyć te same dane w tym samym miejscu.