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
Install the slider component:
npm install @react-native-community/slider
🛠 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);
}
};
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
📱 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>
🧠 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,
},
});
iOS Screenshot
Android screenshot
📱 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)
does this work with bridgeless architecture in react native
Yes