How to Add Background Audio to Expo Apps
Tags: expo, reactnative, programming, typescript
Published: 2024-01-22 03:19:16

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](#get-started)
2. [Install Expo AV](#install-expo-av)
3. [Set Up a Basic Audio Button](#set-up-a-basic-audio-button)
4. [Add Allow Background Playing](#add-allow-background-playing)

---


### Get Started

First, create an Expo app. You can follow [this tutorial](https://docs.expo.dev/tutorial/create-your-first-app/) 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](https://docs.expo.dev/versions/latest/sdk/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](https://icons.expo.fyi/Index).

```
 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](https://josiedaw.com).

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!