Files
paseo/packages/app/src/hooks/use-dictation-audio-source.native.ts
T
Mohamed Boudra 6e1ec21c6a Keep Android dictation running through audio interruptions (#7200)
* fix(android): preserve dictation across audio interruptions

Playback focus loss does not mean microphone capture is unavailable. Keep dictation alive across media takeovers and wait for Android input silencing to clear, while preserving conversation interruption protection.

* test(android): verify dictation clears its selected audio route
2026-10-17 12:53:06 +07:00

78 lines
2.1 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react";
import { Buffer } from "buffer";
import { useState } from "react";
import { createAudioEngine } from "@/audio";
import type {
DictationAudioSource,
DictationAudioSourceConfig,
} from "./use-dictation-audio-source.types";
export function useDictationAudioSource(config: DictationAudioSourceConfig): DictationAudioSource {
const onPcmSegmentRef = useRef(config.onPcmSegment);
const onErrorRef = useRef(config.onError);
const onInterruptionRef = useRef(config.onInterruption);
const [volume, setVolume] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const engineRef = useRef<ReturnType<typeof createAudioEngine> | null>(null);
const getOrCreateEngine = useCallback(() => {
if (engineRef.current) {
return engineRef.current;
}
engineRef.current = createAudioEngine(
{
onCaptureData: (pcm) => {
onPcmSegmentRef.current(Buffer.from(pcm).toString("base64"));
},
onVolumeLevel: (level) => {
setVolume(level);
},
onError: (error) => {
onErrorRef.current?.(error);
},
onInterruption: () => {
onInterruptionRef.current?.();
},
onCaptureAvailabilityChange: (available) => setIsPaused(!available),
},
{ captureMode: "dictation" },
);
return engineRef.current;
}, []);
useEffect(() => {
onPcmSegmentRef.current = config.onPcmSegment;
onErrorRef.current = config.onError;
onInterruptionRef.current = config.onInterruption;
}, [config.onPcmSegment, config.onError, config.onInterruption]);
const start = useCallback(async () => {
const engine = getOrCreateEngine();
await engine.initialize();
await engine.startCapture();
}, [getOrCreateEngine]);
const stop = useCallback(async () => {
await engineRef.current?.stopCapture();
setVolume(0);
}, []);
useEffect(() => {
return () => {
const engine = engineRef.current;
engineRef.current = null;
void engine?.destroy().catch(() => undefined);
};
}, []);
return {
start,
stop,
volume,
isPaused,
};
}