UNPKG

@fishjam-cloud/react-client

Version:
69 lines (68 loc) 3.07 kB
import { useContext, useEffect, useState } from "react"; import { FishjamClientContext } from "../contexts/fishjamClient"; import { usePeers } from "./usePeers"; // This is a dBov-to-linear conversion. -32 dBov number is taken from backend VAD threshold // formula for dBov to linear conversion: linear = 10 ^ (dBov / 20) // So -32 dBov = 10^(-32/20) ≈ 0.025. This is the minimum audio level considered "speech". const THRESHOLD = 10 ** (-32 / 20); // Number of consecutive "silence" ticks before we consider speech to have stopped. Helps with smoothing out brief pauses in speech. const SILENCE_DEBOUNCE_TICKS = 2; /** * Client-side voice activity detection for the local peer. * * Polls the local microphone's audio level every 100ms and derives a speech/silence * state from it. A level above ~0.025 (approximately −32 dBov, scaled to [0, 1]) * is treated as speech. Silence is debounced over 2 consecutive ticks (~200ms) * to prevent rapid flapping. * * This is purely client-side — it does not signal other peers. Remote participants * receive the local peer's VAD status via backend `vadNotification` messages. * * @internal Used by `useVAD` when the local peer's id is included in `peerIds`. * @returns A record mapping the local peer's id to its current speaking state, * or an empty object if `options.disabled` is true, the local peer is not available, or no microphone track is found. */ export const useLocalVAD = (options) => { const fishjamClient = useContext(FishjamClientContext); const [isSpeaking, setIsSpeaking] = useState(false); const { localPeer } = usePeers(); const localPeerId = localPeer?.id; const microphoneTrackId = localPeer?.microphoneTrack?.trackId; useEffect(() => { if (options.disabled || !localPeerId || !microphoneTrackId) return; let silenceTicks = 0; let timeoutId; const controller = new AbortController(); const { signal } = controller; const poll = async () => { if (signal.aborted) return; const trackAudio = await fishjamClient?.current?.getLocalTrackAudioLevel(microphoneTrackId); if (signal.aborted) return; if (trackAudio != null && trackAudio.level > THRESHOLD) { silenceTicks = 0; setIsSpeaking(true); } else { silenceTicks += 1; if (silenceTicks >= SILENCE_DEBOUNCE_TICKS) { setIsSpeaking(false); } } if (signal.aborted) return; timeoutId = setTimeout(poll, 100); }; timeoutId = setTimeout(poll, 0); return () => { controller.abort(); clearTimeout(timeoutId); setIsSpeaking(false); }; }, [options.disabled, fishjamClient, localPeerId, microphoneTrackId]); if (!localPeerId || options.disabled || !microphoneTrackId) return {}; return { [localPeerId]: isSpeaking }; };