diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 813657e..95ad5f1 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -4245,19 +4245,28 @@ async function enableSelfListenerMode(){
try { sendMicState(); } catch(_){}
}
let added = 0;
+ /* INCLUDE self in the enrolment — listeners hear every speaker
+ * (including us), so the canonical "what listeners hear" experience
+ * pulls our own /stream too. Mic is already muted, so the loopback
+ * doesn't create feedback; the user gets full broadcast monitoring,
+ * AND a solo-host alone with listeners still has something to play. */
for (const [uuid, mm] of members){
- if (uuid === myUUID || !canSpeak(mm.role) || !mm.pubkey) continue;
+ if (!canSpeak(mm.role) || !mm.pubkey) continue;
let pubHex;
try { pubHex = hex(unb64(mm.pubkey)); } catch(_){ continue; }
if (streamMode.has(pubHex)) continue;
streamMode.add(pubHex);
startStream(uuid, pubHex);
+ /* mute the matched WebRTC remote so we don't hear both paths.
+ * Self has no remoteAudio entry (we never subscribe to ourselves
+ * via SFU — see the self-echo skip in handleRemoteSfuTrack), so
+ * this is a no-op for the self row. */
const w = remoteAudio.get(uuid);
if (w) try { w.muted = true; } catch(_){}
added++;
}
renderRoom();
- logLine('', 'self-listener ON — '+added+' peers on buffered HTTP path, mic muted');
+ logLine('', 'self-listener ON — '+added+' peers on buffered HTTP path (incl. self), mic muted');
}
function disableSelfListenerMode(){
if (!selfListenerMode) return;
@@ -5042,8 +5051,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');