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

🎵 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.

Top comments (2)

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

does this work with bridgeless architecture in react native

Collapse
 
amitkumar13 profile image
Amit Kumar

Yes

OSZAR »