diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 32b0008..8bdd112 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -1628,17 +1628,20 @@ 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;
+ a.playsInline = true;
document.body.appendChild(a); remoteAudio.set(uuid, a);
applySinkTo(a);
}
a.srcObject = stream;
- if (!audioPath.has(uuid)) audioPath.set(uuid, 'rtc');
- applyAudioMute();
+ /* programmatic play in case autoplay policy needs the nudge after a track
+ * swap — caller already had a user gesture (entered the space). On Firefox
+ * Android the play() promise will reject if the gesture grace has aged
+ * out; the listener's play button re-runs play() inside its own gesture
+ * to recover. */
+ try {
+ const p = a.play();
+ if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid+': '+e.message));
+ } catch(_){}
stopMeter(uuid); startMeter(uuid, stream);
logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || uuid));
}
@@ -3455,12 +3458,6 @@ async function handleSignal(raw){
* present-member-offers: existing speaker offers when a new speaker
* arrives. deterministic by uuid string compare. */
if (canSpeak(myRole) && canSpeak(m.role)) connectToPeer(m.uuid, /*weOffer*/ myUUID < m.uuid);
- /* if I'm a listener and a new speaker arrived, auto-enrol them
- * into my DJ-mode HTTP playback after the SFU has had a moment
- * to wire the Ogg writer for the publisher. */
- if (myRole === 'listener' && canSpeak(m.role) && m.uuid !== myUUID){
- setTimeout(autoEnableDjModeForListener, 600);
- }
renderRoom();
break;
case 'peer-left':
@@ -3587,11 +3584,6 @@ async function handleSignal(raw){
if (canSpeak(myRole) && canSpeak(m.role) && !peers.has(m.uuid)){
connectToPeer(m.uuid, myUUID < m.uuid);
}
- /* listener-side: if a peer just gained the ability to speak,
- * auto-enrol them into our DJ-mode HTTP stream. */
- if (myRole === 'listener' && !canSpeak(prev) && canSpeak(m.role)){
- setTimeout(autoEnableDjModeForListener, 600);
- }
/* If the role transition stripped them of the ability to
* publish (anything -> listener), their camera/screen/game
* tiles can't get fresh RTP anymore. The demoted page calls
@@ -3759,17 +3751,14 @@ async function onRoleEntered(){
if (canSpeak(mm.role)) connectToPeer(uuid, myUUID < uuid);
}
sfuPublish().catch(e => logLine('err','sfu publish: '+e.message));
- } else {
- /* Listener landing — auto-enrol into DJ mode for every speaker so
- * playback rides the deep-buffered HTTP Ogg path. Slight delay so
- * the SFU has time to wire its Ogg writers for the existing
- * publishers (lazy-inits on first /stream listener). Plus start
- * the periodic retry loop so streams come up when the host's mic
- * publish lands AFTER we already auto-enrolled (race the 600ms
- * delay can't cover). */
- setTimeout(autoEnableDjModeForListener, 600);
- startAutoEnrolRetryLoop();
}
+ /* No auto-enrol into DJ mode for listeners. Firefox Android refuses
+ * autoplay on every fresh