diff --git a/js/mobile/src/features/compose/AudioRecordingOverlay.tsx b/js/mobile/src/features/compose/AudioRecordingOverlay.tsx index c0bf7db..7e76c3c 100644 --- a/js/mobile/src/features/compose/AudioRecordingOverlay.tsx +++ b/js/mobile/src/features/compose/AudioRecordingOverlay.tsx @@ -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 }, []); diff --git a/js/mobile/src/features/compose/VideoRecordingOverlay.tsx b/js/mobile/src/features/compose/VideoRecordingOverlay.tsx index 92b998b..e42f6a1 100644 --- a/js/mobile/src/features/compose/VideoRecordingOverlay.tsx +++ b/js/mobile/src/features/compose/VideoRecordingOverlay.tsx @@ -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; diff --git a/js/mobile/src/lib/recording-audio-session.ts b/js/mobile/src/lib/recording-audio-session.ts new file mode 100644 index 0000000..58bfb2a --- /dev/null +++ b/js/mobile/src/lib/recording-audio-session.ts @@ -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); +}