diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index 68cd8a1..5ad8d17 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -1702,7 +1702,7 @@ const VIDEO_REMOVE_MUTE_WINDOW_SCREEN_MS = 120000; * consistent. If you need lower latency for actual * conversation, drop this back to 0.7 and accept the * occasional under-run. */ -const RECV_PLAYOUT_DELAY_SEC = 2.0; +const RECV_PLAYOUT_DELAY_SEC = 4.0; function watchVideoTrackForRemoval(track, removeFn, windowMs){ if (!track) return; if (typeof windowMs !== 'number' || !isFinite(windowMs) || windowMs <= 0){ @@ -3437,6 +3437,12 @@ 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': @@ -3563,6 +3569,11 @@ 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 @@ -3700,6 +3711,12 @@ 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). */ + setTimeout(autoEnableDjModeForListener, 600); } } let inRoleTransition = false; @@ -3719,6 +3736,9 @@ async function onRoleChanged(prev, next){ * the acquire. */ 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). */ + autoDisableDjModeForAll(); await ensureMicAndUI(); for (const [uuid, mm] of members){ if (uuid === myUUID) continue; @@ -3738,6 +3758,9 @@ async function onRoleChanged(prev, next){ await sfuUnpublishScreen(); await sfuUnpublishCamera(); await sfuUnpublishGame(); + /* now a listener — auto-enrol into DJ-mode HTTP playback for + * every audible peer so we get the deep-buffered path. */ + setTimeout(autoEnableDjModeForListener, 600); } updateRoleUI(); } finally { @@ -4012,6 +4035,47 @@ function toggleStreamFor(uuid, pubHex){ renderRoom(); } +/* Default-on DJ mode for listeners: skip the WebRTC mic playback path + * for every audible peer and pull HTTP Ogg/Opus instead. Browser's + *