Skip to main content

TrackPlayer

The core playback API. Get the singleton with getTrackPlayer():

import { getTrackPlayer } from 'react-native-queue-player';

const player = getTrackPlayer();

All mutating methods return a Promise; all get* methods are synchronous snapshots; all on* methods return a disposer function — call it to unsubscribe.

Lifecycle

configure

configure(config: PlayerConfig): Promise<void>

Sets up the engine. Each call rebuilds from a clean slate (it runs an internal destroy() first), so call it once at app start. All PlayerConfig fields are optional:

FieldTypeNotes
httpHeadersRecord<string, string>Applied to every remote request (media, cache, artwork).
userAgentstringOverrides the default user agent.
autoRetriesnumberTransient-error retries; clamped 0–5 (default 3).
retryBackoffMsnumberClamped 200–5000 (default 500).
networkTimeoutMsnumberAndroid only; clamped 1000–60000 (default 30000).
audioContentType'music' | 'speech'Default 'music'. Pinned at first configure.
audioCategoryOptions{ duckOthers?, mixWithOthers?, interruptSpokenAudio? }iOS audio-session behaviour.
visualizationEnabledbooleanDefault true. Disable to skip the visualizer audio chain. Pinned at configure.
clampSeekToBufferedbooleanDefault false. Cap a seek target past the buffered position at that edge (local / fully-buffered tracks seek freely). See Buffer state.
lookaheadCacheMaxSizeMbnumberDisk budget for the look-ahead cache in MB (default 100). Configure-time only — the cache size is fixed at construction. See Look-ahead cache.
lookaheadCacheEvictionPolicy'lru' | 'fifo'Cache eviction order: 'lru' (default) keeps recently-played tracks; 'fifo' keeps the newest-fetched. Configure-time only — changing it clears + rebuilds the cache. See Look-ahead cache.
progressUpdateIntervalMsnumberDefault 500. Interval between onProgress emissions in the foreground. 500 is the floor (the native sampler runs at 2 Hz; lower values clamp to 500). See Progress update rate.
backgroundProgressUpdateIntervalMsnumberDefault = progressUpdateIntervalMs. Reduced onProgress interval while the app is backgrounded (e.g. 10000). Milestones + lock-screen updates are unaffected. See Progress update rate.

destroy

destroy(): Promise<void>

Tears down the engine, audio session, and observers. Idempotent.

Queue

The queue is a flat, app-driven list; the library does not persist it.

setQueue(tracks: TrackItem[], startAtIndex?: number): Promise<void>
addToQueue(tracks: TrackItem[], insertBefore?: number): Promise<void> // appends when insertBefore omitted
removeFromQueue(indices: number[]): Promise<void>
moveInQueue(fromIndex: number, toIndex: number): Promise<void>
getQueue(): TrackItem[]
clearQueue(): Promise<void>

TrackItem

Only url is required. Every other field is optional metadata. id is provided for your convenience — it's echoed back to you in remote-command callbacks and voice-query results — so you don't need to supply one, and duplicate or missing ids are fine.

type TrackItem = {
url: string; // required — http(s):// or file://
id?: string; // optional; echoed back to you in callbacks + voice results
title?: string;
artist?: string;
album?: string;
duration?: number; // seconds
artworkUrl?: string;
extras?: Record<string, string>; // opaque passthrough
};

Transport

play(): Promise<void>
pause(): Promise<void>
stop(): Promise<void>
retry(): Promise<void> // re-attempt the current track after its auto-retries were exhausted
seekTo(position: number): Promise<void> // seconds from the start of the current track
skipToIndex(index: number): Promise<void>
skipToNext(): Promise<void>
skipToPrevious(): Promise<void>

The player auto-retries transient errors (see autoRetries). retry() is the manual escape hatch for a "tap to retry" button: once a track has errored and its automatic retries are exhausted, retry() gives it one fresh attempt (e.g. the user left WiFi coverage so the retries failed, then came back in range and taps retry). A no-op when there is no current track or it is not in the error state.

Seeking is guarded by seekability: seekTo (and a lock-screen interval skip) is a no-op on a genuinely unseekable track — a live stream, or a variable-bitrate MP3 with no seek index — rather than resetting it to the start. Check getIsSeekable() to grey out a scrubber. The lib enables constant-bitrate seeking, so CBR MP3 / ADTS / AMR streams (common for transcoded audio) are seekable and expose an estimated duration.

Playback mode

setPlaybackMode / getPlaybackMode

setPlaybackMode(mode: PlaybackMode): Promise<void>
getPlaybackMode(): PlaybackMode
type PlaybackMode = {
kind: 'gapless' | 'crossfade';
crossfadeDurationMs?: number; // required when kind === 'crossfade'
};

For crossfade, crossfadeDurationMs must be within crossfadeDurationBounds{ minMs: 1000, maxMs: 12000, stepMs: 500 } (1–12 s, in 500 ms steps):

import { crossfadeDurationBounds } from 'react-native-queue-player';

await player.setPlaybackMode({ kind: 'crossfade', crossfadeDurationMs: 4000 });
await player.setPlaybackMode({ kind: 'gapless' });

Playback speed & pitch correction

setPlaybackSpeed(rate: number): Promise<void> // playback rate; min 0.25
getPlaybackSpeed(): number
setPitchCorrectionMode(mode: PitchCorrectionMode): Promise<void>
getPitchCorrectionMode(): PitchCorrectionMode

setPlaybackSpeed changes the playback rate. PitchCorrectionMode controls how pitch behaves when the rate is not 1.0:

type PitchCorrectionMode = 'none' | 'voice' | 'music';
ModeBehaviour
nonePitch follows the rate (chipmunk / slow-down effect).
voicePitch preserved, speech-tuned. Default.
musicPitch preserved, music-tuned (transient-preserving).

The mode persists until you change it and is auto-bypassed at 1.0×, so there is no pitch processing at normal speed.

Volume, repeat, shuffle

setVolume(volume: number): Promise<void> // 0.0 – 1.0
getVolume(): number
setRepeatMode(mode: RepeatMode): Promise<void> // 'off' | 'queue' | 'track'
shuffleQueue(): Promise<ShuffleResult> // one-shot shuffle; resets to index 0

ShuffleResult is { tracks: TrackItem[]; currentIndex: number; currentTrack?: TrackItem }.

Remote controls (lock screen / notification skip)

setRemoteControls(options: RemoteControlOptions): Promise<void>
getRemoteControls(): RemoteControlOptions

interface RemoteControlOptions {
skipMode: 'track' | 'interval'; // default 'track'
forwardJumpInterval: number; // seconds, default 15
backwardJumpInterval: number; // seconds, default 15
}

Choose what the lock-screen / notification / CarPlay skip controls do:

  • 'track' (default) — previous / next track.
  • 'interval' — jump forward / back within the current track by forwardJumpInterval / backwardJumpInterval seconds. The seek is performed natively.

Applies immediately, so you can let the user pick their preferred controls at runtime. The interval is a free number of seconds; a common in-app picker offers 10 / 15 / 30 / 45 / 60. On iOS the two modes map to nextTrack/previousTrack vs skipForward/skipBackward (with the interval as the preferredIntervals); on Android they swap the notification's back/forward buttons.

State (synchronous reads)

getState(): PlayerState // 'none'|'loading'|'buffering'|'playing'|'paused'|'ended'|'error'
getCurrentTrackIndex(): number // -1 when empty / nothing selected yet
getCurrentTrackSource(): TrackSource | undefined // 'local' | 'cached' | 'streaming'
getPlaybackSpeed(): number
getVolume(): number
getCanSkipNext(): boolean
getCanSkipPrevious(): boolean
getSkipCapability(): SkipCapability // { canSkipNext, canSkipPrevious }
getSleepTimer(): SleepTimerState // { active, endsAtEpochMs?, remainingSeconds?, endOfTrack }
getBufferState(): BufferState // 'empty' | 'buffering' | 'stalled' | 'full'
getIsSeekable(): boolean // false for a genuinely unseekable stream
isFullyBuffered(): boolean // whole track downloaded

Events

Each returns a disposer:

const off = player.onStateChange((state, reason) => { /* … */ });
// later:
off();
EventCallback signature
onStateChange(state: PlayerState, reason: StateChangeReason) => void
onTrackChange(track: TrackItem | undefined, index: number, reason: TrackChangeReason, transitionGapMs: number | undefined) => voidtransitionGapMs is the native-measured inter-track silence in ms, set only on a natural gapless auto-advance and undefined otherwise (a < 50ms regression-check seed). Always null-check it: crossfade and cast advances also report reason: 'auto-advance' but carry undefined
onProgress(progress: { position, duration, buffered }) => void (default ~2 Hz, seconds; rate is configurable — see Progress update rate)
onPlaybackMilestone(milestone: number, trackIndex: number) => void (fires once each at 25 / 50 / 75 / 90 % of a track — for scrobbling / "mark as played")
onError(error: PlaybackError) => void
onQueueEnd() => void
onSkipCapabilityChange(capability: SkipCapability) => void (fires once on subscribe)
onSleepTimerChange(state: SleepTimerState) => void (fires once on subscribe, then on arm / clear / end-of-track conversion / fire)
onBufferStateChange(state: BufferState) => void ('empty' | 'buffering' | 'stalled' | 'full'; fires once on subscribe, then on change — native-driven, see Buffer state)
onFullyBufferedChange(fullyBuffered: boolean) => void (fires once on subscribe, then when the whole track finishes downloading — see Buffer state)

PlaybackError includes a normalized code (PlaybackErrorCode), a fatal flag, the offending url, and the underlying native error fields. code is one of: 'TIMEOUT', 'CONNECTION_LOST', 'NETWORK_UNREACHABLE', 'HTTP_ERROR', 'AUTH_ERROR', 'CLEARTEXT_NOT_PERMITTED', 'FILE_FORMAT_NOT_RECOGNIZED', 'DECODER_NOT_FOUND', 'DECODING_FAILED', 'SOURCE_UNREACHABLE', 'CANNOT_OPEN', 'UNKNOWN'.

In React, prefer the hooks — they manage subscribe/unsubscribe for you.

Progress update rate

onProgress fires at a fixed 2 Hz (500 ms) by default. Two configure options tune the rate:

configure({
progressUpdateIntervalMs: 500, // foreground (default 500; 500 is the floor)
backgroundProgressUpdateIntervalMs: 10000, // ~0.1 Hz while backgrounded (default = foreground)
});
  • The throttle applies to the JS onProgress fan-out only. The native sampler keeps running, so onPlaybackMilestone (scrobbling) and the lock-screen / now-playing position are unaffected — reducing the background rate does not delay milestones.
  • 500 ms is the floor: the native sampler runs at 2 Hz, so a value below 500 clamps to 500 (there is no faster-than-2 Hz foreground rate). Non-positive values fall back to the default. There is no upper limit — a large background value (e.g. 60000) is a valid near-paused rate.
  • Effective rates snap to multiples of the 500 ms sampler tick: an interval emits on the nearest tick at or before it, so e.g. 700 still yields ~2 Hz and 1000 yields ~1 Hz.
  • Applied at configure(); a re-configure() updates the live rate with no engine rebuild. Use backgroundProgressUpdateIntervalMs to stop off-screen re-renders while the app is backgrounded without hand-rolling app-state suppression.

Now Playing format

getNowPlayingFormat(): NowPlayingFormat | null
onNowPlayingFormatChange(callback: (format: NowPlayingFormat | null) => void): () => void

NowPlayingFormat reports the active track's codec, container, mimeType, sampleRate, channelCount, bitrate, durationSec, and ReplayGain metadata (replayGainTrackDb, replayGainAlbumDb, replayGainActive, …). It is null until the format resolves and re-fires on every track change.

ReplayGain

setReplayGainMode(mode: ReplayGainMode): Promise<void> // 'off' | 'track' | 'album' (default 'off')
getReplayGainMode(): ReplayGainMode

Applies loudness normalization to ReplayGain-tagged media. Whether it is currently active for the playing track is reported by NowPlayingFormat.replayGainActive.

Sleep timer

setSleepTimer(seconds: number): Promise<void> // pause after a fixed duration
setSleepTimerToTrackEnd(): Promise<void> // pause when the current track finishes
clearSleepTimer(): Promise<void>
getSleepTimer(): SleepTimerState
onSleepTimerChange(callback: (state: SleepTimerState) => void): () => void

The sleep timer pauses playback (it never stops or clears the queue, so tapping play resumes exactly where it left off). It runs on a native wall-clock deadline, so it keeps counting down whether playback is playing or paused, and survives backgrounding.

SleepTimerState:

interface SleepTimerState {
active: boolean;
endsAtEpochMs?: number; // wall-clock deadline; absent when inactive or awaiting track end
remainingSeconds?: number; // absent when inactive or awaiting track end
endOfTrack: boolean; // true while waiting for the track end; flips false on conversion to a countdown
}

Behaviour:

  • Fixed durationsetSleepTimer(15 * 60) pauses in 15 minutes. Pass 0 (or a non-positive value) to clear.
  • End of tracksetSleepTimerToTrackEnd() pauses when the current track finishes. endsAtEpochMs / remainingSeconds are absent while endOfTrack is true; once the track enters the final-minute window the timer converts to a concrete countdown — endOfTrack flips to false and endsAtEpochMs / remainingSeconds appear.
  • 60-second tail rule — if a fixed-duration deadline lands with 60 seconds or less left on the current track, the pause is deferred to that track's natural end rather than cutting it off mid-play.
  • 10-second fade — playback fades out linearly over the final 10 seconds before the pause. The pre-fade volume is restored on pause, clear, or re-arm, so getVolume() stays truthful.
  • The pause emits onStateChange with reason 'sleep-timer'.

Buffer state

getBufferState(): BufferState // 'empty' | 'buffering' | 'stalled' | 'full'
onBufferStateChange(callback: (state: BufferState) => void): () => void

A discrete buffer state for the current track, sourced from the platform's native buffer signals (iOS AVPlayerItem playback-buffer KVO; Android Media3 STATE_BUFFERING / STATE_READY) rather than the ~2 Hz progress tick — so a loading / stall indicator reacts immediately. onBufferStateChange fires once on subscribe with the current value, then only on an actual change.

  • 'full' — enough is buffered to play smoothly from the current position. Hide the spinner.
  • 'buffering' — loading, and playback has not started yet for this track (the initial buffer, even when play was requested), or loading while not actively trying to play. The expected, non-urgent loading case.
  • 'stalled' — playback had started and is now rebuffering mid-play (it ran out of data but wants to continue). This is where a spinner interrupts audio the user was already hearing.
  • 'empty' — nothing is loaded to play (before the first track loads / after teardown).

'full' describes playback health, not that the whole track is downloaded — for buffered-ahead length in seconds read onProgress's buffered field. In React, use useBufferState().

Buffer state tracks the local engine. During an active Cast session the local engine is paused, so getBufferState() reports the local buffer, not the receiver's — it is not meaningful while casting.

Fully buffered (whole track downloaded)

isFullyBuffered(): boolean
onFullyBufferedChange(callback: (fullyBuffered: boolean) => void): () => void

Distinct from BufferState 'full' (which is "enough buffered to keep up"): isFullyBuffered() is true only once the entire track has downloaded and there is nothing left to transfer. On iOS it's the loaded range reaching the duration (or isPlaybackBufferFull for an unknown-length progressive source); on Android it's bufferedPosition reaching the (possibly CBR-estimated) duration. A live / unknown-length stream that never completes is never fully buffered. Use it to push a buffered bar to 100% or enable a full-length scrubber. In React, use useFullyBuffered().

Clamp seeks to the buffered range (opt-in)

configure({ clampSeekToBuffered: true }); // default false

Off by default. When enabled, a seek target past the current buffered position is capped at that buffered edge, so a scrub doesn't overshoot into a re-buffer. A local or fully-buffered track reports buffered >= target, so nothing is capped and a seek reaches the end freely. This applies to seekTo and to a lock-screen / notification interval skip-forward (see remote controls).

This is separate from the always-on seekability guard above (which no-ops seeks on genuinely unseekable tracks). The clamp is a UX opt-in for seekable streams — it trades "seek anywhere, wait for re-buffer" for "seek only within what's downloaded". It uses only lib-observable facts (buffered position) and makes no assumption about any specific server.