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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 "**<name> is sharing — [View]**".
|
||||||
|
6. Clicking **View** spawns `pixelpass <ticket> --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.
|
||||||
Reference in New Issue
Block a user