zebra-report: default-on DJ mode for listeners + 4s RTC playout buffer
Mirror of zebra-report 0cbbc37.
This commit is contained in:
parent
5f9a67b8e7
commit
6d827b29d2
1 changed files with 74 additions and 10 deletions
|
|
@ -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
|
||||
* <audio> element keeps a deep media buffer (~30s in Chrome) that
|
||||
* absorbs glitches WebRTC can't. Speakers stay on WebRTC for low-
|
||||
* latency conversational audio — only pure listeners get the
|
||||
* buffered path. Idempotent: a peer already in streamMode stays as-is.
|
||||
*
|
||||
* Auto-enrolment runs at:
|
||||
* - join time (onRoleEntered, only if landed as listener)
|
||||
* - peer-joined (a new speaker arrived while we're listening)
|
||||
* - role-change (another peer became a speaker; or we got demoted
|
||||
* to listener). */
|
||||
function autoEnableDjModeForListener(){
|
||||
if (myRole !== 'listener') return;
|
||||
for (const [uuid, mm] of members){
|
||||
if (uuid === myUUID) continue;
|
||||
if (!canSpeak(mm.role)) continue;
|
||||
if (!mm.pubkey) continue;
|
||||
let pubHex;
|
||||
try { pubHex = hex(unb64(mm.pubkey)); } catch(_){ continue; }
|
||||
if (streamMode.has(pubHex)) continue;
|
||||
streamMode.add(pubHex);
|
||||
startStream(uuid, pubHex);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tear down all DJ-mode HTTP streams — used when we transition out
|
||||
* of listener into speaker/cohost/host. Speakers need the low-
|
||||
* latency mesh/SFU mic path because they're going to talk back; the
|
||||
* 2s+ Ogg-pull buffer would make conversation painful. */
|
||||
function autoDisableDjModeForAll(){
|
||||
for (const pubHex of [...streamMode]){
|
||||
let foundUuid = null;
|
||||
for (const [u, mm] of members){
|
||||
try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){ foundUuid = u; break; } } catch(_){}
|
||||
}
|
||||
streamMode.delete(pubHex);
|
||||
if (foundUuid) stopStream(foundUuid);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRoom(){
|
||||
const wrap = $('members'); wrap.innerHTML = '';
|
||||
/* sort host → cohosts → speakers → listeners, then by joined_at */
|
||||
|
|
@ -4048,18 +4112,18 @@ function renderRoom(){
|
|||
* the targeted speaker from WebRTC subscribe → HTTP Ogg/Opus tap.
|
||||
* Visibility rule:
|
||||
* - SELF row: always (self-monitor — preview what listeners hear)
|
||||
* - OTHER rows: host only (host controls the room's audio path
|
||||
* for every speaker; a non-host speaker should not be flipping
|
||||
* anyone else's stream because the toggle is rendered as a
|
||||
* ROOM-LEVEL switch in the host's mental model)
|
||||
* Cohosts intentionally get only self-monitor — if fox wants them
|
||||
* room-wide, lift the gate to isMod(myRole). */
|
||||
* - OTHER rows: host (room-wide control), OR listener (their own
|
||||
* playback choice — listeners are auto-enrolled into DJ mode
|
||||
* on join for glitch-free playback; the button lets them opt
|
||||
* back to low-latency WebRTC per-speaker if they want)
|
||||
* Speakers/cohosts intentionally get only self-monitor — no peer-
|
||||
* level stream toggle so the host's room-wide model stays simple. */
|
||||
if (canSpeak(m.role) && m.pubkey){
|
||||
let mPubHex = '';
|
||||
try { mPubHex = hex(unb64(m.pubkey)); } catch(_){}
|
||||
if (mPubHex){
|
||||
const isSelf = (m.uuid === myUUID);
|
||||
const canStreamThisRow = isSelf || myRole === 'host';
|
||||
const canStreamThisRow = isSelf || myRole === 'host' || myRole === 'listener';
|
||||
if (canStreamThisRow){
|
||||
const sb = document.createElement('button'); sb.className = 'small';
|
||||
const on = streamMode.has(mPubHex);
|
||||
|
|
@ -4597,8 +4661,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
|
|||
|
||||
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-03</span><br>
|
||||
md5 <span class="stamp-md5">e5f7ac83724775581e7f3b543f0c7877</span><br>
|
||||
sha256 <span class="stamp-sha">0aa8ed4ff27ace6d5f641cc99377f4f67b89f475605c60ab38300888b11e626f</span><br>
|
||||
md5 <span class="stamp-md5">af202aa8a93119de774c4f9f478dc9fe</span><br>
|
||||
sha256 <span class="stamp-sha">b1fd4c020401134698c76a7ae839ffab0363ddcbffd01fdba4d29e7776bb4405</span><br>
|
||||
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
|
||||
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
|
||||
</footer>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue