d6b54d3247
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>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
/**
|
|
* 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.
|
|
*
|
|
* 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(reason: string): Promise<boolean>;
|
|
stop(): Promise<boolean>;
|
|
}
|
|
|
|
const { BackgroundAudio } = NativeModules as { BackgroundAudio?: BackgroundAudioNative };
|
|
|
|
type Slot = 'tts' | 'rec' | 'wake';
|
|
|
|
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(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 acquireBackgroundAudio(slot: Slot): Promise<void> {
|
|
if (slots.has(slot)) return;
|
|
slots.add(slot);
|
|
await applyState();
|
|
}
|
|
|
|
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');
|