@landr/audio-player
v1.6.0
Published
Shared audio player core (engine-agnostic Player + queue + React provider/hooks).
Readme
@landr/audio-player
Shared, engine-agnostic audio player core: a Player orchestrator + PlayerQueueBasic + a React provider and hooks. Consumers plug in their own IPlayerEngine implementation (mobile uses ExpoAudioPlayer; the package ships a default HtmlAudioEngine for the browser).
Quick start
import { PlayerProvider, usePlayer, useTrackProgress } from '@landr/audio-player';
const App = () => (
<PlayerProvider>
<Controls />
</PlayerProvider>
);
const Controls = () => {
const { currentTrack, state, toggle, skipNext, skipPrevious } = usePlayer();
const progress = useTrackProgress(); // current position in seconds
// ...
};PlayerProvider props
interface PlayerProviderProps {
children: ReactNode;
createEngine?: () => IPlayerEngine; // default: HtmlAudioEngine
createQueue?: () => IPlayerQueue; // default: PlayerQueueBasic
logger?: Logger; // default: noopLogger
onTrackPlayed?: TrackPlayedHandler<Track>; // optional playback analytics hook
recoverPlayback?: RecoverPlaybackHandler<Track>; // optional source-load recovery hook
autoSetup?: boolean; // calls engine.setup() on mount (default: true)
}For mobile or other platforms, supply your own engine:
<PlayerProvider
createEngine={() => new ExpoAudioPlayer({ logger })}
onTrackPlayed={({ track, interactionSource }) => {
void trackPlayedForTrack({ track, interactionSource });
}}
>
<App />
</PlayerProvider>onTrackPlayed is called only after playback starts successfully and only when the action receives a TrackPlayContext.
Use playerTrackPlayInteractionSources for built-in player sources such as next, previous, and queue auto-advance.
recoverPlayback is called when loading a media source fails with PlaybackErrorCode.MediaSourceFailed.
It may return a refreshed queue containing the current track; playback then retries once with the refreshed source.
Logger
interface Logger {
error(message: string, error?: unknown, extraInfo?: Record<string, unknown>): void;
info(message: string, meta?: unknown): void;
debug(message: string, meta?: unknown): void;
}Passed through to the Player and queue decorators for internal diagnostics. extraInfo is forwarded as-is to
your log pipeline (e.g. as Loggly extraInfos) — use it for structured, serializable context such as
{ trackId }, not for the error itself (already the 2nd argument). Defaults to noopLogger.
Hooks
usePlayer()
Returns a UsePlayerResult — the current PlayerSnapshot merged with all player action methods. Subscribe to snapshot changes via useSyncExternalStore under the hood.
const {
// — snapshot fields —
state, // EngineState
currentTrack, // Track | undefined
isShuffleMode, // boolean
repeatMode, // RepeatMode
duration, // seconds
volume, // number (0-1)
hasNext, // boolean
// — actions —
play, // (track: Track, context?: TrackPlayContext) => Promise<void>
playTrack, // (trackId: string, context?: TrackPlayContext) => Promise<void>
reset, // () => Promise<void>
pause, // () => Promise<void>
toggle, // () => Promise<void>
seek, // (position: number) => Promise<void>
setVolume, // (volume: number) => void
skipNext, // (context?: TrackPlayContext) => Promise<void>
skipPrevious, // (context?: TrackPlayContext) => Promise<void>
addNext, // (tracks: Track[]) => void
addToEnd, // (tracks: Track[]) => void
removeFromQueue, // (trackId: string) => void
replaceQueue, // (tracks: Track[], options?: ReplaceQueueOptions<Track>) => Promise<void>
setRepeatMode, // (mode: RepeatMode) => void
cycleRepeatMode, // () => void
setShuffleMode, // (enabled: boolean) => void
toggleShuffleMode, // () => void
} = usePlayer();reset() returns the player to its default runtime state: it stops and unloads active media, clears the queue,
resets progress to 0, clears Media Session controls, sets volume back to 1, disables shuffle, and sets repeat to
RepeatMode.Off.
useTrackProgress()
Returns the current playback position in seconds, updated on every engine timeupdate event.
const position = useTrackProgress(); // number (seconds)usePlayerQueue()
Returns a live QueueSnapshot via useSyncExternalStore.
const {
items, // ReadonlyArray<Track>
currentIndex, // number
currentTrack, // Track | undefined
repeatMode, // RepeatMode
isShuffleEnabled, // boolean
hasNext, // boolean
hasPrevious, // boolean
} = usePlayerQueue();useSeek(params?)
Provides seek UX helpers. Manages a local scrubbing state so the progress bar stays responsive while the user drags.
const { seekValue, isSeeking, onSeekStart, onSeekChange, onSeekEnd } = useSeek();usePlayerInstance()
Returns the raw IPlayer instance from context. Prefer usePlayer() for normal use; reach for this only when you need the full IPlayer interface directly.
const player = usePlayerInstance<MyTrack>();Key types
PlayerTrack
interface PlayerTrack {
id: string;
type: string;
url: string;
title: string;
subtitle?: string;
artworkUrl: string;
source: PlayerItemSource;
}PlayerItemSource
Discriminated union identifying where a track originated.
type PlayerItemSource =
| { type: 'library'; id: string }
| { type: 'playlist'; id: string }
| { type: 'chat'; channelId: string; chatAssetId: string };EngineState
enum EngineState {
Idle,
Loading,
Playing,
Paused,
Ended,
Error,
}RepeatMode
enum RepeatMode {
Off,
Track,
Queue,
}PlayerSnapshot<Track>
interface PlayerSnapshot<Track> {
state: EngineState;
currentTrack?: Track;
isShuffleMode: boolean;
repeatMode: RepeatMode;
duration: number;
volume: number;
hasNext: boolean;
}Custom engine
To run on a non-browser platform, implement IPlayerEngine:
interface IPlayerEngine<Track = PlayerTrack> {
getState(): EngineState;
setup(): Promise<void>;
reset(): Promise<void>;
loadTrack(track: Track, controls: LockScreenControls): Promise<void>;
play(): Promise<void>;
pause(): Promise<void>;
seek(position: number): Promise<void>;
getPosition(): number;
getDuration(): number;
getVolume(): number;
setVolume(volume: number): void;
updateActiveLockScreenControls(controls: LockScreenControls): void;
clearLockScreenControls(): void;
destroy(): Promise<void>;
on<TEvent extends PlayerEngineEvent>(
event: TEvent,
handler: (payload: PlayerEngineEventMap[TEvent]) => void,
): () => void; // returns unsubscribe
}on must support four events:
| Event | Payload | Description |
| ------------ | ------------------------------------- | ------------------------------------------------ |
| 'position' | number | Fired on timeupdate; current position in seconds |
| 'state' | EngineState | Engine state transitions |
| 'remote' | 'next' \| 'previous' \| 'playPause' | Lock screen / remote control commands |
| 'error' | Error | Playback error |
The browser default is HtmlAudioEngine, which wraps HTMLAudioElement and integrates with the Media Session API.
Utils
import {
shuffleModeConfig,
repeatModeConfig,
getNextRepeatMode,
getShuffleModeKey,
noopLogger,
} from '@landr/audio-player';| Symbol | Description |
| ---------------------------- | ----------------------------------------------------------------------------- |
| shuffleModeConfig | Object with 'on' and 'off' keys, each holding accessibility label strings |
| repeatModeConfig | Object keyed by RepeatMode, each entry has a label and nextMode |
| getNextRepeatMode(mode) | Returns the next RepeatMode in the cycle (Off → Queue → Track → Off) |
| getShuffleModeKey(enabled) | Returns 'on' or 'off' for use with shuffleModeConfig |
| noopLogger | A Logger implementation that silently discards all log calls |
Testing entry
import { createTrackMock, createEngineMock } from '@landr/audio-player/testing';The testing entry is Jest-oriented. Load it only from test files or configure it in your Jest globals/types setup.
createTrackMock(id, overrides?)
Builds a PlayerTrack with sensible defaults. Accepts an id and an optional partial override object.
const track = createTrackMock('track-1');
const customTrack = createTrackMock('track-2', {
title: 'My Song',
type: 'original',
source: { type: 'playlist', id: 'p-1' },
});createEngineMock<Track>()
Returns an EngineMock<Track> — a Jest mock implementation of IPlayerEngine with all methods stubbed. Use it to test Player or PlayerProvider in isolation without a real audio element.
const engine = createEngineMock<PlayerTrack>();
// engine.play, engine.pause, engine.loadTrack, etc. are all jest.fn()
render(
<PlayerProvider createEngine={() => engine}>
<Controls />
</PlayerProvider>
);Public API summary
React
| Symbol | Description |
| ---------------------------- | ----------------------------------------------- |
| PlayerProvider | Context provider; accepts PlayerProviderProps |
| usePlayer<Track>() | Snapshot + all player actions |
| usePlayerQueue<Track>() | Live QueueSnapshot |
| useTrackProgress() | Current position in seconds |
| useSeek(params?) | Seek scrubbing helpers |
| usePlayerInstance<Track>() | Raw IPlayer from context |
Classes
| Symbol | Description |
| ------------------------- | ------------------------------------------------------------ |
| Player<Track> | Orchestrates engine + queue; implements IPlayer |
| PlayerQueueBasic<Track> | Default queue with shuffle/repeat; implements IPlayerQueue |
| HtmlAudioEngine<Track> | Default browser engine backed by HTMLAudioElement |
Interfaces & types
IPlayer, IPlayerEngine, IPlayerQueue, IReadableQueue, PlayerTrack, PlayerItemSource, PlayerSnapshot, QueueSnapshot, PlayerDependencies, ReplaceQueueOptions, TrackPlayContext, TrackPlayedEvent, TrackPlayedHandler, PlayerEngineEvent, PlayerEngineEventMap, LockScreenControls, Logger, UsePlayerResult
Enums
EngineState, RepeatMode
Constants
playerTrackPlayInteractionSources
