From 3f43379e000cf287aa6ea2d4348b7b4a3c635b21 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Mon, 1 Jun 2026 20:33:41 -0400 Subject: [PATCH] =?UTF-8?q?zebra-spaces:=20applyConstraints=20+=20log=20ef?= =?UTF-8?q?fective=20mic=20settings=20=E2=80=94=20surfaces=20hidden=20filt?= =?UTF-8?q?ering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a PulseAudio monitor source is selected as the mic and music mode is toggled on, Firefox can silently apply its default audio-processing pipeline (EC/NS/AGC) regardless of the getUserMedia constraints. The broadcast then sounds 'cleaned up' instead of letting the source pass through transparently. Two fixes: - call track.applyConstraints(micConstraints()) after getUserMedia/replace. Some UAs honour applyConstraints when they silently ignored the initial request. Belt-and-suspenders. - log track.getSettings() so we can see what the UA actually applied — ec/ns/agc/channels/sampleRate. If applyConstraints didn't stick, the log shows it instead of failing silently. --- web/zebra-spaces.html | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 4e650b0..c35e358 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -946,10 +946,28 @@ async function refreshMicList(){ } catch(e){ logLine('err','could not list inputs: '+e.message); } } function tagTrack(t){ if (t) t.contentHint = musicMode ? 'music' : 'speech'; } +/* Firefox sometimes ignores the EC/NS/AGC constraints at getUserMedia time + * for non-mic sources (e.g. PulseAudio monitor) and applies its default + * processing pipeline anyway. applyConstraints() after the fact tends to + * stick. Log the actual settings so we can see what the UA ended up with — + * silent disagreement between requested and effective constraints is what + * makes music-mode-on-a-monitor-source sound 'cleaned up'. */ +async function enforceMicConstraints(track){ + if (!track) return; + try { await track.applyConstraints(micConstraints()); } catch(e){ logLine('', 'applyConstraints rejected: '+e.message); } + try { + const s = track.getSettings(); + logLine('', 'mic track settings: '+JSON.stringify({ + ec: s.echoCancellation, ns: s.noiseSuppression, agc: s.autoGainControl, + ch: s.channelCount, hz: s.sampleRate, dev: (s.deviceId||'').slice(0,8) + })); + } catch(_){} +} async function getMic(){ if (micStream) return micStream; micStream = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false }); - tagTrack(micStream.getAudioTracks()[0]); + const t = micStream.getAudioTracks()[0]; + tagTrack(t); await enforceMicConstraints(t); return micStream; } async function setSenderBitrate(sender){ @@ -998,6 +1016,7 @@ async function applyMicMode(){ const ns = await navigator.mediaDevices.getUserMedia({ audio: micConstraints(), video:false }); const nt = ns.getAudioTracks()[0]; tagTrack(nt); nt.enabled = !muted; + await enforceMicConstraints(nt); async function swap(pc){ const sender = pc.getSenders().find(s=>s.track && s.track.kind==='audio') || pc.getSenders()[0]; if (sender){ try { await sender.replaceTrack(nt); } catch(_){} setSenderBitrate(sender); } @@ -1707,8 +1726,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');