Documentation Index
Fetch the complete documentation index at: https://mintlify.com/afkcodes/react-native-audio-pro/llms.txt
Use this file to discover all available pages before exploring further.
CDN-signed URLs from providers like AWS S3 or CloudFront have a finite lifetime. If a track’s URL expires mid-queue the player will fail to load it when it becomes active. React Native Audio Pro solves this with updateTrack(), which lets you silently swap a track’s URL in the queue at any time — no playback interruption, no re-buffering of the current track.
AudioPro.updateTrack()
Replaces the track object at a given queue index. The most common use case is supplying a fresh URL while preserving all other track metadata.
AudioPro.updateTrack(index: number, track: AudioProTrack): void
| Parameter | Type | Description |
|---|
index | number | Zero-based position of the track in the queue. |
track | AudioProTrack | Complete replacement track object (spread the original and override url). |
updateTrack() validates the track object before passing it to the native layer. The fields id, url, title, and artwork must all be present — the call is silently dropped and a warning is logged if any of them are missing.
Sliding Window Pattern
The sliding window strategy pre-fetches fresh URLs for the tracks immediately adjacent to the currently playing one. This runs proactively on every TRACK_CHANGED event, so by the time the user reaches the next or previous track its URL is already fresh.
import { AudioPro, AudioProEventType } from 'react-native-audio-pro';
// Your app's function to get a fresh signed URL for a given track ID
async function fetchFreshUrl(trackId: string): Promise<string> {
const response = await fetch(`https://api.example.com/stream/${trackId}`);
const { url } = await response.json();
return url;
}
const subscription = AudioPro.addEventListener(async (event) => {
if (event.type === AudioProEventType.TRACK_CHANGED) {
const { index } = event.payload || {};
if (typeof index === 'number') {
const queue = await AudioPro.getMediaItems();
// Refresh the next track proactively
if (index + 1 < queue.length) {
const nextTrack = queue[index + 1];
if (nextTrack) {
const freshUrl = await fetchFreshUrl(nextTrack.id);
AudioPro.updateTrack(index + 1, { ...nextTrack, url: freshUrl });
}
}
// Refresh the previous track proactively
if (index - 1 >= 0) {
const prevTrack = queue[index - 1];
if (prevTrack) {
const freshUrl = await fetchFreshUrl(prevTrack.id);
AudioPro.updateTrack(index - 1, { ...prevTrack, url: freshUrl });
}
}
}
}
});
Throttle your refresh API calls to avoid hammering your backend. Track the last-refreshed timestamp per track ID and skip the fetch if it was refreshed less than 30 minutes ago — unless the refresh is being forced by an error. This is especially important in large queues where TRACK_CHANGED fires frequently.const lastRefreshMap = new Map<string, number>();
const REFRESH_THROTTLE_MS = 30 * 60 * 1000; // 30 minutes
function shouldRefresh(trackId: string, force = false): boolean {
if (force) return true;
const last = lastRefreshMap.get(trackId) ?? 0;
return Date.now() - last > REFRESH_THROTTLE_MS;
}
Error Recovery Pattern
When a PLAYBACK_ERROR event fires and includes a track index, it usually means the URL for that track has expired. Force-refresh the URL and retry playback using seekToMediaItem() followed by play().
import { AudioPro, AudioProEventType } from 'react-native-audio-pro';
const subscription = AudioPro.addEventListener(async (event) => {
if (event.type === AudioProEventType.PLAYBACK_ERROR) {
const { index } = event.payload || {};
if (typeof index === 'number' && index >= 0) {
const queue = await AudioPro.getMediaItems();
const track = queue[index];
if (!track) return;
// Force a fresh URL, bypassing any throttle
const freshUrl = await fetchFreshUrl(track.id);
AudioPro.updateTrack(index, { ...track, url: freshUrl });
// Give the native layer a moment to register the updated track,
// then seek back to it and resume playback
setTimeout(() => {
AudioPro.seekToMediaItem(index);
setTimeout(() => {
AudioPro.play();
}, 100);
}, 200);
}
}
});
Full Combined Implementation
The pattern below combines both strategies into a single event listener, matching the approach used in the library’s example app.
import { AudioPro, AudioProEventType } from 'react-native-audio-pro';
import type { AudioProTrack } from 'react-native-audio-pro';
const lastRefreshMap = new Map<string, number>();
const REFRESH_THROTTLE_MS = 30 * 60 * 1000;
async function refreshTrackUrl(
index: number,
track: AudioProTrack,
force = false
): Promise<boolean> {
if (!track?.id) return false;
const now = Date.now();
const lastRefresh = lastRefreshMap.get(track.id) ?? 0;
if (!force && now - lastRefresh < REFRESH_THROTTLE_MS) {
return false; // Skip — refreshed recently
}
try {
const freshUrl = await fetchFreshUrl(track.id);
if (freshUrl && (force || freshUrl !== track.url)) {
lastRefreshMap.set(track.id, Date.now());
AudioPro.updateTrack(index, { ...track, url: freshUrl });
return true;
}
} catch (err) {
console.error('[URLRefresh] Failed to fetch stream URL:', err);
}
return false;
}
// Register once at app startup
const subscription = AudioPro.addEventListener(async (event) => {
// Sliding Window: pre-refresh adjacent tracks on every track change
if (event.type === AudioProEventType.TRACK_CHANGED) {
const { index } = event.payload || {};
if (typeof index === 'number') {
const queue = await AudioPro.getMediaItems();
if (index + 1 < queue.length && queue[index + 1]) {
refreshTrackUrl(index + 1, queue[index + 1]!);
}
if (index - 1 >= 0 && queue[index - 1]) {
refreshTrackUrl(index - 1, queue[index - 1]!);
}
}
}
// Error Recovery: force-refresh the errored track and retry
if (event.type === AudioProEventType.PLAYBACK_ERROR) {
const { index } = event.payload || {};
if (typeof index === 'number' && index >= 0) {
const queue = await AudioPro.getMediaItems();
const track = queue[index];
if (!track) return;
const updated = await refreshTrackUrl(index, track, true /* force */);
if (updated) {
setTimeout(() => {
AudioPro.seekToMediaItem(index);
setTimeout(() => AudioPro.play(), 100);
}, 200);
}
}
}
});