fix(calls): release AFK-monitor mic capture when muted (N95)
useAfkAutoMute opened its own getUserMedia capture for the whole call and only stopped it on unmount, so the OS recording indicator stayed lit even when the user was muted. The capture is now gated on the reactive mic-on state: it runs only while unmuted (there's nothing to auto-mute when already muted), so muting tears down the stream and clears the indicator, and unmuting re-acquires it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { CallEmbed } from '../plugins/call';
|
||||
import { CallEmbed, useCallControlState } from '../plugins/call';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { toastQueueAtom } from '../state/toast';
|
||||
@@ -9,17 +9,25 @@ const SILENCE_RMS_THRESHOLD = 0.008;
|
||||
const CHECK_INTERVAL_MS = 500;
|
||||
|
||||
/**
|
||||
* Monitors microphone audio while in a call. If the mic stays active but
|
||||
* silent for longer than the configured timeout, the mic is muted and a
|
||||
* toast is shown. Cleans up its own AudioContext and stream on unmount.
|
||||
* Monitors microphone audio while in a call. If the mic stays unmuted but
|
||||
* silent for longer than the configured timeout, the mic is muted and a toast
|
||||
* is shown.
|
||||
*
|
||||
* The level-monitoring capture (`getUserMedia`) is opened ONLY while the mic is
|
||||
* unmuted — there is nothing to auto-mute once you are already muted, so
|
||||
* holding the capture would keep the OS recording indicator lit even though the
|
||||
* UI shows you as muted (N95). Muting therefore releases our stream; unmuting
|
||||
* re-acquires it. The AudioContext + stream are also torn down on unmount.
|
||||
*/
|
||||
export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
|
||||
const [enabled] = useSetting(settingsAtom, 'afkAutoMute');
|
||||
const [timeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes');
|
||||
const setToast = useSetAtom(toastQueueAtom);
|
||||
const { microphone } = useCallControlState(callEmbed?.control);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callEmbed || !enabled) return;
|
||||
// Only capture while in a call, enabled, AND unmuted (see N95 note above).
|
||||
if (!callEmbed || !enabled || !microphone) return undefined;
|
||||
|
||||
let stream: MediaStream | undefined;
|
||||
let audioCtx: AudioContext | undefined;
|
||||
@@ -49,12 +57,12 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
|
||||
const rms = Math.sqrt(buffer.reduce((sum, v) => sum + v * v, 0) / buffer.length);
|
||||
|
||||
if (rms > SILENCE_RMS_THRESHOLD) {
|
||||
// Audio detected — reset the silence timer
|
||||
// Audio detected — reset the silence timer.
|
||||
silenceStart = null;
|
||||
} else if (callEmbed.control.microphone) {
|
||||
// Mic is on but silent — start or advance the timer
|
||||
if (silenceStart === null) silenceStart = Date.now();
|
||||
else if (Date.now() - silenceStart >= timeoutMs) {
|
||||
} else if (silenceStart === null) {
|
||||
// Mic is unmuted (effect only runs while unmuted) but silent — start the timer.
|
||||
silenceStart = Date.now();
|
||||
} else if (Date.now() - silenceStart >= timeoutMs) {
|
||||
callEmbed.control.setMicrophone(false);
|
||||
setToast({
|
||||
id: `afk-mute-${Date.now()}`,
|
||||
@@ -65,10 +73,6 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
|
||||
});
|
||||
silenceStart = null;
|
||||
}
|
||||
} else {
|
||||
// Mic is already muted — don't count silence
|
||||
silenceStart = null;
|
||||
}
|
||||
}, CHECK_INTERVAL_MS);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
@@ -79,5 +83,5 @@ export function useAfkAutoMute(callEmbed: CallEmbed | undefined): void {
|
||||
stream?.getTracks().forEach((t) => t.stop());
|
||||
audioCtx?.close().catch(() => undefined);
|
||||
};
|
||||
}, [callEmbed, enabled, timeoutMinutes, setToast]);
|
||||
}, [callEmbed, enabled, timeoutMinutes, setToast, microphone]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user