Invoering
Media Source Extensions (MSE) bieden uitgebreide buffer- en afspeelcontrole voor de HTML5 <audio>
- en <video>
-elementen. Hoewel oorspronkelijk ontwikkeld om op Dynamic Adaptive Streaming via HTTP (DASH) gebaseerde videospelers mogelijk te maken, zullen we hieronder zien hoe ze kunnen worden gebruikt voor audio; specifiek voor gapless afspelen .
Je hebt waarschijnlijk naar een muziekalbum geluisterd waarop nummers naadloos over de nummers vloeiden; Misschien luister je er nu zelfs naar. Artiesten creëren deze ononderbroken afspeelervaringen zowel als een artistieke keuze als als een artefact van vinylplaten en cd's waarbij audio is geschreven als één continue stroom. Helaas gaat deze naadloze auditieve ervaring tegenwoordig vaak verloren door de manier waarop moderne audiocodecs zoals MP3 en AAC werken.
We zullen hieronder ingaan op de details van het waarom, maar laten we voor nu beginnen met een demonstratie. Hieronder staan de eerste dertig seconden van het uitstekende Sintel , in vijf afzonderlijke MP3-bestanden gehakt en opnieuw samengesteld met behulp van MSE. De rode lijnen geven hiaten aan die zijn ontstaan tijdens het maken (codering) van elke MP3; op deze punten hoor je storingen.
Bah! Dat is geen geweldige ervaring; wij kunnen het beter doen. Met wat meer werk, door exact dezelfde MP3-bestanden in de bovenstaande demo te gebruiken, kunnen we MSE gebruiken om die vervelende gaten te verwijderen. De groene lijnen in de volgende demo geven aan waar de bestanden zijn samengevoegd en de gaten zijn verwijderd. Op Chrome 38+ wordt dit naadloos afgespeeld!
Er zijn verschillende manieren om gapless inhoud te creëren . Voor de doeleinden van deze demo concentreren we ons op het type bestanden dat een normale gebruiker rondslingert. Waar elk bestand afzonderlijk is gecodeerd, zonder rekening te houden met de audiosegmenten ervoor of erna.
Basisopstelling
Laten we eerst teruggaan naar de basisinstellingen van een MediaSource
instantie. Mediabronextensies zijn, zoals de naam al aangeeft, slechts uitbreidingen op de bestaande media-elementen. Hieronder wijzen we een Object URL
toe, die onze MediaSource
instantie vertegenwoordigt, aan het bronkenmerk van een audio-element; net zoals u een standaard URL zou instellen.
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);
Zodra het MediaSource
object is verbonden, zal het een initialisatie uitvoeren en uiteindelijk een sourceopen
gebeurtenis activeren; op welk punt we een SourceBuffer
kunnen maken. In het bovenstaande voorbeeld maken we een audio/mpeg
-versie, die onze MP3-segmenten kan parseren en decoderen; er zijn nog verschillende andere soorten beschikbaar.
Afwijkende golfvormen
We komen zo terug op de code, maar laten we nu eens nader kijken naar het bestand dat we zojuist hebben toegevoegd, vooral aan het einde ervan. Hieronder ziet u een grafiek van de laatste 3000 samples, gemiddeld over beide kanalen, van de track sintel_0.mp3
. Elke pixel op de rode lijn is een drijvende-kommamonster in het bereik van [-1.0, 1.0]
.
Hoe zit het met al die nul (stille) samples!? Ze zijn eigenlijk te wijten aan compressieartefacten die tijdens het coderen zijn geïntroduceerd. Bijna elke encoder introduceert een soort opvulling. In dit geval heeft LAME precies 576 opvulvoorbeelden aan het einde van het bestand toegevoegd.
Naast de opvulling aan het einde, werd aan elk bestand ook opvulling aan het begin toegevoegd. Als we vooruit kijken naar het sintel_1.mp3
-nummer, zien we dat er nog eens 576 voorbeelden van opvulling aan de voorkant staan. De hoeveelheid opvulling varieert per coderingsprogramma en inhoud, maar we kennen de exacte waarden op basis van metadata
in elk bestand.
De stukken stilte aan het begin en einde van elk bestand zijn de oorzaak van de haperingen tussen de segmenten in de vorige demo. Om gapless afspelen te bereiken, moeten we deze delen van stilte verwijderen. Gelukkig is dit eenvoudig te doen met MediaSource
. Hieronder zullen we onze onAudioLoaded()
-methode aanpassen om een toevoegvenster en een tijdstempel-offset te gebruiken om deze stilte te verwijderen.
Voorbeeldcode
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);
}
Een naadloze golfvorm
Laten we eens kijken wat onze glimmende nieuwe code heeft bereikt door nog eens naar de golfvorm te kijken nadat we onze toevoegvensters hebben toegepast. Hieronder kun je zien dat het stille gedeelte aan het einde van sintel_0.mp3
(in rood) en het stille gedeelte aan het begin van sintel_1.mp3
(in blauw) zijn verwijderd; waardoor we een naadloze overgang tussen segmenten hebben.
Conclusie
Daarmee hebben we alle vijf de segmenten naadloos tot één samengevoegd en zijn we vervolgens aan het einde van onze demo gekomen. Voordat we verder gaan, is het je misschien opgevallen dat onze methode onAudioLoaded()
geen rekening houdt met containers of codecs. Dat betekent dat al deze technieken zullen werken, ongeacht het container- of codectype. Hieronder kun je de originele demo DASH-ready gefragmenteerde MP4 afspelen in plaats van MP3.
Als je meer wilt weten, bekijk dan de onderstaande bijlagen voor een dieper inzicht in het creëren van content zonder onderbrekingen en het parseren van metagegevens. Je kunt ook gapless.js
verkennen om de code achter deze demo nader te bekijken.
Bedankt voor het lezen!
Bijlage A: Ononderbroken inhoud creëren
Het kan lastig zijn om gapless content te creëren. Hieronder bespreken we de creatie van de Sintel- media die in deze demo worden gebruikt. Om te beginnen heb je een kopie nodig van de verliesvrije FLAC-soundtrack voor Sintel; voor het nageslacht is de SHA1 hieronder opgenomen. Voor tools heb je FFmpeg , MP4Box , LAME en een OSX-installatie met afconvert nodig.
unzip Jan_Morgenstern-Sintel-FLAC.zip
sha1sum 1-Snow_Fight.flac
# 0535ca207ccba70d538f7324916a3f1a3d550194 1-Snow_Fight.flac
Eerst splitsen we de eerste 31,5 seconden op van het 1-Snow_Fight.flac
-nummer. We willen ook een fade-out van 2,5 seconden toevoegen vanaf 28 seconden om klikken te voorkomen zodra het afspelen is voltooid. Met behulp van de onderstaande FFmpeg-opdrachtregel kunnen we dit allemaal bereiken en de resultaten in sintel.flac
plaatsen.
ffmpeg -i 1-Snow_Fight.flac -t 31.5 -af "afade=t=out:st=28:d=2.5" sintel.flac
Vervolgens splitsen we het bestand op in 5 wave- bestanden van elk 6,5 seconden; het is het gemakkelijkst om wave te gebruiken, omdat bijna elke encoder de opname ervan ondersteunt. Nogmaals, we kunnen dit precies doen met FFmpeg, waarna we het volgende hebben: sintel_0.wav
, sintel_1.wav
, sintel_2.wav
, sintel_3.wav
en 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
Laten we vervolgens de MP3-bestanden maken. LAME heeft verschillende opties voor het creëren van gapless content. Als u de controle heeft over de inhoud, kunt u overwegen --nogap
te gebruiken met een batchcodering van alle bestanden om opvulling tussen segmenten helemaal te voorkomen. Voor de doeleinden van deze demo willen we echter die opvulling, dus gebruiken we een standaard VBR-codering van hoge kwaliteit voor de wave-bestanden.
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
Dat is alles wat nodig is om de MP3-bestanden te maken. Laten we nu het maken van de gefragmenteerde MP4-bestanden bespreken. We zullen de aanwijzingen van Apple volgen voor het maken van media die geschikt zijn voor iTunes . Hieronder zullen we de wave-bestanden converteren naar tussenliggende CAF- bestanden, volgens de instructies, voordat we ze coderen als AAC in een MP4- container met behulp van de aanbevolen parameters.
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
We hebben nu verschillende M4A-bestanden die we op de juiste manier moeten fragmenteren voordat ze met MediaSource
kunnen worden gebruikt. Voor onze doeleinden gebruiken we een fragmentgrootte van één seconde. MP4Box schrijft elke gefragmenteerde MP4 uit als sintel_#_dashinit.mp4
samen met een MPEG-DASH-manifest ( sintel_#_dash.mpd
) dat kan worden weggegooid.
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
Dat is het! We hebben nu gefragmenteerde MP4- en MP3-bestanden met de juiste metagegevens die nodig zijn voor naadloos afspelen. Zie bijlage B voor meer details over hoe die metadata er precies uitzien.
Bijlage B: Ononderbroken metadata parseren
Net zoals het creëren van gapless content, kan het parseren van de gapless metadata lastig zijn, omdat er geen standaardmethode voor opslag bestaat. Hieronder bespreken we hoe de twee meest voorkomende encoders, LAME en iTunes, hun gapless metadata opslaan. Laten we beginnen met het opzetten van enkele hulpmethoden en een overzicht voor de ParseGaplessData()
die hierboven is gebruikt.
// 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.
We zullen eerst het iTunes-metagegevensformaat van Apple bespreken, omdat dit het gemakkelijkst te ontleden en uit te leggen is. Binnen MP3- en M4A-bestanden schrijft iTunes (en afconvert) een kort gedeelte in ASCII, zoals:
iTunSMPB[ 26 bytes ]0000000 00000840 000001C0 0000000000046E00
Dit wordt geschreven in een ID3-tag in de MP3-container en in een metadata-atoom in de MP4-container. Voor onze doeleinden kunnen we het eerste token 0000000
negeren. De volgende drie tokens zijn de voorste opvulling, de eindopvulling en het totale aantal niet-opgevulde monsters. Door elk van deze te delen door de bemonsteringssnelheid van de audio, krijgen we de duur van elk.
// 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);
}
Aan de andere kant zullen de meeste open source MP3-encoders de gapless metadata opslaan in een speciale Xing-header die in een stil MPEG-frame is geplaatst (het is stil, dus decoders die de Xing-header niet begrijpen, zullen gewoon stilte afspelen). Helaas is deze tag niet altijd aanwezig en heeft deze een aantal optionele velden. Voor de doeleinden van deze demo hebben we controle over de media, maar in de praktijk zullen er enkele extra controles nodig zijn om te weten wanneer gapless metadata daadwerkelijk beschikbaar zijn.
Eerst analyseren we het totale aantal monsters. Voor de eenvoud lezen we dit uit de Xing-header, maar het zou kunnen worden opgebouwd uit de normale MPEG-audioheader . Xing-headers kunnen worden gemarkeerd met een Xing
of Info
tag. Precies 4 bytes na deze tag staan er 32 bits die het totale aantal frames in het bestand vertegenwoordigen; Door deze waarde te vermenigvuldigen met het aantal samples per frame, krijgen we het totale aantal samples in het bestand.
// 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.
Nu we het totale aantal monsters hebben, kunnen we verder gaan met het uitlezen van het aantal opvulmonsters. Afhankelijk van uw coderingsprogramma kan dit worden geschreven onder een LAME- of Lavf-tag, genest in de Xing-header. Precies 17 bytes na deze header zijn er 3 bytes die de voor- en eindopvulling vertegenwoordigen in respectievelijk 12 bits.
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
};
}
Daarmee hebben we een complete functie voor het ontleden van de overgrote meerderheid van de gapless inhoud. Er zijn echter zeker randgevallen, dus voorzichtigheid is geboden voordat u soortgelijke code in de productie gebruikt.
Bijlage C: Over afvalinzameling
Geheugen dat bij SourceBuffer
-instanties hoort, wordt actief verzameld op basis van inhoudstype, platformspecifieke limieten en de huidige afspeelpositie. In Chrome wordt eerst geheugen teruggewonnen uit reeds afgespeelde buffers. Als het geheugengebruik echter de platformspecifieke limieten overschrijdt, wordt geheugen uit niet-afgespeelde buffers verwijderd.
Wanneer het afspelen een gat in de tijdlijn bereikt als gevolg van teruggewonnen geheugen, kan het haperen als het gat klein genoeg is, of volledig vastlopen als het gat te groot is. Geen van beide is een geweldige gebruikerservaring, dus het is belangrijk om te voorkomen dat u te veel gegevens tegelijk toevoegt en handmatig bereiken van de mediatijdlijn verwijdert die niet langer nodig zijn.
Bereiken kunnen worden verwijderd via de remove()
methode op elke SourceBuffer
; wat een [start, end]
bereik in seconden duurt. Net als bij appendBuffer()
zal elke remove()
een updateend
gebeurtenis activeren zodra deze is voltooid. Andere verwijderingen of toevoegingen mogen pas worden uitgegeven nadat het evenement is geactiveerd.
In desktop-Chrome kunt u ongeveer 12 megabytes aan audio-inhoud en 150 megabytes aan video-inhoud tegelijk in het geheugen bewaren. U moet niet op deze waarden vertrouwen in verschillende browsers of platforms; Ze zijn bijvoorbeeld zeker niet representatief voor mobiele apparaten.
Het verzamelen van afval heeft alleen invloed op gegevens die zijn toegevoegd aan SourceBuffers
; er zijn geen grenzen aan de hoeveelheid gegevens die u in JavaScript-variabelen kunt bufferen. Indien nodig kunt u dezelfde gegevens ook opnieuw op dezelfde positie toevoegen.