Implementing react-native-track-player with Expo, including lock screen (Part 1: iOS)

Gionata Brunel
6 min readJan 22, 2023

--

The purpose of this article is to provide all the information you need to display a music player on the lock screen. The article is divided in two parts, the first is dedicated to iOS, and the second to Android.

Background story (you can skip that)

I searched for a solution for my personal audio project and came across two posts. The first said that ‘You can now use React Native Music Control with Expo.’ (https://expo.canny.io/feature-requests/p/react-native-music-control). All you need to do is create a custom development client for your Expo app, install React Native Music Control and enable audio playback in background via your app.json.

The second post was very similar: ‘You can now use React Native Track Player with Expo.’ (https://expo.canny.io/feature-requests/p/audio-playback-in-background). Here again, all you need to do is create a custom development client for your Expo app, install React Native Track Player and enable audio playback in background via your app.json. Finishing with ‘And don’t forget to register a playback service’.

I didn’t know about the possibility of creating a development build with Expo. Using react-native-track-player didn’t require an additional package, therefore I made my choice, even if I found the link for the app.json referring to the expo-av implementation peculiar — an alternative to the react-native-track-player. After struggling a little to implement it (just like many other in the community), I found few hints in the Expo forums (https://forums.expo.dev/t/expo-react-native-track-player/62714).

I decided to publish this prototype to ease the process for others — and here it is.

Note: I even asked some help from ChatGPT, the answer was positive but without details — no more than the two posts.

Simple implementation for iOS

Initialize your project with Expo (the name of the project used in this article is ‘miniplayerios’ and the Expo version is 47.0.12 — the latest available at this time).

npx create-expo-app miniplayerios

You can start your App once with ‘npx expo start’ (and then kill it) to be sure that there’s no issues (but it’s not really necessary).

Then install EAS globally and the development client:

cd miniplayerios 
npm install -g eas-cli
npx expo install expo-dev-client

Now, before creating the build, you need to tell EAS what are the packages you will use. That because EAS will look into package.json and create all the necessary resources for you. In particular, you won’t need to use Xcode as described in the react-native-track-player (https://react-native-track-player.js.org/docs/basics/installation#ios-setup), all is taking care by Expo EAS (implied but not explicitly stated here https://react-native-track-player.js.org/docs/basics/installation#expo).

npm install --save react-native-track-player
npx expo install @react-native-community/slider

Note: Probably the following points could be done after creating the build, but I prefer do it now.

Edit package.json and delete the line “main”. After it should look like that:

{
"name": "miniplayerios",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"expo": "~47.0.12",
"expo-dev-client": "~2.0.1",
"expo-status-bar": "~1.4.2",
"react": "18.1.0",
"react-native": "0.70.5",
"react-native-track-player": "^3.2.0",
"@react-native-community/slider": "4.2.4"
},
"devDependencies": {
"@babel/core": "^7.12.9"
},
"private": true
}

Create service.js and add:

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-next', () => TrackPlayer.skipToNext());
TrackPlayer.addEventListener('remote-previous', () => TrackPlayer.skipToPrevious());

};

Create index.js and add:

import { registerRootComponent } from 'expo';
import TrackPlayer from 'react-native-track-player';
import App from './App';

registerRootComponent(App);
TrackPlayer.registerPlaybackService(() => require('./service'));

Edit app.json and add the background audio mode under “ios” :

    "ios": {
"supportsTablet": true,
"infoPlist": {
"UIBackgroundModes": [
"audio"
]
}
},

And finally create eas.json and copy the following. This will speed up the EAS build using the M1 workers (https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad):

{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m-medium"
}
},
"preview": {
"distribution": "internal"
},
"production": {
"ios": {
"resourceClass": "m-medium"
}
}
},
"cli": {
"promptToConfigurePushNotifications": false
}
}

Now you are ready to create a build for an iOS device. If you prefer using a simulator, please refer to the documentation (https://docs.expo.dev/development/create-development-builds/).

Important: as stated in the Expo documentation, Apple Developer membership is required to create and install a development build on an iOS device.

If not already done, register your iPhone and follow the registration process:

eas device:create

Finally, let’s create the build following the interactive dialog provided by EAS:

eas build --profile development --platform ios

Once it’s done, you can install the App in your iPhone simply using the QR code provided by EAS in the terminal and in their website under your account. It will trigger the download and installation of the development App directly from the Apple Store.

Now it’s time to code your app.

The App.js is pretty simple, basically it’s just for displaying the MusicPlayer screen.

import React from 'react';
import {View, StatusBar, StyleSheet } from 'react-native';
import MusicPlayer from './MusicPlayer';

const App = () => {

return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<MusicPlayer />
</View>
);
};

export default App;

const styles = StyleSheet.create({
container: {
flex: 1,
},
});

And herewith the MusicPlayer.js where you will find a simple implementation of the react-native-track-player working on lock screen. For more details and explainations please refer to the react-native-track-player documentation (https://react-native-track-player.js.org/). The whole prototype is here: https://github.com/JonnaryMotors/miniplayerios

import React, {useEffect, useState} from 'react';
import {
View,
Text,
StyleSheet,
SafeAreaView,
Dimensions,
TouchableOpacity,
Image,
} from 'react-native';
import TrackPlayer, {
Capability,
State,
Event,
usePlaybackState,
useProgress,
useTrackPlayerEvents,
} from 'react-native-track-player';
import Slider from '@react-native-community/slider';
import Ionicons from 'react-native-vector-icons/Ionicons';
import podcasts from './assets/data';

function MusicPlayer() {

const podcastsCount = podcasts.length;
const [trackIndex, setTrackIndex] = useState(0);
const [trackTitle, setTrackTitle] = useState();
const [trackArtist, setTrackArtist] = useState();
const [trackArtwork, setTrackArtwork] = useState();

const playBackState = usePlaybackState();
const progress = useProgress();

const setupPlayer = async () => {
try {
await TrackPlayer.setupPlayer();
await TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious
],
});
await TrackPlayer.add(podcasts);
await gettrackdata();
await TrackPlayer.play();
} catch (error) { console.log(error); }
};

useTrackPlayerEvents([Event.PlaybackTrackChanged], async event => {
if (event.type === Event.PlaybackTrackChanged && event.nextTrack !== null) {
const track = await TrackPlayer.getTrack(event.nextTrack);
const {title, artwork, artist} = track;
console.log(event.nextTrack);
setTrackIndex(event.nextTrack);
setTrackTitle(title);
setTrackArtist(artist);
setTrackArtwork(artwork);
}
});

const gettrackdata = async () => {
let trackIndex = await TrackPlayer.getCurrentTrack();
let trackObject = await TrackPlayer.getTrack(trackIndex);
console.log(trackIndex);
setTrackIndex(trackIndex);
setTrackTitle(trackObject.title);
setTrackArtist(trackObject.artist);
setTrackArtwork(trackObject.artwork);
};

const togglePlayBack = async playBackState => {
const currentTrack = await TrackPlayer.getCurrentTrack();
if (currentTrack != null) {
if ((playBackState == State.Paused) | (playBackState == State.Ready)) {
await TrackPlayer.play();
} else {
await TrackPlayer.pause();
}
}
};

const nexttrack = async () => {
if (trackIndex < podcastsCount-1) {
await TrackPlayer.skipToNext();
gettrackdata();
};
};

const previoustrack = async () => {
if (trackIndex > 0) {
await TrackPlayer.skipToPrevious();
gettrackdata();
};
};

useEffect(() => {
setupPlayer();
}, []);

return (
<SafeAreaView style={styles.container}>
<View style={styles.mainContainer}>
<View style={styles.mainWrapper}>
<Image source={trackArtwork} style={styles.imageWrapper} />
</View>
<View style={styles.songText}>
<Text style={[styles.songContent, styles.songTitle]} numberOfLines={3}>{trackTitle}</Text>
<Text style={[styles.songContent, styles.songArtist]} numberOfLines={2}>{trackArtist}</Text>
</View>
<View>
<Slider
style={styles.progressBar}
value={progress.position}
minimumValue={0}
maximumValue={progress.duration}
thumbTintColor="#FFD369"
minimumTrackTintColor="#FFD369"
maximumTrackTintColor="#fff"
onSlidingComplete={async value => await TrackPlayer.seekTo(value) }
/>
<View style={styles.progressLevelDuraiton}>
<Text style={styles.progressLabelText}>
{new Date(progress.position * 1000)
.toLocaleTimeString()
.substring(3)}
</Text>
<Text style={styles.progressLabelText}>
{new Date((progress.duration - progress.position) * 1000)
.toLocaleTimeString()
.substring(3)}
</Text>
</View>
</View>
<View style={styles.musicControlsContainer}>
<TouchableOpacity onPress={previoustrack}>
<Ionicons
name="play-skip-back-outline"
size={35}
color="#FFD369"
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => togglePlayBack(playBackState) }>
<Ionicons
name={
playBackState === State.Playing
? 'ios-pause-circle'
: playBackState === State.Connecting
? 'ios-caret-down-circle'
: 'ios-play-circle'
}
size={75}
color="#FFD369"
/>
</TouchableOpacity>
<TouchableOpacity onPress={nexttrack}>
<Ionicons
name="play-skip-forward-outline"
size={35}
color="#FFD369"
/>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};

export default MusicPlayer;

const {width, height} = Dimensions.get('window');

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#222831',
},
mainContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
mainWrapper: {
width: width,
height: width,
justifyContent: 'center',
alignItems: 'center',
},
imageWrapper: {
alignSelf: "center",
width: '90%',
height: '90%',
borderRadius: 15,
},
songText: {
marginTop:2,
height: 70
},
songContent: {
textAlign: 'center',
color: '#EEEEEE',
},
songTitle: {
fontSize: 18,
fontWeight: '600',
},
songArtist: {
fontSize: 16,
fontWeight: '300',
},
progressBar: {
alignSelf: "stretch",
marginTop: 40,
marginLeft:5,
marginRight:5
},
progressLevelDuraiton: {
width: width,
padding: 5,
flexDirection: 'row',
justifyContent: 'space-between',
},
progressLabelText: {
color: '#FFF',
},
musicControlsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 20,
marginBottom: 20,
width: '60%',
},
});

To start your App you need to use:

npx expo start --dev-client

Then start the build you installed in your device.

--

--