From e8bb5aadbd3cefb75e2f053f54e5d5aa802df437 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Wed, 3 Jun 2026 21:02:29 -0400 Subject: [PATCH] zebra-report: 4s auto-enrol retry loop covers post-join publish race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the host's mic publish lands after the listener's initial DJ auto- enrol attempt, the 404 from the SFU was permanent — no event re- triggered enrol. 4s interval re-checks every speaker and starts streams that are still missing. Stops on transition out of listener. --- zebra-report/zebra-spaces.html | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) 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');