Einführung
Media Source Extensions (MSE) bieten eine erweiterte Pufferung und Wiedergabesteuerung für die HTML5-Elemente <audio>
und <video>
. Sie wurden ursprünglich entwickelt, um Videoplayer auf der Grundlage von DASH (Dynamic Adaptive Streaming over HTTP) zu ermöglichen. Im Folgenden erfahren Sie, wie sie für Audio verwendet werden: speziell für die lückenlose Wiedergabe.
Wahrscheinlich hast du schon einmal ein Musikalbum gehört, auf dem Songs nahtlos über verschiedene Titel hinweg abgespielt wurden. vielleicht hörst du dir ja gerade eins an. Künstler schaffen diese lückenlose Wiedergabe nicht nur als künstlerische Alternative, sondern auch als Artefakt aus Schallplatten und CDs, bei denen Audioinhalte in einem fortlaufenden Stream geschrieben wurden. Leider gehen moderne Audio-Codecs wie MP3 und AAC leider oft verloren.
Die Gründe dafür werden wir weiter unten näher erläutern. Beginnen wir zunächst mit einer Demonstration. Unten sehen Sie die ersten 30 Sekunden des hervorragenden Sintel, das in fünf separate MP3-Dateien zerlegt und mithilfe von MSE wieder zusammengesetzt wurde. Die roten Linien kennzeichnen Lücken, die bei der Erstellung (Codierung) der einzelnen MP3-Dateien entstanden sind. sind an diesen Stellen Störungen zu hören.
Igitt! Das ist kein gutes Erlebnis. können wir es besser machen. Wenn wir dieselben MP3-Dateien wie in der obigen Demo verwenden, können wir diese lästigen Lücken mit MSE entfernen. Die grünen Linien in der nächsten Demo zeigen an, wo die Dateien verbunden und die Lücken entfernt wurden. Bei Chrome 38+ funktioniert die Wiedergabe nahtlos.
Es gibt verschiedene Möglichkeiten, unterbrechungsfreie Inhalte zu erstellen. In dieser Demo konzentrieren wir uns auf die Dateitypen, die ein normaler Nutzer in der Nähe liegen könnte. Dabei wurde jede Datei separat codiert, ohne die Audiosegmente davor oder danach zu berücksichtigen.
Grundlegende Einrichtung
Sehen wir uns zuerst die grundlegende Einrichtung einer MediaSource
-Instanz an. Medienquellenerweiterungen sind, wie der Name schon sagt, lediglich Erweiterungen der vorhandenen Medienelemente. Im Folgenden weisen wir dem Quellattribut eines Audioelements ein Object URL
-Element zu, das unsere MediaSource
-Instanz darstellt. genau wie eine Standard-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);
Sobald das MediaSource
-Objekt verbunden ist, führt es eine Initialisierung durch und löst schließlich ein sourceopen
-Ereignis aus. Dann können wir eine SourceBuffer
erstellen. Im obigen Beispiel erstellen wir ein audio/mpeg
-Element, das unsere MP3-Segmente parsen und decodieren kann. sind mehrere weitere Typen verfügbar.
Anomale Wellenformen
Wir kommen gleich auf den Code zurück, aber schauen wir uns nun die Datei, die wir gerade angehängt haben, genauer an, insbesondere am Ende. Unten siehst du ein Diagramm mit den letzten 3.000 Samples, deren Durchschnitt aus beiden Kanälen aus dem sintel_0.mp3
-Track ermittelt wurde. Jedes Pixel auf der roten Linie ist ein Gleitkommabeispiel im Bereich von [-1.0, 1.0]
.
Was ist das denn mit diesen null (stummen) Samples? Tatsächlich sind sie auf Komprimierungsartefakte zurückzuführen, die während der Codierung eingeführt werden. Fast jeder Encoder bietet eine Art von Padding. In diesem Fall fügte LAME am Ende der Datei genau 576 Padding-Samples hinzu.
Zusätzlich zum Abstand am Ende wurde jeder Datei am Anfang ein Innenrand hinzugefügt. Wenn wir einen Blick auf den sintel_1.mp3
-Track werfen, sehen wir, dass am Anfang weitere 576 Beispiele für einen Abstand vorhanden sind. Das Ausmaß des Abstands variiert je nach Encoder und Inhalt. Wir kennen jedoch die genauen Werte basierend auf dem metadata
in den einzelnen Dateien.
In der vorherigen Demo verursachen die Abschnitte der Stille am Anfang und Ende jeder Datei Fehler zwischen den Segmenten. Für eine unterbrechungsfreie Wiedergabe müssen wir diese Abschnitte der Stille entfernen. Zum Glück ist das mit MediaSource
ganz einfach. Im Folgenden ändern wir unsere onAudioLoaded()
-Methode, um ein Anfügefenster und einen Zeitstempelversatz zu verwenden, um diese Stille zu entfernen.
Beispielcode
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);
}
Eine nahtlose Wellenform
Sehen wir uns nun an, was unser nagelneuer Code erreicht hat, indem wir uns die Wellenform noch einmal ansehen, nachdem wir unsere Anfüge-Fenster angewendet haben. Unten sehen Sie, dass der lautlose Abschnitt am Ende von sintel_0.mp3
(in rot) und der lautlose Abschnitt am Anfang von sintel_1.mp3
(in Blau) entfernt wurden. So ist ein nahtloser Wechsel
zwischen den Segmenten möglich.
Fazit
Damit haben wir alle fünf Segmente nahtlos zu einem zusammengeführt und sind somit am Ende unserer Demo angelangt. Ihnen ist vielleicht schon aufgefallen, dass bei der Methode onAudioLoaded()
keine Container oder Codecs berücksichtigt werden. Das bedeutet, dass alle diese Techniken unabhängig vom Container- oder Codec-Typ funktionieren. Unten können Sie die ursprüngliche Demo als DASH-fähige, fragmentierte MP4-Datei anstelle von MP3 wiedergeben.
In den nachfolgenden Anhängen findest du weitere Informationen zur lückenlosen Inhaltserstellung und zum Parsen von Metadaten. Sie können sich auch den Code für diese Demo in gapless.js
genauer ansehen.
Vielen Dank, dass Sie sich die Zeit zum Lesen dieser E-Mail genommen haben.
Anhang A: Lückenlose Inhalte erstellen
Es ist nicht immer einfach, Inhalte ohne Unterbrechungen zu erstellen. Im Folgenden zeigen wir Ihnen Schritt für Schritt, wie die in dieser Demo verwendeten Sintel-Medien erstellt werden. Zuerst benötigen Sie eine Kopie des verlustfreien FLAC-Soundtracks für Sintel. Der SHA1-Wert ist nachfolgend angegeben. Für Tools benötigen Sie FFmpeg, MP4Box, LAME und eine OSX-Installation mit afconvert.
unzip Jan_Morgenstern-Sintel-FLAC.zip
sha1sum 1-Snow_Fight.flac
# 0535ca207ccba70d538f7324916a3f1a3d550194 1-Snow_Fight.flac
Zuerst teilen wir die ersten 31, 5 Sekunden des Titels 1-Snow_Fight.flac
auf. Außerdem möchten wir ein 2,5-sekündiges Ausblenden nach 28 Sekunden hinzufügen, um nach dem Ende der Wiedergabe Klicks zu vermeiden. Mit der folgenden FFmpeg-Befehlszeile können wir all dies erreichen und die Ergebnisse in sintel.flac
einfügen.
ffmpeg -i 1-Snow_Fight.flac -t 31.5 -af "afade=t=out:st=28:d=2.5" sintel.flac
Als Nächstes teilen wir die Datei in fünf Wave-Dateien mit je 6, 5 Sekunden auf. Es ist am einfachsten zu verwenden, da fast jeder Encoder die Datenaufnahme unterstützt. Auch hier können wir genau dies mit FFmpeg tun. Danach geben wir sintel_0.wav
, sintel_1.wav
, sintel_2.wav
, sintel_3.wav
und sintel_4.wav
ein.
ffmpeg -i sintel.flac -acodec pcm_f32le -map 0 -f segment \
-segment_list out.list -segment_time 6.5 sintel_%d.wav
Als Nächstes erstellen wir die MP3-Dateien. LAME bietet mehrere Möglichkeiten, lückenlose Inhalte zu erstellen. Wenn du die Kontrolle über den Inhalt hast, kannst du --nogap
mit einer Batchcodierung aller Dateien verwenden, um einen Abstand zwischen den Segmenten ganz zu vermeiden. Für diese Demo verwenden wir jedoch eine standardmäßige VBR-Codierung der Wave-Dateien mit hoher Qualität.
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
Das ist zum Erstellen der MP3-Dateien nicht erforderlich. Kommen wir nun zur Erstellung der fragmentierten MP4-Dateien. Wir folgen der Anleitung von Apple zum Erstellen von Medien, die für iTunes gemastert werden. Unten werden die Wave-Dateien gemäß der Anleitung in CAF-Zwischendateien konvertiert, bevor sie mit den empfohlenen Parametern als AAC in einem MP4-Container codiert werden.
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
Wir haben jetzt mehrere M4A-Dateien, die wir entsprechend fragmentieren müssen, bevor sie mit MediaSource
verwendet werden können. Für unsere Zwecke verwenden wir eine Fragmentgröße von einer Sekunde. MP4Box gibt jede fragmentierte MP4-Datei als sintel_#_dashinit.mp4
zusammen mit einem MPEG-DASH-Manifest aus (sintel_#_dash.mpd
), das verworfen werden kann.
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
Fertig! Wir haben jetzt MP4- und MP3-Dateien mit den richtigen Metadaten fragmentiert, die für eine Wiedergabe ohne Unterbrechung erforderlich sind. Weitere Details dazu, wie diese Metadaten aussehen, finden Sie in Anhang B.
Anhang B: Lückenlose Metadaten parsen
Genau wie das Erstellen von lückenlosen Inhalten kann das Parsen der lückenlosen Metadaten schwierig sein, da es keine Standardmethode für die Speicherung gibt. Im Folgenden zeigen wir dir, wie die beiden gängigsten Encoder, LAME und iTunes, ihre Metadaten ohne Unterbrechung speichern. Zuerst werden einige Hilfsmethoden und eine Übersicht für das oben verwendete ParseGaplessData()
eingerichtet.
// 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.
Wir sprechen zunächst über das iTunes-Metadatenformat von Apple, da es am einfachsten zu parsen und zu erklären ist. Schreiben Sie in MP3- und M4A-Dateien in iTunes (und afconvert) einen kurzen Abschnitt in ASCII wie folgt:
iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00
Diese wird in ein ID3-Tag innerhalb des MP3-Containers und in einem Metadaten-Atom im MP4-Container geschrieben. Für unsere Zwecke können wir das erste 0000000
-Token ignorieren. Die nächsten drei Tokens sind der vordere Padding-Wert, der Endabstand und die Gesamtzahl der Stichproben ohne Padding. Wenn wir jedes dieser Elemente durch die Abtastrate der Audiodaten dividieren, erhalten wir die jeweilige Dauer.
// 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);
}
Andererseits speichern die meisten Open-Source-MP3-Encoder die lückenlosen Metadaten in einem speziellen Xing-Header, der sich innerhalb eines stillen MPEG-Frames befindet. Decoder, die den Xing-Header nicht verstehen, spielen einfach nur Stille ab. Leider ist dieses Tag nicht immer vorhanden und verfügt über eine Reihe optionaler Felder. Für diese Demo haben wir Kontrolle über die Medien, aber in der Praxis sind einige zusätzliche Prüfungen erforderlich, um festzustellen, wann lückenlose Metadaten tatsächlich verfügbar sind.
Zunächst analysieren wir die Gesamtstichprobenanzahl. Der Einfachheit halber lesen wir diesen Header aus dem Xing-Header ab, er könnte aber auch aus dem normalen MPEG-Audio-Header erstellt werden. Xing-Header können entweder mit einem Xing
- oder einem Info
-Tag gekennzeichnet werden. Genau 4 Byte nach diesem Tag befinden sich 32 Bits, die die Gesamtzahl der Frames in der Datei darstellen. Multiplizieren Sie diesen Wert mit der Anzahl der Samples pro Frame, erhalten wir die Gesamtzahl der Stichproben in der Datei.
// 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.
Da wir nun die Gesamtzahl der Stichproben haben, können wir mit dem Auslesen der Anzahl der Padding-Beispiele fortfahren. Je nach Encoder kann dies in einem LAME- oder Lavf-Tag angegeben werden, das im Xing-Header verschachtelt ist. Genau 17 Byte nach diesem Header befinden sich 3 Byte für das Front- und End-Padding in jeweils 12 Bit.
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
};
}
Damit haben wir eine vollständige Funktion zum Parsen der überwiegenden Mehrheit der lückenlosen Inhalte. Es gibt jedoch zahlreiche Grenzfälle, daher sollten Sie vorsichtig sein, bevor Sie ähnlichen Code in der Produktion verwenden.
Anhang C: Über die automatische Speicherbereinigung
Der zu SourceBuffer
-Instanzen gehörende Arbeitsspeicher wird je nach Inhaltstyp, plattformspezifischen Limits und der aktuellen Wiedergabeposition aktiv automatisch bereinigt. In Chrome wird der Arbeitsspeicher zuerst aus bereits abgespielten Zwischenspeichern freigegeben. Wenn die Arbeitsspeichernutzung jedoch plattformspezifische Limits überschreitet, wird der Arbeitsspeicher aus nicht wiedergegebenen Zwischenspeichern entfernt.
Wenn die Wiedergabe aufgrund von freigegebenem Speicher eine Lücke in der Zeitachse erreicht, kann es zu einer Störung kommen, wenn die Lücke klein genug ist, oder vollständig verzögert werden, wenn die Lücke zu groß ist. Beides ist keine gute Nutzererfahrung. Daher ist es wichtig, nicht zu viele Daten auf einmal anzuhängen und Bereiche, die nicht mehr erforderlich sind, manuell aus der Medienzeitachse zu entfernen.
Bereiche können mit der Methode remove()
auf jedem SourceBuffer
entfernt werden. Das ist ein [start, end]
-Bereich in Sekunden. Ähnlich wie bei appendBuffer()
löst jedes remove()
-Element ein updateend
-Ereignis aus, sobald es abgeschlossen ist. Andere Elemente zum Entfernen oder Anhängen sollten erst ausgeführt werden, wenn das Ereignis ausgelöst wird.
In der Desktopversion von Chrome können Sie etwa 12 Megabyte Audio- und 150 Megabyte Videoinhalte gleichzeitig speichern. Sie sollten sich nicht bei verschiedenen Browsern oder Plattformen auf diese Werte verlassen. Sie sind beispielsweise höchstwahrscheinlich nicht repräsentativ für Mobilgeräte.
Die automatische Speicherbereinigung wirkt sich nur auf Daten aus, die „SourceBuffers
“ hinzugefügt wurden. Es gibt keine Einschränkungen dafür, wie viele Daten in JavaScript-Variablen zwischengespeichert werden können. Falls erforderlich, können Sie dieselben Daten an derselben Position noch einmal anhängen.