# Screen-share integration (peerspeak ↔ pixelpass) Design decisions for surfacing pixelpass screen-sharing from inside a peerspeak room. Locked 2026-06-06; **implemented 2026-06-06** (`src/screenshare/mod.rs` + presence-borne ticket; tests-green, the 2-machine gossip path not yet field-verified). peerspeak owns voice, pixelpass owns pixels — this doc is the seam between them. ## Open questions — resolved (Discord-modelled) - **Concurrency:** multiple simultaneous sharers, Discord-style. Sharing rides *presence* (`PeerState.sharing: Option`), so each sharer shows one offer; no artificial one-at-a-time cap. - **Late joiners:** yes — because sharing is presence, the existing gossip re-announce delivers an in-progress offer to a new joiner for free (no separate `ScreenShareOffer` message was needed; the doc's sketch is superseded by the presence-field approach). - **Viewer limit:** not overridden — pixelpass bandwidth-measures its own safe cap; a full host's `viewer_refused` surfaces as a UI error. - **UI placement:** per-peer card (🔴 Live + 👁 Watch on the sharer's card; 🔴 Sharing badge + Stop on the self card) — the Discord "Live badge" model. - **Player:** viewer parses `connected{url}` and opens mpv (vlc fallback), mirroring pixelpass's own low-latency invocation. ## 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.