This blogpost will walk you through the process of creating a basic music player in React Native as well as adding a floating player component to keep track of the song's progress across all screens of your app.
The two main libraries we will use are react-native-track-player and react-navigation .
Setup your app Implement basic routing Setup the music player Create the Player modal Create the Home screen Implement the floating player Setup your app Generate an app, my example uses React Native 0.70.6
and React 18.1.0
:
npx react-native init RNFloatingPlayer --template react-native-template-typescript
NPM v8.19.0 - generate a React Native app Install the necessary dependencies:
yarn add react-native-track-player @react-native-community/slider @react-navigation/native react-native-screens react-native-safe-area-context @react-navigation/native-stack @react-navigation/bottom-tabs
NPM v8.19.0 - install dependencies Additional steps for react-navigation:
For Android you need to add the following inside: android/app/src/main/java/com/rnfloatingplayer/MainActivity.java
:
package com.rnfloatingplayer;
import android.os.Bundle; // <- this import
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
public class MainActivity extends ReactActivity {
// this method inside MainActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
MainActivity.java Install pods for iOS:
(cd ios && pod install)
NPM v8.19.0 - install pods We're all setup, let's code!
Implement basic routing The goal here is to setup the minimum amount of routes to demonstrate the floating player.
We have 1 screen + 1 modal:
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import {Track} from 'react-native-track-player';
export enum Routes {
ROOT = 'root',
PLAYER = 'player',
HOME = 'home',
}
export type RootStackParamList = {
[Routes.ROOT]: undefined;
[Routes.PLAYER]: {index: number; position?: number; queue: Track[]};
[Routes.HOME]: undefined;
};
export type RootStackScreenProps<T extends Routes> = NativeStackScreenProps<
RootStackParamList,
T
>;
// ...
routes.tsx - typing Root
contains the tab stack, Home
will be a tab screen and Player
is a modal.
And two navigators:
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
// ...
const RootStack = createNativeStackNavigator<RootStackParamList>();
const TabStack = createBottomTabNavigator<RootStackParamList>();
// ...
routes.tsx - navigation stacks Create the two navigators where the bottom tab is nested in a native stack:
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
// ...
const TabNavigator = () => {
return (
<TabStack.Navigator>
<TabStack.Screen
component={HomeScreen}
name={Routes.HOME}
options={{
headerTitle: 'Home',
title: 'Home',
}}
/>
</TabStack.Navigator>
);
};
export const RootNavigator = () => {
return (
<RootStack.Navigator>
<RootStack.Screen
name={Routes.ROOT}
component={TabNavigator}
options={{headerShown: false}}
/>
<RootStack.Screen
name={Routes.PLAYER}
component={PlayerModal}
options={{presentation: 'modal'}}
/>
</RootStack.Navigator>
);
};
routes.tsx - navigators Wrap the app entrypoint with our new navigator:
import {NavigationContainer} from '@react-navigation/native';
const App = () => {
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
};
App.tsx Setup the music player We will prepare all of the utilities required to run our music player.
First create a custom hook to initialise our player:
import TrackPlayer from 'react-native-track-player';
export const useInitPlayer = () => {
useEffect(() => {
TrackPlayer.setupPlayer();
return () => {
TrackPlayer.reset();
};
}, []);
};
// ...
player.utils.ts - useInitPlayer custom hook Use this in the app's entrypoint:
import {NavigationContainer} from '@react-navigation/native';
const App = () => {
useInitPlayer();
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
};
App.tsx Create a hook to handle player controls and state:
import TrackPlayer, {
State,
Track
} from 'react-native-track-player';
// ...
export type Controls = {
position: number;
isPlaying: boolean;
duration: number;
startTrack: () => Promise<void>;
skipToNextTrack: () => Promise<void>;
skipToPreviousTrack: () => Promise<void>;
};
export type UsePlayerControlsResponse = {
currentTrack?: Track;
currentTrackIndex?: number;
setCurrentTrack: Dispatch<SetStateAction<Track | undefined>>;
controls: Controls;
};
export const usePlayerControls = (): UsePlayerControlsResponse => {
const [playerState, setPlayerState] = useState<State>();
const [currentTrack, setCurrentTrack] = useState<Track>();
const [currentTrackIndex, setCurrentTrackIndex] = useState<number>();
// ...
player.utils.ts - usePlayerControls playerState
is needed to determine the isPlaying
state and change the play-pause button state.
duration
is the total length of a song used for the progress bar of a song.
position
is the current progress in seconds of a song, which is also used for the progress bar.
startTrack
skipToNextTrack
skipToPreviousTrack
are handler for the player's control buttons (respectively play-pause, skip to next, skip to previous).
currentTrack
setCurrentTrack
are used to keep track of the current song playing.
currentTrackIndex
setCurrentTrackIndex
are used to keep track of the position of the current song in a queue.
We need to listen to changes in specific events to update our states with useTrackPlayerEvents
a hook from react-native-track-player
:
import TrackPlayer, {
Event,
State,
Track,
useTrackPlayerEvents,
} from 'react-native-track-player';
// ...
export const usePlayerControls = (): UsePlayerControlsResponse => {
// ...
useTrackPlayerEvents(
[Event.PlaybackTrackChanged, Event.PlaybackState],
async event => {
if (
event.type === Event.PlaybackTrackChanged &&
event.nextTrack != null
) {
const track = await TrackPlayer.getTrack(event.nextTrack);
if (track) {
if (track.url !== currentTrack?.url) {
setCurrentTrack(track);
}
if (currentTrackIndex !== event.nextTrack) {
setCurrentTrackIndex(event.nextTrack);
}
}
}
if (event.type === Event.PlaybackState) {
setPlayerState(event.state);
}
},
);
// ...
player.utils.ts - usePlayerControls Setup the player control handlers and get the progress of the current song playing with useProgress
, a hook from react-native-track-player
:
import TrackPlayer, {
Event,
State,
Track,
useProgress,
useTrackPlayerEvents,
} from 'react-native-track-player';
// ...
export const usePlayerControls = (): UsePlayerControlsResponse => {
// ...
const {position, duration} = useProgress();
const skipToNextTrack = () => TrackPlayer.skipToNext();
const skipToPreviousTrack = () => TrackPlayer.skipToPrevious();
const startTrack = async () => {
const state = await TrackPlayer.getState();
if (state !== State.Playing) {
await TrackPlayer.play();
} else {
await TrackPlayer.pause();
}
};
return {
controls: {
startTrack,
skipToNextTrack,
skipToPreviousTrack,
duration: duration || 25,
isPlaying:
playerState !== State.Playing && playerState !== State.Buffering,
position,
},
currentTrack,
currentTrackIndex,
setCurrentTrack,
};
};
player.utils.ts - usePlayerControls Create the Player modal This modal takes 3 parameters: the queue
of songs to play, the index
of the initial song to play and the position
of the initial song to play.
export const PlayerModal: FC<RootStackScreenProps<Routes.PLAYER>> = ({
route: {
params: {position = 0, index, queue},
},
}) => {
// ...
PlayerModal.tsx We will handle creating the queue in the player and changing the initial song's progress (position
) and its position in the queue (index
):
import TrackPlayer from 'react-native-track-player';
export const PlayerModal: FC<RootStackScreenProps<Routes.PLAYER>> = ({
route: {
params: {position = 0, index, queue},
},
}) => {
useEffect(() => {
const handleQueue = async () => {
await TrackPlayer.add(queue);
if (index >= 0) {
await TrackPlayer.skip(index);
}
if (position && position > 0) {
await TrackPlayer.seekTo(position);
}
};
handleQueue();
}, [index, queue, position]);
// ...
PlayerModal.tsx Get the current song to play, its progress and display it:
import TrackPlayer from 'react-native-track-player';
export const PlayerModal: FC<RootStackScreenProps<Routes.PLAYER>> = ({
route: {
params: {position = 0, index, queue},
},
}) => {
// ...
const {controls, currentTrack} = usePlayerControls();
if (!currentTrack) {
return <ActivityIndicator style={styles.centered_horizontal} />;
}
return (
<View style={styles.centered_horizontal}>
<Text>
{currentTrack.title} - {currentTrack.artist}
</Text>
{currentTrack.artwork && typeof currentTrack.artwork === 'string' && (
<Image
resizeMode="cover"
style={styles.image_dimensions}
source={{uri: currentTrack.artwork}}
/>
)}
<Controls {...controls} />
</View>
);
};
PlayerModal.tsx Here is the <Controls />
component, which takes the handlers defined in player.utils.ts
:
import Slider from '@react-native-community/slider';
import {Controls as ControlProps} from '../player.utils';
export const Controls: FC<ControlProps> = ({
startTrack,
skipToNextTrack,
skipToPreviousTrack,
duration,
position,
isPlaying,
}) => {
return (
<View>
<Slider maximumValue={duration} minimumValue={0} value={position} />
<View style={styles.row_spaced_evenly}>
<Button title="prev" onPress={skipToPreviousTrack} />
<Button title={isPlaying ? 'play' : 'pause'} onPress={startTrack} />
<Button title="next" onPress={skipToNextTrack} />
</View>
</View>
);
};
Controls.tsx Here's the result:
PlayerModal Create the Home screen This screen will just display a button to open the Player modal:
export const HomeScreen: FC<RootStackScreenProps<Routes.HOME>> = ({
navigation: {navigate},
}) => {
const openPlayer = () => navigate(Routes.PLAYER, {index: 0, queue: songs});
return (
<View style={styles.centered_horizontal}>
<Button title="Open player" onPress={openPlayer} />
</View>
);
};
This button launches the Player with a list of songs, the first song in the list is played.
For testing purpose songs
is a simple array of tracks fetched from the network:
import {Track} from 'react-native-track-player';
const ARTWORK_BASE_URL = 'https://source.unsplash.com/random/400?animals&sig=';
const SONG_BASE_URL = 'https://bigsoundbank.com/UPLOAD/mp3/';
export const songs: Track[] = Array.from(Array(15)).map((_, index) => ({
album: 'free sound',
artist: 'bigsoundbank',
artwork: `${ARTWORK_BASE_URL}${index}`,
date: new Date().toISOString(),
duration: 26,
title: (index + 1).toString().padStart(4, '0'),
url: `${SONG_BASE_URL}${(index + 1).toString().padStart(4, '0')}.mp3`,
}));
Here's the result:
HomeScreen Implement the floating player Once the music player is implemented and playable, we will add a floating minimal player component on every screen to keep track of a playing song's progress.
There are multiple ways to do it based on the design you wish to integrate:
Integrate with react-navigation by modifying a component (header, tap bar, ...) Add a component next to the <NavigationContainer />
and use absolute positioning In this case we will display the floating player as a thin bar above the bottom tap bar, like you would see in Spotify.
In the routes
file modify the TabNavigator
to add a custom bottom tap-bar for the tab navigator:
const TabNavigator = () => {
return (
<TabStack.Navigator tabBar={CustomBottomTabBar}> // <- here
<TabStack.Screen
component={HomeScreen}
name={Routes.HOME}
options={{
headerTitle: 'Home',
title: 'Home',
}}
/>
</TabStack.Navigator>
);
};
CustomBottomTabBar
is composed of the default react-navigation
tab-bar and a new component FloatingPlayer
that will show the playing song's progress:
import {BottomTabBar, BottomTabBarProps} from '@react-navigation/bottom-tabs';
export const CustomBottomTabBar = (props: BottomTabBarProps) => (
<View>
<FloatingPlayer />
<BottomTabBar {...props} />
</View>
);
The FloatingPlayer
component simply displays a minimal version of the Player modal:
import Slider from '@react-native-community/slider';
export const FloatingPlayer = () => {
const {navigate} = useNavigation<NavigationProp<RootStackParamList>>();
const {
currentTrackIndex,
currentTrack,
controls: {isPlaying, startTrack, position, duration},
} = usePlayerControls();
if (!currentTrack) {
return null;
}
const {artist, title} = currentTrack;
const playerPressHandler = () =>
navigate(Routes.PLAYER, {
index:
currentTrackIndex && currentTrackIndex >= 0 ? currentTrackIndex : 0,
position: position,
queue: songs,
});
return (
<Pressable onPress={playerPressHandler}>
<View style={styles.container}>
<View style={styles.centered_row_space_between}>
<Text>
{title} - {artist}
</Text>
<Button title={isPlaying ? 'play' : 'pause'} onPress={startTrack} />
</View>
<Slider maximumValue={duration} minimumValue={0} value={position} />
</View>
</Pressable>
);
};
FloatinPlayer.tsx With the usePlayerControls
hook we created, we can get the handlers and state of the current song playing. Then you can modify your floating player as you see fit.
Here we just display the title, artist, song progress and a play-pause button.
Pressing on the FloatingPlayer
component will open the Player modal at the correct song position
and index
.
Here's the result:
HomeScreen with the current track in FloatingPlayer And that's it !
Visit this documentation to learn more about react-native-track-player and all of the cool features you can add to your new music player!
Check out our other React Native blogpost