npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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