◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-03-28Intermediate

Build a Music Player App with Rork — Audio Playback Implementation Guide Using expo-av

Learn how to build a music player app with Rork and expo-av. This step-by-step guide covers playback controls, seek bars, playlist management, and background audio with practical code examples.

Rork585expo-av6audio playbackmusic playerReact Native239app development39

Setup and context — Why Build a Music Player App?

A music player app is one of the best learning projects for mobile developers. It naturally combines essential skills — state management for playback controls, real-time UI updates for seek bars, list management for playlists, and platform-specific configuration for background audio.

With Rork, you can leverage AI assistance to scaffold a React Native-based audio player quickly, then refine the implementation with your own logic. In this guide, we'll walk through building a practical music player from scratch using expo-av, Expo's built-in audio and video library.

What you'll learn:

  • Loading and controlling audio files with expo-av
  • Implementing seek bars and volume controls
  • Managing playlists with track switching
  • Configuring background audio playback

This guide is aimed at developers who have some experience with Rork and understand basic React Native concepts.

Setting Up expo-av in Your Rork Project

Start by creating a new project in Rork. Tell Rork's AI something like "I want to build a music player app," and it will generate a solid project structure for you.

expo-av comes bundled with the Expo SDK, so it works out of the box in Rork projects. If you need to install it manually:

# Install expo-av
npx expo install expo-av

Next, configure the audio mode when your app starts. Call Audio.setAudioModeAsync to set up playback behavior:

// hooks/useAudioSetup.ts
import { Audio } from 'expo-av';
import { useEffect } from 'react';
 
export function useAudioSetup() {
  useEffect(() => {
    async function configureAudio() {
      await Audio.setAudioModeAsync({
        allowsRecordingIOS: false,
        staysActiveInBackground: true,  // Enable background playback
        playsInSilentModeIOS: true,     // Play even in silent mode
        shouldDuckAndroid: true,         // Lower other apps' audio
      });
    }
    configureAudio();
  }, []);
}
 
// Expected behavior:
// - Audio mode is configured automatically on app launch
// - Music continues playing when the app is backgrounded
// - Audio plays even with iOS silent switch enabled

This setup only needs to run once. Setting staysActiveInBackground: true ensures that music keeps playing when the user switches to another app.

Audio Playback Basics — Loading, Playing, and Pausing

The Audio.Sound object from expo-av handles loading and controlling audio files. Here's a reusable custom hook that encapsulates the core playback logic:

// hooks/useAudioPlayer.ts
import { useState, useCallback, useRef } from 'react';
import { Audio, AVPlaybackStatus } from 'expo-av';
 
interface Track {
  id: string;
  title: string;
  artist: string;
  uri: string;       // Local or remote audio file URL
  artwork?: string;   // Album artwork URL
}
 
interface PlayerState {
  isPlaying: boolean;
  isLoaded: boolean;
  positionMillis: number;
  durationMillis: number;
}
 
export function useAudioPlayer() {
  const soundRef = useRef<Audio.Sound | null>(null);
  const [playerState, setPlayerState] = useState<PlayerState>({
    isPlaying: false,
    isLoaded: false,
    positionMillis: 0,
    durationMillis: 0,
  });
 
  // Callback for playback status updates
  const onPlaybackStatusUpdate = useCallback((status: AVPlaybackStatus) => {
    if (status.isLoaded) {
      setPlayerState({
        isPlaying: status.isPlaying,
        isLoaded: true,
        positionMillis: status.positionMillis,
        durationMillis: status.durationMillis ?? 0,
      });
    }
  }, []);
 
  // Load a track
  const loadTrack = useCallback(async (track: Track) => {
    // Release the existing sound object
    if (soundRef.current) {
      await soundRef.current.unloadAsync();
    }
 
    const { sound } = await Audio.Sound.createAsync(
      { uri: track.uri },
      { shouldPlay: false },
      onPlaybackStatusUpdate
    );
    soundRef.current = sound;
  }, [onPlaybackStatusUpdate]);
 
  // Toggle play/pause
  const togglePlayPause = useCallback(async () => {
    if (!soundRef.current) return;
    if (playerState.isPlaying) {
      await soundRef.current.pauseAsync();
    } else {
      await soundRef.current.playAsync();
    }
  }, [playerState.isPlaying]);
 
  // Seek to a specific position
  const seekTo = useCallback(async (positionMillis: number) => {
    if (!soundRef.current) return;
    await soundRef.current.setPositionAsync(positionMillis);
  }, []);
 
  return { playerState, loadTrack, togglePlayPause, seekTo };
}

The key here is the onPlaybackStatusUpdate callback. expo-av fires this callback whenever the playback position changes, enabling real-time UI updates without polling.

Building the Seek Bar and Playback Controls

The seek bar and playback controls define how intuitive your music player feels. Let's build a clean, responsive control interface:

// components/PlayerControls.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import Slider from '@react-native-community/slider';
import { Ionicons } from '@expo/vector-icons';
 
interface PlayerControlsProps {
  isPlaying: boolean;
  positionMillis: number;
  durationMillis: number;
  onTogglePlayPause: () => void;
  onSeek: (position: number) => void;
  onPrevious: () => void;
  onNext: () => void;
}
 
// Format milliseconds to "MM:SS"
function formatTime(millis: number): string {
  const totalSeconds = Math.floor(millis / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
 
export function PlayerControls({
  isPlaying,
  positionMillis,
  durationMillis,
  onTogglePlayPause,
  onSeek,
  onPrevious,
  onNext,
}: PlayerControlsProps) {
  return (
    <View style={styles.container}>
      {/* Seek bar */}
      <Slider
        style={styles.slider}
        minimumValue={0}
        maximumValue={durationMillis}
        value={positionMillis}
        onSlidingComplete={onSeek}
        minimumTrackTintColor="#1DB954"
        maximumTrackTintColor="#535353"
        thumbTintColor="#1DB954"
      />
      {/* Elapsed / remaining time */}
      <View style={styles.timeRow}>
        <Text style={styles.timeText}>
          {formatTime(positionMillis)}
        </Text>
        <Text style={styles.timeText}>
          -{formatTime(durationMillis - positionMillis)}
        </Text>
      </View>
      {/* Playback control buttons */}
      <View style={styles.controls}>
        <TouchableOpacity onPress={onPrevious}>
          <Ionicons name="play-skip-back" size={32} color="#fff" />
        </TouchableOpacity>
        <TouchableOpacity
          onPress={onTogglePlayPause}
          style={styles.playButton}
        >
          <Ionicons
            name={isPlaying ? 'pause-circle' : 'play-circle'}
            size={64}
            color="#1DB954"
          />
        </TouchableOpacity>
        <TouchableOpacity onPress={onNext}>
          <Ionicons name="play-skip-forward" size={32} color="#fff" />
        </TouchableOpacity>
      </View>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: { padding: 20 },
  slider: { width: '100%', height: 40 },
  timeRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginTop: -8,
  },
  timeText: { color: '#b3b3b3', fontSize: 12 },
  controls: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    gap: 32,
    marginTop: 16,
  },
  playButton: { marginHorizontal: 24 },
});
 
// Expected rendering:
// - A green seek bar that moves with playback progress
// - Elapsed and remaining time displayed in "M:SS" format
// - Previous, play/pause, and next buttons in a row

Using onSlidingComplete instead of onValueChange prevents excessive seek operations while the user is still dragging the slider, resulting in smoother performance.

Playlist Management and Track Switching

A playlist feature is the heart of any music player. Here's how to manage track indices and handle next/previous navigation with repeat and shuffle modes:

// hooks/usePlaylist.ts
import { useState, useCallback } from 'react';
 
interface Track {
  id: string;
  title: string;
  artist: string;
  uri: string;
  artwork?: string;
}
 
export function usePlaylist(tracks: Track[]) {
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isShuffled, setIsShuffled] = useState(false);
  const [repeatMode, setRepeatMode] = useState<'off' | 'all' | 'one'>('off');
 
  const currentTrack = tracks[currentIndex] ?? null;
 
  // Advance to the next track
  const playNext = useCallback(() => {
    setCurrentIndex((prev) => {
      if (repeatMode === 'one') return prev;  // Single track repeat
      const next = prev + 1;
      if (next >= tracks.length) {
        return repeatMode === 'all' ? 0 : prev;  // Loop or stop
      }
      return next;
    });
  }, [tracks.length, repeatMode]);
 
  // Go back to the previous track
  const playPrevious = useCallback(() => {
    setCurrentIndex((prev) => {
      const prevIndex = prev - 1;
      if (prevIndex < 0) {
        return repeatMode === 'all' ? tracks.length - 1 : 0;
      }
      return prevIndex;
    });
  }, [tracks.length, repeatMode]);
 
  // Toggle shuffle mode
  const toggleShuffle = useCallback(() => {
    setIsShuffled((prev) => !prev);
  }, []);
 
  // Cycle repeat mode: off → all → one → off
  const toggleRepeat = useCallback(() => {
    setRepeatMode((prev) => {
      if (prev === 'off') return 'all';
      if (prev === 'all') return 'one';
      return 'off';
    });
  }, []);
 
  return {
    currentTrack,
    currentIndex,
    isShuffled,
    repeatMode,
    playNext,
    playPrevious,
    toggleShuffle,
    toggleRepeat,
  };
}

The repeat mode cycles through three states: off, repeat all, and repeat one. For a production-ready shuffle implementation, consider using the Fisher-Yates algorithm to randomize the track order while keeping the original list intact.

For more on animation techniques you can apply to your player transitions, check out the Rork Animation Implementation Guide.

Background Audio and Lock Screen Controls

Background audio is essential for any music player. Users expect their music to keep playing when they switch apps or lock their screen. Here's how to set it up for both platforms.

iOS Configuration

Add the audio background mode to your app.json (or app.config.js):

{
  "expo": {
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": ["audio"]
      }
    }
  }
}

Android Configuration

Android uses foreground services to maintain background playback. In addition to the staysActiveInBackground: true setting, add the following permission:

{
  "expo": {
    "android": {
      "permissions": ["FOREGROUND_SERVICE"]
    }
  }
}

Lock Screen Media Controls

expo-av's Audio.Sound automatically integrates with the system media session for basic lock screen controls. However, if you want to display album artwork and track metadata on the lock screen, consider adopting react-native-track-player:

// For advanced media controls, consider react-native-track-player:
//
// import TrackPlayer from 'react-native-track-player';
//
// await TrackPlayer.setupPlayer();
// await TrackPlayer.add({
//   id: 'track-1',
//   url: 'https://example.com/song.mp3',
//   title: 'Track Title',
//   artist: 'Artist Name',
//   artwork: 'https://example.com/artwork.jpg',
// });
// await TrackPlayer.play();

react-native-track-player provides full support for lock screen controls, notification bars, Apple CarPlay, and Android Auto. It's the recommended upgrade path when you need production-grade media features.

For design patterns on building reusable custom hooks like the ones in this article, see the Rork Custom Hooks Guide.

Summary

In this guide, we walked through building a music player app with Rork and expo-av. We covered audio configuration, playback controls, seek bar UI implementation, playlist management with repeat and shuffle modes, and background audio setup for both iOS and Android.

The custom hooks we built — useAudioPlayer and usePlaylist — are designed to be reusable across projects. Feel free to adapt them to your own needs. If you want to take your app's user experience to the next level, the Rork UX Design Patterns Complete Guide is an excellent next read.

To deepen your understanding of the topics covered here,

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-05-05
Taking a Rork Podcast App to Production — Migrating to expo-audio, Recovering from Interruptions, and Resumable Downloads
Implementation notes for making a Rork podcast app survive real use: migrating off expo-av before SDK 55 removes it, recovering from call interruptions, resuming broken downloads, and budgeting storage against iCloud backup.
⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.