Gapless audio player for the web. Takes an array of audio tracks and uses HTML5 audio with the Web Audio API to enable seamless, gapless transitions between tracks.
Though the earnest goal is not bundle-size driven, it has only one production dependency (xstate) so it operates in a rigid manner according to a well-designed state machine.
It has a dead simple API and is easy to get up and running.
Built for Relisten.net, where playing back gapless live tracks is paramount.
pnpm install gaplessimportQueuefrom'gapless';constplayer=newQueue({tracks: ['https://example.com/track1.mp3','https://example.com/track2.mp3','https://example.com/track3.mp3',],onProgress: (track)=>{console.log(`${track.currentTime} / ${track.duration}`);},onEnded: ()=>{console.log('Queue finished');},});player.play();constplayer=newQueue({tracks: [],// Initial list of track URLsonProgress: (info)=>{},// Called at ~60fps while playingonEnded: ()=>{},// Called when the last track endsonPlayNextTrack: (info)=>{},// Called when advancing to next trackonPlayPreviousTrack: (info)=>{},// Called when going to previous trackonStartNewTrack: (info)=>{},// Called whenever a new track becomes currentonError: (error)=>{},// Called on audio errorsonPlayBlocked: ()=>{},// Called when autoplay is blocked by the browseronDebug: (msg)=>{},// Internal debug messages (development only)playbackMethod: 'HYBRID',// 'HYBRID' | 'HTML5_ONLY' | 'WEBAUDIO_ONLY'trackMetadata: [],// Per-track metadata (aligned by index)volume: 1,// Initial volume, 0.0–1.0preloadNumTracks: 2,// Number of tracks to preload ahead (0 to disable)playbackRate: 1,// Initial playback rate, 0.25–4.0});| Method | Description |
|---|---|
play() | Start or resume playback |
pause() | Pause playback |
togglePlayPause() | Toggle between play and pause |
next() | Advance to the next track |
previous() | Go to previous track (restarts current track if > 8s in) |
gotoTrack(index, playImmediately?) | Jump to a track by index |
seek(time) | Seek to a position in seconds |
setVolume(volume) | Set volume (0.0–1.0) |
setPlaybackRate(rate) | Set playback rate (0.25–4.0), reschedules gapless transitions |
addTrack(url, options?) | Add a track to the end of the queue |
removeTrack(index) | Remove a track by index |
resumeAudioContext() | Resume the AudioContext (for browsers that require user gesture) |
destroy() | Clean up all resources |
| Getter | Type | Description |
|---|---|---|
currentTrack | TrackInfo | undefined | Snapshot of the current track |
currentTrackIndex | number | Index of the current track |
tracks | readonly TrackInfo[] | Snapshot of all tracks |
isPlaying | boolean | Whether the queue is playing |
isPaused | boolean | Whether the queue is paused |
volume | number | Current volume |
playbackRate | number | Current playback rate |
preloadNumTracks | number | Number of tracks to preload ahead (read/write) |
All callbacks and getters return TrackInfo objects — plain data snapshots with no methods:
interfaceTrackInfo{index: number;// Position in the queuecurrentTime: number;// Playback position in secondsduration: number;// Total duration (NaN until loaded)isPlaying: boolean;isPaused: boolean;volume: number;trackUrl: string;// Resolved audio URLplaybackType: 'HTML5'|'WEBAUDIO';webAudioLoadingState: 'NONE'|'LOADING'|'LOADED'|'ERROR';metadata?: TrackMetadata;playbackRate: number;// Current playback ratemachineState: string;// Internal state machine state}player.addTrack('https://example.com/track.mp3',{skipHEAD: true,// Skip HEAD request for URL resolutionmetadata: {title: 'Track Title',artist: 'Artist',album: 'Album',artwork: [{src: 'https://example.com/art.jpg',sizes: '512x512',type: 'image/jpeg'}],},});Metadata is used for the Media Session API (lock screen controls, browser media UI) and can contain arbitrary additional fields:
interfaceTrackMetadata{title?: string;artist?: string;album?: string;artwork?: MediaImage[];[key: string]: unknown;}The playbackMethod option controls how audio is rendered:
| Value | Behavior | Use case |
|---|---|---|
'HYBRID' (default) | Starts with HTML5 audio, switches to Web Audio after decode | Remote files — instant playback + gapless transitions |
'HTML5_ONLY' | HTML5 audio exclusively, no Web Audio | When Web Audio is unavailable or unwanted; gapless playback disabled |
'WEBAUDIO_ONLY' | Web Audio API exclusively, no HTML5 fallback | Very small or local files where buffering is instant |
// Web Audio only — waits for decode before playingconstplayer=newQueue({tracks: ['track1.mp3','track2.mp3'],playbackMethod: 'WEBAUDIO_ONLY',});player.play();// Waits for decode, then plays via Web AudioControl how many tracks are preloaded ahead of the current track:
constplayer=newQueue({tracks: ['a.mp3','b.mp3','c.mp3','d.mp3'],preloadNumTracks: 1,// Only preload 1 track ahead (default: 2)});// Can also be changed at runtime:player.preloadNumTracks=0;// Disable preloadingplayer.preloadNumTracks=3;// Preload 3 aheadControl the speed of playback (0.25x to 4x). Gapless scheduling automatically adjusts for the current rate:
constplayer=newQueue({tracks: ['a.mp3','b.mp3'],playbackRate: 1.5,// Start at 1.5x});player.play();player.setPlaybackRate(2);// Change to 2x mid-playbackconsole.log(player.playbackRate);// 2v4 is a complete rewrite. The public API has changed:
| v3 | v4 |
|---|---|
import GaplessQueue from 'gapless.js' | import Queue from 'gapless' (or import { Queue }) |
player.playNext() | player.next() |
player.playPrevious() | player.previous() |
player.resetCurrentTrack() | player.seek(0) |
player.disableWebAudio() | Pass playbackMethod: 'HTML5_ONLY' in constructor |
player.nextTrack | player.tracks[player.currentTrackIndex + 1] |
track.completeState | Callbacks now receive TrackInfo objects |
| Callbacks receive Track instances | Callbacks receive plain TrackInfo data snapshots |
- State machines: Internally uses XState for queue and track state management. XState is bundled — no extra dependency needed.
- ESM only: Published as ES module only. No CommonJS build.
- TrackInfo: All callbacks and getters return plain data objects (
TrackInfo) instead of Track class instances. - Media Session: Built-in support for the Media Session API via
trackMetadata. - Volume: Volume is now set via
setVolume(n)and readable via thevolumegetter.
MIT