Spotify is a digital music, podcast, and video service that gives you access to millions of songs and other content from creators around the world, however, the most important feature is the track player, so on this blogpost we will create a similar track player to Spotify’s using React Native and the track-player library.
React native Track-player
It is a fully-fledged audio module created for music apps. It provides audio playback, external media controls, background mode and more!
Features
-
Feels native – As everything is built together, it follows the same design principles as real music apps do.
-
Multi-platform – Supports Android, iOS and Windows.
-
Media Controls support – Provides events for controlling the app from a Bluetooth device, lock screen, a notification, a smartwatch or even a car.
-
Local or network, files or streams – It does not matter where the media come from you are all covered.
-
Adaptive bitrate streaming support – Support for DASH, HLS or SmoothStreaming.
-
Caching support – Cache media files to play them again without internet connection.
-
Background support – Keep playing audio even when the app is in background.
-
Fully Customizable – Even the notification icons are customizable!
-
Supports React Hooks – Includes React Hooks for common use-cases so you do not have to write them.
-
Lightweight – Optimized to use the least amount of resources according to your needs.
Installation
Before starting with the installation you should have already created a react native app.
Install the module from npm or yarn.
npm install --save react-native-track-player
Or
yarn add react-native-track-player
Automatic link only supports RN 0.60 and above, the module should be autolinked. If you are using iOS also use the command pod install
.
Getting started
There are some things you can do when configuring track player, first you need to create a music object to add to the music player, here you have an example:
const music = [{
title: 'death bed',
artist: 'Powfu',
artwork: 'https://images-na.ssl-images-amazon.com/images/I/A1LVEJikmZL._AC_SX425_.jpg',
url: 'https://sample-music.netlify.app/death%20bed.mp3',
duration: 2 * 60 + 53,
id: '1',
},{
title: 'bad liar',
artist: 'Imagine Dragons',
artwork: 'https://images-na.ssl-images-amazon.com/images/I/A1LVEJikmZL._AC_SX425_.jpg',
url: 'https://sample-music.netlify.app/Bad%20Liar.mp3',
duration: 2 * 60,
id: '2',
track_number: '2'
}
]
Now we need to import the Trackplayer module to be able to use the player functions:
setupPlayer(options: PlayerOptions)
: Initializes the player with the specified options.
add(tracks, insertBeforeIndex)
: Adds one or more tracks to the queue.
play()
: Plays or resumes the current track.
Example:
//player.js
import TrackPlayer from 'react-native-track-player';
const trackPlayer = async () => {
await TrackPlayer.setupPlayer();
await TrackPlayer.add(music);
await TrackPlayer.play();
};
trackPlayer();
Now with this configuration we can list the track, but we cannot see the name nor artwork, but we can pause and change the track, so we are going to add more code.
Controllers
The principal track player controls are: play, pause, next track, and previous track. The library allows us to use functions to handle those actions but we need to add our code.
Capability.Skip
: Capability indicating the ability to skip to any song in the queue.
Capability.SkipToNext
: Capability indicating the ability to skip to the next track.
Capability.SkipToPrevious
: Capability indicating the ability to skip to the previous track.
Capability.Pause
: Capability indicating the ability to pause.
Now that we have identified the functions, we are going to write some code to create controller.js. .
import TrackPlayer, {
usePlaybackState,
} from 'react-native-track-player';
export default function Controller() {
.........
.........
const playbackState = usePlaybackState();
useEffect(() => {
if (playbackState === 'playing' || playbackState === 3) {
isPlaying.current = 'playing';
} else if (playbackState === 'paused' || playbackState === 2) {
isPlaying.current = 'paused';
} else {
isPlaying.current = 'loading';
}
}, [playbackState]);
const returnPlayBtn = () => {
switch (playbackState) {
case 'playing':
return <Icon color="#fff" name="pause" size={45} />;
case 'paused':
return <Icon color="#fff" name="play-arrow" size={45} />;
default:
return <ActivityIndicator size={45} color="#fff" />;
}
};
const onPlayPause = () => {
if (isPlaying.current === 'playing') {
TrackPlayer.pause();
} else if (isPlaying.current === 'paused') {
TrackPlayer.play();
}
The component has 3 touchables buttons with different actions, the skip-previous
button skips the song to the previous song using TrackPlayer.SkipToPrevious().
The pause/play button plays or pauses the song depending on the playbackState
of the track-player hook, const playbackState = usePlaybackState().
The skip-next
button skips the song to the next song using TrackPlayer.SkipToNext().
Show current track
To get the current track first is necessary to use the function getCurrentrack()
and use the response to use the function getTrack(param)
by doing so we get the current track attributes, such as title, artwork, artist, and we also update the state with the response.
const [song, setSong] = useState(null);
const setCurrentSong = async() => {
const current = await TrackPlayer.getCurrentTrack();
const song = await TrackPlayer.getTrack(current);
setSongs(song)
}
Now we can show the title, artwork and, artist using the useState hook and the text component to display the attributes:
<SafeAreaView style={{height: 320}}>
<Animated.FlatList
ref={slider}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
data={musicAlbum}
renderItem={renderItem}
keyExtractor={(item) => item.id}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
{useNativeDriver: true},
)}
/>
</SafeAreaView>
<View>
<Text style={styles.title}>{songs?.title}</Text>
<Text style={styles.artist}>{songs?.artist}</Text>
</View>
<SliderComp />
<Controller onNext={goNext} onPrv={goPrv} />
</SafeAreaView>
Slider component
This component shows the song length and the position of the slider we will use.
Capability.SeekTo
: Capability indicating the ability to seek to a position in the timeline
The formatTime function returns the remaining seconds of the song and in order to get the music length we use useTrackPlayerProgress hook
.
import React, {useEffect, useState, useRef} from 'react';
import {View, Text, StyleSheet, Dimensions} from 'react-native';
import Slider from '@react-native-community/slider';
import TrackPlayer, {usePlaybackState} from 'react-native-track-player';
import {useTrackPlayerProgress} from 'react-native-track-player/lib/hooks';
import {PLAYBACK_TRACK_CHANGED} from 'react-native-track-player/lib/eventTypes';
const {height, width} = Dimensions.get('window');
export default function SliderComp() {
const {position, duration} = useTrackPlayerProgress(1000, null);
const [isSeeking, setIsSeeking] = useState(false);
const isMountedRef = useRef(false);
const [seek, setSeek] = useState(0);
useEffect(() => {
if (isMountedRef.current) {
TrackPlayer.addEventListener(PLAYBACK_TRACK_CHANGED, () => {
setIsSeeking(false);
});
}
return () => {
isMountedRef.current = false;
};
}, []);
const formatTime = (secs) => {
let minutes = Math.floor(secs / 60);
let seconds = Math.ceil(secs - minutes * 60);
if (seconds < 10) {
seconds = `0${seconds}`;
}
return `${minutes}:${seconds}`;
};
const handleChange = (val) => {
TrackPlayer.seekTo(val);
TrackPlayer.play().then(() => {
setTimeout(() => {
setIsSeeking(false);
}, 1000);
});
};
//components
return (
<View style={styles.container}>
<Slider
style={{
width: 320,
height: 40,
Left: 0,
}}
minimumValue={0}
value={isSeeking ? seek : position}
onValueChange={(value) => {
TrackPlayer.pause();
setIsSeeking(true);
setSeek(value);
}}
maximumValue={duration}
minimumTrackTintColor="#ffffff"
onSlidingComplete={handleChange}
maximumTrackTintColor="rgba(255, 255, 255, .5)"
thumbTintColor={isMiniPlayer ? 'rgba(255, 255, 255, .0)' : '#ffffff'}
/>
<View style={styles.timeContainer}>
<Text style={styles.timers}>
{formatTime(isSeeking ? seek : position)}
</Text>
<Text style={styles.timers}>{formatTime(duration)}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
height: 30,
},
timers: {
color: '#fff',
fontSize: 16,
},
timeContainer: {
marginHorizontal: 3,
flexDirection: 'row',
justifyContent: 'space-between',
},
});
Enable button notification on iOS and Android
To enable the notification button and use the controls first we need to configure the event listeners, create a service.js file and then add all the listeners that you need.
import TrackPlayer from 'react-native-track-player';
module.exports = async function () {
TrackPlayer.addEventListener(
'remote-play',
async () => await TrackPlayer.play(),
);
TrackPlayer.addEventListener(
'remote-pause',
async () => await TrackPlayer.pause(),
);
TrackPlayer.addEventListener(
'remote-stop',
async () => await TrackPlayer.stop(),
);
TrackPlayer.addEventListener('remote-jump-backward', async () => {
TrackPlayer.seekTo((await TrackPlayer.getPosition()) - 15);
});
TrackPlayer.addEventListener('remote-jump-forward', async () => {
TrackPlayer.seekTo((await TrackPlayer.getPosition()) + 15);
});
TrackPlayer.addEventListener(
'remote-next',
async () => await TrackPlayer.skipToNext(),
);
TrackPlayer.addEventListener(
'remote-previous',
async () => await TrackPlayer.skipToPrevious(),
);
TrackPlayer.addEventListener('remote-seek', ({ position }) => {
TrackPlayer.seekTo(position);
});
};
Complete component
import React, {useRef, useEffect, useState} from 'react';
import {
View,
SafeAreaView,
Text,
Platform,
Dimensions,
TouchableOpacity,
Animated,
StyleSheet,
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import {useDispatch, useSelector} from 'react-redux';
import TrackPlayer, {
usePlaybackState,
TrackPlayerEvents,
} from 'react-native-track-player';
import ImageColors from 'react-native-image-colors';
import {setSongs} from '../redux/actions/index';
import musicAlbum from '../components/MediaPlayer/data';
import Controller from '../components/MediaPlayer/Controller';
import SliderComp from '../components/MediaPlayer/SliderComp';
export default function Player() {
const [song, setSong] = useState(null);
const [songIndex, setSongIndex] = useState(0);
const scrollX = useRef(new Animated.Value(0)).current;
const slider = useRef(null);
const index = useRef(0);
const setCurrentSong = async() => {
const current = await TrackPlayer.getCurrentTrack();
const song = await TrackPlayer.getTrack(current);
setSongs(song)
}
useEffect(()=> {
scrollX.addListener(({value}) => {
const val = Math.round(value / width);
setSongIndex(val);
});
})
const goNext = async () => {
slider.current.scrollToOffset({
offset: (index.current + 1) * width,
});
await TrackPlayer.play();
};
const goPrv = async () => {
slider.current.scrollToOffset({
offset: (index.current - 1) * width,
});
await TrackPlayer.play();
};
useEffect(() => {
TrackPlayer.skip(musicAlbum[songIndex].id)
.then((_) => {
console.log('changed track');
})
.catch((e) => console.log('error in changing track ', e));
index.current = songIndex;
setCurrentSong()
TrackPlayer.play();
}, [songIndex]);
useEffect(() => {
trackPlayer(music)
}, []);
const trackPlayer = (music) => {
if (playSong) {
scrollX.addListener(({value}) => {
const val = Math.round(value / width);
setSongIndex(val);
});
TrackPlayer.setupPlayer({waitForBuffer: true}).then(async () => {
await TrackPlayer.reset();
await TrackPlayer.add(music);
TrackPlayer.play();
const current = await TrackPlayer.getCurrentTrack();
const song = await TrackPlayer.getTrack(current);
dispatch(setSongs(song));
index.current = current - 1;
setSongIndex(index.current);
setTimeout(() => {
slider.current.scrollToOffset({
offset: index.current * width,
});
}, 300);
await TrackPlayer.updateOptions(TRACK_PLAYER_CONTROLS_OPTS);
TrackPlayer.addEventListener(PLAYBACK_TRACK_CHANGED, async (e) => {
if (!isSongList.current) {
const trackId = (await TrackPlayer.getCurrentTrack()) - 1;
if (trackId !== index.current) {
setSongIndex(trackId);
isItFromUser.current = false;
if (trackId > index.current) {
goNext();
} else {
goPrv();
}
setTimeout(() => {
isItFromUser.current = true;
}, 200);
}
}
TrackPlayer.play();
isPlayerReady.current = true;
});
TrackPlayer.addEventListener(TrackPlayerEvents.REMOTE_DUCK, (e) => {
if (e.paused) {
TrackPlayer.pause();
} else {
TrackPlayer.play();
}
});
});
}
};
return (
<SafeAreaView
style={[
styles.container,
{
backgroundColor: "black"
},
]}>
<TouchableOpacity style={styles.button} onPress={onPress}>
<View styles={{flex: 1}}>
<Icon
style={{right: 136, top: 0}}
color="#fff"
name="expand-more"
size={45}
/>
</View>
</TouchableOpacity>
<SafeAreaView style={{height: 320}}>
<Animated.FlatList
ref={slider}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
data={musicAlbum}
renderItem={renderItem}
keyExtractor={(item) => item.id}
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
{useNativeDriver: true},
)}
/>
</SafeAreaView>
<View>
<Text style={styles.title}>{songs?.title}</Text>
<Text style={styles.artist}>{songs?.artist}</Text>
</View>
<SliderComp />
<Controller onNext={goNext} onPrv={goPrv} />
</SafeAreaView>
)}
const styles = StyleSheet.create({
title: {
fontSize: 28,
textAlign: 'center',
fontWeight: '600',
textTransform: 'capitalize',
color: '#ffffff',
},
artist: {
fontSize: 18,
textAlign: 'center',
color: '#ffffff',
textTransform: 'capitalize',
},
container: {
justifyContent: 'space-evenly',
alignItems: 'center',
height: height,
},
});
Finally we have the player with the current song’s information with the basic controls and the slider working.
Finally…
Now we have a simple audio player for iOS and Android. If you want to add more features and functunialities read this documentation and keep on learning!
Thanks for reading.