feat(audio): Background-Service auch fuer Wake-Word + Aufnahme + Doku-Split
Erweitert den Foreground-Service um den microphone-Type damit nicht nur TTS, sondern auch Wake-Word-Lauschen und aktive Aufnahmen weiterlaufen wenn die App im Hintergrund ist. Slot-System (backgroundAudio.ts): - 'tts' : ARIA spricht - 'rec' : Aufnahme laeuft - 'wake' : Wake-Word lauscht passiv (Ohr aktiv) Mehrere Slots koennen unabhaengig acquired/released werden, der Service laeuft solange mindestens einer aktiv ist. Notification-Text passt sich dynamisch an den hoechstprioren Slot an (tts > rec > wake). Wiring (ChatScreen): - onPlaybackStarted/Finished → 'tts' Slot - audioService.onStateChange (recording) → 'rec' Slot - wakeWordService.onStateChange (off→armed/conversing) → 'wake' Slot AndroidManifest: - foregroundServiceType="mediaPlayback|microphone" (Pflicht ab Android 14 fuer Background-Mic-Zugriff) - FOREGROUND_SERVICE_MICROPHONE Permission Doku: - issue.md Erledigt-Sektion in "Bugs / Fixes", "App Features" und "Infrastruktur" gesplittet - README: Background-Service-Beschreibung erweitert Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,45 +1,76 @@
|
||||
/**
|
||||
* 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.
|
||||
* Background-Audio: ARIAs TTS, Mic-Aufnahme und Wake-Word-Lauschen sollen
|
||||
* auch bei minimierter App weiterlaufen. Wir starten dafuer einen Foreground-
|
||||
* Service mit foregroundServiceType=mediaPlayback|microphone, der eine
|
||||
* persistente Notification zeigt waehrend irgendein Audio-Slot aktiv ist.
|
||||
*
|
||||
* API ist intentional simpel — start() vor TTS-Wiedergabe, stop() danach.
|
||||
* Idempotent: mehrfaches start/stop ist sicher.
|
||||
* Mehrere Komponenten koennen den Service unabhaengig "halten":
|
||||
* - 'tts' : ARIA spricht
|
||||
* - 'rec' : Aufnahme laeuft
|
||||
* - 'wake' : Wake-Word lauscht passiv (Ohr aktiv)
|
||||
*
|
||||
* Solange mindestens ein Slot aktiv ist, laeuft der Service. Wenn alle
|
||||
* Slots leer sind, wird er gestoppt. Der Notification-Text passt sich an
|
||||
* den hoechstprioren Slot an (tts > rec > wake).
|
||||
*/
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
interface BackgroundAudioNative {
|
||||
start(): Promise<boolean>;
|
||||
start(reason: string): Promise<boolean>;
|
||||
stop(): Promise<boolean>;
|
||||
}
|
||||
|
||||
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
||||
|
||||
let active = false;
|
||||
type Slot = 'tts' | 'rec' | 'wake';
|
||||
|
||||
export async function startBackgroundAudio(): Promise<void> {
|
||||
if (active || !BackgroundAudio) return;
|
||||
const slots = new Set<Slot>();
|
||||
|
||||
// Prioritaet fuer den Notification-Text — hoechste zuerst.
|
||||
const PRIORITY: Slot[] = ['tts', 'rec', 'wake'];
|
||||
|
||||
function topReason(): string {
|
||||
for (const s of PRIORITY) {
|
||||
if (slots.has(s)) return s;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function applyState(): Promise<void> {
|
||||
if (!BackgroundAudio) return;
|
||||
if (slots.size === 0) {
|
||||
try { await BackgroundAudio.stop(); } catch {}
|
||||
console.log('[BackgroundAudio] Service gestoppt (keine Slots)');
|
||||
return;
|
||||
}
|
||||
const reason = topReason();
|
||||
try {
|
||||
await BackgroundAudio.start();
|
||||
active = true;
|
||||
console.log('[BackgroundAudio] Foreground-Service gestartet');
|
||||
await BackgroundAudio.start(reason);
|
||||
console.log('[BackgroundAudio] Service aktiv (slot=%s, slots=%s)',
|
||||
reason, [...slots].join('+'));
|
||||
} 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 async function acquireBackgroundAudio(slot: Slot): Promise<void> {
|
||||
if (slots.has(slot)) return;
|
||||
slots.add(slot);
|
||||
await applyState();
|
||||
}
|
||||
|
||||
export function isBackgroundAudioActive(): boolean {
|
||||
return active;
|
||||
export async function releaseBackgroundAudio(slot: Slot): Promise<void> {
|
||||
if (!slots.has(slot)) return;
|
||||
slots.delete(slot);
|
||||
await applyState();
|
||||
}
|
||||
|
||||
export function backgroundAudioActive(): boolean {
|
||||
return slots.size > 0;
|
||||
}
|
||||
|
||||
// --- Legacy API (nur tts-Slot) — fuer Aufruf-Sites die noch nichts vom Slot-
|
||||
// system wissen. Mappt auf den 'tts'-Slot. ---
|
||||
export const startBackgroundAudio = () => acquireBackgroundAudio('tts');
|
||||
export const stopBackgroundAudio = () => releaseBackgroundAudio('tts');
|
||||
|
||||
Reference in New Issue
Block a user