Skip to main content

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
ParameterTypeDescription
indexnumberZero-based position of the track in the queue.
trackAudioProTrackComplete 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);
      }
    }
  }
});

Build docs developers (and LLMs) love