| .. | .. |
|---|
| 4 | 4 | setAudioModeAsync, |
|---|
| 5 | 5 | } from "expo-audio"; |
|---|
| 6 | 6 | import * as LegacyFileSystem from "expo-file-system/legacy"; |
|---|
| 7 | +import { AppState } from "react-native"; |
|---|
| 7 | 8 | |
|---|
| 8 | 9 | export interface RecordingResult { |
|---|
| 9 | 10 | uri: string; |
|---|
| 10 | 11 | durationMs: number; |
|---|
| 12 | +} |
|---|
| 13 | + |
|---|
| 14 | +// --- Autoplay suppression --- |
|---|
| 15 | +// Don't autoplay voice messages when the app is in the background |
|---|
| 16 | +// or when the user is on a phone call (detected via audio interruption). |
|---|
| 17 | +let _autoplayEnabled = true; |
|---|
| 18 | +let _audioInterrupted = false; |
|---|
| 19 | + |
|---|
| 20 | +// Track app state — suppress autoplay when backgrounded |
|---|
| 21 | +AppState.addEventListener("change", (state) => { |
|---|
| 22 | + _autoplayEnabled = state === "active"; |
|---|
| 23 | +}); |
|---|
| 24 | + |
|---|
| 25 | +/** Check if autoplay is safe right now (app in foreground, no interruption). */ |
|---|
| 26 | +export function canAutoplay(): boolean { |
|---|
| 27 | + return _autoplayEnabled && !_audioInterrupted; |
|---|
| 28 | +} |
|---|
| 29 | + |
|---|
| 30 | +/** Called externally to signal audio interruption (e.g., phone call started/ended). */ |
|---|
| 31 | +export function setAudioInterrupted(interrupted: boolean): void { |
|---|
| 32 | + _audioInterrupted = interrupted; |
|---|
| 11 | 33 | } |
|---|
| 12 | 34 | |
|---|
| 13 | 35 | // --- Singleton audio player --- |
|---|
| .. | .. |
|---|
| 94 | 116 | |
|---|
| 95 | 117 | while (audioQueue.length > 0) { |
|---|
| 96 | 118 | const item = audioQueue.shift()!; |
|---|
| 97 | | - await playOneAudio(item.uri, item.onFinish); |
|---|
| 119 | + await playOneAudio(item.uri, item.onFinish, false); |
|---|
| 98 | 120 | } |
|---|
| 99 | 121 | |
|---|
| 100 | 122 | processingQueue = false; |
|---|
| 101 | 123 | } |
|---|
| 102 | 124 | |
|---|
| 103 | | -function playOneAudio(uri: string, onFinish?: () => void): Promise<void> { |
|---|
| 125 | +function playOneAudio(uri: string, onFinish?: () => void, cancelPrevious = true): Promise<void> { |
|---|
| 104 | 126 | return new Promise<void>(async (resolve) => { |
|---|
| 105 | 127 | let settled = false; |
|---|
| 106 | 128 | const finish = () => { |
|---|
| .. | .. |
|---|
| 118 | 140 | resolve(); |
|---|
| 119 | 141 | }; |
|---|
| 120 | 142 | |
|---|
| 121 | | - // Stop any currently playing audio first |
|---|
| 122 | | - if (cancelCurrent) { |
|---|
| 143 | + // Stop any currently playing audio first (only for non-queued calls) |
|---|
| 144 | + if (cancelPrevious && cancelCurrent) { |
|---|
| 123 | 145 | cancelCurrent(); |
|---|
| 124 | 146 | } |
|---|
| 125 | 147 | |
|---|
| .. | .. |
|---|
| 138 | 160 | notifyListeners(uri); |
|---|
| 139 | 161 | |
|---|
| 140 | 162 | player.addListener("playbackStatusUpdate", (status) => { |
|---|
| 141 | | - if (!status.playing && status.currentTime > 0 && |
|---|
| 142 | | - (status.duration <= 0 || status.currentTime >= status.duration)) { |
|---|
| 143 | | - finish(); |
|---|
| 163 | + if (!status.playing && status.currentTime > 0) { |
|---|
| 164 | + if (status.duration <= 0 || status.currentTime >= status.duration) { |
|---|
| 165 | + // Playback finished naturally |
|---|
| 166 | + finish(); |
|---|
| 167 | + } else { |
|---|
| 168 | + // Paused mid-playback — likely audio interruption (phone call) |
|---|
| 169 | + setAudioInterrupted(true); |
|---|
| 170 | + } |
|---|
| 171 | + } else if (status.playing && _audioInterrupted) { |
|---|
| 172 | + // Resumed after interruption |
|---|
| 173 | + setAudioInterrupted(false); |
|---|
| 144 | 174 | } |
|---|
| 145 | 175 | }); |
|---|
| 146 | 176 | |
|---|