This tutorial is going to teach you how to build your own music streaming application using react-native. I’m writing this tutorial for beginners. Even if you’ve just started learning react-native, you will be able to get a good understanding of the app.
Prerequisites:
- A good understanding of javascript and async/await functions.
- You should know how to set up a basic react-native app (“hello world” app).
- It would also help if you already know how basic react hooks like useEffect() and useState() work.
If you don’t know how to set up a react-native app, you can follow this official getting started tutorial. Choose the “React Native CLI Quickstart” and follow the instructions.
I’ll be using an android device to show the outputs but the same code should work on ios as well.
Note: If you’re on ios, please install cocoapods using homebrew.
Before I dive into the teaching, let’s see how the app would finally look like.
Audio package we’ll use: react-native-track-player
This tutorial doesn’t use any class based components. All components are functional.
If you just want the code, you can check the final code on github.
NPM Packages That We’ll Use
react-native-track-player
As our primary audio package, we’ll use react-native-track-player.
This library is a full fledged audio module created for music apps. It provides audio playback, external media controls, and background mode!
I like this package for two reasons:
- It allows you to play all kinds of audio files. You can play local mp3 files with this and even stream hls files.
- It eliminates the need for multiple libraries. For example: if you use react-native-sound as an audio library, you need to use react-native-music-controls separately for controlling the music from the background (from the notification panel and lock screen). Syncing the two libraries can be a big hassle.
react-native-slider
This package allows us to add a slider in our application.
We’ll use this slider as a progress bar and seek bar for our music player.
We’ll be using only these two external libraries for this project.
Now that we’re all set, let’s dive into the code.
Setting Up The Basic App
Create A Basic React Native Project
As I said before, this tutorial is going to be beginner friendly. So the first thing we’ll do is initialize a react-native app using the following command:
npx react-native init
Now run the app using the run-android or run-ios command to check if the app is initialized properly.
This is what your app should look like at this point:
Delete everything inside the App.js file (this should be present in the root folder of the app) and replace it with this code:
import React from 'react';
import {Text} from 'react-native';
const App = () => {
return (
<>
<Text>Hello World</Text>
</>
);
};
export default App;
This code is pretty simple and should be self explanatory. Now we have a basic “Hello World” app.
Next, we’ll install the packages that we require.
Install Required Packages
Use the following npm command to install react-native-track-player and react-native-slider:
npm install react-native-track-player @react-native-community/slider
This command will install react-native-track-player and react-native-slider.
For the app to work properly on ios, you’ll need to install react-native-swift cocoapods.
So install rect-native-swift with the following command:
npm install react-native-swift
And install cocoapods for the slider with the following command:
pod install
Restart your app with the npx react-native run-android command again. Hot reloading doesn’t always work when installing new packages.
In most cases, you don’t need to link the react-native-track-player library, but if it doesn’t work then you might need to link it using the react-native link command.
Done? Great! Now we can start writing code that actually matters.
Playing The Music
Just to understand the concept and working of react-native-track-player, we’ll play a stock song that I found on envato.
The song is pretty catchy if you ask me!
Add A Simple Play/Pause Button
We’ll add a simple button on the app that will play the song.
Let’s just add a button first.
Add this code in the return statement of the App.js file, after the hello world text.
<Button
title="Play"
onPress={() => {
console.log('pressed');
}}
/>
Don’t forget to import “Button” from react-native.
What this code does is add a simple button to our screen. On pressing the button, a message (“pressed”) will be printed on the terminal.
Integrate react-native-track-player
As per the documentation we need to make changes in the index.js file (that is present in the root folder of the app) and we also need to create a service.js file which needs to be placed in the root folder.
First, create a new file – “service.js” in the root folder of your app. right now, we are just initializing this file with an empty export statement.
Add the following export statement to the file:
module.exports = async function () {
};
This code just exports an empty async function. react-native-track-player requires this file to run. We will add more code to this file later to actually make this functional.
Secondly, we need to register a playback service right. This needs to happen right after the main component of the app is registered. The main component should be registered in the index.js file in the root folder of the app.
So we need to add a few lines of code in the index.js file. This is how our root index.js file should look finally.
/**
* @format
*/
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
//add this line to import the TrackPlayer
import TrackPlayer from 'react-native-track-player';
AppRegistry.registerComponent(appName, () => App);
//add this line to register the TrackPlayer
TrackPlayer.registerPlaybackService(() => require('./service.js'));
Now we can use the track player in any file in the app.
We’ll continue writing code in the App.js file now.
We already had the button setup over there. We should add the track player now.
Update the App.js file and add the following code:
//import react hooks
import React, {useEffect, useState} from 'react';
import {Text, Button} from 'react-native';
//import the TrackPlayer
import TrackPlayer from 'react-native-track-player';
//function to initialize the Track Player
const trackPlayerInit = async () => {
await TrackPlayer.setupPlayer();
await TrackPlayer.add({
id: '1',
url:
'https://audio-previews.elements.envatousercontent.com/files/103682271/preview.mp3',
type: 'default',
title: 'My Title',
album: 'My Album',
artist: 'Rohan Bhatia',
artwork: 'https://picsum.photos/100',
});
return true;
};
const App = () => {
//state to manage whether track player is initialized or not
const [isTrackPlayerInit, setIsTrackPlayerInit] = useState(false);
//initialize the TrackPlayer when the App component is mounted
useEffect(() => {
const startPlayer = async () => {
let isInit = await trackPlayerInit();
setIsTrackPlayerInit(isInit);
}
startPlayer();
}, []);
//start playing the TrackPlayer when the button is pressed
const onButtonPressed = () => {
TrackPlayer.play();
};
return (
<>
<Text>Music Player</Text>
<Button
title="Play"
onPress={onButtonPressed}
disabled={!isTrackPlayerInit}
/>
</>
);
};
export default App;
When you press the Play button, the song should start playing! You should also be seeing the music player in the notification panel.
The song would start playing when you press the play button for the first time. After that, pressing the button would have no effect.
Let me explain how this is working.
All the magic is being done by react-native-track-player. We just need to import TrackPlayer and call three functions on it to make it work.
These functions are:
- setupPlayer()
- add()
- play()
To initialize the player, we need to call the setupPlayer() function. This is an async function so we need to wait for it to finish before we can do anything with the TrackPlayer.
The setupPlayer() function does not take in any arguments.
After the track player has been set up, we can add songs to our playlist.
To do that, we have to call the add() function.
add() requires one argument – it requires a Track Object or an array of Track Objects.
What is a Track Object? This is just a JSON object that defines a single music file.
This is how our Track Object looks like:
{
id: '1',
url:
'https://audio-previews.elements.envatousercontent.com/files/103682271/preview.mp3',
type: 'default',
title: 'My Title',
album: 'My Album',
artist: 'Rohan Bhatia',
artwork: 'https://picsum.photos/100',
}
Only the id, url, title and artist properties are required for basic playback.
You can check all the available properties in the documentation.
So we’ve set the id, url, title and artist of the Track Object.
The artwork property is an image that will be displayed in the notification bar and lock screen when the song is playing.
I’ve used the picsum api to get a random photo of size 100×100. I really like the picsum api and I use it whenever I need a random image in my application.
The two functions (setupPlayer and add) are called in a separate function which I’ve called trackPlayerInit().
trackPlayerInit() is called inside useEffect().
useEffect() is a react hook which is called whenever the component is mounted and when the state changes.
If you aren’t clear about what useEffect and useState do, then you should go through their official documentation and examples to get a better understanding of these hooks. You can go to the following links to learn more about them.
State Hook Documentation || Effect Hook Documentation
When the main app component is mounted, trackPlayerInit is called. This function sets up the player and adds the song to our playlist.
When the button is pressed, we call trackPlayer.play() to start playing the song.
I’m using the useState hook to check whether the Track Player has been initialized or not. The button stays disabled until the Track Player has been initialized.
The app is now playing the music, although we can’t pause or stop it. This is how our app should be looking:
Pausing/Resuming The Music
Let’s add the functionality to pause and resume the song now. For that, we’ll set the state of the player. We’ll set the state to playing when we first press the button. Then we’ll change the state every time the button is pressed.
We’ll make three changes in our current code to do this.
First, create a new state variable called isPlaying using the below statement.
const [isPlaying, setIsPlaying] = useState(false);
Second, update the onButtonPress function.
const onButtonPressed = () => {
if (!isPlaying) {
TrackPlayer.play();
setIsPlaying(true);
} else {
TrackPlayer.pause();
setIsPlaying(false);
}
}
And finally, change the title of the button (in the return statement) to this:
title={isPlaying ? 'Pause' : 'Play'}
Through these changes, we have created a way to store the current state of the music player. When the Play/Pause button is pressed, we check whether the song is playing or is it paused.
Based on the state, we call the play() or pause() function on the Track Player.
The title of the button is also changes based on the current state of the music player. If the player is paused, then the title is ‘Play’. If the player is playing, then the title of the button becomes ‘Pause’.
Adding a Progress + Seek Bar
Let’s add a slider now. We’ll use this slider to display the progress of the song and also to seek the song. This part is the most challenging to understand. Just go through the code below, then I’ll explain the whole thing line by line.
This is how App.js file should look after we’ve added the slider and all its functionalities:
import React, {useEffect, useState} from 'react';
import {Text, Button} from 'react-native';
import TrackPlayer from 'react-native-track-player';
//import the hook provided by react-native-track-player to manage the progress
import {useTrackPlayerProgress} from 'react-native-track-player/lib/hooks';
//import statement for slider
import Slider from '@react-native-community/slider';
const trackPlayerInit = async () => {
await TrackPlayer.setupPlayer();
await TrackPlayer.add({
id: '1',
url:
'https://audio-previews.elements.envatousercontent.com/files/103682271/preview.mp3',
type: 'default',
title: 'My Title',
album: 'My Album',
artist: 'Rohan Bhatia',
artwork: 'https://picsum.photos/100',
});
return true;
};
const App = () => {
const [isTrackPlayerInit, setIsTrackPlayerInit] = useState(false);
const [isPlaying, setIsPlaying] = useState(false);
//the value of the slider should be between 0 and 1
const [sliderValue, setSliderValue] = useState(0);
//flag to check whether the use is sliding the seekbar or not
const [isSeeking, setIsSeeking] = useState(false);
//useTrackPlayerProgress is a hook which provides the current position and duration of the track player.
//These values will update every 250ms
const {position, duration} = useTrackPlayerProgress(250);
useEffect(() => {
const startPlayer = async () => {
let isInit = await trackPlayerInit();
setIsTrackPlayerInit(isInit);
}
startPlayer();
}, []);
//this hook updates the value of the slider whenever the current position of the song changes
useEffect(() => {
if (!isSeeking && position && duration) {
setSliderValue(position / duration);
}
}, [position, duration]);
const onButtonPressed = () => {
if (!isPlaying) {
TrackPlayer.play();
setIsPlaying(true);
} else {
TrackPlayer.pause();
setIsPlaying(false);
}
};
//this function is called when the user starts to slide the seekbar
const slidingStarted = () => {
setIsSeeking(true);
};
//this function is called when the user stops sliding the seekbar
const slidingCompleted = async value => {
await TrackPlayer.seekTo(value * duration);
setSliderValue(value);
setIsSeeking(false);
};
return (
<>
<Text>Music Player</Text>
<Button
title={isPlaying ? 'Pause' : 'Play'}
onPress={onButtonPressed}
disabled={!isTrackPlayerInit}
/>
{/* defining our slider here */}
<Slider
style={{width: 400, height: 40}}
minimumValue={0}
maximumValue={1}
value={sliderValue}
minimumTrackTintColor="#111000"
maximumTrackTintColor="#000000"
onSlidingStart={slidingStarted}
onSlidingComplete={slidingCompleted}
/>
</>
);
};
export default App;
This is how the app should look as of now. If you slide the seekbar, the song should seek to the new position. The slider should also keep moving forward as the song progresses.
Understanding The Slider Component
First we added a slider. For that we imported the Slider from the react-native-slider package that we installed. We assigned the following properties in the Slider:
- style={{width: 400, height: 40}}
- minimumValue={0}
- maximumValue={1}
- value={sliderValue}
- minimumTrackTintColor=”#111000″
- maximumTrackTintColor=”#000000″
- onSlidingStart={slidingStarted}
- onSlidingComplete={slidingCompleted}
Style is used to specify the height and width of the slider.
minimumValue and maximumValue are used to specify the endpoints of the slider. By default they are 0 and 1 only so you don’t need to explicitly define them. These endpoints mean that the value of the slider will remain between 0 and 1 only at all times.
Value is the current value of the slider. I’ve defined value as sliderValue which is a state that is assigned to our App component. We’ll change the value based on the current position of the song.
minimumTrackTintColor is the color of the slider that is on the left side of the main slide button.
maximumTrackTintColor is the color of the slider that is on the right side of the main slide button.
Using onSlideStart, we can call a function whenever the user starts sliding the slider.
Using onSlideEnd, we can call a function whenever the user stops sliding.
Understanding The Hooks Attached To The Slider
react-native-track-player provides a hook called useTrackPlayerProgress which updates the current position of the song at regular intervals.
First import the hook using the below import statement.
import {useTrackPlayerProgress} from 'react-native-track-player/lib/hooks';
const {position, duration} = useTrackPlayerProgress(250);
This line of code would create two constants in our component – position and duration.
position contains the current position of the song and the duration contains the total duration of the song.
These values are updated at regular intervals. By default this regular interval is 1000ms (1 second) but we can pass a parameter while defining the hook to change this interval. I’ve passed 250 into the hook, which means our values would be updated every 250 milliseconds.
Through this hook, we basically get the current position of the song (in seconds).
We also get the total duration of the song every 250ms (even though the duration doesn’t change – maybe I can optimize this later).
After this, we change the value of the slider whenever our position changes. We use a new useEffect hook to achieve this.
useEffect(() => {
if (!isSeeking && position && duration) {
setSliderValue(position / duration);
}
}, [position, duration]);
Using this hook, we set the value of the slider as a decimal (remember that the value of the slider should be between 0 and 1).
We divide the position by duration to get a fraction which indicates the progress of the song.
Why do we need the if (!isSeeking) condition?
We only set the value of the slider while the user is not moving the slider. If we don’t have the if condition then during the time that the user is sliding, the slider would have two values at the same time (one that is being the set by the user and one that we are setting). This would make the slider behave in an undesired way.
Edit: Why do we need the if (position && duration) condition?
As suggested by Paul Crussaire, we added these conditions to avoid the NaN error. The duration of the song can be 0 and dividing by 0 will give us an NaN error. In order to avoid this error, we added the above conditions to our if statement.
These two hooks are enough to maintain the progress of the song.
With these hooks, the progress bar is being maintained. Now we need to add the functionality so that the user can seek to their desired position.
Understanding The Seek Function
To add the seek functionality, we’ll use the onSlidingStart and onSlidingComplete properties provided by react-native-slider.
const slidingStarted = () => {
setIsSeeking(true);
};
const slidingCompleted = async value => {
await TrackPlayer.seekTo(value * duration);
setSliderValue(value);
setIsSeeking(false);
};
When the sliding starts, all we need to do is set isSeeking as true.
When the sliding stops, we get the value of where the user stopped the slider. This value is between 0 and 1. We use the TrackPlayer’s seekTo() function to jump to this value.
Then we set the isSeeking to false to tell our app that the user is no more sliding.
That’s how the slider works. It’s a bit complicated, so go through the code multiple times. If you still have difficulty understanding it, feel free to comment on this post.
Controlling The Music From Outside The App
The basics of the app work, now let’s add the code to control the music from outside the app (from the notification panel or lockscreen).
To add the different options to control the music player from outside the app we need to use the updateOptions function provided by react-native-track-player.
Using this function, we can add capabilities to the TrackPlayer so that it can be controlled from outside the app.
Add this code in the trackPlayerInit function (after the setupPlayer function has been called):
TrackPlayer.updateOptions({
stopWithApp: true,
capabilities: [
TrackPlayer.CAPABILITY_PLAY,
TrackPlayer.CAPABILITY_PAUSE,
TrackPlayer.CAPABILITY_JUMP_FORWARD,
TrackPlayer.CAPABILITY_JUMP_BACKWARD,
],
});
For this app, I’ve added the play, pause, seek forward and seek backward capabilities. We don’t need next/previous song capability as we only have one song right now.
But if you’re interested, this is the full list of capabilities available to us:
CAPABILITY_PLAY
CAPABILITY_PLAY_FROM_ID
CAPABILITY_PLAY_FROM_SEARCH
CAPABILITY_PAUSE
CAPABILITY_STOP
CAPABILITY_SEEK_TO
CAPABILITY_SKIP
CAPABILITY_SKIP_TO_NEXT
CAPABILITY_SKIP_TO_PREVIOUS
CAPABILITY_SET_RATING
CAPABILITY_JUMP_FORWARD
CAPABILITY_JUMP_BACKWARD
This is how our notification panel will look now:
Even though we have the buttons now, they don’t do anything. To assign a task to those buttons, we need to update our service.js file. Remember the file we created in the beginning with an empty export function? That’s the file we’ll be updating now.
This is how the service.js file should look now:
/**
* This is the code that will run tied to the player.
*
* The code here might keep running in the background.
*
* You should put everything here that should be tied to the playback but not the UI
* such as processing media buttons or analytics
*/
import TrackPlayer from 'react-native-track-player';
module.exports = async function() {
TrackPlayer.addEventListener('remote-play', () => {
TrackPlayer.play();
});
TrackPlayer.addEventListener('remote-pause', () => {
TrackPlayer.pause();
});
TrackPlayer.addEventListener('remote-jump-forward', async () => {
let newPosition = await TrackPlayer.getPosition();
let duration = await TrackPlayer.getDuration();
newPosition += 10;
if (newPosition > duration) {
newPosition = duration;
}
TrackPlayer.seekTo(newPosition);
});
TrackPlayer.addEventListener('remote-jump-backward', async () => {
let newPosition = await TrackPlayer.getPosition();
newPosition -= 10;
if (newPosition < 0) {
newPosition = 0;
}
TrackPlayer.seekTo(newPosition);
});
};
Every button that we added through the updateOptions function needs to be assigned a listener. So we have four event listeners here, one for each button.
These event listeners play, pause or move the position of the TrackPlayer.
I think the code here is pretty self explanatory.
The last problem we have is – when we remotely play/pause the song, the change isn’t reflected in our app (our button does not change it’s title and the isPlaying state doesn’t change).
To manage that, we’ll use the useTrackPlayerEvents hook provided by react-native-track-player. This hook will be called whenever the state of the TrackPlayer changes.
Import this hook in the App.js file using the following import statement:
import {useTrackPlayerEvents} from 'react-native-track-player/lib/hooks';
We also need a few constants to make this hook work. Import these constants using the following import statement:
import {TrackPlayerEvents,STATE_PLAYING} from 'react-native-track-player';
Then we can define the hook using the following code:
useTrackPlayerEvents([TrackPlayerEvents.PLAYBACK_STATE], event => {
if (event.state === STATE_PLAYING) {
setIsPlaying(true);
} else {
setIsPlaying(false);
}
});
This hook requires an array of events. The hook is called when any one of the events in the array is fired. We only have one event in the array – TrackPlayerEvents.PLAYBACK_STATE.
This event is fired whenever the state of the player changes (whenever it is paused, stopped etc). This event is fired even if the state is changed remotely.
We check the state of the event received, if the event state is playing, then we set our isPlaying state as true else we set it as false.
Great! Now the functionality of the app is ready.
All that remains is to style the app and make it look beautiful.
Styling The App
To style the app, we’ll create a stylesheet and import it in our App.js file. We’ll add the style classes to every component.
Create a new file style.js in the root folder.
Add the following code to the file:
import {StyleSheet} from 'react-native';
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: '#EDEDED',
},
imageContainer: {
flex: 0.5,
justifyContent: 'center',
},
detailsContainer: {
flex: 0.05,
justifyContent: 'center',
alignItems: 'center',
},
controlsContainer: {
flex: 0.45,
justifyContent: 'flex-start',
},
albumImage: {
width: 250,
height: 250,
alignSelf: 'center',
borderRadius: 40,
},
progressBar: {
height: 20,
paddingBottom: 90,
},
songTitle: {
fontSize: 16,
fontWeight: 'bold',
},
artist: {
fontSize: 14,
},
});
export default styles;
Now we’ll update the return statement in the App.js file.
First import the style file:
import styles from './styles';
Then change the return statement of the App component. This is how your return statement should look now:
return (
<View style={styles.mainContainer}>
<View style={styles.imageContainer}>
<Image
source={{
uri: songDetails.artwork,
}}
resizeMode="contain"
style={styles.albumImage}
/>
</View>
<View style={styles.detailsContainer}>
<Text style={styles.songTitle}>{songDetails.title}</Text>
<Text style={styles.artist}>{songDetails.artist}</Text>
</View>
<View style={styles.controlsContainer}>
<Slider
style={styles.progressBar}
minimumValue={0}
maximumValue={1}
value={sliderValue}
minimumTrackTintColor="#111000"
maximumTrackTintColor="#000000"
onSlidingStart={slidingStarted}
onSlidingComplete={slidingCompleted}
thumbTintColor="#000"
/>
<Button
title={isPlaying ? 'Pause' : 'Play'}
onPress={onButtonPressed}
style={styles.playButton}
disabled={!isTrackPlayerInit}
color="#000000"
/>
</View>
</View>
);
Wonderful! Our app is ready now. This is how it finally looks.
We wrote and updated the code in 4 files. You can view the final version of each file at these links:
App.js || styles.js || index.js || service.js
You can also view the project on GitHub.
Please share this article if it helped you. If you’re looking for a remote react-native developer, feel free to contact me 🙂
Background photo for main header created by freepik – www.freepik.com
Hey Rohan,
Thanks for your article.
To avoid the NaN Error, you should add “&& position && duration” on this useEffect because you cannot divide 0 by 0.
useEffect(() => {
if (!isSeeking && position && duration) {
setSliderValue(position / duration);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [position, duration]);
Thanks again for this post.
Thank you for the suggestion Paul, I’ve updated the article!