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:
| Field | Type | Notes |
|---|---|---|
httpHeaders | Record<string, string> | Applied to every remote request (media, cache, artwork). |
userAgent | string | Overrides the default user agent. |
autoRetries | number | Transient-error retries; clamped 0–5 (default 3). |
retryBackoffMs | number | Clamped 200–5000 (default 500). |
networkTimeoutMs | number | Android only; clamped 1000–60000 (default 30000). |
audioContentType | 'music' | 'speech' | Default 'music'. Pinned at first configure. |
audioCategoryOptions | { duckOthers?, mixWithOthers?, interruptSpokenAudio? } | iOS audio-session behaviour. |
visualizationEnabled | boolean | Default true. Disable to skip the visualizer audio chain. Pinned at configure. |
clampSeekToBuffered | boolean | Default false. Cap a seek target past the buffered position at that edge (local / fully-buffered tracks seek freely). See Buffer state. |
lookaheadCacheMaxSizeMb | number | Disk 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. |
progressUpdateIntervalMs | number | Default 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. |
backgroundProgressUpdateIntervalMs | number | Default = 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';
| Mode | Behaviour |
|---|---|
none | Pitch follows the rate (chipmunk / slow-down effect). |
voice | Pitch preserved, speech-tuned. Default. |
music | Pitch 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 byforwardJumpInterval/backwardJumpIntervalseconds. 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();
| Event | Callback signature |
|---|---|
onStateChange | (state: PlayerState, reason: StateChangeReason) => void |
onTrackChange | (track: TrackItem | undefined, index: number, reason: TrackChangeReason, transitionGapMs: number | undefined) => void — transitionGapMs 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
onProgressfan-out only. The native sampler keeps running, soonPlaybackMilestone(scrobbling) and the lock-screen / now-playing position are unaffected — reducing the background rate does not delay milestones. 500ms 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.
700still yields ~2 Hz and1000yields ~1 Hz. - Applied at
configure(); a re-configure()updates the live rate with no engine rebuild. UsebackgroundProgressUpdateIntervalMsto 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 duration —
setSleepTimer(15 * 60)pauses in 15 minutes. Pass0(or a non-positive value) to clear. - End of track —
setSleepTimerToTrackEnd()pauses when the current track finishes.endsAtEpochMs/remainingSecondsare absent whileendOfTrackistrue; once the track enters the final-minute window the timer converts to a concrete countdown —endOfTrackflips tofalseandendsAtEpochMs/remainingSecondsappear. - 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
onStateChangewith 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.
Related
- Equalizer, Visualizer, Look-ahead cache, Cast manager
- React hooks
- Automotive & voice registration (
setBrowseSnapshot,onBrowseRequest,registerPlaybackService,isCarConnected, …) — see the Android Auto, CarPlay, and Voice guides.