zebra-report: zebra-spaces queues SFU ontrack until the matching member joins

This commit is contained in:
russell@unturf.com 2026-05-31 14:08:54 -04:00
parent a6047bfeaa
commit 958ce6331b
No known key found for this signature in database

View file

@ -447,6 +447,32 @@ const SFU_BASE = (new URLSearchParams(location.search).get('sfu')
let sfuPubPC = null, sfuPubPeerID = null;
let sfuSubPC = null, sfuSubPeerID = null, sfuSubEvents = null;
/* SFU forwarded tracks whose publisher pubkey we couldn't match to a room
* member yet — ontrack fires from RTP; the signal's peer-joined may arrive
* a beat later. Without queueing, the host's rejoined audio would be lost. */
const sfuPendingTracks = new Map(); // pubHex -> MediaStream
function attachSfuTrack(uuid, stream){
let a = remoteAudio.get(uuid);
if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(uuid, a); }
a.srcObject = stream;
stopMeter(uuid); startMeter(uuid, stream);
logLine('', 'sfu: receiving '+((members.get(uuid)||{}).handle || shortHex(uuid)));
}
function flushSfuPendingTracks(){
if (!sfuPendingTracks.size) return;
for (const [pubHex, stream] of [...sfuPendingTracks]){
for (const [uuid, mm] of members){
try {
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
attachSfuTrack(uuid, stream);
sfuPendingTracks.delete(pubHex);
break;
}
} catch(_){}
}
}
}
async function sfuPublish(){
if (sfuPubPC){ logLine('','sfu publish: already publishing'); return; }
@ -487,17 +513,15 @@ async function sfuSubscribe(){
pc.ontrack = (ev) => {
const pubHex = ev.streams[0] ? ev.streams[0].id : '';
if (!pubHex) return;
/* map track.streamID (= publisher's pubkey hex) -> room uuid */
/* try to match streamID (= publisher's pubkey hex) -> room uuid; if the
* member's not in our roster yet, queue the stream — flushSfuPendingTracks
* will pick it up on the next peer-joined / host-promoted / state event */
let matchUuid = null;
for (const [uuid, m] of members){
try { if (m.pubkey && hex(unb64(m.pubkey)) === pubHex){ matchUuid = uuid; break; } } catch(_){}
for (const [uuid, mm] of members){
try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){ matchUuid = uuid; break; } } catch(_){}
}
if (!matchUuid) return;
let a = remoteAudio.get(matchUuid);
if (!a){ a = document.createElement('audio'); a.autoplay = true; document.body.appendChild(a); remoteAudio.set(matchUuid, a); }
a.srcObject = ev.streams[0];
stopMeter(matchUuid); startMeter(matchUuid, ev.streams[0]);
logLine('', 'sfu: receiving '+((members.get(matchUuid)||{}).handle || shortHex(matchUuid)));
if (matchUuid){ attachSfuTrack(matchUuid, ev.streams[0]); }
else { sfuPendingTracks.set(pubHex, ev.streams[0]); }
};
/* server-initiated offer: POST /subscribe (empty body) — SFU answers with
* an SDP offer containing one m-line per current publisher. We answer it
@ -541,6 +565,7 @@ async function sfuSubscribe(){
async function sfuUnsubscribe(){
if (sfuSubEvents){ try { sfuSubEvents.close(); } catch(_){} sfuSubEvents = null; }
if (sfuSubPC){ try { sfuSubPC.close(); } catch(_){} sfuSubPC = null; sfuSubPeerID = null; }
sfuPendingTracks.clear();
}
/* ==================================================================
@ -748,7 +773,7 @@ async function handleSignal(raw){
renderRoom();
break;
case 'state':
roomEpoch = m.epoch; applyState(m.state); renderRoom(); break;
roomEpoch = m.epoch; applyState(m.state); flushSfuPendingTracks(); renderRoom(); break;
case 'peer-joined':
members.set(m.uuid, { uuid:m.uuid, pubkey:m.pubkey, handle:m.handle, role:m.role, joined_at: Date.now()/1000 });
logLine('', m.handle+' joined as '+m.role);
@ -758,6 +783,9 @@ async function handleSignal(raw){
hostUUID = m.uuid;
setStatus('connected as '+myRole, 'ok');
}
/* a new member may resolve a queued SFU track (e.g. host's rejoin
* race where ontrack fired before peer-joined) */
flushSfuPendingTracks();
/* establish mesh PC if both us and them are speakers (or mods).
* present-member-offers: existing speaker offers when a new speaker
* arrives. deterministic by uuid string compare. */
@ -870,6 +898,7 @@ async function handleSignal(raw){
if (m.new_host_uuid === myUUID){ myRole = 'host'; setStatus('connected as host','ok'); onRoleChanged('cohost','host'); }
else { setStatus('connected as '+myRole, 'ok'); } /* clears the space-closing warning */
logLine('', (mm.handle||shortHex(m.new_host_uuid))+' is now host'); }
flushSfuPendingTracks();
renderRoom();
}
break;
@ -1222,8 +1251,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> &nbsp;·&nbsp; built <span class="stamp-date">2026-05-31</span><br>
md5 <span class="stamp-md5">1142cb5adb9819dc4ab6e4ec83710110</span><br>
sha256 <span class="stamp-sha">fbe075e39aed18c4c705d1021b6d601ebc6a22a51710429b700c7443c83848ee</span><br>
md5 <span class="stamp-md5">1a023c7203c2e2aec7c607e3524f9843</span><br>
sha256 <span class="stamp-sha">7d64ded39b3813cf2b47c0b1c5ba3bfa7a99a941c6f90860aed6a49b8eec7647</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>