zebra-spaces: spotlight broadcast — log + popularity-sort thumbnails
Per fox — make viewing public so the room can see what's holding
attention and so thumbnail order reflects what people actually watch
(prevents a speaker dropping inappropriate content from drifting into
peripheral view unless folks tune in).
- spotlights: Map<uuid, key> tracking who's viewing what big tile
- send {type:'spotlight', key:'kind:pubHex'} on every local spotlight
change; broadcast back via signal server (already deployed) as
{type:'spotlight', uuid, key}
- log line on each change: 'alice now viewing bob's screen' /
'alice looked away' — social pressure tool, names what the room
is focused on
- tileScore = role boost on the OWNER (host 10000, cohost 5000,
speaker 0) plus viewer count. Thumbnails re-sorted descending so
host content sits at top, cohost second, speakers ranked by
viewer count
- reorderTiles fires on: own spotlight change, inbound spotlight
signal, peer-left, role-change, host-promoted, and at welcome
(broadcasts the auto-spotlit first tile)
- pubHex + kind stored as tile.dataset for the sort to read
Paired with proxy.unturf.com#719e0e5 which routes the new message
type through the signal server.
This commit is contained in:
parent
a64dcc29c7
commit
cdcc15365a
1 changed files with 86 additions and 2 deletions
|
|
@ -1010,8 +1010,58 @@ const TILE_KINDS = {
|
|||
};
|
||||
/* the active spotlight, or null when nothing is spotlit */
|
||||
let spotlight = null; // { kind, pubHex, tile (DOM), video (DOM) }
|
||||
/* who in the room is viewing what big tile. Keyed by uuid → key
|
||||
* ("kind:pubHex" or "" for none). Drives popularity sort + log lines. */
|
||||
const spotlights = new Map();
|
||||
function spotlightKey(){ return spotlight ? spotlight.kind+':'+spotlight.pubHex : ''; }
|
||||
function getEntry(kind, pubHex){ const k = TILE_KINDS[kind]; return k ? k.store.get(pubHex) : null; }
|
||||
function memberOf(pubHex){
|
||||
for (const [uuid, mm] of members){
|
||||
try { if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex) return { uuid, mm }; } catch(_){}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function ownerLabel(kind, pubHex){
|
||||
const m = memberOf(pubHex);
|
||||
const handle = m ? (m.mm.handle || shortHex(pubHex)) : shortHex(pubHex);
|
||||
return handle + "'s " + (TILE_KINDS[kind] ? TILE_KINDS[kind].labelPrefix : kind);
|
||||
}
|
||||
/* tile score = role-based boost on the OWNER + count of current viewers.
|
||||
* Host always at top, co-hosts second, then speakers sorted by viewers.
|
||||
* Listeners can't publish so they never appear. */
|
||||
function tileScore(kind, pubHex){
|
||||
let boost = 0;
|
||||
const m = memberOf(pubHex);
|
||||
if (m){
|
||||
if (m.mm.role === 'host') boost = 10000;
|
||||
else if (m.mm.role === 'cohost') boost = 5000;
|
||||
else if (m.mm.role === 'speaker') boost = 0;
|
||||
}
|
||||
const key = kind + ':' + pubHex;
|
||||
let viewers = 0;
|
||||
for (const [, k] of spotlights) if (k === key) viewers++;
|
||||
return boost + viewers;
|
||||
}
|
||||
function sortThumbContainer(containerId, kind){
|
||||
const c = $(containerId); if (!c) return;
|
||||
const els = Array.from(c.children);
|
||||
els.sort((a, b) => tileScore(kind, b.dataset.pubHex) - tileScore(kind, a.dataset.pubHex));
|
||||
for (const el of els) c.appendChild(el);
|
||||
}
|
||||
function reorderTiles(){
|
||||
sortThumbContainer('cameras', 'camera');
|
||||
sortThumbContainer('screens-thumbs', 'screen');
|
||||
}
|
||||
function broadcastSpotlight(){
|
||||
/* fire-and-forget to the signal server; no-op if not connected yet */
|
||||
try { send({ type: 'spotlight', key: spotlightKey() }); } catch(_){}
|
||||
}
|
||||
function logSpotlightChange(uuid, key){
|
||||
const handle = (members.get(uuid) || {}).handle || shortHex(uuid);
|
||||
if (!key){ logLine('', handle + ' looked away'); return; }
|
||||
const [kind, pubHex] = key.split(':');
|
||||
logLine('', handle + ' now viewing ' + ownerLabel(kind, pubHex));
|
||||
}
|
||||
|
||||
function updateContainerVisibility(){
|
||||
$('sec-spotlight').classList.toggle('hidden', !spotlight);
|
||||
|
|
@ -1028,6 +1078,8 @@ function buildTile(kind, pubHex, label, opts){
|
|||
const isThumb = !!(opts && opts.thumb);
|
||||
const tile = document.createElement('div');
|
||||
tile.className = k.tileClass + (isThumb ? ' tile-thumb' : '');
|
||||
tile.dataset.pubHex = pubHex;
|
||||
tile.dataset.kind = kind;
|
||||
const video = document.createElement('video');
|
||||
video.setAttribute('autoplay', '');
|
||||
video.setAttribute('playsinline', '');
|
||||
|
|
@ -1081,6 +1133,13 @@ function setSpotlight(kind, pubHex){
|
|||
entry.tile.classList.add('viewing');
|
||||
spotlight = { kind, pubHex, tile: big.tile, video: big.video };
|
||||
updateContainerVisibility();
|
||||
/* track our own choice, broadcast it, log it, re-sort */
|
||||
if (myUUID){
|
||||
spotlights.set(myUUID, spotlightKey());
|
||||
logSpotlightChange(myUUID, spotlightKey());
|
||||
broadcastSpotlight();
|
||||
}
|
||||
reorderTiles();
|
||||
}
|
||||
function clearSpotlightDOM(){
|
||||
const sp = $('spotlight');
|
||||
|
|
@ -1098,6 +1157,11 @@ function pickNextSpotlight(){
|
|||
if (spotlight) clearSpotlightDOM();
|
||||
spotlight = null;
|
||||
updateContainerVisibility();
|
||||
if (myUUID){
|
||||
spotlights.set(myUUID, '');
|
||||
logSpotlightChange(myUUID, '');
|
||||
broadcastSpotlight();
|
||||
}
|
||||
}
|
||||
|
||||
function renderVideoTile(kind, pubHex, stream, opts){
|
||||
|
|
@ -1753,6 +1817,11 @@ async function handleSignal(raw){
|
|||
case 'welcome':
|
||||
myUUID = m.your_uuid; myRole = m.role; roomEpoch = m.epoch;
|
||||
applyState(m.state);
|
||||
/* any tile auto-spotlit before welcome (during fast-path subscribe)
|
||||
* needs to be broadcast now so the room sees our viewing state */
|
||||
spotlights.set(myUUID, spotlightKey());
|
||||
broadcastSpotlight();
|
||||
reorderTiles();
|
||||
logLine('', 'joined as '+myRole+' — uuid '+shortHex(myUUID));
|
||||
setStatus('connected as '+myRole, 'ok');
|
||||
$('dot-call').className='dot ok';
|
||||
|
|
@ -1816,9 +1885,11 @@ async function handleSignal(raw){
|
|||
}
|
||||
members.delete(m.uuid);
|
||||
handraise.delete(m.uuid);
|
||||
spotlights.delete(m.uuid);
|
||||
tearPeer(m.uuid);
|
||||
if (hostUUID === m.uuid) hostUUID = '';
|
||||
renderRoom();
|
||||
reorderTiles();
|
||||
}
|
||||
break;
|
||||
case 'sdp-from':
|
||||
|
|
@ -1829,6 +1900,17 @@ async function handleSignal(raw){
|
|||
try { const s = JSON.parse(await aesDecrypt(sigKey, unb64(m.data)));
|
||||
const mm = members.get(m.uuid); if (mm){ mm.muted = !!s.muted; renderRoom(); } } catch(_){}
|
||||
break;
|
||||
case 'spotlight':
|
||||
/* who's looking at what — drives thumbnail popularity sort + log
|
||||
* line so the room can see where attention is going */
|
||||
{ const prev = spotlights.get(m.uuid) || '';
|
||||
const next = m.key || '';
|
||||
if (prev === next) break;
|
||||
spotlights.set(m.uuid, next);
|
||||
if (m.uuid !== myUUID) logSpotlightChange(m.uuid, next);
|
||||
reorderTiles();
|
||||
}
|
||||
break;
|
||||
case 'hand-raised':
|
||||
handraise.add(m.uuid); renderRoom();
|
||||
{ const mm = members.get(m.uuid); if (mm) logLine('', mm.handle+' raised hand'); }
|
||||
|
|
@ -1885,6 +1967,7 @@ async function handleSignal(raw){
|
|||
}
|
||||
}
|
||||
renderRoom();
|
||||
reorderTiles();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -1926,6 +2009,7 @@ async function handleSignal(raw){
|
|||
logLine('', (mm.handle||shortHex(m.new_host_uuid))+' is now host'); }
|
||||
flushSfuStreams();
|
||||
renderRoom();
|
||||
reorderTiles();
|
||||
}
|
||||
break;
|
||||
case 'space-closing':
|
||||
|
|
@ -2572,8 +2656,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-02</span><br>
|
||||
md5 <span class="stamp-md5">cb57f9dfa52c34a5aaa29f7f76968206</span><br>
|
||||
sha256 <span class="stamp-sha">ddc8747d462fd1cf61f96b976cfc11760b232e0e7eb5a4dc0561f6e4a8dbd1e8</span><br>
|
||||
md5 <span class="stamp-md5">ad6a80d1c54036ff0d7782df2b98c4db</span><br>
|
||||
sha256 <span class="stamp-sha">77b28cfbf2690bdf0b12697c141ab460d0c95301344ce0245936a1c498aa35a9</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