Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions apps/native-component-list/src/screens/Audio/Recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ export default function Recorder({ onDone, style }: RecorderProps) {
}, []);

const audioRecorder = useAudioRecorder(recorderOptions, (status) => {
if (status.mediaServicesDidReset) {
console.warn('[Recorder] Media services were reset');
Alert.alert(
'Recording Interrupted',
status.hasError
? 'The system interrupted your recording and recovery failed.'
: 'The system interrupted your recording. Tap the mic to start a new recording.',
[{ text: 'OK' }]
);
setState(status);
return;
}

setState(status);

// Handle automatic recording completion (from forDuration or atTime+forDuration)
Expand Down Expand Up @@ -110,12 +123,21 @@ export default function Recorder({ onDone, style }: RecorderProps) {
setState((state) => ({ ...state, options: undefined, durationMillis: 0 }));
};

const clearError = () => {
setState((prev) => ({ ...prev, error: null, hasError: false }));
};

const maybeRenderErrorOverlay = () => {
if (state.error) {
return (
<ScrollView style={styles.errorMessage}>
<Text style={styles.errorText}>{state.error}</Text>
</ScrollView>
<View style={styles.errorMessage}>
<ScrollView style={styles.errorScroll}>
<Text style={styles.errorText}>{state.error}</Text>
</ScrollView>
<TouchableOpacity style={styles.dismissButton} onPress={clearError}>
<Text style={styles.dismissButtonText}>Dismiss</Text>
</TouchableOpacity>
</View>
);
}
return null;
Expand Down Expand Up @@ -274,11 +296,30 @@ const styles = StyleSheet.create({
errorMessage: {
...StyleSheet.absoluteFillObject,
backgroundColor: Colors.errorBackground,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorScroll: {
maxHeight: '60%',
},
errorText: {
margin: 8,
fontWeight: 'bold',
color: Colors.errorText,
textAlign: 'center',
},
dismissButton: {
marginTop: 20,
paddingVertical: 12,
paddingHorizontal: 24,
backgroundColor: 'white',
borderRadius: 8,
},
dismissButtonText: {
color: Colors.errorText,
fontWeight: 'bold',
fontSize: 16,
},
bigRoundButton: {
width: 100,
Expand Down
13 changes: 7 additions & 6 deletions docs/pages/router/advanced/native-tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -359,13 +359,11 @@ Pass a string with the asset name to use the same icon for both default and sele
```tsx app/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

const { Icon } = NativeTabs.Trigger;

export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Icon xcasset="home-icon" />
<NativeTabs.Trigger.Icon xcasset="home-icon" />
</NativeTabs.Trigger>
</NativeTabs>
);
Expand All @@ -377,13 +375,16 @@ To use different icons for default and selected states, pass an object:
```tsx app/_layout.tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

const { Icon } = NativeTabs.Trigger;

export default function TabLayout() {
return (
<NativeTabs>
<NativeTabs.Trigger name="index">
<Icon xcasset={{ default: 'home-outline', selected: 'home-filled' }} />
<NativeTabs.Trigger.Icon
xcasset={{
default: 'home-outline',
selected: 'home-filled',
}}
/>
</NativeTabs.Trigger>
</NativeTabs>
);
Expand Down
154 changes: 134 additions & 20 deletions docs/pages/versions/unversioned/sdk/audio.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ You can configure `expo-audio` using its built-in [config plugin](/config-plugin
[
"expo-audio",
{
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone."
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone.",
"enableBackgroundPlayback": true,
"enableBackgroundRecording": false
}
]
]
Expand Down Expand Up @@ -69,9 +71,15 @@ You can configure `expo-audio` using its built-in [config plugin](/config-plugin
{
name: 'enableBackgroundRecording',
description:
'A boolean that determines whether to enable background audio recording. On Android, this adds foreground service permissions and displays a persistent notification during recording. On iOS, this adds the `audio` background mode. **Note:** Background recording can significantly impact battery life.',
'A boolean that determines whether to enable background audio recording. On Android, this adds a recording foreground service and permissions and displays a persistent notification during recording. On iOS, this adds the `audio` background mode. **Note:** Background recording can significantly impact battery life.',
default: 'false',
},
{
name: 'enableBackgroundPlayback',
description:
'A boolean that determines whether to enable background audio playback. On Android, this adds a media playback foreground service and allows you to display the lockscreen controls and is required for sustained background playback. On iOS, this adds the `audio` background mode.',
default: 'true',
},
]}
/>

Expand Down Expand Up @@ -187,31 +195,136 @@ const styles = StyleSheet.create({

</SnackInline>

### Playing audio in background&ensp;<PlatformTags platforms={['ios']} />
### Playing audio in background

Background audio playback allows your app to continue playing audio when it moves to the background or when the device screen locks.

On iOS, audio playback and recording in background is only available in standalone apps, and it requires some extra configuration.
On iOS, each background feature requires a special key in `UIBackgroundModes` array in your **Info.plist** file.
In standalone apps this array is empty by default, so to use background features you will need to add appropriate keys to your **app.json** configuration.
#### Configuration

See an example of **app.json** that enables audio playback in background:
To enable background audio playback, use the config plugin in your [app config](/workflow/configuration/):

```json
```json app.json
{
"expo": {
...
"ios": {
...
"infoPlist": {
...
"UIBackgroundModes": [
"audio"
]
}
}
"plugins": [
[
"expo-audio",
{
"enableBackgroundPlayback": true
}
]
]
}
}
```

The above configuration automatically configures the required native settings:

- <PlatformTags platforms={['android']} /> Adds `FOREGROUND_SERVICE` and
`FOREGROUND_SERVICE_MEDIA_PLAYBACK` permissions. Also declares a media playback foreground service
(`AudioControlsService`) in app's **AndroidManifest.xml**.
- <PlatformTags platforms={['ios']} /> Adds the `audio` `UIBackgroundMode` capability

#### Usage

After configuring your app with the config plugin, you need to:

1. **Configure the audio session** to allow background playback
2. **Enable lock screen controls** (required on Android for sustained background playback)

```jsx
import { View, Button } from 'react-native';
import { useAudioPlayer, setAudioModeAsync } from 'expo-audio';
import { useEffect } from 'react';

export default function AudioPlayerScreen() {
const audioSource = require('./assets/audio.mp3');
const player = useAudioPlayer(audioSource);

useEffect(() => {
// Configure audio session for background playback
setAudioModeAsync({
playsInSilentMode: true,
shouldPlayInBackground: true,
interruptionMode: 'doNotMix',
});
}, []);

const handlePlay = () => {
// Enable lock screen controls with metadata
player.setActiveForLockScreen(true, {
title: 'My Audio Title',
artist: 'Artist Name',
albumTitle: 'Album Name',
artworkUrl: 'https://example.com/artwork.jpg', // optional
});

// Start playback - this will continue in the background
player.play();
};

const handleStop = () => {
player.pause();
// Optionally disable lock screen controls when done
player.setActiveForLockScreen(false);
};

return (
<View>
<Button title="Play" onPress={handlePlay} />
<Button title="Stop" onPress={handleStop} />
</View>
);
}
```

<PlatformTags platforms={['android']} />

- > **Note**: On Android, you have to enable the lockscreen controls with [`setActiveForLockScreen`](#setactiveforlockscreenactive-metadata-options) for sustained background playback. Otherwise, the audio will stop after approximately 3 minutes of background playback (OS limitation). Make sure to also appropriately [configure the config-plugin](#configuration-in-app-config)

* A media notification appears in the notification drawer with playback controls
* Audio continues playing indefinitely in the background
* Users can control playback from the lock screen and notification
* The foreground service keeps the playback alive during playback

<PlatformTags platforms={['ios']} />

On iOS, audio playback continues seamlessly in the background once the audio session is configured with `shouldPlayInBackground: true`. Lock screen controls are optional but enhance the user experience by providing playback controls on the lock screen and Control Center.

<ConfigReactNative>

If you're not using Continuous Native Generation ([CNG](/workflow/continuous-native-generation/)) (you're using native **android** and **ios** projects manually), then you need to configure the following for background playback:

- For Android, add to **android/app/src/main/AndroidManifest.xml**:

```xml android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application>
<!-- Other application components -->
<service
android:name="expo.modules.audio.service.AudioControlsService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
</application>
```

- For iOS, add to **ios/YourApp/Info.plist**:

```xml ios/YourApp/Info.plist
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
```

</ConfigReactNative>

### Recording audio in background

> **warning** Background recording can significantly impact battery life. Only enable it when necessary for your app's functionality.
Expand All @@ -238,8 +351,9 @@ To enable background recording, use the config plugin in your [app config](/work

The above configuration automatically configures the required native settings:

- <PlatformTags platforms={['android']} /> Adds `FOREGROUND_SERVICE_MICROPHONE` and
`POST_NOTIFICATIONS` permissions
- <PlatformTags platforms={['android']} /> Adds `FOREGROUND_SERVICE`,
`FOREGROUND_SERVICE_MICROPHONE` and `POST_NOTIFICATIONS` permissions. Also declares an audio
recording foreground service in app's `AndroidManifest.xml`.
- <PlatformTags platforms={['ios']} /> Adds the `audio` `UIBackgroundMode` capability

<ConfigReactNative>
Expand Down
2 changes: 1 addition & 1 deletion docs/public/static/data/unversioned/expo-audio.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/public/static/data/v55.0.0/expo-audio.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/expo-audio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

### 🎉 New features

- [iOS] Add support for `shouldRouteThroughEarpiece`. ([#43089](https://github.com/expo/expo/pull/43089) by [@alanjhughes](https://github.com/alanjhughes))
- [Android] Make it possible to add/remove the foreground service and foreground service permissions with a config plugin. ([#43014](https://github.com/expo/expo/pull/43014) by [@behenate](https://github.com/behenate))

### 🐛 Bug fixes

- [Android] Fix memory leaks when refreshing the app. ([#42785](https://github.com/expo/expo/pull/42785) by [@behenate](https://github.com/behenate))
- [iOS] Fixes `mediaServicesDidReset` not being correctly implemented. ([#42898](https://github.com/expo/expo/pull/42898) by [@alanjhughes](https://github.com/alanjhughes))

### 💡 Others

Expand Down
17 changes: 0 additions & 17 deletions packages/expo-audio/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application>
<service
android:name=".service.AudioControlsService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
<service
android:name=".service.AudioRecordingService"
android:exported="false"
android:foregroundServiceType="microphone" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ class AudioModule : Module() {
Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.RECORD_AUDIO)
}

AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
Permissions.askForPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.POST_NOTIFICATIONS)
}

AsyncFunction("getRecordingPermissionsAsync") { promise: Promise ->
Permissions.getPermissionsWithPermissionsManager(appContext.permissions, promise, Manifest.permission.RECORD_AUDIO)
}
Expand Down
20 changes: 19 additions & 1 deletion packages/expo-audio/build/Audio.types.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading