diff --git a/packages/app-expo/app.config.js b/packages/app-expo/app.config.js index e5c3dd82..60668be9 100644 --- a/packages/app-expo/app.config.js +++ b/packages/app-expo/app.config.js @@ -39,7 +39,9 @@ module.exports = { permissions: [ "android.permission.CAMERA", "android.permission.RECORD_AUDIO", + "android.permission.FOREGROUND_SERVICE", "android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK", + "android.permission.WAKE_LOCK", "android.permission.MODIFY_AUDIO_SETTINGS", ], }, diff --git a/packages/app-expo/src/lib/platform/expo-speech-player.ts b/packages/app-expo/src/lib/platform/expo-speech-player.ts index a81ad536..a7fa2386 100644 --- a/packages/app-expo/src/lib/platform/expo-speech-player.ts +++ b/packages/app-expo/src/lib/platform/expo-speech-player.ts @@ -7,7 +7,7 @@ import type { ITTSPlayer, TTSConfig } from "@readany/core/tts"; * the system kills it. */ import * as Speech from "expo-speech"; -import { AppState, type AppStateStatus, type NativeEventSubscription } from "react-native"; +import { AppState, type AppStateStatus, type NativeEventSubscription, Platform } from "react-native"; export class ExpoSpeechTTSPlayer implements ITTSPlayer { onStateChange?: (state: "playing" | "paused" | "stopped") => void; @@ -20,7 +20,9 @@ export class ExpoSpeechTTSPlayer implements ITTSPlayer { private _appStateSubscription: NativeEventSubscription | null = null; private _handleAppStateChange = (nextAppState: AppStateStatus): void => { - if (nextAppState === "background") { + // Only stop on iOS — Apple's TextToSpeech.framework crashes if speech is + // active when backgrounded. Android handles background speech fine. + if (Platform.OS === "ios" && nextAppState === "background") { if (!this._stopped) { this.stop(); } diff --git a/packages/app-expo/src/lib/platform/track-player-dashscope-player.ts b/packages/app-expo/src/lib/platform/track-player-dashscope-player.ts index 4920153a..b054cd31 100644 --- a/packages/app-expo/src/lib/platform/track-player-dashscope-player.ts +++ b/packages/app-expo/src/lib/platform/track-player-dashscope-player.ts @@ -519,10 +519,9 @@ export class TrackPlayerDashScopeTTSPlayer implements ITTSPlayer { }, ); - // On iOS, keep the audio session alive by inserting a silent track. - if (Platform.OS === "ios") { - void this._insertSilenceKeepAlive(); - } + // Keep the audio session alive by inserting a silent track. + // Prevents OS from killing playback during network stalls on both platforms. + void this._insertSilenceKeepAlive(); } private async _insertSilenceKeepAlive(): Promise { diff --git a/packages/app-expo/src/lib/platform/track-player-edge-player.ts b/packages/app-expo/src/lib/platform/track-player-edge-player.ts index f30880f9..1f636d19 100644 --- a/packages/app-expo/src/lib/platform/track-player-edge-player.ts +++ b/packages/app-expo/src/lib/platform/track-player-edge-player.ts @@ -531,11 +531,10 @@ export class TrackPlayerEdgeTTSPlayer implements ITTSPlayer { total: this._chunks.length, }); - // On iOS, keep the audio session alive by inserting a silent track. - // This prevents the OS from suspending JS when audio stops between real tracks. - if (Platform.OS === "ios") { - void this._insertSilenceKeepAlive(); - } + // Keep the audio session alive by inserting a silent track. + // iOS: Prevents OS from suspending JS when audio stops between real tracks. + // Android: Maintains audio focus and prevents system from killing playback during network stalls. + void this._insertSilenceKeepAlive(); } private async _insertSilenceKeepAlive(): Promise {