feat(audio): Foreground-Service haelt TTS am Leben bei minimierter App

ARIAs Antwort wird jetzt auch dann fertig vorgelesen wenn der User die
App im Hintergrund schickt. Vorher hat Android den Prozess kurz nach
dem Minimieren eingefroren — TTS verstummte mitten im Satz.

Native:
- AriaPlaybackService.kt: Service mit foregroundServiceType=mediaPlayback,
  zeigt persistente Notification "ARIA spricht — antippen oeffnet die App"
  (channel low-priority, ongoing, tap → MainActivity)
- BackgroundAudioModule.kt: RN-Bridge mit start()/stop()
- AndroidManifest: FOREGROUND_SERVICE + FOREGROUND_SERVICE_MEDIA_PLAYBACK
  + POST_NOTIFICATIONS Permissions, Service deklariert

JS:
- backgroundAudio.ts: idempotenter Wrapper (active-Flag verhindert
  doppelte start/stop calls)
- ChatScreen onPlaybackStarted → startBackgroundAudio
- ChatScreen onPlaybackFinished → stopBackgroundAudio
- audio.ts stopPlayback ruft auch stopBackgroundAudio damit die
  Notification bei Cancel/Barge-In/Anruf nicht haengen bleibt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 23:37:46 +02:00
parent f682aad4ff
commit ead28cf09a
10 changed files with 237 additions and 1 deletions
+5
View File
@@ -27,6 +27,7 @@ import audioService from '../services/audio';
import wakeWordService from '../services/wakeword';
import phoneCallService from '../services/phoneCall';
import { playWakeReadySound } from '../services/wakeReadySound';
import { startBackgroundAudio, stopBackgroundAudio } from '../services/backgroundAudio';
import updateService from '../services/updater';
import VoiceButton from '../components/VoiceButton';
import FileUpload, { FileData } from '../components/FileUpload';
@@ -568,12 +569,16 @@ const ChatScreen: React.FC = () => {
// TTS-Lifecycle: solange ARIA spricht und Wake-Word verfuegbar ist,
// parallel mitlauschen — User kann "Computer" sagen statt manuell tappen.
// PLUS: Foreground-Service starten damit Android den App-Prozess nicht
// killt wenn die App im Hintergrund ist (TTS waere sonst mitten im Satz weg).
const unsubTtsStart = audioService.onPlaybackStarted(() => {
startBackgroundAudio().catch(() => {});
if (wakeWordService.isConversing() && wakeWordService.hasWakeWord()) {
wakeWordService.startBargeListening().catch(() => {});
}
});
const unsubTtsEnd = audioService.onPlaybackFinished(() => {
stopBackgroundAudio().catch(() => {});
// Vor naechster Aufnahme: barge-listening aus damit der AudioRecorder
// das Mikro greifen kann.
wakeWordService.stopBargeListening().catch(() => {});
+4
View File
@@ -10,6 +10,7 @@ import { Platform, PermissionsAndroid, NativeModules, ToastAndroid } from 'react
import Sound from 'react-native-sound';
import RNFS from 'react-native-fs';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { stopBackgroundAudio } from './backgroundAudio';
import AudioRecorderPlayer, {
AudioEncoderAndroidType,
AudioSourceAndroidType,
@@ -898,6 +899,9 @@ class AudioService {
/** Laufende Wiedergabe stoppen + Queue leeren */
stopPlayback(): void {
// Foreground-Service auch stoppen — sonst bleibt die Notification haengen
// wenn Wiedergabe abgebrochen wird (Anruf, Cancel, Barge-In).
stopBackgroundAudio().catch(() => {});
this.audioQueue = [];
this.isPlaying = false;
if (this.currentSound) {
+45
View File
@@ -0,0 +1,45 @@
/**
* Background-Audio: ARIAs TTS soll auch bei minimierter App weiterlaufen.
* Wir starten dafuer einen Foreground-Service mit foregroundServiceType=
* mediaPlayback, der eine persistente Notification zeigt waehrend ARIA spricht.
*
* API ist intentional simpel — start() vor TTS-Wiedergabe, stop() danach.
* Idempotent: mehrfaches start/stop ist sicher.
*/
import { NativeModules } from 'react-native';
interface BackgroundAudioNative {
start(): Promise<boolean>;
stop(): Promise<boolean>;
}
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
let active = false;
export async function startBackgroundAudio(): Promise<void> {
if (active || !BackgroundAudio) return;
try {
await BackgroundAudio.start();
active = true;
console.log('[BackgroundAudio] Foreground-Service gestartet');
} catch (err: any) {
console.warn('[BackgroundAudio] start fehlgeschlagen:', err?.message || err);
}
}
export async function stopBackgroundAudio(): Promise<void> {
if (!active || !BackgroundAudio) return;
try {
await BackgroundAudio.stop();
active = false;
console.log('[BackgroundAudio] Foreground-Service gestoppt');
} catch (err: any) {
console.warn('[BackgroundAudio] stop fehlgeschlagen:', err?.message || err);
}
}
export function isBackgroundAudioActive(): boolean {
return active;
}