diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index f692ee6..becfdce 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -3763,8 +3763,12 @@ async function onRoleEntered(){
/* 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). */
+ * 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();
}
}
let inRoleTransition = false;
@@ -3785,7 +3789,9 @@ async function onRoleChanged(prev, next){
muted = true;
try { sessionStorage.setItem(MUTE_STATE_KEY, '1'); } catch(_){}
/* coming out of listener — tear down every HTTP DJ-mode tap so
- * the WebRTC audio path takes over (low-latency for conversation). */
+ * the WebRTC audio path takes over (low-latency for conversation).
+ * Also stop the auto-enrol retry loop — speakers don't need it. */
+ stopAutoEnrolRetryLoop();
autoDisableDjModeForAll();
await ensureMicAndUI();
for (const [uuid, mm] of members){
@@ -4195,6 +4201,26 @@ function autoEnableDjModeForListener(){
startStream(uuid, pubHex);
}
}
+/* Periodic retry — covers the timing race where the host's mic publish
+ * hasn't been registered at the SFU yet when the listener first
+ * auto-enrols. onFail removes the pubHex from streamMode so the next
+ * sweep will re-attempt; without this poll the only retry trigger was
+ * peer-joined / role-change events, which never fire when a speaker
+ * who was already in the room simply starts publishing later. Cheap:
+ * idempotent on already-streaming pubHexes, no-op if not a listener. */
+let autoEnrolRetryTimer = null;
+function startAutoEnrolRetryLoop(){
+ if (autoEnrolRetryTimer) return;
+ autoEnrolRetryTimer = setInterval(() => {
+ if (myRole !== 'listener'){ stopAutoEnrolRetryLoop(); return; }
+ autoEnableDjModeForListener();
+ }, 4000);
+}
+function stopAutoEnrolRetryLoop(){
+ if (!autoEnrolRetryTimer) return;
+ clearInterval(autoEnrolRetryTimer);
+ autoEnrolRetryTimer = null;
+}
/* Tear down all DJ-mode HTTP streams — used when we transition out
* of listener into speaker/cohost/host. Speakers need the low-
@@ -4861,8 +4887,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');