RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
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.

Rork515expo-av6audio playbackmusic playerReact Native209app development40

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-07-15
My Rork sleep timer faded out on time — but the sound didn't
Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.
Dev Tools2026-05-13
Three Things I Got Stuck on When Implementing Ambient Sound in a Healing App with Rork
Three common pitfalls when implementing ambient looping audio in meditation and healing apps built with Rork: loop clicks, iOS silent mode, and battery drain — with working expo-av code.
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.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →