Commit graph

21 commits

Author SHA1 Message Date
d9852e661e
zebra-spaces: local preview tile so publisher sees their own screen share
The SFU does not echo a publisher's stream back, so anyone sharing alone
in a room had no visual confirmation — game iframe stayed up, no tile
appeared. Render a muted local preview keyed by myKeys.pubHex on publish,
remove on unpublish. Subscribe path skips own pubHex to defend against
SFUs that do echo publishers.

Also refreshes integrity stamps on the other web pages (date drift).
2026-06-01 19:45:15 -04:00
e8ba836b9c
zebra-spaces: shared screens render in the left column (full width), game iframe hides while sharing
Previously sec-screens lived inside the controls aside, capped at 520px
wide — useless for any real screen view. Move it into the timeline
column at the top, where there's actual horizontal space. CSS :has()
on .timeline hides the game tabs + iframe whenever a screen tile is
visible, so the screen owns the full left column. As soon as the last
sharer stops, the game switcher returns.
2026-06-01 15:21:38 -04:00
70de089754
zebra-spaces: log out button next to backup/restore — wipe identity + generate fresh 2026-06-01 15:10:13 -04:00
5e23ad53ff
zebra-spaces: screen share — share button, separate sfuScreenPC, render incoming :screen streams as <video> tiles
UI:
- sec-screen-share: 'share screen' button visible only when canSpeak
  (host/cohost/speaker). On click: getDisplayMedia({video, audio}).
- sec-screens: video tile per active screen share; auto-shows when
  the first tile arrives, auto-hides when the last leaves.
- Each tile has a 'fullscreen' button.

Publish:
- sfuPublishScreen opens a SEPARATE sfuScreenPC, adds the
  display-media tracks (video + optional audio), POSTs
  /publish?kind=screen so the SFU's TrackLocal streamID gets a
  ':screen' suffix.
- Browser-native 'stop sharing' bar fires the video track's 'ended'
  event — we trap it to do a clean sfuUnpublishScreen.

Subscribe:
- sfuSubscribe ontrack checks streams[0].id for the ':screen' suffix
  — screen streams route to renderScreenTile (creates / updates a
  <video> element). Mic streams continue down the existing audio
  path. The label uses the publisher's handle from the room state.
- Cleanup paths: peer-left (drops the matching pubHex's tile),
  self-boot, self-blocked, leave button — all call
  sfuUnpublishScreen + removeScreenTile.

CSS: screen-tile has its own 1fr grid, max-height 70vh so a screen
share never bullies the controls column off the page on a small
display.

Pairs with the multi-track + kind=screen support that just landed in
zebra-spaces-sfu (b7c5a8a).
2026-06-01 14:41:27 -04:00
95d68fafac
zebra-spaces: timeline column gets a game switcher (unmario / cake murder adventure) 2026-05-31 18:35:04 -04:00
c7a8a938b3
zebra-spaces: load unmario.com in the timeline column until v0.3 lands 2026-05-31 16:57:19 -04:00
f3b154da2b
zebra-spaces: terminal block + share URL + autofill; host-your-own: SFU section
- Block is now terminal: any 'blocked' signal-server error or 403 from
  the SFU calls handleBlocked() which shows a clear notice, stops the
  WS reconnect loop, tears mesh + SFU + mic, and unlocks the enter
  button. Stale UI no longer spins trying to rejoin a room the user
  was kicked from. Reset on a fresh joinSpace.
- Share URL: after joining, sec-share shows a copyable link with
  ?code=…  embedded; opening that link autofills the rendezvous code
  field. QR canvas placeholder is in place; encoder lands in the next
  push (defer to keep this commit small).
- host-your-own.html gains section 8: zebra-spaces (multi-party rooms)
  describing both Go services that make spaces work — the
  zebra-spaces-signal authority-chain rendezvous and the Pion-based
  zebra-spaces-sfu audio fan-out. Includes the two Caddyfile routes
  (with flush_interval -1 for SSE), the NAT1To1 env var, and the
  single UDP mux port — so someone wanting to host their own community
  can stand the whole stack up. Sections 9-11 renumbered.
2026-05-31 15:53:14 -04:00
e72f2e8eed
zebra-spaces: pass identity pubkey on SFU /subscribe so boots evict the listener path; tear SFU + mesh on self-boot 2026-05-31 15:20:03 -04:00
f56bd39980
zebra-spaces: invite banner stacks message above accept/decline buttons 2026-05-31 15:10:12 -04:00
a1ce6c0598
zebra-spaces: fix mobile horizontal scroll + host rejoin audio + timeline below
Three issues fixed together:

1. Mobile horizontal scroll: dynamically-appended <audio> sinks render
   their default ~300px control strip on Firefox mobile, pushing the
   page wider than the viewport. Hide them globally with
   'audio { display: none }' — they're just sinks for the WebRTC track,
   no UI required.

2. Don't use literal 50/50 split: page grid is now
   'minmax(0, 1fr) minmax(0, 520px)' so the controls cap at 520px and
   the timeline soaks the remainder. min-width:0 on both children so
   they shrink without forcing an outer scrollbar.

3. Mobile order: on phones the controls were stacked ABOVE the
   timeline placeholder; fox wants them first. Use 'order: 1' on
   .controls and 'order: 2' on .timeline inside the 800px breakpoint.

4. Host rejoin audio: when a host leaves and rejoins, Pion frequently
   reuses the same transceiver on the SFU side, so 'ontrack' doesn't
   fire a second time on the listener — but the listener already tore
   down the audio element on peer-left. The audio plays into nothing.

   Replace the pending-tracks queue with sfuStreamsByPubHex (keyed by
   the publisher's pubkey, which is the streamID). The cached
   MediaStream survives the host's session uuid changing across
   leave/rejoin. On peer-joined / host-promoted / state we look up the
   cached stream by pubkey and attach to the fresh audio element.

   Also call a.play() after the srcObject swap so the autoplay policy
   doesn't quietly drop the resumed playback.
2026-05-31 14:19:43 -04:00
c525c7df11
web: bump mobile body padding from 1rem to 1.25rem so the right edge has visible breathing room 2026-05-31 14:13:25 -04:00
cc46d96d30
zebra-spaces: queue SFU ontrack until peer-joined catches up
Race when the host rejoined during their 30s grace window: the SFU
renegotiation that delivered the new mic track arrived over SSE BEFORE
the signal-server's peer-joined for the rejoined host. ontrack's
streams[0].id (= host's pubkey hex) had no matching member yet, so
the track was silently discarded — the listener saw no audio.

Cache the unmatched stream by pubkey hex and flush whenever the
roster updates (peer-joined, host-promoted, state). When the rejoined
host's peer-joined arrives, the queued stream pops out and attaches
to their row. Cleared on sfuUnsubscribe so stale streams don't carry
across rejoins.
2026-05-31 14:08:53 -04:00
2051a712af
zebra-spaces: clear the 'space closing in 30s' status when host returns
space-closing flips the top status line to a red 'host left — space
closing in Ns' warning. Nothing was clearing it when the host actually
came back during grace (peer-joined with role=host) or when a senior
cohost auto-promoted (host-promoted). Both paths now reset the status
line to 'connected as <my role>' so the warning doesn't stick after
the rescue.
2026-05-31 14:02:58 -04:00
3a07426994
zebra-spaces: own mute flips own mic icon
mic-state broadcasts only inform OTHER peers, so the local view of our
own member row stayed muted=false forever — the green mic-on icon
never flipped to the red cross when the host (or any speaker) muted.
Set members.get(myUUID).muted before sendMicState so renderRoom
reflects it immediately on our own row.
2026-05-31 13:54:23 -04:00
1a6e955424
zebra-spaces: client wires grant-mic for hand-raised listeners
The 'give the mic' button (visible only when the listener has raised
their hand) now sends grant-mic, which the server promotes directly
without waiting for accept-mic. The 'invite mic' button (cold invite)
still triggers the existing mic-invite + accept dance.

Also fold in the earlier sfuPublish diagnostic logging so silent
returns surface in the page log.
2026-05-31 13:25:51 -04:00
9ed1cefcd5
zebra-spaces: SFU bridge + role-change notice banner
Listeners now hear all speakers via a Pion-based audio-only SFU on
proxy.uncloseai.com. Speakers publish to it; listeners subscribe and
get one PC carrying every speaker's track. Mesh remains the
low-latency speaker<->speaker path; SFU is the broadcast fan-out.

Wiring:
- sfuPublish/Unpublish for speaker role transitions
- sfuSubscribe/Unsubscribe for listener role transitions
- onRoleEntered + onRoleChanged + leave hooks
- ontrack on the subscribe PC maps streams[0].id (= publisher pubkey
  hex, set as the SFU TrackLocal StreamID) -> room member uuid; audio
  element + meter attach to the matched member row
- SSE renegotiation: SFU pushes offer when speakers come/go; browser
  answers via POST /answer
- applyMicMode now hot-swaps the SFU publish sender's track too, so
  voice/music mode toggles apply over the SFU just like the mesh

Notice banner — fox flagged that booted/demoted users had no visible
signal. Added a #sec-notice section with .notice-banner (warn for
boot/demote-to-listener, info for promote). Boot also sets
wantConnected=false to prevent the WS auto-reconnect loop from
rejoining into a boot loop.
2026-05-31 12:48:20 -04:00
7e54e1b7ef
zebra-spaces: persistent per-peer meter runner (survives re-renders + wires local mic)
Two real defects in the meter wiring:
- the local mic was never fed to its meter — only remote streams were
- renderRoom replaces every member's meter DOM element, so the old
  meterFor tick loop (which captured the OLD fill via closure) exited
  on the next frame and the bar froze forever

Replace with a per-uuid meterCtl Map that holds the analyser + buffer
once and re-reads members.get(uuid)._meterEl fresh every animation
frame. Cleanup is automatic when the uuid drops from members (peer-
left, boot) or explicitly via stopMeter (dropMic, applyMicMode).
applyMicMode re-wires the local meter against the freshly acquired
stream since the old MediaStreamSource dies with the old track.
2026-05-31 12:10:57 -04:00
cd4a0ff6f5
zebra-spaces: mod-action buttons drop to a new row under each member
The grid was packing badge | handle | pubkey | mic | meter | buttons on
one line, which got crowded with three or four actions visible. Drop
mod-actions to a second grid row spanning the full width, indented
under the handle column. Hides itself entirely when empty so rows with
nothing to act on don't reserve dead space.
2026-05-31 12:00:10 -04:00
a038adde7b
zebra-spaces: two-column layout — right=controls (50%), left=timeline (placeholder)
The control panel (identity, space, room, log) moves to a right column
occupying 50% of the page width. Left column is reserved for a timeline
to come; for now it shows a one-line placeholder. Stacks to single
column below 800px so mobile stays usable. Footer spans full width.
2026-05-31 11:59:11 -04:00
a2d43e40c3
zebra-spaces: show mic input + music-mode controls before getting the mic
They were hidden behind the speaker role, so listeners (and the host
during the brief window before mic permission resolves) could not
disable echo/noise cancellation. Make both rows always visible; the
preference is picked up by micConstraints() the next time getMic runs,
so toggling before becoming a speaker is honored.
2026-05-31 11:52:33 -04:00
1e76f62f71
zebra-spaces v0.1: multi-party voice rooms with roles, identity, vault
New page: web/zebra-spaces.html. Extends the rendezvous + WebRTC mesh
model from zebra-audio (1:1) to a room of up to ~8 speakers (host + 2
co-hosts + speakers) with listeners (deferred to v0.2 for SFU fan-out).

Identity: per-browser persistent Ed25519 keypair in localStorage,
plus a per-session UUID for in-room "whose who". Password-vault
backup/restore (PBKDF2 600k + AES-GCM, matches zebra-audio's crypto)
emits a self-contained 'zspc-id-v1|...' blob.

Roles: everyone joins as listener; mods (host + co-hosts) extend
mic-invites that listeners accept/decline; host promotes to co-host;
mods demote and boot. Co-hosts cannot remove the host. Every role
transition is signed by the acting mod's Ed25519 over a canonical
input bound to room_id + epoch, so a compromised relay cannot forge
promotions, only refuse to relay them.

Pairs with cmd/zebra-spaces-signal in proxy.unturf.com.

Nav: zebra-audio and host-your-own cross-link to spaces.
Makefile: stamp target now covers zebra-spaces.html (web/chat.html
and web/how-it-works.html stamps refreshed today as a side effect).
2026-05-31 10:54:36 -04:00