From 5dad86db57b5d70ce17c728345eab758d76c3a4b Mon Sep 17 00:00:00 2001 From: Mollusk Date: Sat, 6 Jun 2026 06:14:14 -0400 Subject: [PATCH] docs: lock screen-share integration design (peerspeak <-> pixelpass) Approach B: peerspeak spawns pixelpass --host --output json, scrapes the ticket from its JSON stdout, and distributes it over the existing gossip plane as a ScreenShareOffer; peers get a one-click pixelpass viewer. Mutually optional, runtime-only coupling -- neither tool is a Cargo dependency of the other; the contract is pixelpass's CLI + JSON protocol. Video-only, separate viewer window, PATH binary discovery. Not yet built. Co-Authored-By: Claude Opus 4.8 --- docs/screenshare-integration.md | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/screenshare-integration.md diff --git a/docs/screenshare-integration.md b/docs/screenshare-integration.md new file mode 100644 index 0000000..86a1f86 --- /dev/null +++ b/docs/screenshare-integration.md @@ -0,0 +1,88 @@ +# Screen-share integration (peerspeak ↔ pixelpass) + +Design decisions for surfacing pixelpass screen-sharing from inside a peerspeak +room. Locked 2026-06-06; **not yet implemented.** peerspeak owns voice, +pixelpass owns pixels — this doc is the seam between them. + +## Core principle: mutually optional, runtime-only coupling + +**Neither tool is a Cargo dependency of the other, in either direction.** + +- peerspeak works fully without pixelpass installed; the screen-share control + just degrades (greyed / "needs pixelpass — install it"). +- pixelpass works fully without peerspeak; nothing here makes it require voice. +- The integration is **runtime subprocess + a stable contract**, never a crate + import. The contract peerspeak depends on is pixelpass's **CLI flags + JSON + event stream**, treated as a public API. +- Keep the contract **symmetric** so the reverse direction (pixelpass launching + peerspeak for a "call these viewers" button) stays possible later. No + master/slave assumption. + +## Chosen approach: B — auto ticket-exchange over peerspeak's gossip plane + +1. User clicks **Share screen** in a peerspeak room. +2. peerspeak spawns `pixelpass --host --output json` as a child process (video + only — see audio decision below). +3. peerspeak scrapes the `{"event":"ticket","value":"..."}` line from the + child's **stdout** (emitted once at host startup). +4. peerspeak broadcasts a **new typed gossip message**, + `ScreenShareOffer { from, ticket }`, over the same gossip plane that already + carries chat + presence. +5. Each peer's peerspeak shows "** is sharing — [View]**". +6. Clicking **View** spawns `pixelpass --output json` as a viewer; it + opens in **its own pixelpass window** (not embedded in iced). +7. When the sharer stops, peerspeak kills the host child and broadcasts a + `ScreenShareEnded { from }` (or equivalent) so viewers' indicators clear. + +Rejected: **A** (manual ticket via chat — barely integrated) and **C** (deep +embed / shared iroh identity — large, fights pixelpass's window model and the +two-tools philosophy). + +## Why this is cheap + +pixelpass was built to be driven as a child process: + +- `--output json` exists explicitly for a front-end (its own `--gui` re-execs + `pixelpass --host --output json` and parses the stream). See + `pixelpass/src/common/output.rs`. +- Host emits: `ticket`, `host_info`, `viewer_joined`, `viewer_left`, `capture`, + `viewer_refused`. Viewer emits: `connected { url }`. Host reads a `kick` + command on **stdin**. +- **The ticket is the capability.** The data-plane host (`serve.rs`) gates + viewers only by `max_viewers` — there is **no friends-list ACL** on the + stream (friends is a separate control-plane/presence concept the headless CLI + ignores). So distributing the ticket over gossip = granting room access, with + no ACL to fight. +- pixelpass is **1 host → N viewers** (asymmetric), which maps cleanly onto + "I share, the room watches." + +## Locked sub-decisions + +- **Audio: video only** on the room-launch path. Do not pass pixelpass's audio + capture, so screen-share audio can't echo/double with peerspeak's voice mix. +- **Separate window** for the viewer (pixelpass's own), not embedded in iced. +- **Binary discovery:** look up `pixelpass` on `$PATH`; allow a config override + for a non-standard location. Absence is a normal, handled state, not an error. + +## Open questions for implementation + +- **Concurrency / who can share:** one sharer at a time, or multiple + simultaneous shares (N independent host processes + N offers + a list UI)? +- **Subprocess supervision:** how peerspeak tracks child lifetime, surfaces + `viewer_refused` / capture failures, and cleans up on room-leave or crash + (pixelpass has `--repair` for orphaned PipeWire state). +- **Offer lifecycle on the gossip plane:** late joiners — do we re-announce + active shares to someone who joins mid-share? +- **UI placement:** where the Share button + incoming-offer indicators live + across the three room layouts (3-Column / Bottom Dock / Drawer). + +## Build sketch (peerspeak side, for next session) + +1. New gossip message variant(s): `ScreenShareOffer`/`ScreenShareEnded` in the + gossip wire type; bump/relax parsing compatibly. +2. A `screenshare` subprocess supervisor module (spawn host, parse JSON stdout, + track child, kill on stop) — keep the JSON-parsing a pure testable seam. +3. `AppMessage` + state: `StartScreenShare`, `StopScreenShare`, + `ViewShare(EndpointId)`, active-share map, "pixelpass present?" probe. +4. UI: Share button + per-peer "View" affordance; graceful disabled state when + the binary is absent.