Files
gods-eye-view/mobile/ui/liveViewer.js
T
Claude 8b7fcb09df feat(mobile): rooms, live and upload capture, and the altitude-band identity
The Now feed mixes sensor, eye, track, number, witness and room cards
across Orbit, Sky, Ground and Inside, ordered so neighbours differ, with
composited thumbnails and live public-camera frames.

Every situation has a room: typed, place- and time-anchored posts,
evidence badges, bridging-rated notes and a cited Known / Disputed /
Unknown local synthesis, with cross-tab presence over BroadcastChannel.
Example posts are labelled as such.

Capture adds Live (10 s segments sealed into a hash chain, shown on the
globe as a view frustum) and Upload (EXIF placement, Unverified tier),
and an attach sheet that files any capture into a nearby situation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
2026-10-19 02:49:03 +00:00

94 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Watch a live broadcast from another tab on this device: frames arrive over
* BroadcastChannel about once a second with the broadcaster's telemetry and
* sealed segment count. Watching reports back so the broadcaster's viewer
* count is real.
*/
import { esc, haptic } from './dom.js';
import { createSheet } from './sheet.js';
import { frame } from './cards.js';
import { llShort, cardinal } from './bands.js';
export function createLiveViewer({ rt, globe }) {
const sheet = createSheet({
label: 'Live broadcast',
detents: ['large'],
modal: true,
dismissible: true,
closeButton: true,
className: 'eye-sheet opaque',
heights: { large: (H) => Math.round(Math.min(H * 0.86, 760)) },
});
let cur = null;
let tick = 0;
let pin = null;
function paint(l) {
if (!cur || l.id !== cur.id) return;
cur = { ...cur, ...l };
const img = sheet.body.querySelector('.ev-frame img');
if (img && l.frame) img.src = l.frame;
const t = sheet.body.querySelector('[data-lv-tele]');
if (t)
t.textContent = `INSIDE · ${llShort(l.lat, l.lon, 3)} · HDG ${Math.round(l.headingDeg || 0)}° ${cardinal(l.headingDeg)} · ${l.segments || 0} SEALED SEGMENTS`;
if (pin) globe.removePin(pin);
if (Number.isFinite(l.lat))
pin = globe.addPin({
id: `watch:${l.id}`,
kind: 'live',
lon: l.lon,
lat: l.lat,
headingDeg: l.headingDeg || 0,
fovDeg: 62,
label: `${l.handle} · live`,
});
}
function open(w) {
const l = rt.lives().find((x) => x.id === w.liveId) || {
id: w.liveId,
handle: w.handle,
startedAt: w.startedAt,
};
cur = l;
sheet.head.innerHTML = `<div class="c-band tele" data-band="inside"><i class="bd"></i>Inside<b>·</b>Live from a person</div>
<h2 class="sheet-title">${esc(l.handle || 'Someone')} is live</h2>
<div class="sheet-sub">${esc(l.situationTitle ? `Near ${l.situationTitle}` : 'Broadcasting from this device’s other tab')}</div>`;
sheet.body.innerHTML = `<div class="ev-frame" data-band="inside"><img class="front" alt="Latest broadcast frame" ${l.frame ? `src="${esc(l.frame)}"` : ''}/>${frame()}
<div class="c-top"><span class="c-live tele onair"><i></i>LIVE <span data-lv-since></span></span><span class="c-sid tele">SEALED EVERY 10 S</span></div>
<div class="c-strip tele" data-lv-tele></div></div>
<p class="footnote" style="padding:12px 2px 0">Frames reach this tab over BroadcastChannel about once a second (a prototype stand-in for a streaming server). The broadcaster’s file is sealed as a hash chain every 10 seconds; the bundle appears in their You tab when they end.</p>`;
paint(l);
sheet.open('large');
rt.watch(l.id);
haptic('light');
clearInterval(tick);
tick = setInterval(() => {
const el = sheet.body.querySelector('[data-lv-since]');
if (!el || !cur?.startedAt) return;
const s = Math.max(0, Math.round((Date.now() - cur.startedAt) / 1000));
el.textContent = `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
}, 1000);
}
rt.on('live', paint);
rt.on('live-end', ({ id }) => {
if (cur?.id !== id) return;
const t = sheet.body.querySelector('[data-lv-tele]');
if (t)
t.textContent =
'BROADCAST ENDED · BUNDLE SEALED ON THE BROADCASTER’S DEVICE';
sheet.body.querySelector('.c-live')?.classList.add('ended');
});
sheet.on('close', () => {
clearInterval(tick);
rt.watch(null);
if (pin) globe.removePin(pin);
pin = null;
cur = null;
});
return { open };
}