One of the most common problems that I've come across with audio with Expo apps is trying to get the audio to play even while the app is no longer on screen/in focus. There are many use cases where you want audio to persist from your app even while using another app or when the screen is locked.
The guide below is a simple tutorial for how to use Expo AV to play audio in the background of your Expo app.
Table of Contents
Get Started
First, create an Expo app. You can follow this tutorial if it is your first time using Expo.
Once you have your basic app set up, you can run it on Expo Go (or an emulator/device if you prefer).
Install Expo AV
Expo has a great audio library ready for you to use, you just need to install it. Run npx expo install expo-av
.
Set Up a Basic Audio Button
For this tutorial, we will create a very basic audio player based on a simple play button (in Typescript)
First, create a new file called AudioButton.tsx
. In this case, we will pass a URL from the parent component, so we will add it as a props with AudioButtonProps
.
Then create two basic states to hold our playing status and sound status.
Next, we want to create a useEffect
to prepare the audio on load. Inside the useEffect
, we will set all of the important options to enable audio to play in the background in an Expo app.
You will notice that we have staysActiveInBackground
set to true
. This is a basic requirement, but is not enough on its own for the background audio to work.
The most common problem that people have with Expo AV is that they don't set all of the options:
staysActiveInBackground: true,
playsInSilentModeIOS: true,
interruptionModeIOS: InterruptionModeIOS.DuckOthers,
interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: true,
Each of these options needs to be set for audio to play in the background in every case on different devices.
For interruptionModeIOS
and interruptionModeAndroid
, you can choose MixWithOthers
, DoNotMix
, or DuckOthers
. We have set DuckOthers
so that any other audio nicely fades out, which makes for a more pleasant user experience.
You can read more details about these options on the Expo Audio page.
// AudioButton.tsx
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av';
import { Sound } from 'expo-av/build/Audio';
interface AudioButtonProps {
audioUrl: string;
}
const AudioButton = ({ audioUrl }: AudioButtonProps) => {
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [sound, setSound] = useState<Sound | null>(null);
useEffect(() => {
Audio.setAudioModeAsync({
staysActiveInBackground: true,
playsInSilentModeIOS: true,
interruptionModeIOS: InterruptionModeIOS.DuckOthers,
interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: true,
});
return sound
? () => {
sound.unloadAsync();
}
: undefined;
}, [sound]);
};
export default AudioButton;
Next, we will create a function to play the sound and a little button to trigger it with. Expo comes with Vector icons built-in, so you can change the FontAwesome
icon to any you prefer. You can search the icons here.
const playAudio = async () => {
// Set and play the sound
const { sound: newSound } = await Audio.Sound.createAsync({ uri: audioUrl });
setSound(newSound);
setIsPlaying(true);
await newSound.playAsync();
// After the sound has finished, update the state so that the icon changes
newSound.setOnPlaybackStatusUpdate((status) => {
if ('didJustFinish' in status && status.didJustFinish) {
setIsPlaying(false);
}
});
};
return (
<TouchableOpacity onPress={playAudio}>
<FontAwesome name={isPlaying ? 'volume-up' : 'play'} size={15} color="#6b7280" />
</TouchableOpacity>
);
Let's also add a bit of styling so it looks like a nice round play button.
<TouchableOpacity style={styles.button} onPress={playAudio}>
<FontAwesome name={isPlaying ? 'volume-up' : 'play'} size={15} color="#6b7280" />
</TouchableOpacity>
// Include this after the AudioButton export
const styles = StyleSheet.create({
button: {
height: 30,
width: 30,
borderRadius: 15, // Half of the height/width
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#e5e7eb',
marginHorizontal: 5,
},
});
Now we will put all of the code together so you can see where all of the pieces go.
import { FontAwesome } from '@expo/vector-icons';
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av';
import { Sound } from 'expo-av/build/Audio';
import React, { useEffect, useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
interface AudioButtonProps {
audioUrl: string;
}
const AudioButton = ({ audioUrl}: AudioButtonProps) => {
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [sound, setSound] = useState<Sound | null>(null);
// Prepare the audio
useEffect(() => {
Audio.setAudioModeAsync({
staysActiveInBackground: true,
playsInSilentModeIOS: true,
interruptionModeIOS: InterruptionModeIOS.DuckOthers,
interruptionModeAndroid: InterruptionModeAndroid.DuckOthers,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: true,
});
return sound
? () => {
sound.unloadAsync();
}
: undefined;
}, [sound]);
// Trigger the audio
const playAudio = async () => {
const { sound: newSound } = await Audio.Sound.createAsync({ uri: audioUrl });
setSound(newSound);
setIsPlaying(true);
await newSound.playAsync();
newSound.setOnPlaybackStatusUpdate((status) => {
if ('didJustFinish' in status && status.didJustFinish) {
setIsPlaying(false);
}
});
}
};
return (
<TouchableOpacity style={styles.button} onPress={playAudio}>
<FontAwesome name={isPlaying ? 'volume-up' : 'play'} size={15} color="#6b7280" />
</TouchableOpacity>
);
};
export default AudioButton;
const styles = StyleSheet.create({
button: {
height: 30,
width: 30,
borderRadius: 15, // Half of the height/width
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#e5e7eb',
marginHorizontal: 5,
},
});
Now we have our audio button with some background settings included. You can include this button anywhere in your app with <AudioButton audioUrl="https://some-audio-url-here.com" />
However, we still have to make some extra changes to ensure that it will work with iOS and Android devices.
Add Allow Background Playing
For iOS and Android to allow background audio, you should update your app.json
/app.config.js
to include UIBackgroundModes: ['audio']
and permissions: ['WAKE_LOCK']
.
permissions: ['WAKE_LOCK']
will keep the Android version of the app active while the screen is locked, but can drain power, so be careful of using this permission if it is not required.
If you do not include UIBackgroundModes: ['audio']
your app can be rejected by the App Store reviewers.
"ios": {
"buildNumber": "1",
"infoPlist": {
"UIBackgroundModes": ["audio"]
},
"bundleIdentifier": "com.fakeapp"
},
"android": {
"versionCode": 1,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFF"
},
"package": "com.fakeapp",
"permissions": ["WAKE_LOCK"],
"googleServicesFile": "./google-services.json"
}
After adding all of these, you may be wondering during your own testing why it doesn't seem to work. If you are using the Expo Go app or Expo development build, the background audio mode will not work correctly, because when you close your screen or move to another app, your app will lose connection to Expo metro and stop working.
In order to confirm that the background audio definitely works, you will need to create a real build and try it on TestFlight/Internal Testing. (Just make sure to test your audio button while the app is open before creating a build for it!)
Thanks for following along with this short tutorial for how to add background audio to Expo apps! If you're interested in learning more about me, you can visit my portfolio here.
If you are facing an error anywhere along the way, feel free to leave a comment below and we can try to debug it together!
Top comments (5)
For people having issue on react native 0.73 and android 14, here's the solution: github.com/expo/expo/issues/30371#...
It looks like the latest version of Expo introduced some bugs with background audio, hopefully they will patch it soon!
Thank You Josie!
I am working on alarm app in react native, I don't want to use expo-notifications or any other notification api because I want to play a sound even though the app is in background. can you suggest the solution?
I do not know of any other way to schedule a sound/alarm without using expo-notifications, sorry!