Phase 1 foundation: CLI, iroh tunnel, lazy capture wiring

Scaffolding for PixelPass per ~/Documents/p2p-screenshare-plan.md §7
"Phase 1 — MVP". The full QUIC tunnel handshake works end-to-end
(verified locally: host generates ticket, viewer dials through iroh's
relay, open_bi succeeds, lazy capture is wired correctly).

What's implemented:

- Cargo project with deps locked: iroh 1.0.0-rc.0, iroh-tickets,
  tokio, clap, ashpd, pipewire-rs, x11rb, ashpd, anyhow, thiserror,
  tracing, nix, directories, uuid.
- src/cli.rs: complete clap surface per plan §6 (--window, --app,
  --mic, --display-server, --bitrate, --framerate, --no-hwencode,
  --low-latency, --port, --verbose, --repair).
- Mode dispatch in main.rs: EndpointTicket::from_str is the
  authoritative check; no regex / heuristics.
- common/display.rs: WAYLAND_DISPLAY → DISPLAY → XDG_SESSION_TYPE
  precedence with --display-server override.
- common/deps.rs: per-distro install hints (pacman/apt/dnf/zypper)
  parsing /etc/os-release.
- common/alpn.rs: ALPN = b"pixelpass/0".
- common/tunnel.rs: generic bidirectional bridge between an iroh
  bi-stream and any AsyncRead+AsyncWrite (typically a TCP socket).
- common/signal.rs: ctrl-c -> CancellationToken; second ctrl-c hard
  exit.
- host/mod.rs: build Endpoint, generate ticket, print banner, await
  first peer (lazy — no ffmpeg until peer connects), accept_bi,
  spawn capture, bridge to localhost ffmpeg HTTP listener.
- host/capture.rs: stub returning Phase-2 error; the place X11
  x11grab and Wayland ashpd+gst pipelines will land.
- viewer/mod.rs: Endpoint, connect with ALPN, open_bi, TcpListener
  on 127.0.0.1, print copy-ready mpv/vlc commands, bridge.
- repair.rs: stub for --repair PipeWire scan.

iroh 1.0-rc renamed Node* -> Endpoint* and moved EndpointTicket into
a sibling crate (iroh-tickets); no design impact. Plan still locked.
This commit is contained in:
2026-05-15 15:13:28 -04:00
commit 6ad92081aa
17 changed files with 5886 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
//! Capture-and-encode pipeline.
//!
//! Phase 1 stub: the actual ffmpeg / gst-launch spawn lives here. Returns a
//! [`CaptureHandle`] whose `Drop` impl kills the children so a panic on the
//! bridge side can't leave a black-hole encoder running.
//!
//! Phase 2 will fill in X11 (x11grab) and Wayland (ashpd portal +
//! pipewiresrc → fdsink | ffmpeg) bodies; per-app PipeWire routing hangs off
//! the Wayland branch.
use anyhow::{Result, bail};
use crate::cli::HostOpts;
use crate::common::display::DisplayServer;
pub struct CaptureHandle {
port: u16,
// Future: child handles, PipewireState, etc.
}
impl CaptureHandle {
pub fn local_port(&self) -> u16 {
self.port
}
}
impl Drop for CaptureHandle {
fn drop(&mut self) {
// Future: SIGTERM children with a short grace period, then SIGKILL.
// Tear down any PipeWire null sinks / loopbacks created in spawn().
}
}
pub async fn spawn(display: DisplayServer, _opts: &HostOpts) -> Result<CaptureHandle> {
match display {
DisplayServer::X11 => {
bail!("X11 capture pipeline not yet implemented (Phase 2)");
}
DisplayServer::Wayland => {
bail!("Wayland capture pipeline not yet implemented (Phase 2)");
}
DisplayServer::Unknown => unreachable!("caller guarantees display != Unknown"),
}
}