refactor: replace unpublishTrack mute with setMicrophoneEnabled (Phase 1 RNNoise prep)
- syncMic now uses setMicrophoneEnabled(false/true) to mute/unmute the mic track in-place instead of tearing down and re-publishing via unpublishTrack(). This eliminates WebRTC renegotiation on every mute cycle and preserves the Web Audio pipeline for future AudioWorklet injection (RNNoise WASM). - Unify DmCallView mute/deafen with the primary syncMic path — removed direct setMicrophoneEnabled calls, added missing auto-mute/unmute on deafen toggle. - Strip redundant applyConstraints from VoiceControls noise suppression toggle that was hardcoding echoCancellation:true/autoGainControl:true and fighting AudioManager's constraint pipeline.
This commit is contained in:
@@ -33,31 +33,23 @@ export function DmCallView() {
|
|||||||
const otherUser = dmChannel?.members.find(m => m.id !== authUser?.id);
|
const otherUser = dmChannel?.members.find(m => m.id !== authUser?.id);
|
||||||
const otherName = otherUser?.displayName ?? otherUser?.username ?? 'User';
|
const otherName = otherUser?.displayName ?? otherUser?.username ?? 'User';
|
||||||
const handleMute = () => {
|
const handleMute = () => {
|
||||||
const room = getActiveRoom();
|
|
||||||
if (room) {
|
|
||||||
room.localParticipant.setMicrophoneEnabled(isMuted);
|
|
||||||
}
|
|
||||||
toggleMic();
|
toggleMic();
|
||||||
};
|
};
|
||||||
const handleDeafen = () => {
|
const handleDeafen = () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
|
const willDeafen = !isDeafened;
|
||||||
if (room) {
|
if (room) {
|
||||||
const newDeafened = !isDeafened;
|
|
||||||
room.remoteParticipants.forEach((p) => {
|
room.remoteParticipants.forEach((p) => {
|
||||||
p.audioTrackPublications.forEach((pub) => {
|
p.audioTrackPublications.forEach((pub) => {
|
||||||
if (pub.track) {
|
if (pub.track) {
|
||||||
pub.track.setVolume?.(newDeafened ? 0 : 1);
|
pub.track.setVolume?.(willDeafen ? 0 : 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (newDeafened) {
|
|
||||||
room.localParticipant.setMicrophoneEnabled(false);
|
|
||||||
}
|
|
||||||
else if (!isMuted) {
|
|
||||||
room.localParticipant.setMicrophoneEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
toggleDeafen();
|
toggleDeafen();
|
||||||
|
if (willDeafen && !isMuted) toggleMic();
|
||||||
|
if (!willDeafen && isMuted) toggleMic();
|
||||||
};
|
};
|
||||||
const handleCamera = async () => {
|
const handleCamera = async () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
|
|||||||
@@ -37,32 +37,29 @@ export function DmCallView() {
|
|||||||
const otherUser = dmChannel?.members.find(m => m.id !== authUser?.id);
|
const otherUser = dmChannel?.members.find(m => m.id !== authUser?.id);
|
||||||
const otherName = otherUser?.displayName ?? otherUser?.username ?? 'User';
|
const otherName = otherUser?.displayName ?? otherUser?.username ?? 'User';
|
||||||
|
|
||||||
|
// Mute/deafen toggling is handled by syncMic in useLiveKit via store state.
|
||||||
|
// DmCallView only needs to toggle store + handle remote audio silencing for deafen.
|
||||||
const handleMute = () => {
|
const handleMute = () => {
|
||||||
const room = getActiveRoom();
|
|
||||||
if (room) {
|
|
||||||
room.localParticipant.setMicrophoneEnabled(isMuted);
|
|
||||||
}
|
|
||||||
toggleMic();
|
toggleMic();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeafen = () => {
|
const handleDeafen = () => {
|
||||||
const room = getActiveRoom();
|
const room = getActiveRoom();
|
||||||
|
const willDeafen = !isDeafened;
|
||||||
|
// Silence/restore remote participant audio for deafen
|
||||||
if (room) {
|
if (room) {
|
||||||
const newDeafened = !isDeafened;
|
|
||||||
room.remoteParticipants.forEach((p) => {
|
room.remoteParticipants.forEach((p) => {
|
||||||
p.audioTrackPublications.forEach((pub) => {
|
p.audioTrackPublications.forEach((pub) => {
|
||||||
if (pub.track) {
|
if (pub.track) {
|
||||||
(pub.track as any).setVolume?.(newDeafened ? 0 : 1);
|
(pub.track as any).setVolume?.(willDeafen ? 0 : 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (newDeafened) {
|
|
||||||
room.localParticipant.setMicrophoneEnabled(false);
|
|
||||||
} else if (!isMuted) {
|
|
||||||
room.localParticipant.setMicrophoneEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
toggleDeafen();
|
toggleDeafen();
|
||||||
|
// Auto-mute on deafen, auto-unmute on undeafen
|
||||||
|
if (willDeafen && !isMuted) toggleMic();
|
||||||
|
if (!willDeafen && isMuted) toggleMic();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCamera = async () => {
|
const handleCamera = async () => {
|
||||||
|
|||||||
@@ -54,24 +54,7 @@ export function VoiceControls() {
|
|||||||
console.error('[VoiceControls] Failed to toggle screen share:', err);
|
console.error('[VoiceControls] Failed to toggle screen share:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleNoiseSuppression = async () => {
|
const handleNoiseSuppression = () => {
|
||||||
const room = getActiveRoom();
|
|
||||||
if (room) {
|
|
||||||
try {
|
|
||||||
const micPub = room.localParticipant.getTrackPublications().find(p => p.source === 'microphone');
|
|
||||||
const mediaTrack = micPub?.track?.mediaStreamTrack;
|
|
||||||
if (mediaTrack) {
|
|
||||||
await mediaTrack.applyConstraints({
|
|
||||||
noiseSuppression: !noiseSuppression,
|
|
||||||
echoCancellation: true,
|
|
||||||
autoGainControl: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error('[VoiceControls] Failed to toggle noise suppression:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleNoiseSuppression();
|
toggleNoiseSuppression();
|
||||||
};
|
};
|
||||||
const handleDisconnect = () => {
|
const handleDisconnect = () => {
|
||||||
|
|||||||
@@ -56,25 +56,8 @@ export function VoiceControls() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNoiseSuppression = async () => {
|
const handleNoiseSuppression = () => {
|
||||||
const room = getActiveRoom();
|
// Store toggle triggers syncMic → AudioManager re-acquires stream with correct constraints
|
||||||
if (room) {
|
|
||||||
try {
|
|
||||||
const micPub = room.localParticipant.getTrackPublications().find(
|
|
||||||
p => p.source === 'microphone'
|
|
||||||
);
|
|
||||||
const mediaTrack = micPub?.track?.mediaStreamTrack;
|
|
||||||
if (mediaTrack) {
|
|
||||||
await mediaTrack.applyConstraints({
|
|
||||||
noiseSuppression: !noiseSuppression,
|
|
||||||
echoCancellation: true,
|
|
||||||
autoGainControl: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[VoiceControls] Failed to toggle noise suppression:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toggleNoiseSuppression();
|
toggleNoiseSuppression();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ export function useLiveKit() {
|
|||||||
catch { }
|
catch { }
|
||||||
}, [updateParticipants]);
|
}, [updateParticipants]);
|
||||||
// Handle Input Device & Mute Logic via AudioManager
|
// Handle Input Device & Mute Logic via AudioManager
|
||||||
|
// Mute uses setMicrophoneEnabled(false) to keep the track published (silence frames)
|
||||||
|
// instead of unpublishTrack() which tears down the WebRTC transport.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const r = roomRef.current;
|
const r = roomRef.current;
|
||||||
if (!r || !isConnected)
|
if (!r || !isConnected)
|
||||||
@@ -196,31 +198,34 @@ export function useLiveKit() {
|
|||||||
const syncMic = async () => {
|
const syncMic = async () => {
|
||||||
try {
|
try {
|
||||||
const audioManager = AudioManager.getInstance();
|
const audioManager = AudioManager.getInstance();
|
||||||
// Sync voice processing settings to AudioManager
|
|
||||||
audioManager.setVoiceProcessing({ echoCancellation, noiseSuppression, autoGainControl });
|
audioManager.setVoiceProcessing({ echoCancellation, noiseSuppression, autoGainControl });
|
||||||
// If muted or deafened, unpublish mic
|
audioManager.setScreenShareActive(isScreenSharing);
|
||||||
|
const micPub = r.localParticipant.getTrackPublications()
|
||||||
|
.find(p => p.source === Track.Source.Microphone);
|
||||||
|
// If muted or deafened, mute the track in-place (keep it published)
|
||||||
if (isMuted || isDeafened) {
|
if (isMuted || isDeafened) {
|
||||||
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
if (micPub?.track && !micPub.isMuted) {
|
||||||
if (pub) {
|
await r.localParticipant.setMicrophoneEnabled(false);
|
||||||
await r.localParticipant.unpublishTrack(pub.track);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Ensure device is set and volume is sync'd
|
// Not muted — ensure mic is published and live
|
||||||
await audioManager.setInputDevice(inputDeviceId);
|
await audioManager.setInputDevice(inputDeviceId);
|
||||||
audioManager.setInputVolume(inputVolume);
|
audioManager.setInputVolume(inputVolume);
|
||||||
const currentGen = audioManager.getStreamGeneration();
|
const currentGen = audioManager.getStreamGeneration();
|
||||||
// Check if already published
|
if (micPub?.track) {
|
||||||
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
// Track already published — check if it's still current
|
||||||
if (existingPub && existingPub.track) {
|
if (micPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
|
||||||
// If track is alive AND settings haven't changed, we are good.
|
// Current and live — just unmute if needed
|
||||||
if (existingPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
|
if (micPub.isMuted) {
|
||||||
|
await r.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Settings changed or track died — unpublish so we can republish
|
// Track is stale (device or constraint change) — replace it
|
||||||
await r.localParticipant.unpublishTrack(existingPub.track);
|
await r.localParticipant.unpublishTrack(micPub.track);
|
||||||
}
|
}
|
||||||
// Get a FRESH track (clone) for this specific publication
|
// Publish fresh track from AudioManager pipeline
|
||||||
const audioTrack = audioManager.getFreshTrack();
|
const audioTrack = audioManager.getFreshTrack();
|
||||||
if (!audioTrack)
|
if (!audioTrack)
|
||||||
return;
|
return;
|
||||||
@@ -236,14 +241,13 @@ export function useLiveKit() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
syncMic();
|
syncMic();
|
||||||
// Re-sync when AudioManager resumes
|
|
||||||
const unsubscribe = AudioManager.getInstance().onResumed(() => {
|
const unsubscribe = AudioManager.getInstance().onResumed(() => {
|
||||||
syncMic();
|
syncMic();
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected, echoCancellation, noiseSuppression, autoGainControl]);
|
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected, echoCancellation, noiseSuppression, autoGainControl, isScreenSharing]);
|
||||||
const connect = useCallback(async (channelId) => {
|
const connect = useCallback(async (channelId) => {
|
||||||
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected)
|
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -241,6 +241,9 @@ export function useLiveKit() {
|
|||||||
}, [updateParticipants]);
|
}, [updateParticipants]);
|
||||||
|
|
||||||
// Handle Input Device & Mute Logic via AudioManager
|
// Handle Input Device & Mute Logic via AudioManager
|
||||||
|
// Mute uses setMicrophoneEnabled(false) to keep the track published (silence frames)
|
||||||
|
// instead of unpublishTrack() which tears down the WebRTC transport.
|
||||||
|
// This preserves the Web Audio pipeline for future AudioWorklet nodes (e.g. RNNoise).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const r = roomRef.current;
|
const r = roomRef.current;
|
||||||
if (!r || !isConnected) return;
|
if (!r || !isConnected) return;
|
||||||
@@ -254,34 +257,37 @@ export function useLiveKit() {
|
|||||||
// Keep screen share state in sync (handles edge cases like remounts)
|
// Keep screen share state in sync (handles edge cases like remounts)
|
||||||
audioManager.setScreenShareActive(isScreenSharing);
|
audioManager.setScreenShareActive(isScreenSharing);
|
||||||
|
|
||||||
// If muted or deafened, unpublish mic
|
const micPub = r.localParticipant.getTrackPublications()
|
||||||
|
.find(p => p.source === Track.Source.Microphone);
|
||||||
|
|
||||||
|
// If muted or deafened, mute the track in-place (keep it published)
|
||||||
if (isMuted || isDeafened) {
|
if (isMuted || isDeafened) {
|
||||||
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
if (micPub?.track && !micPub.isMuted) {
|
||||||
if (pub) {
|
await r.localParticipant.setMicrophoneEnabled(false);
|
||||||
await r.localParticipant.unpublishTrack(pub.track as LocalAudioTrack);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure device is set and volume is sync'd
|
// Not muted — ensure mic is published and live
|
||||||
await audioManager.setInputDevice(inputDeviceId);
|
await audioManager.setInputDevice(inputDeviceId);
|
||||||
audioManager.setInputVolume(inputVolume);
|
audioManager.setInputVolume(inputVolume);
|
||||||
|
|
||||||
const currentGen = audioManager.getStreamGeneration();
|
const currentGen = audioManager.getStreamGeneration();
|
||||||
|
|
||||||
// Check if already published
|
if (micPub?.track) {
|
||||||
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
|
// Track already published — check if it's still current
|
||||||
|
if (micPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
|
||||||
if (existingPub && existingPub.track) {
|
// Current and live — just unmute if needed
|
||||||
// If track is alive AND settings haven't changed, we are good.
|
if (micPub.isMuted) {
|
||||||
if (existingPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
|
await r.localParticipant.setMicrophoneEnabled(true);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Settings changed or track died — unpublish so we can republish
|
// Track is stale (device or constraint change) — replace it
|
||||||
await r.localParticipant.unpublishTrack(existingPub.track as LocalAudioTrack);
|
await r.localParticipant.unpublishTrack(micPub.track as LocalAudioTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a FRESH track (clone) for this specific publication
|
// Publish fresh track from AudioManager pipeline
|
||||||
const audioTrack = audioManager.getFreshTrack();
|
const audioTrack = audioManager.getFreshTrack();
|
||||||
if (!audioTrack) return;
|
if (!audioTrack) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user