zebra-report: zebra-spaces mobile + host-rejoin fixes
This commit is contained in:
parent
a8b184b853
commit
910d961dac
1 changed files with 60 additions and 28 deletions
|
|
@ -16,26 +16,38 @@
|
|||
font-family: monospace; background: #fff; color: #000;
|
||||
padding: 2rem; max-width: 1440px; margin: 0 auto;
|
||||
}
|
||||
/* two-column page: left = timeline (blank for now, ~50%), right = controls.
|
||||
* stacks below 800px so the controls stay usable on a phone. */
|
||||
/* two-column page on desktop: left = timeline (1fr, takes remaining
|
||||
* space — will host the running event feed once v0.3 lands), right =
|
||||
* controls (capped at 520px so they never get awkwardly wide on a
|
||||
* full-screen monitor). Both columns get min-width:0 so they shrink
|
||||
* without forcing a horizontal scrollbar. */
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 520px);
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
.timeline {
|
||||
border-right: 1px solid #ddd; padding-right: 2rem; min-height: 60vh;
|
||||
min-width: 0;
|
||||
}
|
||||
.controls { min-width: 0; }
|
||||
.timeline-placeholder {
|
||||
font-size: 0.8rem; color: #999; padding: 1rem 0; line-height: 1.6;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
body { padding: 1.25rem; }
|
||||
.page { grid-template-columns: 1fr; gap: 1rem; }
|
||||
.timeline { border-right: none; border-bottom: 1px solid #ddd;
|
||||
padding-right: 0; padding-bottom: 1rem; min-height: 0; }
|
||||
/* stack on mobile, controls first, timeline below */
|
||||
.page { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
|
||||
.controls { order: 1; }
|
||||
.timeline { order: 2;
|
||||
border-right: none; border-top: 1px solid #ddd;
|
||||
padding-right: 0; padding-top: 1rem; min-height: 0; }
|
||||
}
|
||||
/* never let dynamically-appended <audio> sinks (one per remote
|
||||
* speaker) render their default ~300px control strip — they're just
|
||||
* sinks for the WebRTC track, no UI required */
|
||||
audio { display: none; }
|
||||
h1 {
|
||||
font-family: 'chunkfiveregular', serif;
|
||||
font-size: 3rem; font-weight: normal; letter-spacing: 0.02em;
|
||||
|
|
@ -447,26 +459,43 @@ 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
|
||||
/* SFU MediaStream cache keyed by PUBLISHER pubkey hex (= streamID set by the
|
||||
* SFU's TrackLocal). Survives across host leave/rejoin: when a speaker drops
|
||||
* we tear their audio element, but Pion often REUSES the transceiver on
|
||||
* their rejoin so ontrack doesn't fire a second time — the same MediaStream
|
||||
* object keeps receiving new RTP under the hood. By caching by pubkey we
|
||||
* can re-attach that same stream to a fresh audio element on peer-joined
|
||||
* even when no fresh ontrack event arrives. */
|
||||
const sfuStreamsByPubHex = 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;
|
||||
/* programmatic play in case autoplay policy needs the nudge after a track
|
||||
* swap — caller already had a user gesture (entered the space) */
|
||||
try { const p = a.play(); if (p && p.catch) p.catch(()=>{}); } catch(_){}
|
||||
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]){
|
||||
/* If we already have a cached SFU stream for this member's pubkey (from a
|
||||
* prior subscribe-side ontrack), attach it. Used on peer-joined / host
|
||||
* promotions / room state updates so a rejoined speaker's audio reattaches
|
||||
* without needing the SFU to emit a fresh ontrack. */
|
||||
function attachCachedSfuStreamFor(uuid){
|
||||
const mm = members.get(uuid);
|
||||
if (!mm || !mm.pubkey) return;
|
||||
let pubHex;
|
||||
try { pubHex = hex(unb64(mm.pubkey)); } catch(_){ return; }
|
||||
const stream = sfuStreamsByPubHex.get(pubHex);
|
||||
if (stream) attachSfuTrack(uuid, stream);
|
||||
}
|
||||
function flushSfuStreams(){
|
||||
for (const [pubHex, stream] of sfuStreamsByPubHex){
|
||||
for (const [uuid, mm] of members){
|
||||
try {
|
||||
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
|
||||
attachSfuTrack(uuid, stream);
|
||||
sfuPendingTracks.delete(pubHex);
|
||||
if (!remoteAudio.has(uuid)) attachSfuTrack(uuid, stream);
|
||||
break;
|
||||
}
|
||||
} catch(_){}
|
||||
|
|
@ -513,15 +542,18 @@ async function sfuSubscribe(){
|
|||
pc.ontrack = (ev) => {
|
||||
const pubHex = ev.streams[0] ? ev.streams[0].id : '';
|
||||
if (!pubHex) return;
|
||||
/* 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;
|
||||
/* cache stream by publisher pubkey so it survives the member's session
|
||||
* uuid changing across leave/rejoin — see flushSfuStreams */
|
||||
sfuStreamsByPubHex.set(pubHex, ev.streams[0]);
|
||||
for (const [uuid, mm] of members){
|
||||
try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){ matchUuid = uuid; break; } } catch(_){}
|
||||
try {
|
||||
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
|
||||
attachSfuTrack(uuid, ev.streams[0]);
|
||||
return;
|
||||
}
|
||||
} catch(_){}
|
||||
}
|
||||
if (matchUuid){ attachSfuTrack(matchUuid, ev.streams[0]); }
|
||||
else { sfuPendingTracks.set(pubHex, ev.streams[0]); }
|
||||
/* no matching member yet — flushSfuStreams will attach on peer-joined */
|
||||
};
|
||||
/* server-initiated offer: POST /subscribe (empty body) — SFU answers with
|
||||
* an SDP offer containing one m-line per current publisher. We answer it
|
||||
|
|
@ -565,7 +597,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();
|
||||
sfuStreamsByPubHex.clear();
|
||||
}
|
||||
|
||||
/* ==================================================================
|
||||
|
|
@ -773,7 +805,7 @@ async function handleSignal(raw){
|
|||
renderRoom();
|
||||
break;
|
||||
case 'state':
|
||||
roomEpoch = m.epoch; applyState(m.state); flushSfuPendingTracks(); renderRoom(); break;
|
||||
roomEpoch = m.epoch; applyState(m.state); flushSfuStreams(); 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);
|
||||
|
|
@ -785,7 +817,7 @@ async function handleSignal(raw){
|
|||
}
|
||||
/* a new member may resolve a queued SFU track (e.g. host's rejoin
|
||||
* race where ontrack fired before peer-joined) */
|
||||
flushSfuPendingTracks();
|
||||
flushSfuStreams();
|
||||
/* 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. */
|
||||
|
|
@ -898,7 +930,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();
|
||||
flushSfuStreams();
|
||||
renderRoom();
|
||||
}
|
||||
break;
|
||||
|
|
@ -1251,8 +1283,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-05-31</span><br>
|
||||
md5 <span class="stamp-md5">b6cc79823e4897e91fd798b328bac81d</span><br>
|
||||
sha256 <span class="stamp-sha">cf0360f7b16465f256b22bd6c8f44790d909bc933fd9993a890c21137916b4e3</span><br>
|
||||
md5 <span class="stamp-md5">4dc64b26953ef97c743bc54fb9a084d5</span><br>
|
||||
sha256 <span class="stamp-sha">ef1848a02178345ebe976ce392a049eaa17aab5c7d3f1d6291db1575b4acb126</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