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
+57
View File
@@ -0,0 +1,57 @@
mod tunnel;
use anyhow::Result;
use iroh::Endpoint;
use iroh::endpoint::presets;
use iroh_tickets::endpoint::EndpointTicket;
use tokio::net::TcpListener;
use crate::cli::ViewerOpts;
use crate::common::{alpn::ALPN, signal};
pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
let cancel = signal::install_ctrl_c();
let endpoint = Endpoint::builder(presets::N0)
.alpns(vec![ALPN.to_vec()])
.bind()
.await?;
let addr = ticket.endpoint_addr().clone();
tracing::info!(remote = %addr.id, "connecting to host");
let conn = endpoint.connect(addr, ALPN).await?;
let (quic_send, quic_recv) = conn.open_bi().await?;
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
let port = listener.local_addr()?.port();
print_viewer_banner(port);
let result = tokio::select! {
accepted = listener.accept() => {
let (tcp, peer) = accepted?;
tracing::info!(%peer, "local viewer connected");
crate::common::tunnel::bridge(quic_send, quic_recv, tcp).await
}
_ = cancel.cancelled() => {
tracing::info!("ctrl-c received before local viewer connected");
Ok(())
}
};
endpoint.close().await;
result
}
fn print_viewer_banner(port: u16) {
let url = format!("http://127.0.0.1:{port}");
eprintln!();
eprintln!("┌─ PixelPass · viewer ───────────────────────────────────────");
eprintln!("│ Connected to host. Open the stream in your player:");
eprintln!("");
eprintln!("│ mpv --profile=low-latency --untimed {url}");
eprintln!("│ vlc --network-caching=200 --live-caching=200 {url}");
eprintln!("");
eprintln!("│ Press Ctrl+C to disconnect.");
eprintln!("└────────────────────────────────────────────────────────────");
eprintln!();
}