RelistenNet/gapless.js: Gapless audio playback javascript plugin (for seamless audio playback)

✨ Discover this insightful post from Hacker News 📖

📂 **Category**:

✅ **What You’ll Learn**:

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.

Live Demo

import Queue from 'gapless';

const player = new Queue( 'LOADING' );

player.play();

Constructor Options (GaplessOptions)

‘LOADED’ , // Called at ~60fps while playing onEnded: () => ⚡, // Called when the last track ends onPlayNextTrack: (info) => {}, // Called when advancing to next track onPlayPreviousTrack: (info) => {},// Called when going to previous track onStartNewTrack: (info) => {}, // Called whenever a new track becomes current onError: (error) => {}, // Called on audio errors onPlayBlocked: () => {}, // Called when autoplay is blocked by the browser onDebug: (msg) => {}, // Internal debug messages (development only) webAudioIsDisabled: false, // Disable Web Audio API (disables gapless playback) trackMetadata: [], // Per-track metadata (aligned by index) volume: 1, // Initial volume, 0.0–1.0 });”>

const player = new Queue({
  tracks: [],              // Initial list of track URLs
  onProgress: (info) => {},     // Called at ~60fps while playing
  onEnded: () => {},            // Called when the last track ends
  onPlayNextTrack: (info) => {},    // Called when advancing to next track
  onPlayPreviousTrack: (info) => {},// Called when going to previous track
  onStartNewTrack: (info) => {},    // Called whenever a new track becomes current
  onError: (error) => {},       // Called on audio errors
  onPlayBlocked: () => {},      // Called when autoplay is blocked by the browser
  onDebug: (msg) => {},         // Internal debug messages (development only)
  webAudioIsDisabled: false,    // Disable Web Audio API (disables gapless playback)
  trackMetadata: [],            // Per-track metadata (aligned by index)
  volume: 1,                    // Initial volume, 0.0–1.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)
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

All callbacks and getters return TrackInfo objects — plain data snapshots with no methods:

interface TrackInfo {
  index: number;                    // Position in the queue
  currentTime: number;              // Playback position in seconds
  duration: number;                 // Total duration (NaN until loaded)
  isPlaying: boolean;
  isPaused: boolean;
  volume: number;
  trackUrl: string;                 // Resolved audio URL
  playbackType: 'HTML5' | 'WEBAUDIO';
  webAudioLoadingState: 'NONE' | 'LOADING' | 'LOADED' | 'ERROR';
  metadata?: TrackMetadata;
  machineState: string;             // Internal state machine state
}
player.addTrack('https://example.com/track.mp3', {
  skipHEAD: true,       // Skip HEAD request for URL resolution
  metadata: {
    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:

interface TrackMetadata {
  title?: string;
  artist?: string;
  album?: string;
  artwork?: MediaImage[];
  [key: string]: unknown;
}

v4 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 webAudioIsDisabled: true 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 the volume getter.

MIT

{💬|⚡|🔥} **What’s your take?**
Share your thoughts in the comments below!

#️⃣ **#RelistenNetgapless.js #Gapless #audio #playback #javascript #plugin #seamless #audio #playback**

🕒 **Posted on**: 1772480607

🌟 **Want more?** Click here for more info! 🌟

By

Leave a Reply

Your email address will not be published. Required fields are marked *