feat(slides): Live broadcast - #293
Open
niemes wants to merge 38 commits into
Open
Conversation
…t 400/10s
Owner-signed plaintext control frames beside the collab path: {ctl:'nav',n,g}
(sig over nav.${n}), {ctl:'laser',p,g} / {ctl:'laser',off:1,g} (sig over
laser.${p} / laser.off, slide-fraction point, never stored), and
{ctl:'black',on:1|0,g} (sig over black.on/black.off, persisted as lastBlack
and replayed to late joiners). Every block verifies the signature against
the socket's OWN pinned key AND that the key hash-commits to the room name —
a member/chain socket's key never hash-matches, so only the room owner can
broadcast. RATE_BURST 200→400: a 30fps laser stroke is 300 frames/10s; the
count limiter is an abuse guard, laser frames are ~60 bytes so the byte
budget is the real cap.
BroadcastSocket: lightweight WS (viewer ?tok=&since=0, owner ?w=<pub>&tok= &since=0), backoff 800ms×1.8 cap 30s, ping 25s; ignores everything except nav/presence/laser/black frames so it safely replays ciphertext noise from collab rooms. resolveBroadcastCreds: case 1 reuses the EXISTING collab room and signs with the owner key; case 2 mints a device-local broadcast-only room (bento-broadcast-<docId> in localStorage, fresh keypair + room key — the private key never leaves the machine). Transport-agnostic sendNav/ sendLaser/sendBlack helpers + onCtl passthrough on OnlineTransport for the directOwner path.
…ings
Share menu gains 'Broadcast copy…': exports a standalone .bento.html that
boots straight into present-follow mode, carrying only {room, tok, relay} —
no owner key, no symmetric key, no CRDT state. model.ts: collab.broadcast
field (additive, old shells ignore it) and room/key made optional for
broadcast-only copies. New broadcast icon + 13 UI strings across all 9
catalogs.
…ack screen, remote trail Speaker view gains a broadcast arm/teardown toggle (📡): on arm, current slide is sent and every slidechange fans out an owner-signed nav frame; viewer-count badge + popup clipboard script. Laser became hold-to-draw (L arms, pointer-down draws) and streams slide-fraction points at ~30fps (33ms throttle, Excalidraw's CURSOR_SYNC_TIMEOUT) with an off frame on release; black-screen toggle syncs and replays to late joiners. Broadcast copies re-synthesize the trail Excalidraw-style: a rAF sampler feeds the dot's RENDERED position into the shared trail ring buffer so the head stays glued to the pointer (never pre-baked ahead of it), 0.8s fade, and a re-shown dot snaps to the new stroke instead of tweening a ghost line across the gap. B toggles black from the audience document too; text selection is locked while the laser is armed.
Copies carrying doc.collab.broadcast boot straight into a locked follow-mode viewer: real present overlay, read-only broadcast socket driving navigation, status chip (connecting / waiting / live · N viewers). ?b=<viewer url> on the copy's own URL re-points it at ANOTHER broadcaster's room — e.g. the owner takes over a presentation the exporter can't do. The relay's auth is a per-room trust-on-first-use token, so the room alone is not a capability: the owner's broadcast link (relay + room + tok) must come along; everything after ?b= is that link, raw. The copy stays a passive viewer — no signing key, view access only. Malformed overrides fall back to the embedded room.
23 asserts against a local relay: unsigned/forged/member-key drops, valid owner nav/laser/black fan-out, laser off, lastBlack replay to late joiners, laser non-replay, rate limiter intact.
Builds two self-contained .bento.html fixtures (owner + broadcast copy) minted with real case-1 collab credentials for manual testing against a local relay.
docs/broadcast-design.md: nav-frame + broadcast-room model, threat model, wire format. docs/broadcast-plan.md: 7-phase implementation plan with verified anchors. DECISIONS.md: live-broadcast control channel (laser + black frames) and the ?b= re-point override entries.
…e popup script listeners
…eral The setHost validation regex was written as /^https?:\/\//i inside the bcastScript template literal, where \/ is an identity escape that emits /, so the injected script contained /^https?:///i and failed to parse — the popup never bound its message listener and the broadcast link row stayed dead. Escape as \\/ in the template so the emitted script is a valid regex literal.
Owner decks sign with the owner key, shared editor copies with their per-copy invite key, legacy copies with the writer key, and non-collab decks with a device-local key — so every presenter broadcasts into their own room (the docId-derived room collided across invite copies). The connect token is derived from the room name, so URLs and files carry no secret; the relay TOFU-pins the signer key per room and verifies nav/laser/black frames against it.
The row and viewer-count badge set display:flex/inline-block, which
overrode the UA's [hidden]{display:none} — the empty URL row stayed
visible with the broadcast off. The [hidden] variants now restate
display:none. The broadcast button also reuses the editor's broadcast
SVG icon instead of the 📡 emoji, and openSpeaker re-posts the link so
a popup reopened mid-broadcast shows it.
# Conflicts: # slides/src/i18n/de.ts # slides/src/i18n/es.ts # slides/src/i18n/fr.ts # slides/src/i18n/it.ts # slides/src/i18n/ja.ts # slides/src/i18n/packed.ts # slides/src/i18n/pt.ts # slides/src/i18n/zh-Hans.ts # slides/src/i18n/zh-Hant.ts
… toast, popup style dedup
# Conflicts: # CHANGELOG.md
Author
|
Small note for local testing, the sync worker variable |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Live slide broadcast - present any deck online, no accounts for viewers
This implements live broadcasting for bento/slides: the presenter's current slide syncs to any number of viewers in near-real-time over the existing collab relay. Viewers need no account, no write access, no collaboration keys. The broadcast copy of the file is the whole capability.
What it adds
collab.broadcastis an optional field; existing files open unchanged.The two sharing cases
Not hosted — the file is the link. A deck without a hosting URL exports a pure snapshot copy embedding only the room name and relay. Give the file to viewers: when you start presenting and arm the broadcast, every open copy connects straight to the room derived from your signing key and follows — nothing to share, no URL involved.
Hosted — a URL instead of the file. A deck with a hosting URL (Document properties →
meta.hostClient) exports a copy that additionally carries reader credentials and joins the deck's collab room as a live reader replica, so slide content updates in real time as the deck is edited mid-show. The speaker view shows a full viewer URL (host?room=<name>) with a Copy button, and the same hosted copy can be re-pointed at any presenter's room via?room=— a replacement presenter takes over without re-exporting files.Laser pointer
The presenter laser got some work in this pass:
offframe on pointerup. For performance with the sync worker and it is also a more natural behavior.user-select: noneon the slide subtree). Text selection is not broadcasted, so endup as visual noise for the speaker.Relay
server/sync-workergains broadcast control-frame handling (nav/laser/black with signature verification, per-room TOFU key pinning,lastNav/lastBlackreplay for late joiners, presence counts).The relay must be deployed (
wrangler deploy).Verification
node scripts/test-nav.mjs— 15/15: unsigned, forged, and member-key frames dropped; valid frames fanned;lastNavreplayed ahead of live frames; presence rises on connect and falls on close; rate limiter intact; laser/black security.tsc -bclean,npm run build:singleOK; no CRDT changes (scripts/test-sync.tsuntouched).Checklist
npm run build:singlesucceeds (fromslides/)node scripts/test-sync.tsif I touchedslides/src/sync/slides/src/i18n/