Stitching Together MP3 Playback in HTML5 Audio with ES6
npm install @alonetone/stitches
Stitching Together MP3 Playback in HTML5 Audio with ES6
To distill and codify 13+ years experience of building dozens of web music players into a tight, usable library with 0 dependencies.
* Is written in ES6+
* Deals with "unlocking" audio elements from their auto-play restrictions to enable playlist playback
* Completely ignores the Web Audio API (which doesn't allow buffering, therefore useless for music playback)
* Only handles the MP3 format (pragmatically, the only format that matters)
* Lets you decide if you want to babel things or just include in a
* Is defensive, but doesn't test for browsers or feature detect
* Aims to perform well by doing the minimum amount of work necessary
This is what stitches assumes your default case is:
* You have at least one playlist (you can have more than one on a page)
* You might want to preload one of the tracks
* You'd love to have continuous playback within a playlist (preload upcoming tracks)
* You care as much about mobile as you do desktop
* You might have a SPA or a Rails turbolinks app and don't want to create and destroy tags willy nilly but instead reuse them.
* Provide support for a global player
* Deal with volume
* Spport any other format than mp3 (might work, might not, who knows)
Unfortunately the state of HTML5 Audio support on browsers has not evolved much in the last decade, leaving the API incomplete and unreliable across platforms. Stitches has your back by:
* Unlocking multiple audio nodes on an interaction so you can play through multiple tracks
* Abstracts out and normalizes HTML5 audio events so that they actually work cross-browser (For example, onended in iOS was broken for years).
* Comes with defaults that enable gapless playback, with an ability to tune.
yarn add @alonetone/stitches
With a bundler like Webpack you can:
import Playlist from 'stitches'
With raw html, you can:
```
We stitch together
Ideally browsers would be able to play a list of audio tracks. Instead we are stuck with creating individual
From an experience point of view, we want to provide gapless playback of an album or playlist. We definitely don't want to force the user to keep interacting to get the next song. Ideally, they can open up new browser tabs and do their thing and an album will happily stream in the background.
To mitigate the fact that browsers sabotage this ability, we create a NodePool that holds AudioNodes (a wrapper around an individual
For continuous/gapless playback we really only need two
The following options can be passed to new Playlist
The only required option. No default, so you need to pass in some selector such as .track.
This selector determines which elements are considered to be tracks in this playlist.
The element type doesn't matter. What matters is other selectors such as playButtonSelector need to be children of this selector.
Optional. Defaults to -1 which doesn't load a track in the playlist.
If set to an integer, it'll preload that index, so 0 will preload the first track in the playlist.
Optional. Defaults to the first child of tracksSelector.
Which child element of tracksSelector should be considered the playButton?
Optional. Defaults to the first progress child of tracksSelector and fails silently if not present.
if the element is a progress element, this will update the attribute called value during the whilePlaying callback.
For other elements, it will set the element's style.width to be the appropriate percentage.
Optional. Defaults to the first progress child of tracksSelector and fails silently if not present.
This will register a click handler on the element so it can be used to seek the track.
Optional. Defaults to the first
Optional. No default. Expects a function.
The provided function is called repeatedly during a track's playback.
Optional. No default. Expects a function.
The provided function is called if there's a problem with loading or playback.
Good for registering error notification such as Bugsnag, etc.
Returns an attribute named error which contains a name and a message as described in the standard.
Optional. Defaults to false, keeping the console nice and clear.
This is also exposed a static setter Log.logToConsole in case there's a need for a lil runtime funtime.
Stitches emits a number of events to deliver what an app typically expects from a player. Underneath the hook, many of these are built upon the dumpster fire that is HTML5 audio events, but with additional sanity checks and detail.
The follow event detail are included in all track events.
time: A Number in seconds specifying how far in the track the audio element has playedfileName: A String containing all characters after the last / in the mp3 url (or data for inline data)duration: A Number in seconds specifying how long the mp3 istimeFromEnd: The number in seconds before the end of the mp3percentPlayed: A float number between 0 and 1.0 specifying the current playback positioncurrentTime: A formatted String representing the elapsed time, such as "0:00" or "1:23"`
Please be aware that for the earlier events like track:create or track:loading, most of these values will be 0 or NaN, as their values are not yet known.
Custom event detail can also be emitted on each of these events when it's specifyed in the html via data attributes. Simply prefix the data attribute with "stitches" and make sure the attribute is on each element of .tracksSelector.
For example, if your tracks are each an
and have a database track ID you'd like to send with all events, you could specify and event.detail in js will contain a property trackId with the value "5".$3
track:create
Fires from the javascript's constructor when a track is found in the DOM.track:grabNodeAndSetSrc
Fired right before the track becomes associated with an audio node, either on preload or right before play.track:preload
Fires on attempt to preload a track in a playlist on page load, if and only if preloadIndex is set.track:play
Fires as soon as play() has been called on a track.
Note: This does not mean the track is actively playing yet, only that play has been called.track:pause
Fires when pause() is called.track:loading
An AudioNode was assigned for the track and it has been told to play the appropriate url via the src attribute being set.track:notPlaying
Fired if the attempt to grab an AudioNode fails.track:playing
This is fired as soon as we know for sure the track is actually producing audio and happily playing.
It fires on every transition from a stopped or paused state to a playing one.
Note: it does not fire after seeking if seeking occurred while track was already playing.track:whilePlaying
This is repeatedly called, a few times a second, while a track is actively producing audio.track:ended
This is called when a track is finished. It does not rely on the somewhat sketchy nature of