DEV Community

Cover image for 🎵 Build a Custom Music Player in React Native with react-native-track-player
Amit Kumar
Amit Kumar

Posted on • Edited on

3 1 1 1 2

🎵 Build a Custom Music Player in React Native with react-native-track-player

In today's era of audio streaming, crafting a sleek, interactive music player is a common feature in mobile apps. Thanks to the react-native-track-player library, building a fully functional audio player is easier than ever in React Native.

In this tutorial, we’ll walk through building a custom music player component that includes:

✅ HLS(m3u8) Streaming Support
✅ Album art, song title, and artist display
✅ Play/pause toggle
✅ Track scrubbing with a slider
✅ 10-second skip forward/back
✅ Real-time UI sync with playback position
✅ Lock screen controls (play, pause, skip, metadata)
✅ Draggable lock screen slider
✅ Track load and end detection


🧰 Installing Dependencies

Install react-native-track-player and its dependencies:

npm install react-native-track-player
npx pod-install

Enter fullscreen mode Exit fullscreen mode

Install the slider component:

npm install @react-native-community/slider

Enter fullscreen mode Exit fullscreen mode

🛠 Setting Up TrackPlayer Service

To enable background playback and lock screen controls, you need a playback service.

1️⃣ Create a service.js file:

// service.js
import TrackPlayer, { Event } from 'react-native-track-player';

module.exports = async function () {
  try {
    TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
    TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
    TrackPlayer.addEventListener(Event.RemoteNext, () => TrackPlayer.skipToNext());
    TrackPlayer.addEventListener(Event.RemotePrevious, () => TrackPlayer.skipToPrevious());
    TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.destroy());
    TrackPlayer.addEventListener('remote-seek', async ({ position }) => {
      await TrackPlayer.seekTo(position);
    });
  } catch (error) {
    console.log('TrackPlayer Service Error:', error);
  }
};


Enter fullscreen mode Exit fullscreen mode

2️⃣ Register the service in your index.js:

// index.js
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import TrackPlayer from 'react-native-track-player'; // Add this

AppRegistry.registerComponent(appName, () => App);
TrackPlayer.registerPlaybackService(() => require('./service.js')); // Add this

Enter fullscreen mode Exit fullscreen mode

📱 Enabling Background Playback on iOS

To allow audio to keep playing in the background:

Edit ios/YourApp/Info.plist:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>

Enter fullscreen mode Exit fullscreen mode

🧠 Implementing the Track Player UI

Here's the core component code:

/* eslint-disable react-hooks/exhaustive-deps */
import {
  Image,
  Platform,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from 'react-native';
import React, {useEffect, useState} from 'react';
import TrackPlayer, {
  Capability,
  Event,
  State,
  useProgress,
} from 'react-native-track-player';
import Slider from '@react-native-community/slider';

const TrackPlayerComponent = ({route}) => {
  const [isPlaying, setIsPlaying] = useState(false);
  const {artist, artwork, id, title, url} = route.params.data;
  const {position, duration} = useProgress();

const DefaultAudioServiceBehaviour = AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification;

> Note: disable playback after app is killed (you can adjust this based on your needs)

  const tracks = [
    {
      id: id,
      url: url,
      title: title,
      artist: artist,
      artwork: artwork,
      type: 'hls', ---> Add type: 'hls' only if you are streaming or using an M3U8 URL.
    },
  ];

  useEffect(() => {
    TrackPlayer.addEventListener('playback-error', error => {
      console.log('Playback error:', error);
    });
  }, []);

  useEffect(() => {
    const onEndListener = TrackPlayer.addEventListener(
      Event.PlaybackQueueEnded,
      ({track, position}) => {
        if (typeof position === 'number' && track != null) {
          console.log('🚀 ~ track finished:', track, 'at position:', position);
        } else {
          console.log('🚀 ~ PlaybackQueueEnded with undefined data');
        }
      },
    );

    return () => onEndListener.remove();
  }, []);


  useEffect(() => {
    TrackPlayer.addEventListener('playback-error', error => {
      console.log('Playback error:', error);
    });
  }, []);

  useEffect(() => {
    const listener = TrackPlayer.addEventListener(
      Event.PlaybackActiveTrackChanged,
      async ({nextTrack}) => {
        if (nextTrack != null) {
          const track = await TrackPlayer.getTrack(nextTrack);
          console.log('🚀 ~ track Loaded', track);
        } else {
          console.log('🚀 ~ nextTrack is null');
        }
      },
    );
    return () => listener.remove();
  }, []);


  useEffect(() => {
    TrackPlayer.addEventListener(Event.RemotePlay, () => {
      setIsPlaying(true);
      TrackPlayer.play();
    });

    TrackPlayer.addEventListener(Event.RemotePause, () => {
      setIsPlaying(false);
      TrackPlayer.pause();
    });
  }, []);

  useEffect(() => {
    const startPlayer = async () => {
      await TrackPlayer.setupPlayer();
      await TrackPlayer.reset();
      await TrackPlayer.updateOptions({
        android: {
          appKilledPlaybackBehavior: DefaultAudioServiceBehaviour,
          stoppingAppPausesPlayback: true,
          alwaysPauseOnInterruption: true,
        },
        stopWithApp: false,
        capabilities: [Capability.Play, Capability.Pause, Capability.SeekTo],
        compactCapabilities: [Capability.Play, Capability.Pause, Capability.SeekTo],
progressUpdateEventInterval: 2,
      });
      await TrackPlayer.add(tracks);
      const playerState = await TrackPlayer.getState();
      if (playerState !== State.Playing) {
        await TrackPlayer.play();
        setIsPlaying(true);
      }
    };

    startPlayer();

    return () => {
      TrackPlayer.destroy();
      TrackPlayer.stop();
    };
  }, []);

  const togglePlayback = async () => {
    const currentState = await TrackPlayer.getState();
    if (currentState === State.Playing) {
      await TrackPlayer.pause();
      setIsPlaying(false);
    } else {
      await TrackPlayer.play();
      setIsPlaying(true);
    }
  };

  const skipForward = async () => {
    const currentPosition = await TrackPlayer.getPosition();
    const newPosition = Math.min(currentPosition + 10, duration);
    await TrackPlayer.seekTo(newPosition);
  };

  const skipBackward = async () => {
    const currentPosition = await TrackPlayer.getPosition();
    const newPosition = Math.max(currentPosition - 10, 0);
    await TrackPlayer.seekTo(newPosition);
  };

  const formatTime = seconds => {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
  };

  return (
    <View style={styles.container}>
      <Image source={{uri: artwork}} style={styles.albumArt} />

      <View style={styles.infoContainer}>
        <View>
          <Text style={styles.title}>{title}</Text>
          <Text style={styles.artist}>{artist}</Text>
        </View>
      </View>

      <Slider
        step={1}
        minimumValue={0}
        maximumValue={duration}
        value={position}
        onSlidingComplete={async value => {
          await TrackPlayer.seekTo(value);
        }}
        minimumTrackTintColor="#fff"
        maximumTrackTintColor="#888"
        thumbTintColor="#fff"
        style={styles.slider}
      />

      <View style={styles.timeRow}>
        <Text style={styles.timeText}>{formatTime(position)}</Text>
        <Text style={styles.timeText}>{formatTime(duration)}</Text>
      </View>

      <View style={styles.controls}>
        <TouchableOpacity onPress={skipBackward}>
          <Image
            style={styles.controlIcon}
            source={require('../../icons/SkipBack.png')}
          />
        </TouchableOpacity>

        <TouchableOpacity onPress={togglePlayback} style={styles.playButton}>
          <Image
            style={{width: 30, height: 30}}
            source={
              isPlaying
                ? require('../../icons/Pause.png')
                : require('../../icons/Play.png')
            }
          />
        </TouchableOpacity>

        <TouchableOpacity onPress={skipForward}>
          <Image
            style={styles.controlIcon}
            source={require('../../icons/SkipFwd.png')}
          />
        </TouchableOpacity>
      </View>
    </View>
  );
};

export default TrackPlayerComponent;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: '#1f0036',
  },
  albumArt: {
    width: '85%',
    alignSelf: 'center',
    height: 400,
    borderRadius: 20,
  },
  infoContainer: {
    marginTop: 20,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 190,
    marginHorizontal: 30,
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#fff',
  },
  artist: {
    fontSize: 16,
    color: '#ccc',
  },
  slider: {
    width: '90%',
    alignSelf: 'center',
    position: 'absolute',
    bottom: Platform.OS === 'ios' ? 160 : 170,
  },
  timeRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginHorizontal: 30,
  },
  timeText: {
    color: '#fff',
  },
  controls: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  playButton: {
    width: 70,
    height: 70,
    backgroundColor: '#8a4fff',
    borderRadius: 35,
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#8a4fff',
    shadowOffset: {width: 0, height: 0},
    shadowOpacity: 0.5,
    shadowRadius: 10,
    marginHorizontal: 40,
  },
  controlIcon: {
    width: 50,
    height: 50,
  },
});

Enter fullscreen mode Exit fullscreen mode

iOS Screenshot

Image description


Android screenshot

Image description


📱 Final Thoughts

The react-native-track-player library makes it seamless to build robust and customizable audio players for both iOS and Android. With a few lines of code, we implemented playback, seek functionality, real-time syncing, and lock screen control.

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (2)

Collapse
 
__c0db63ab13a4 profile image
Ангел Иванов

does this work with bridgeless architecture in react native

Collapse
 
amitkumar13 profile image
Amit Kumar

Yes

AI Agent image

How to Build an AI Agent with Semantic Kernel (and More!)

Join Developer Advocate Luce Carter for a hands-on tutorial on building an AI-powered dinner recommendation agent. Discover how to integrate Microsoft Semantic Kernel, MongoDB Atlas, C#, and OpenAI for ingredient checks and smart restaurant suggestions.

Watch the video 📺

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay