fix(mobile): properly pause and resume other audio during recording
This commit is contained in:
@@ -3,11 +3,14 @@ import { Pressable, StyleSheet, Text, View } from "react-native";
|
|||||||
import { Mic } from "lucide-react-native";
|
import { Mic } from "lucide-react-native";
|
||||||
import {
|
import {
|
||||||
RecordingPresets,
|
RecordingPresets,
|
||||||
setAudioModeAsync,
|
|
||||||
useAudioRecorder,
|
useAudioRecorder,
|
||||||
useAudioRecorderState,
|
useAudioRecorderState,
|
||||||
} from "expo-audio";
|
} from "expo-audio";
|
||||||
import { logError } from "@/lib/errors";
|
import { logError } from "@/lib/errors";
|
||||||
|
import {
|
||||||
|
acquireRecordingAudioSession,
|
||||||
|
releaseRecordingAudioSession,
|
||||||
|
} from "@/lib/recording-audio-session";
|
||||||
|
|
||||||
const MAX_DURATION_S = 60;
|
const MAX_DURATION_S = 60;
|
||||||
|
|
||||||
@@ -28,10 +31,7 @@ export function AudioRecordingOverlay({
|
|||||||
let active = true;
|
let active = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
await setAudioModeAsync({
|
await acquireRecordingAudioSession();
|
||||||
allowsRecording: true,
|
|
||||||
playsInSilentMode: true,
|
|
||||||
});
|
|
||||||
await recorder.prepareToRecordAsync();
|
await recorder.prepareToRecordAsync();
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
recorder.record();
|
recorder.record();
|
||||||
@@ -47,10 +47,9 @@ export function AudioRecordingOverlay({
|
|||||||
finalizedRef.current = true;
|
finalizedRef.current = true;
|
||||||
recorder.stop().catch(() => {});
|
recorder.stop().catch(() => {});
|
||||||
}
|
}
|
||||||
void setAudioModeAsync({
|
void releaseRecordingAudioSession().catch((err) =>
|
||||||
allowsRecording: false,
|
logError(err, { scope: "compose.audio.exit" }),
|
||||||
playsInSilentMode: true,
|
);
|
||||||
}).catch((err) => logError(err, { scope: "compose.audio.exit" }));
|
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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 { Platform, Pressable, StyleSheet, Text, View } from "react-native";
|
||||||
import { CameraView } from "expo-camera";
|
import { CameraView } from "expo-camera";
|
||||||
import { logError } from "@/lib/errors";
|
import { logError } from "@/lib/errors";
|
||||||
|
import {
|
||||||
|
acquireRecordingAudioSession,
|
||||||
|
releaseRecordingAudioSession,
|
||||||
|
} from "@/lib/recording-audio-session";
|
||||||
|
|
||||||
const MAX_DURATION_S = 60;
|
const MAX_DURATION_S = 60;
|
||||||
const VIDEO_BITRATE_BPS = 1_200_000;
|
const VIDEO_BITRATE_BPS = 1_200_000;
|
||||||
@@ -25,6 +29,9 @@ export function VideoRecordingOverlay({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
cancelledRef.current = true;
|
cancelledRef.current = true;
|
||||||
|
void releaseRecordingAudioSession().catch((err) =>
|
||||||
|
logError(err, { scope: "compose.video.exit" }),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -32,6 +39,14 @@ export function VideoRecordingOverlay({
|
|||||||
const cam = cameraRef.current;
|
const cam = cameraRef.current;
|
||||||
if (!cam || recording || !cameraReady) return;
|
if (!cam || recording || !cameraReady) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await acquireRecordingAudioSession();
|
||||||
|
} catch (err) {
|
||||||
|
logError(err, { scope: "compose.video.audioSession" });
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setRecording(true);
|
setRecording(true);
|
||||||
startedAtRef.current = Date.now();
|
startedAtRef.current = Date.now();
|
||||||
|
|
||||||
@@ -46,6 +61,10 @@ export function VideoRecordingOverlay({
|
|||||||
logError(err, { scope: "compose.video.recordAsync" });
|
logError(err, { scope: "compose.video.recordAsync" });
|
||||||
onCancel();
|
onCancel();
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
void releaseRecordingAudioSession().catch((err) =>
|
||||||
|
logError(err, { scope: "compose.video.release" }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (cancelledRef.current) return;
|
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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user