diff --git a/web/chat.html b/web/chat.html index d0af6ea..ddce7e2 100644 --- a/web/chat.html +++ b/web/chat.html @@ -1747,9 +1747,9 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry })(); diff --git a/web/host-your-own.html b/web/host-your-own.html index 9385b5c..4355e63 100644 --- a/web/host-your-own.html +++ b/web/host-your-own.html @@ -507,9 +507,9 @@ handle /zebra-spaces-sfu* { reverse_proxy localhost:8092 { flush_interval -1

diff --git a/web/how-it-works.html b/web/how-it-works.html index 83f40f4..d04e551 100644 --- a/web/how-it-works.html +++ b/web/how-it-works.html @@ -341,9 +341,9 @@ try {

diff --git a/web/zebra-audio.html b/web/zebra-audio.html index 64fe765..3acec16 100644 --- a/web/zebra-audio.html +++ b/web/zebra-audio.html @@ -747,9 +747,9 @@ else wirePuppet(); diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 760af78..f692ee6 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -1628,13 +1628,17 @@ function attachSfuTrack(uuid, stream){ let a = remoteAudio.get(uuid); if (!a){ a = document.createElement('audio'); a.autoplay = true; + /* always create muted=true so autoplay is universally allowed + * (muted autoplay is unconditional in all browsers). applyAudio- + * Mute() flips muted=false the moment we have a user gesture + * (btn-mute click) AND this peer's canonical path is 'rtc'. */ + a.muted = true; document.body.appendChild(a); remoteAudio.set(uuid, a); applySinkTo(a); } a.srcObject = stream; - /* programmatic play in case autoplay policy needs the nudge after a track - * swap — caller already had a user gesture (entered the space) */ - try { const p = a.play(); if (p && p.catch) p.catch(()=>{}); } catch(_){} + if (!audioPath.has(uuid)) audioPath.set(uuid, 'rtc'); + applyAudioMute(); stopMeter(uuid); startMeter(uuid, stream); logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid)); } @@ -3847,6 +3851,18 @@ function updateScreenShareUI(){ function updateRoleUI(){ $('sec-listener-actions').classList.toggle('hidden', myRole !== 'listener'); updateScreenShareUI(); + /* Listener mute-button repurposed as the audio-unlock gesture + + * local output toggle. Enabled regardless of micStream so a + * listener can tap it to provide the user gesture mobile browsers + * demand before unmuted audio is allowed. Speakers/cohosts/host + * still get the original self-mic-mute behavior (enabled by + * ensureMicAndUI once the mic is live). */ + const btn = $('btn-mute'); + if (myRole === 'listener'){ + btn.disabled = false; + btn.textContent = listenerOutputMuted ? 'play' : 'mute'; + btn.className = listenerOutputMuted ? '' : 'invert'; + } } /* ================================================================== @@ -4047,6 +4063,37 @@ function rankOf(role){ return {host:4, cohost:3, speaker:2, listener:1}[role] || * by pubHex (not uuid) so it survives session-uuid churn on rejoin. */ const streamMode = new Set(); // pubHex strings currently streaming const streamAudio = new Map(); // uuid ->