CONTACT US
TABLE OF CONTENTS

How to Build a Music Streaming App with React Native

How to Build a Music Streaming App with React Native

Introduction

Daily, most of us use various music streaming apps. Without a doubt, the most popular application is Spotify and YouTube music. In this article, I will present a React Native library which we can use to create our own music app. We will discuss its most important functions and finally, I will demonstrate how to build a music streaming app with React Native.

The library we will be talking about is React Native Track Player. It is currently the best audio playback library. It is characterized primarily by ease of implementation, simple configuration and functions that will make your music streaming application look fully professional on digital platforms and with a little work – in terms of functionality it will not differ from the top music mobile apps that you can find on Google Play and the App Store. And so it’s the right place for both music lovers and mobile app developers!

React Native Track Player

As I mentioned earlier, the article will be devoted to the React Native Track Player package. So it’s worth explaining what it is at the very beginning. As the welcome text in the documentation says:

A fully fledged audio module created for music apps. Provides audio playback, external media controls, background mode and more!

Sounds good right? In short, we can say that by using it we will create a fully functional music application that we can control from the application level and externally.

Since we’re talking about the React Native library, we don’t have to build separate native apps for iOS and Android, but one music streaming service for both of them using the same code.

So let’s discuss the most important features of React Native Track Player:

  • Optimized Resource Usage: Tailored to use minimal device resources efficiently, ensuring optimal performance for your app’s needs.
  • Cross-platform compatibility: Works harmoniously across iOS and Android platforms, aligning with the native design principles of music applications.
  • Media Control Accessibility: Enables control of the application through various avenues like Bluetooth devices, lock screens, notifications, smartwatches, and car systems.
  • Caching Capability: Allows for the caching of media files, enabling seamless playback even in scenarios where the internet connection is disrupted.
  • Background Playback Support: Provides the capability to continue audio playback even when the application operates in the background.
  • Extensive Customization: Offers comprehensive customization options, including customizable notification icons, enabling tailored own app’s UI experiences as desired by developers.

Native modules

React Native Track Player, known for its cross-platform compatibility, seamlessly operates on both iOS and Android devices. Underlying this compatibility are two pivotal native modules: SwiftAudioEx and KotlinAudio.

SwiftAudioEx is dedicated to empowering seamless operations tailored explicitly for iOS devices. On the other hand, KotlinAudio is meticulously designed for Android, ensuring an optimized and tailored experience on this platform.

These native modules play a critical role in boosting performance by leveraging platform-specific functionalities. Specifically, they enable the storage of playlists in native data structures, contributing significantly to the app’s efficiency and performance. By employing SwiftAudioEx and KotlinAudio, React Native Track Player strives to deliver an exceptional user experience, optimizing the library’s functionalities to the core features of each platform.

Why React Native Track Player?


React Native Track Player stands out as an optimal choice for music app development due to its streamlined integration process and cross-platform compatibility across iOS and Android.

With a rich feature set encompassing audio playback controls, background mode support, and seamless integration with external devices, it offers efficient and optimized performance, utilizing minimal device resources.

Leveraging native modules for platform-specific functionalities ensures high performance, while its flexibility allows for easy customization of the app’s UI and functionalities. Supported by extensive documentation and an active community, React Native Track Player proves to be a reliable and robust solution for creating music streaming apps in React Native.

“Working with React Native Track Player has truly simplified my approach to building music streaming apps. Its efficiency and user-friendly design make it a standout choice for seamless cross-platform development. From synchronizing audio playback controls to creating immersive user experiences, this library has transformed the way I work with the apps.”

Michal Moroz, React Native Developer at Pagepro
Michal Moroz, React Native Developer

Don’t have time to build a music app on your own?

Platform Support

As we gear up to craft our very own music streaming app, understanding the compatibility across different platforms is key. In this section, we’re delving deep into a comparison that highlights the supported streaming types and casting features on both iOS and Android platforms.

Stream Types

Casting

Miscellaneous

React Native Track Player functions

In this chapter, I will discuss some of the most basic functions provided by the library. Some of them we will use later while building your own music streaming app. If you are interested in all the events and functions provided by the library (and there are a lot of them’s quite a lot of them) – I refer you to the documentation.

Player

FeatureDescription


setupPlayer(options)
Setup player with options. Sample options: minBuffer, maxBuffer, maxCacheSize. 
play()Plays/resumes current track
pause()Pause track
stop()Stops playback
retry()Replays the current track if it was stopped due to an error
seekBy(offset)Seeks by a relative time offset in the current track
seekTo(seconds)Seeks current track to specified time position
setVolume(volume)Sets the volume
getVolume()Gets player volume
setRate(rate)Sets the playback rate
getProgress()Gets the playback progress – position, duration
getPlaybackState()Gets the PlaybackState – e.g.ready, playing, paused, stopped.
getPlayWhenReady()Gets the state of playWhenReady

Queue

FunctionDescription
add(tracks, insertBeforeIndex)Adds tracks to the queue
remove(tracks)Clears the current queue and adds tracks to the empty queue
skip(index, initialPosition)Skips to a track in the queue
skipToNext(initialPosition)Skips to the next track
skipToPrevious(initialPosition)Skips to the previous track
reset()Resets the player and clear the queue
getTrack(index)Gets a track object
getActiveTrack()Gets active track object
getQueue()Gets a queue
removeUpcomingTracks()Clears upcoming tracks from the queue
setRepeatMode(mode)Sets the repeat mode – Loops the current track / Repeats the whole queue / Does Not repeat

Building a Music Streaming App with React Native Track Player

Now that you’re familiar with React Native Track Player and its capabilities, let’s start with the development of our music app! Here’s a glimpse of what our application will feature:

  • A fully functional music player showcasing a sample playlist of diverse music tracks.
  • Core functionalities including play and pause controls for the active song.
  • An interactive progress bar illustrating the playback status of the currently selected track.

The application’s interface will resemble the following structure:

Let’s write our simple music streaming application

I assume you already have a new React Native project created. If not, I refer you to the Expo documentation – https://docs.expo.dev/get-started/create-a-project/.

In the next chapters, we will focus on installing the necessary packages and preparing the basic configuration of our streaming app.

To begin, we’ll initiate the installation process by adding the React Native Track Player library:

yarn add react-native-track-player

Following that, we’ll proceed by installing the progress bar package. For this scenario, we’ve opted for ‘@miblanchard/react-native-slider’ due to its extensive functionality and ease of styling:

yarn add @miblanchard/react-native-slider

Add Service

Next, let’s incorporate the playback service. It’s crucial to register the playback service immediately after registering the primary React Native application component, typically found in the ‘index.js’ file located in the project’s root directory:

IMPORTANT NOTE: The configuration and components will be stored in the src directory. So if you don’t have it, create it or do it according to your own convention. Above the code I will add the paths to the files I used.

To enable background mode functionality in RN Track Player, we’re required to configure the player to sustain audio playback even when the application is in the background.

Let’s create a file named ‘RNTP-service.js’ to manage this configuration.

src/setup/RNTP-service.js

import TrackPlayer, { Event } from 'react-native-track-player'

export const RNTPService = async () => {
 TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play())
 TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause())
}

Then, in the project root, create an index.js file and import the service and App there.

index.js

import { registerRootComponent } from 'expo'
import TrackPlayer from 'react-native-track-player'

import { RNTPService } from './src/setup/rntp-service'

import App from './App'

registerRootComponent(App)

TrackPlayer.registerPlaybackService(() => RNTPService)

Then, we need to set up the player. In App.js file add setupPlayer.

App.js

import { SafeAreaView, StyleSheet, Text, View } from 'react-native'
import { useEffect } from 'react'
import TrackPlayer, {
 AppKilledPlaybackBehavior,
 Capability,
} from 'react-native-track-player'

export default function App() {
 useEffect(() => {
   const setupPlayer = async () => {
     await TrackPlayer.setupPlayer()

     await TrackPlayer.updateOptions({
       android: {
         appKilledPlaybackBehavior:
           AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification,
         alwaysPauseOnInterruption: true,
       },
       capabilities: [
         Capability.Play,
         Capability.Pause,
         Capability.SkipToNext,
         Capability.SkipToPrevious,
         Capability.SeekTo,
       ],
       compactCapabilities: [Capability.Play, Capability.Pause],
       progressUpdateEventInterval: 1,
     })
   }

   setupPlayer()
 }, [])

 return (
   <View style={styles.container}>
     <SafeAreaView />
     <Text style={styles.header}>MUSIC APP</Text>
   </View>
 )
}

const styles = StyleSheet.create({
 container: {
   flex: 1,
   paddingHorizontal: 16,
   backgroundColor: '#f53340',
 },
 header: {
   fontSize: 28,
   fontWeight: '700',
   textAlign: 'center',
   color: '#FFF',
   marginTop: 20,
   marginBottom: 48,
 },
})

Once you’ve completed these steps, you’ll have the groundwork laid for a fully functional music player.

Add player playlist

Before creating the first component, create an array of songs that the music player will use. For each song, ensure to include a URL pointing to the audio file, which could be either a file path or a direct URL.

App.js

export const tracks = [
  {
    id: 1,
    url: YOUR_URL,
    title: '01. Track 1 sample',
    playlistName: 'Playlist sample',
    playlistId: 1,
  },
  {
    id: 2,
    url: YOUR_URL,    
    title: '02. Track 2 sample',
    playlistName: 'Playlist sample',
    playlistId: 1,
  },
  {
    id: 3,
    url: YOUR_URL,    
    title: '03. Track 3 sample',
    playlistName: 'Playlist sample',
    playlistId: 1,
  },
  {
    id: 4,
    url: YOUR_URL,    
    title: '04. Track 4 sample',
    playlistName: 'Playlist sample',
    playlistId: 1,
  },
  {
    id: 5,
    url: YOUR_URL,    
    title: '05. Track 5 sample',
    playlistName: 'Playlist sample',
    playlistId: 1,
  },
]

Create Track List

In the components directory, the initial addition is the TrackList component. This component serves as a clickable list of songs. Upon selecting a song, it activates the track and generates the music queue. Pay special attention to the PlaybackActiveTrackChanged event and the onTrackPress function.

App.js

import { FlatList, Pressable, StyleSheet, View, Text } from 'react-native'
import TrackPlayer, {
 Event,
 useTrackPlayerEvents,
} from 'react-native-track-player'
import { useState } from 'react'

const TrackList = ({ items }) => {
 const [activeTrackIndex, setActiveTrackIndex] = useState(0)

 useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], async (event) => {
   if (event.index === undefined) {
     setActiveTrackIndex(0)
     return
   }

   if (
     event.type === Event.PlaybackActiveTrackChanged &&
     event.index != null
   ) {
     const activeTrackItem = await TrackPlayer.getActiveTrack()

     if (!activeTrackItem) {
       return
     }

     setActiveTrackIndex(activeTrackItem.id)
   }
 })

 useTrackPlayerEvents([Event.PlaybackQueueEnded], async () => {
   const repeatMode = await TrackPlayer.getRepeatMode()
   TrackPlayer.seekTo(0)

   if (repeatMode === 0) {
     await TrackPlayer.pause()
   } else {
     await TrackPlayer.play()
   }
 })

 const onTrackPress = async (id) => {
   const currentTrackIndex = items.findIndex((item) => item.id === id)
   const trackPlayerQueue = await TrackPlayer.getQueue()
   const sameTrackFromQueue = trackPlayerQueue[currentTrackIndex]
   const currentTrack = items[currentTrackIndex]

   if (currentTrack.id !== sameTrackFromQueue?.id) {
     const newQueue = items.map((item) => ({
       id: item.id,
       url: item.url,
       title: item.title,
       headers: {
         playlist: item.playlistName,
       },
       playlistId: item.playlistId,
     }))

     await TrackPlayer.reset()
     await TrackPlayer.add(newQueue[currentTrackIndex])
     await TrackPlayer.add(newQueue)
     await TrackPlayer.skip(currentTrackIndex + 1)
     await TrackPlayer.play()
     await TrackPlayer.remove(0)
   } else {
     await TrackPlayer.skip(currentTrackIndex)
     await TrackPlayer.play()
   }
 }

 return (
   <View style={styles.container}>
     <FlatList
       data={items}
       renderItem={({
         item: { id, url, title, playlistName, playlistId },
       }) => {
         const isActiveTrack = activeTrackIndex === id

         return (
           <View
             key={id}
             style={[styles.listItem, isActiveTrack && styles.listItemActive]}
           >
             <Pressable
               onPress={() =>
                 onTrackPress(id)
               }
             >
               <Text
                 style={[styles.title, isActiveTrack && styles.textActive]}
               >
                 {title}
               </Text>
               <Text
                 style={[
                   styles.description,
                   isActiveTrack && styles.textActive,
                 ]}
               >
                 {playlistName}
               </Text>
             </Pressable>
           </View>
         )
       }}
       alwaysBounceVertical={false}
       keyExtractor={({ id }) => id.toString()}
     />
   </View>
 )
}

export default TrackList

const styles = StyleSheet.create({
 container: {
   flex: 1,
 },
 listItem: {
   paddingVertical: 4,
   backgroundColor: '#E21515',
   marginBottom: 8,
   borderRadius: 8,
   paddingHorizontal: 8,
   paddingVertical: 12,
 },
 listItemActive: {
   backgroundColor: '#FFF',
 },
 header: {
   marginBottom: 8,
   color: '#FFF',
   fontWeight: '700',
 },
 title: {
   fontSize: 14,
   color: '#FFF',
 },
 description: {
   fontSize: 12,
   color: '#FFF',
 },
 textActive: {
   color: '#E21515',
   fontWeight: '700',
 },
})

Create Player

The final element to be created is the component called player. This component will encompass the following elements:

  • Current song display
  • Song timer
  • Clickable progress bar (utilizing the @miblanchard/react-native-slider package)
  • Play/pause button functionality
import { StyleSheet, View, Text, Pressable } from 'react-native'
import { Slider } from '@miblanchard/react-native-slider'
import TrackPlayer, {
 useProgress,
 usePlaybackState,
 State,
 Event,
 useTrackPlayerEvents,
} from 'react-native-track-player'
import { Dimensions } from 'react-native'
import { useCallback, useState, useEffect } from 'react'

const AudioPlayer = () => {
 const { position, duration } = useProgress(100)
 const { state: playbackState } = usePlaybackState()

 const [isPlaying, setIsPlaying] = useState(playbackState === State.Playing)
 const [activeTrackData, setActiveTrackData] = useState()
 const [isPlayButtonDisabled, setIsPlayButtonDisabled] = useState(true)

 // Util for time format
 const formatTime = (timeInSeconds) => {
   const minutes = Math.floor(timeInSeconds / 60)
     .toString()
     .padStart(1, '0')
   const seconds = (Math.trunc(timeInSeconds) % 60).toString().padStart(2, '0')

   return `${minutes}:${seconds}`
 }

 const play = useCallback(async () => {
   await TrackPlayer.play()
   setIsPlayButtonDisabled(false)
 }, [])

 const pause = useCallback(async () => {
   await TrackPlayer.pause()
 }, [])

 const trackTimeProgress = formatTime(position)
 const trackTimeLeft = formatTime(duration - position)

 useEffect(() => {
   if (playbackState === State.Playing && !isPlaying) {
     setIsPlaying(true)
   }

   if (
     playbackState === State.Paused ||
     (playbackState === State.Stopped && isPlaying)
   ) {
     setIsPlaying(false)
   }
 }, [isPlaying, playbackState])

 useTrackPlayerEvents([Event.PlaybackActiveTrackChanged], async (event) => {
   if (
     event.type === Event.PlaybackActiveTrackChanged &&
     event.index != null
   ) {
     const activeTrackItem = await TrackPlayer.getActiveTrack()

     if (!activeTrackItem) {
       return
     }

     setIsPlayButtonDisabled(false)

     setActiveTrackData({
       title: activeTrackItem.title,
       playlist: activeTrackItem.headers.playlist,
     })
   }
 })

 return (
   <View style={styles.container}>
     {activeTrackData && (
       <View style={styles.activeTrack}>
         <Text style={styles.activeTrackTitle}>{activeTrackData.title}</Text>
         <Text style={styles.activeTrackDescription}>
           {activeTrackData.playlist}
         </Text>
       </View>
     )}
     <Slider
       style={styles.progressBar}
       trackStyle={{
         height: 6,
         width: Dimensions.get('window').width - 32,
       }}
       thumbTouchSize={{ width: 4, height: 4 }}
       animationType="timing"
       trackClickable
       value={position}
       minimumValue={0}
       maximumValue={duration}
       thumbTintColor={'#fff'}
       minimumTrackTintColor={'#fff'}
       maximumTrackTintColor={'#fff'}
       onSlidingComplete={async (value) => {
         TrackPlayer.seekTo(Number(value))
       }}
     />
     <Text style={[styles.text, styles.timer]}>
       {trackTimeProgress} / {trackTimeLeft}
     </Text>
     <View style={styles.buttons}>
       <Pressable
         disabled={isPlayButtonDisabled}
         style={[styles.button, isPlayButtonDisabled && styles.buttonDisabled]}
         onPress={isPlaying ? pause : play}
       >
         <Text style={styles.buttonText}>{isPlaying ? 'PAUSE' : 'PLAY'}</Text>
       </Pressable>
     </View>
   </View>
 )
}

export default AudioPlayer

const styles = StyleSheet.create({
 container: {
   justifyContent: 'flex-end',
   width: '100%',
   backgroundColor: '#f53340',
   paddingVertical: 32,
 },
 activeTrack: {
   marginBottom: 32,
 },
 activeTrackTitle: {
   fontSize: 28,
   fontWeight: '700',
   color: '#fff',
 },
 activeTrackDescription: { fontSize: 20, fontWeight: '700', color: '#fff' },
 buttons: {
   flexDirection: 'row',
   justifyContent: 'flex-end',
   alignItems: 'flex-end',
   marginBottom: 12,
   width: '100%',
 },
 button: {
   backgroundColor: '#fff',
   paddingVertical: 8,
   paddingHorizontal: 16,
   marginLeft: 16,
   borderRadius: 8,
 },
 buttonText: {
   fontWeight: '700',
   textAlign: 'center',
   color: '#f53340',
   fontSize: 18,
 },
 buttonDisabled: {
   opacity: 0,
 },
 progressBar: {
   alignItems: 'center',
   flex: 1,
   width: '100%',
 },
 timer: {
   fontSize: 18,
   marginTop: 8,
   marginBottom: 20,
   fontWeight: '700',
 },
 text: {
   color: '#FFF',
 },
})

Getting it all together

Now that we have the components ready, all that remains is to put everything together. In the App.js file we import the list and player components. In addition to the components, we need to import a const containing a list of songs.

App.js

import AudioPlayer from './src/components/AudioPlayer'
import TrackList from './src/components/TrackList'

import { tracks } from './src/consts/index'

Subsequently, we add the components and pass the tracks array as props to the list component.

 return (
   <View style={styles.container}>
     <SafeAreaView />
     <Text style={styles.header}>MUSIC APP</Text>
     <TrackList items={tracks} />
     <AudioPlayer />
   </View>
 )

And that’s all. Now you should see the player presented at the beginning of this chapter.

I recommend that you play around with the player’s functionalities and then add additional functions such as scrolling to the next/previous song, volume control or support for more than one playlist.

Summary

In this short tutorial, we managed to create a very simple music player that supports a playlist. As you have noticed, the functions and events provided by React Native Track Player make our work much easier, so we can focus more on how the functionality should work rather than how to create it. And this is only a fraction of what this music library provides us. 

After a thorough analysis of the API, you will realize that you can create a React Native track player that supports several playlists with full control over the song being played. Offline support will allow you to download a few tracks to your device and play them offline.

Of course, you can always use the expertise of a mobile app development company to help you build a music streaming app that will work anywhere and that you will be able to monetize.

Read more

Why React Native updates are important?

Publishing Expo React Native apps

Best React Native Tech Stack in 2024

Need someone to build a music streaming app for you?

Sources:

Michał Moroz

Joining Pagepro in 2018, he excel in front-end development, showcasing his proficiency and passion for creating engaging, user-centric web applications. In the beginning of 2022, Michał transitioned to the role of a React Native Developer at Pagepro, marking a significant shift in his career path. This shift is driven by a desire to leverage his front-end expertise in a more dynamic and rapidly evolving environment, focusing on building cross-platform mobile applications.

Article link copied

Close button

Leave a Reply

* Required informations.