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