zebra-report: demotion fixes — keep mesh, drop all publishes, no looked-away noise — mirror

This commit is contained in:
russell@unturf.com 2026-06-02 14:45:17 -04:00
parent 4b3adb0bfd
commit 9f89786b87
No known key found for this signature in database

View file

@ -1631,7 +1631,12 @@ function reorderTiles(){
for (const el of els) c.appendChild(el);
}
function broadcastSpotlight(){
/* fire-and-forget to the signal server; no-op if not connected yet */
/* fire-and-forget to the signal server; no-op if not connected yet.
* Suppressed during role transitions — otherwise removeScreenTile +
* removeCameraTile triggered by sfuUnpublishScreen/Camera during a
* demotion fire pickNextSpotlight → broadcasts an empty spotlight,
* which every other peer logs as 'X looked away'. False signal. */
if (typeof inRoleTransition !== 'undefined' && inRoleTransition) return;
try { send({ type: 'spotlight', key: spotlightKey() }); } catch(_){}
}
function logSpotlightChange(uuid, key){
@ -2797,12 +2802,13 @@ async function handleSignal(raw){
onRoleChanged(prev, m.role);
} else {
logLine('', mm.handle+' is now '+m.role);
/* mesh adjustments */
if (canSpeak(myRole)){
if (canSpeak(m.role) && !peers.has(m.uuid)) connectToPeer(m.uuid, myUUID < m.uuid);
if (!canSpeak(m.role) && peers.has(m.uuid)) tearPeer(m.uuid);
} else if (peers.has(m.uuid)){
tearPeer(m.uuid);
/* mesh adjustments — fox: 'never drop people out of the mesh
* automatically'. Promote: connect if we can speak + they
* can speak + we don't already have them. Demote: do NOT
* tear existing mesh peer connections; let them ride as a
* back-channel until one side actually leaves the room. */
if (canSpeak(myRole) && canSpeak(m.role) && !peers.has(m.uuid)){
connectToPeer(m.uuid, myUUID < m.uuid);
}
}
renderRoom();
@ -2894,23 +2900,39 @@ async function onRoleEntered(){
sfuPublish().catch(e => logLine('err','sfu publish: '+e.message));
}
}
let inRoleTransition = false;
async function onRoleChanged(prev, next){
if (!canSpeak(prev) && canSpeak(next)){
await ensureMicAndUI();
for (const [uuid, mm] of members){
if (uuid === myUUID) continue;
if (canSpeak(mm.role)) connectToPeer(uuid, myUUID < uuid);
/* suppress spotlight broadcasts triggered by tile cleanup during the
* role transition — otherwise removeScreenTile / removeCameraTile
* fires pickNextSpotlight which broadcasts an empty spotlight key,
* which every other peer logs as 'X looked away'. */
inRoleTransition = true;
try {
if (!canSpeak(prev) && canSpeak(next)){
await ensureMicAndUI();
for (const [uuid, mm] of members){
if (uuid === myUUID) continue;
if (canSpeak(mm.role)) connectToPeer(uuid, myUUID < uuid);
}
/* keep sfuSub alive — screen + camera tracks still ride it. The
* ontrack handler suppresses SFU mic when mesh is also up. */
sfuPublish().catch(e => logLine('err','sfu publish: '+e.message));
} else if (canSpeak(prev) && !canSpeak(next)){
/* demote to listener: drop everything we PUBLISH (mic + screen +
* camera + game) but DON'T tear mesh peers. fox: 'never drop
* people out of the mesh automatically'. The mesh connections
* stay up as a bonus low-latency audio path; they get GC'd
* naturally when the other end leaves or also demotes. */
dropMic(); muted = false;
await sfuUnpublish();
await sfuUnpublishScreen();
await sfuUnpublishCamera();
await sfuUnpublishGame();
}
/* keep sfuSub alive — screen + camera tracks still ride it. The
* ontrack handler suppresses SFU mic when mesh is also up. */
sfuPublish().catch(e => logLine('err','sfu publish: '+e.message));
} else if (canSpeak(prev) && !canSpeak(next)){
for (const u of [...peers.keys()]) tearPeer(u);
dropMic(); muted = false;
await sfuUnpublish();
/* still subscribed — listener role needs the same incoming streams */
updateRoleUI();
} finally {
inRoleTransition = false;
}
updateRoleUI();
}
async function ensureMicAndUI(){
/* mic input + music-mode rows are always visible; here we just grant the
@ -3525,8 +3547,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-06-02</span><br>
md5 <span class="stamp-md5">933f3fed73b0a824ed75b1cea22bba9a</span><br>
sha256 <span class="stamp-sha">836f1145f451e86c871813a9a1ff1dd2adbd4c61ddceb2e59a8a58fe24b14f08</span><br>
md5 <span class="stamp-md5">f908c19e47433151020f76df10fe81d0</span><br>
sha256 <span class="stamp-sha">f751785bdcb457d172dee013fbe0a445ee01b0aacdb20c2c1fe11352ef98111c</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>