11// hooks/useParticipants.ts
2- import { useState , useRef , useCallback , MutableRefObject } from 'react' ;
2+ import { useState , useRef , useCallback , useEffect , MutableRefObject } from 'react' ;
33
44// Type definitions
55interface Participant {
@@ -43,6 +43,7 @@ interface UseParticipantsReturn {
4343 talkerAudioLevelsRef : MutableRefObject < TalkerAudioLevels > ;
4444 pinnedParticipantIdRef : MutableRefObject < string | null > ;
4545 updateTalkerLevel : ( userId : string , level : number ) => void ;
46+ resetTalkers : ( ) => void ;
4647 clearParticipant : ( streamId : string ) => void ;
4748 guestParticipantRequestList : string [ ] ;
4849 setGuestParticipantRequestList : React . Dispatch < React . SetStateAction < string [ ] > > ;
@@ -52,6 +53,13 @@ interface UseParticipantsReturn {
5253 removeFakeParticipant : ( ) => void ;
5354}
5455
56+ // One threshold, and a hold window so short dips between words don't blink the indicator.
57+ // A lower "still speaking" threshold does not work here: the level decays gradually after
58+ // speech stops, so it would keep re-arming the hold long after the person went quiet.
59+ const SPEAKING_LEVEL = 75 ;
60+ const SPEAKING_HOLD_MS = 800 ;
61+ const SPEAKING_SWEEP_MS = 100 ;
62+
5563const FAKE_PARTICIPANT_NAMES = [
5664 'Alice' ,
5765 'Bob' ,
@@ -83,24 +91,76 @@ export const useParticipants = (): UseParticipantsReturn => {
8391 return participant ?. name || participant ?. streamName || 'Unknown' ;
8492 } , [ ] ) ;
8593
86- const updateTalkers = useCallback ( ( ) : void => {
87- const updatedTalkers = Object . keys ( talkerAudioLevelsRef . current ) . filter (
88- ( streamId ) => talkerAudioLevelsRef . current [ streamId ] > 75 ,
89- ) ;
90- setTalkers ( updatedTalkers ) ;
94+ // streamId -> timestamp of the last sample loud enough to count as speech
95+ const speakingSinceRef = useRef < Record < string , number > > ( { } ) ;
96+ const talkersRef = useRef < string [ ] > ( [ ] ) ;
97+ const sweepTimerRef = useRef < ReturnType < typeof setInterval > | null > ( null ) ;
98+
99+ // Only push a new array when the set of talkers actually changed, so consumers
100+ // don't re-render on every audio-level event.
101+ const commitTalkers = useCallback ( ( ) : void => {
102+ const next = Object . keys ( speakingSinceRef . current ) ;
103+ const prev = talkersRef . current ;
104+
105+ if ( next . length === prev . length && next . every ( ( id ) => prev . includes ( id ) ) ) return ;
106+
107+ talkersRef . current = next ;
108+ setTalkers ( next ) ;
109+ } , [ ] ) ;
110+
111+ const stopSweep = useCallback ( ( ) : void => {
112+ if ( sweepTimerRef . current ) {
113+ clearInterval ( sweepTimerRef . current ) ;
114+ sweepTimerRef . current = null ;
115+ }
91116 } , [ ] ) ;
92117
118+ // Drops talkers whose hold window expired (also covers users that stop sending levels).
119+ const startSweep = useCallback ( ( ) : void => {
120+ if ( sweepTimerRef . current ) return ;
121+
122+ sweepTimerRef . current = setInterval ( ( ) => {
123+ const now = Date . now ( ) ;
124+ let changed = false ;
125+
126+ Object . keys ( speakingSinceRef . current ) . forEach ( ( streamId ) => {
127+ if ( now - speakingSinceRef . current [ streamId ] > SPEAKING_HOLD_MS ) {
128+ delete speakingSinceRef . current [ streamId ] ;
129+ changed = true ;
130+ }
131+ } ) ;
132+
133+ if ( changed ) commitTalkers ( ) ;
134+ if ( Object . keys ( speakingSinceRef . current ) . length === 0 ) stopSweep ( ) ;
135+ } , SPEAKING_SWEEP_MS ) ;
136+ } , [ commitTalkers , stopSweep ] ) ;
137+
93138 const updateTalkerLevel = useCallback (
94139 ( userId : string , level : number ) : void => {
95- talkerAudioLevelsRef . current = {
96- ...talkerAudioLevelsRef . current ,
97- [ userId ] : level ,
98- } ;
99- updateTalkers ( ) ;
140+ talkerAudioLevelsRef . current [ userId ] = level ;
141+
142+ if ( level < SPEAKING_LEVEL ) return ;
143+
144+ const wasSpeaking = userId in speakingSinceRef . current ;
145+ speakingSinceRef . current [ userId ] = Date . now ( ) ;
146+
147+ if ( ! wasSpeaking ) {
148+ commitTalkers ( ) ;
149+ startSweep ( ) ;
150+ }
100151 } ,
101- [ updateTalkers ] ,
152+ [ commitTalkers , startSweep ] ,
102153 ) ;
103154
155+ const resetTalkers = useCallback ( ( ) : void => {
156+ talkerAudioLevelsRef . current = { } ;
157+ speakingSinceRef . current = { } ;
158+ stopSweep ( ) ;
159+ commitTalkers ( ) ;
160+ } , [ commitTalkers , stopSweep ] ) ;
161+
162+ useEffect ( ( ) => stopSweep , [ stopSweep ] ) ;
163+
104164 const [ guestParticipantRequestList , setGuestParticipantRequestList ] = useState < string [ ] > ( [ ] ) ;
105165 const [ guestsWaitingApproval , setGuestsWaitingApproval ] = useState < Participants > ( { } ) ;
106166
@@ -137,23 +197,29 @@ export const useParticipants = (): UseParticipantsReturn => {
137197 } ) ;
138198 } , [ ] ) ;
139199
140- const clearParticipant = useCallback ( ( streamId : string ) : void => {
141- setParticipants ( ( prev ) => {
142- const newParticipants = { ...prev } ;
143- delete newParticipants [ streamId ] ;
144- return newParticipants ;
145- } ) ;
146-
147- setSubscribedParticipants ( ( prev ) => {
148- const newSubscribed = { ...prev } ;
149- delete newSubscribed [ streamId ] ;
150- return newSubscribed ;
151- } ) ;
152-
153- const newTalkers = { ...talkerAudioLevelsRef . current } ;
154- delete newTalkers [ streamId ] ;
155- talkerAudioLevelsRef . current = newTalkers ;
156- } , [ ] ) ;
200+ const clearParticipant = useCallback (
201+ ( streamId : string ) : void => {
202+ setParticipants ( ( prev ) => {
203+ const newParticipants = { ...prev } ;
204+ delete newParticipants [ streamId ] ;
205+ return newParticipants ;
206+ } ) ;
207+
208+ setSubscribedParticipants ( ( prev ) => {
209+ const newSubscribed = { ...prev } ;
210+ delete newSubscribed [ streamId ] ;
211+ return newSubscribed ;
212+ } ) ;
213+
214+ delete talkerAudioLevelsRef . current [ streamId ] ;
215+
216+ if ( streamId in speakingSinceRef . current ) {
217+ delete speakingSinceRef . current [ streamId ] ;
218+ commitTalkers ( ) ;
219+ }
220+ } ,
221+ [ commitTalkers ] ,
222+ ) ;
157223
158224 return {
159225 participants,
@@ -173,6 +239,7 @@ export const useParticipants = (): UseParticipantsReturn => {
173239 talkerAudioLevelsRef,
174240 pinnedParticipantIdRef,
175241 updateTalkerLevel,
242+ resetTalkers,
176243 clearParticipant,
177244 guestsWaitingApproval,
178245 setGuestsWaitingApproval,
0 commit comments