From 12fed78a91582a521820fb44f87fcf412e2fc4d4 Mon Sep 17 00:00:00 2001 From: bealqiu Date: Wed, 27 May 2026 20:12:40 +0800 Subject: [PATCH] fix(tts): fix Android background playback stopping Three root causes identified and fixed: 1. Missing Android permissions (app.config.js): - Add FOREGROUND_SERVICE (parent permission for all foreground services) - Add WAKE_LOCK (prevents CPU sleep during background audio) 2. ExpoSpeechTTSPlayer unconditionally stops on background: - The AppState handler called this.stop() on ANY platform going to bg - Apple's TextToSpeech.framework crashes if active when backgrounded, but Android handles it fine - Fix: only stop on iOS, let Android continue playing 3. Silent keep-alive track only enabled on iOS: - When TTS audio queue starves (network delay between chunks), a 1s silent track keeps the audio session alive so the OS doesn't reclaim it - This was iOS-only; Android has the same issue - Fix: enable on both platforms Closes #60 Closes #228 Closes #242 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/app-expo/app.config.js | 2 ++ packages/app-expo/src/lib/platform/expo-speech-player.ts | 6 ++++-- .../src/lib/platform/track-player-dashscope-player.ts | 7 +++---- .../src/lib/platform/track-player-edge-player.ts | 9 ++++----- 4 files changed, 13 insertions(+), 11 deletions(-) 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 {