zebra-report: per-speaker "stream" toggle (DJ mode, Ogg/Opus over HTTP)
Each speaker row carries a stream toggle that swaps their audio from WebRTC subscribe (~700ms latency, glitchy under loss) to a plain HTTP <audio> pulling Ogg/Opus from the SFU's new /stream endpoint (~2s latency, browser-managed jitter buffer, bulletproof for DJ playback). Requires matching proxy.unturf.com SFU build.
This commit is contained in:
parent
147dbc0f9e
commit
9b55fd96ce
1 changed files with 75 additions and 2 deletions
|
|
@ -3853,6 +3853,10 @@ function tearPeer(uuid){
|
|||
if (pc){ try { pc.close(); } catch(_){} peers.delete(uuid); }
|
||||
const a = remoteAudio.get(uuid);
|
||||
if (a){ try { a.srcObject = null; a.remove(); } catch(_){} remoteAudio.delete(uuid); }
|
||||
/* stream-mode <audio> for this uuid (DJ mode) — close the HTTP pull
|
||||
* so the SFU stops fanning Ogg pages to a dead client */
|
||||
const s = streamAudio.get(uuid);
|
||||
if (s){ try { s.pause(); s.removeAttribute('src'); s.load(); s.remove(); } catch(_){} streamAudio.delete(uuid); }
|
||||
}
|
||||
function waitForIceGathering(p, timeoutMs=6000){
|
||||
return new Promise(res=>{
|
||||
|
|
@ -3930,6 +3934,56 @@ const MIC_OFF = '<svg viewBox="0 0 24 24" fill="none" stroke="#b00" stroke-width
|
|||
|
||||
function rankOf(role){ return {host:4, cohost:3, speaker:2, listener:1}[role] || 0; }
|
||||
|
||||
/* ----- HTTP broadcast stream (DJ mode) — parallel to SFU mic subscribe.
|
||||
* For a given speaker's pubHex, the user can flip into "stream" mode:
|
||||
* we mute the WebRTC remote audio for that uuid and attach a separate
|
||||
* <audio> element pulling /zebra-spaces-sfu/stream?room=R&pub=PUBHEX
|
||||
* as Ogg/Opus. ~1.5–3s extra latency, but the browser's jitter buffer
|
||||
* smooths out network glitches the WebRTC path can't. State is keyed
|
||||
* by pubHex (not uuid) so it survives session-uuid churn on rejoin. */
|
||||
const streamMode = new Set(); // pubHex strings currently streaming
|
||||
const streamAudio = new Map(); // uuid -> <audio> pulling stream
|
||||
function streamUrlFor(pubHex){
|
||||
return SFU_BASE + '/stream?room=' + encodeURIComponent(roomID) + '&pub=' + pubHex;
|
||||
}
|
||||
function startStream(uuid, pubHex){
|
||||
/* mute the WebRTC mic for this uuid so they don't double-play */
|
||||
const w = remoteAudio.get(uuid);
|
||||
if (w) try { w.muted = true; } catch(_){}
|
||||
let a = streamAudio.get(uuid);
|
||||
if (!a){
|
||||
a = document.createElement('audio');
|
||||
a.autoplay = true; a.controls = false;
|
||||
document.body.appendChild(a);
|
||||
streamAudio.set(uuid, a);
|
||||
applySinkTo(a);
|
||||
}
|
||||
a.muted = false;
|
||||
a.src = streamUrlFor(pubHex);
|
||||
try { const p = a.play(); if (p && p.catch) p.catch(()=>{}); } catch(_){}
|
||||
logLine('', 'stream on for '+pubHex.slice(0,12)+' — DJ mode (~2s delay, glitch-free)');
|
||||
}
|
||||
function stopStream(uuid){
|
||||
const a = streamAudio.get(uuid);
|
||||
if (a){
|
||||
try { a.pause(); } catch(_){}
|
||||
try { a.removeAttribute('src'); a.load(); } catch(_){}
|
||||
}
|
||||
/* unmute the WebRTC mic so conversation resumes */
|
||||
const w = remoteAudio.get(uuid);
|
||||
if (w) try { w.muted = false; } catch(_){}
|
||||
}
|
||||
function toggleStreamFor(uuid, pubHex){
|
||||
if (streamMode.has(pubHex)){
|
||||
streamMode.delete(pubHex);
|
||||
stopStream(uuid);
|
||||
} else {
|
||||
streamMode.add(pubHex);
|
||||
startStream(uuid, pubHex);
|
||||
}
|
||||
renderRoom();
|
||||
}
|
||||
|
||||
function renderRoom(){
|
||||
const wrap = $('members'); wrap.innerHTML = '';
|
||||
/* sort host → cohosts → speakers → listeners, then by joined_at */
|
||||
|
|
@ -3956,6 +4010,25 @@ function renderRoom(){
|
|||
m._meterEl = fill;
|
||||
/* mod controls — only render when we can act on this row */
|
||||
const acts = document.createElement('span'); acts.className = 'mod-actions';
|
||||
/* per-speaker "stream" toggle: anyone can flip another speaker's
|
||||
* audio from WebRTC subscribe → HTTP Ogg/Opus tap. Higher latency,
|
||||
* cleaner playback under loss. Not a mod action — every listener
|
||||
* gets to pick their own path per speaker. */
|
||||
if (canSpeak(m.role) && m.uuid !== myUUID && m.pubkey){
|
||||
let mPubHex = '';
|
||||
try { mPubHex = hex(unb64(m.pubkey)); } catch(_){}
|
||||
if (mPubHex){
|
||||
const sb = document.createElement('button'); sb.className = 'small';
|
||||
const on = streamMode.has(mPubHex);
|
||||
sb.textContent = on ? '◉ stream' : '○ stream';
|
||||
sb.title = on
|
||||
? 'streaming — click to switch back to live (low-latency WebRTC)'
|
||||
: 'switch to HTTP Ogg/Opus stream (~2s delay, glitch-free for DJ sets)';
|
||||
if (on) sb.classList.add('invert');
|
||||
sb.onclick = () => toggleStreamFor(m.uuid, mPubHex);
|
||||
acts.appendChild(sb);
|
||||
}
|
||||
}
|
||||
if (isMod(myRole) && m.uuid !== myUUID){
|
||||
if (m.role === 'listener'){
|
||||
const b = document.createElement('button'); b.className='small invert';
|
||||
|
|
@ -4466,8 +4539,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">438b3d7f452d60edd79baae0a03094af</span><br>
|
||||
sha256 <span class="stamp-sha">203ace58efa704b3499137bd26d0a881df84d40ef73913e20aa2d7ba6d001fa6</span><br>
|
||||
md5 <span class="stamp-md5">beebb8a6a5d5675bfcf12f83230fe283</span><br>
|
||||
sha256 <span class="stamp-sha">898175e9ffac08ae9e56390a594701728bd94ec8b0d36942589df921efcfa716</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