@@ -61,9 +61,13 @@ interface VoipContextType {
6161 availableDevices : MediaDeviceInfo [ ] ;
6262 selectedDeviceId : string ;
6363 isDocked : boolean ;
64+ echoCancellation : boolean ;
65+ noiseSuppression : boolean ;
6466
6567 // Actions
6668 setIsDocked : ( value : boolean ) => void ;
69+ setEchoCancellation : ( value : boolean ) => void ;
70+ setNoiseSuppression : ( value : boolean ) => void ;
6771 createCall : ( roomId : string , roomType : 'group' | 'dm' , roomName ?: string ) => Promise < void > ;
6872 joinCall : ( callId : string ) => Promise < void > ;
6973 leaveCall : ( ) => void ;
@@ -98,7 +102,11 @@ export const useVoip = () => {
98102 availableDevices : [ ] ,
99103 selectedDeviceId : '' ,
100104 isDocked : false ,
105+ echoCancellation : true ,
106+ noiseSuppression : true ,
101107 setIsDocked : ( ) => { } ,
108+ setEchoCancellation : ( ) => { } ,
109+ setNoiseSuppression : ( ) => { } ,
102110 createCall : async ( ) => { } ,
103111 joinCall : async ( ) => { } ,
104112 leaveCall : ( ) => { } ,
@@ -140,6 +148,8 @@ export const VoipProvider: React.FC<VoipProviderProps> = ({ children }) => {
140148 const [ availableDevices , setAvailableDevices ] = useState < MediaDeviceInfo [ ] > ( [ ] ) ;
141149 const [ selectedDeviceId , setSelectedDeviceId ] = useState ( '' ) ;
142150 const [ isDocked , setIsDocked ] = useState ( false ) ;
151+ const [ echoCancellation , setEchoCancellation ] = useState ( true ) ;
152+ const [ noiseSuppression , setNoiseSuppression ] = useState ( true ) ;
143153
144154 // State for remote streams (explicit rendering to prevent GC)
145155 const [ remoteStreams , setRemoteStreams ] = useState < Map < string , MediaStream > > ( new Map ( ) ) ;
@@ -154,6 +164,8 @@ export const VoipProvider: React.FC<VoipProviderProps> = ({ children }) => {
154164
155165 // Refs to track current values for VAD closure
156166 const activeCallRef = useRef < VoipCall | null > ( null ) ;
167+ // Track if effect update is needed to avoid initial mount double-trigger
168+ const isFirstMountRef = useRef ( true ) ;
157169 const socketRef = useRef ( socket ) ;
158170 const connectedRef = useRef ( connected ) ;
159171 const userRef = useRef ( user ) ;
@@ -176,13 +188,29 @@ export const VoipProvider: React.FC<VoipProviderProps> = ({ children }) => {
176188 if ( savedThreshold ) setMicrophoneThreshold ( parseInt ( savedThreshold , 10 ) ) ;
177189 const savedDevice = localStorage . getItem ( 'voip_selected_device' ) ;
178190 if ( savedDevice ) setSelectedDeviceId ( savedDevice ) ;
191+
192+ const savedEcho = localStorage . getItem ( 'voip_echo_cancellation' ) ;
193+ if ( savedEcho !== null ) setEchoCancellation ( savedEcho === 'true' ) ;
194+
195+ const savedNoise = localStorage . getItem ( 'voip_noise_suppression' ) ;
196+ if ( savedNoise !== null ) setNoiseSuppression ( savedNoise === 'true' ) ;
179197 } , [ ] ) ;
180198
181199 const handleSetMicrophoneThreshold = useCallback ( ( value : number ) => {
182200 setMicrophoneThreshold ( value ) ;
183201 localStorage . setItem ( 'voip_microphone_threshold' , value . toString ( ) ) ;
184202 } , [ ] ) ;
185203
204+ const handleSetEchoCancellation = useCallback ( ( value : boolean ) => {
205+ setEchoCancellation ( value ) ;
206+ localStorage . setItem ( 'voip_echo_cancellation' , String ( value ) ) ;
207+ } , [ ] ) ;
208+
209+ const handleSetNoiseSuppression = useCallback ( ( value : boolean ) => {
210+ setNoiseSuppression ( value ) ;
211+ localStorage . setItem ( 'voip_noise_suppression' , String ( value ) ) ;
212+ } , [ ] ) ;
213+
186214 const refreshDevices = useCallback ( async ( ) => {
187215 try {
188216 const devices = await navigator . mediaDevices . enumerateDevices ( ) ;
@@ -268,15 +296,22 @@ export const VoipProvider: React.FC<VoipProviderProps> = ({ children }) => {
268296 } , [ ] ) ;
269297
270298 const getUserMedia = useCallback ( async ( deviceId ?: string ) : Promise < MediaStream > => {
271- const constraints = { audio : deviceId ? { deviceId : { exact : deviceId } } : true , video : false } ;
299+ const constraints = {
300+ audio : {
301+ deviceId : deviceId ? { exact : deviceId } : undefined ,
302+ echoCancellation : echoCancellation ,
303+ noiseSuppression : noiseSuppression ,
304+ } ,
305+ video : false
306+ } ;
272307 try {
273308 return await navigator . mediaDevices . getUserMedia ( constraints ) ;
274309 } catch ( error : any ) {
275310 console . error ( '[VOIP] getUserMedia failed:' , error ) ;
276311 toast . error ( t ( 'voip.microphoneError' ) || 'Microphone error' ) ;
277312 throw error ;
278313 }
279- } , [ t ] ) ;
314+ } , [ t , echoCancellation , noiseSuppression ] ) ;
280315
281316 const createPeerConnection = useCallback ( ( targetUserId : string ) : RTCPeerConnection => {
282317 const pc = new RTCPeerConnection ( { iceServers : ICE_SERVERS } ) ;
@@ -563,6 +598,48 @@ export const VoipProvider: React.FC<VoipProviderProps> = ({ children }) => {
563598 }
564599 } , [ activeCall , getUserMedia , stopVoiceActivityDetection , startVoiceActivityDetection ] ) ;
565600
601+ // Handle changes to echo/noise settings while active
602+ useEffect ( ( ) => {
603+ if ( isFirstMountRef . current ) {
604+ isFirstMountRef . current = false ;
605+ return ;
606+ }
607+
608+ // Only trigger if we have an active stream and device is selected
609+ if ( localStreamRef . current && selectedDeviceId ) {
610+ const updateStream = async ( ) => {
611+ console . log ( '[VOIP] Updating audio constraints...' , { echoCancellation, noiseSuppression } ) ;
612+ try {
613+ const newStream = await getUserMedia ( selectedDeviceId ) ;
614+
615+ // Replace tracks in all peer connections
616+ const audioTrack = newStream . getAudioTracks ( ) [ 0 ] ;
617+ peerConnectionsRef . current . forEach ( ( { connection } ) => {
618+ const sender = connection . getSenders ( ) . find ( s => s . track ?. kind === 'audio' ) ;
619+ if ( sender ) {
620+ sender . replaceTrack ( audioTrack ) ;
621+ }
622+ } ) ;
623+
624+ // Stop old tracks
625+ if ( localStreamRef . current ) {
626+ localStreamRef . current . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
627+ }
628+
629+ localStreamRef . current = newStream ;
630+
631+ // Restart VAD with new stream
632+ stopVoiceActivityDetection ( ) ;
633+ startVoiceActivityDetection ( newStream ) ;
634+ } catch ( error ) {
635+ console . error ( '[VOIP] Failed to update audio constraints:' , error ) ;
636+ }
637+ } ;
638+
639+ updateStream ( ) ;
640+ }
641+ } , [ echoCancellation , noiseSuppression , selectedDeviceId , getUserMedia , startVoiceActivityDetection , stopVoiceActivityDetection ] ) ;
642+
566643
567644
568645
@@ -1000,6 +1077,10 @@ export const VoipProvider: React.FC<VoipProviderProps> = ({ children }) => {
10001077 refreshDevices,
10011078 isDocked,
10021079 setIsDocked,
1080+ echoCancellation,
1081+ noiseSuppression,
1082+ setEchoCancellation : handleSetEchoCancellation ,
1083+ setNoiseSuppression : handleSetNoiseSuppression ,
10031084 } ;
10041085
10051086 return (
0 commit comments