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:
Jannis Braun
2026-02-22 23:17:16 +01:00
parent 5fe43bc0c8
commit dbebd38576
6 changed files with 55 additions and 90 deletions
+20 -14
View File
@@ -241,6 +241,9 @@ export function useLiveKit() {
}, [updateParticipants]);
// 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(() => {
const r = roomRef.current;
if (!r || !isConnected) return;
@@ -254,34 +257,37 @@ export function useLiveKit() {
// Keep screen share state in sync (handles edge cases like remounts)
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) {
const pub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
if (pub) {
await r.localParticipant.unpublishTrack(pub.track as LocalAudioTrack);
if (micPub?.track && !micPub.isMuted) {
await r.localParticipant.setMicrophoneEnabled(false);
}
return;
}
// Ensure device is set and volume is sync'd
// Not muted — ensure mic is published and live
await audioManager.setInputDevice(inputDeviceId);
audioManager.setInputVolume(inputVolume);
const currentGen = audioManager.getStreamGeneration();
// Check if already published
const existingPub = r.localParticipant.getTrackPublications().find(p => p.source === Track.Source.Microphone);
if (existingPub && existingPub.track) {
// If track is alive AND settings haven't changed, we are good.
if (existingPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
if (micPub?.track) {
// Track already published — check if it's still current
if (micPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
// Current and live — just unmute if needed
if (micPub.isMuted) {
await r.localParticipant.setMicrophoneEnabled(true);
}
return;
}
// Settings changed or track died — unpublish so we can republish
await r.localParticipant.unpublishTrack(existingPub.track as LocalAudioTrack);
// Track is stale (device or constraint change) — replace it
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();
if (!audioTrack) return;