fix(mobile): properly pause and resume other audio during recording

This commit is contained in:
Arjun Patel
2026-05-26 10:21:42 -07:00
parent e7fed5f037
commit 0eddf4639a
3 changed files with 51 additions and 9 deletions
@@ -3,11 +3,14 @@ import { Pressable, StyleSheet, Text, View } from "react-native";
import { Mic } from "lucide-react-native";
import {
RecordingPresets,
setAudioModeAsync,
useAudioRecorder,
useAudioRecorderState,
} from "expo-audio";
import { logError } from "@/lib/errors";
import {
acquireRecordingAudioSession,
releaseRecordingAudioSession,
} from "@/lib/recording-audio-session";
const MAX_DURATION_S = 60;
@@ -28,10 +31,7 @@ export function AudioRecordingOverlay({
let active = true;
(async () => {
try {
await setAudioModeAsync({
allowsRecording: true,
playsInSilentMode: true,
});
await acquireRecordingAudioSession();
await recorder.prepareToRecordAsync();
if (!active) return;
recorder.record();
@@ -47,10 +47,9 @@ export function AudioRecordingOverlay({
finalizedRef.current = true;
recorder.stop().catch(() => {});
}
void setAudioModeAsync({
allowsRecording: false,
playsInSilentMode: true,
}).catch((err) => logError(err, { scope: "compose.audio.exit" }));
void releaseRecordingAudioSession().catch((err) =>
logError(err, { scope: "compose.audio.exit" }),
);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -2,6 +2,10 @@ import { useEffect, useRef, useState } from "react";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import { CameraView } from "expo-camera";
import { logError } from "@/lib/errors";
import {
acquireRecordingAudioSession,
releaseRecordingAudioSession,
} from "@/lib/recording-audio-session";
const MAX_DURATION_S = 60;
const VIDEO_BITRATE_BPS = 1_200_000;
@@ -25,6 +29,9 @@ export function VideoRecordingOverlay({
useEffect(() => {
return () => {
cancelledRef.current = true;
void releaseRecordingAudioSession().catch((err) =>
logError(err, { scope: "compose.video.exit" }),
);
};
}, []);
@@ -32,6 +39,14 @@ export function VideoRecordingOverlay({
const cam = cameraRef.current;
if (!cam || recording || !cameraReady) return;
try {
await acquireRecordingAudioSession();
} catch (err) {
logError(err, { scope: "compose.video.audioSession" });
onCancel();
return;
}
setRecording(true);
startedAtRef.current = Date.now();
@@ -46,6 +61,10 @@ export function VideoRecordingOverlay({
logError(err, { scope: "compose.video.recordAsync" });
onCancel();
return;
} finally {
void releaseRecordingAudioSession().catch((err) =>
logError(err, { scope: "compose.video.release" }),
);
}
if (cancelledRef.current) return;
@@ -0,0 +1,24 @@
import { setAudioModeAsync, setIsAudioActiveAsync } from "expo-audio";
// Around camera/mic recording we switch the iOS audio session to playAndRecord
// with `doNotMix`, which cleanly interrupts other apps' audio (Spotify, Apple
// Music, podcasts). On release we restore a mixable mode and then *deactivate*
// the session — the deactivation is what passes notifyOthersOnDeactivation to
// AVAudioSession, which is the signal that lets the previously-playing app
// auto-resume. Just changing the category is not enough.
export async function acquireRecordingAudioSession() {
await setAudioModeAsync({
allowsRecording: true,
playsInSilentMode: true,
interruptionMode: "doNotMix",
});
}
export async function releaseRecordingAudioSession() {
await setAudioModeAsync({
allowsRecording: false,
playsInSilentMode: true,
interruptionMode: "mixWithOthers",
});
await setIsAudioActiveAsync(false);
}