DEV Community

Cover image for How to Add Background Audio to Expo Apps
Josie Daw
Josie Daw

Posted on

How to Add Background Audio to Expo Apps

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

  1. Get Started
  2. Install Expo AV
  3. Set Up a Basic Audio Button
  4. Add Allow Background Playing

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,
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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>
  );
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

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,
  },
});

Enter fullscreen mode Exit fullscreen mode

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"
  }
Enter fullscreen mode Exit fullscreen mode

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 (6)

Collapse
 
meteorsd profile image
meteorSD

For people having issue on react native 0.73 and android 14, here's the solution: github.com/expo/expo/issues/30371#...

Collapse
 
josie profile image
Josie Daw

It looks like the latest version of Expo introduced some bugs with background audio, hopefully they will patch it soon!

Collapse
 
drebakare profile image
emmanuel damilare

I successfully implemented this without ejecting from the Expo and wrote an article detailing the step-by-step process.
Here is the link: drebakare.medium.com/enabling-back...

Collapse
 
saeid_houti profile image
Saeid

Thank You Josie!

Collapse
 
pranav1924 profile image
Pranav • Edited

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?

Collapse
 
josie profile image
Josie Daw

I do not know of any other way to schedule a sound/alarm without using expo-notifications, sorry!