# pixelpass P2P screen sharing CLI for Linux. Single binary, hole-punched over [iroh](https://www.iroh.computer/) — no port forwarding, no signup, no server-side accounts. Hardware-encoded H.264 + AAC audio, viewed in mpv or VLC. Built for people who just want to show their screen to a friend without spinning up a Discord call or fighting with NAT. ## Status **v0.1.0** — verified end-to-end on the public internet (LTE relay path, ~2s latency, real carrier-grade NAT) as of 2026-05-20. Working: - Wayland capture via the screencast portal (KDE Plasma 6 confirmed; other Wayland compositors with the portal should work but are untested) - X11 capture via `ximagesrc` (whole screen, or a single window with `--window`); selected automatically, or forced with `--display-server x11` - VAAPI H.264 encode in GStreamer (RDNA3 confirmed; other VAAPI-capable GPUs should work), with a software x264 fallback via `--no-hwencode` - Audio capture of the default sink's monitor, with optional per-app routing (`--app `) - `--repair` cleanup of orphaned PipeWire state left by a crashed host - iroh QUIC bi-stream tunnel, direct-UDP and relay paths both verified - Interactive Host/View menu with clipboard auto-copy and mpv/VLC picker - Headless mode for scripts (`pixelpass `) - Multi-viewer fanout (default 2, configurable via `--max-viewers`; shared gst pipeline, one broadcast channel per host) - First-run upstream bandwidth pre-flight, persisted to `~/.config/pixelpass/config.toml` and used to auto-size the default viewer cap Not yet built (deferred, not blocking): - Per-monitor selection on a multi-monitor X11 host — `ximagesrc` grabs the whole root canvas; single-monitor cropping needs xrandr region coords - `use-damage=true` CPU optimization for the X11 capture path ## Quick start ### Interactive (recommended) ```sh pixelpass ``` On the host machine: pick "Host", share a monitor via the portal dialog, the ticket lands on your clipboard. Send it to your viewer however you like (chat, email, paste in a note). The same ticket works for multiple viewers up to your `--max-viewers` cap. The very first host launch offers a one-time upstream bandwidth test (~5 s, ~5 MB to Cloudflare's open speed-test endpoint) so it can pick a sensible default for the viewer cap. You can skip it and a conservative default (2 viewers) is used; re-run it later with `pixelpass --reconfigure`. On the viewer machine: run `pixelpass`, pick "View", paste the ticket, pick mpv or VLC. The player launches detached and the stream starts. ### Headless ```sh # host: prints a ticket on stdout, waits for a peer pixelpass # viewer: skips the menu pixelpass # then run the printed mpv command in another terminal ``` ## Requirements - Linux (Wayland or X11; the backend is autodetected) - A VAAPI-capable GPU and the right driver: - AMD: `libva-mesa-driver` - Intel: `intel-media-driver` (modern iGPUs) or `intel-vaapi-driver` (older) - NVIDIA: `libva-nvidia-driver` (untested) - `vainfo` from `libva-utils` should list at least one H.264 entrypoint - GStreamer with these plugin packages installed: - `gstreamer`, `gst-plugins-base`, `gst-plugins-good`, `gst-plugins-bad`, `gst-plugins-ugly`, `gst-libav`, `gst-plugin-va`, `gst-plugin-pipewire` - A player: `mpv` (recommended) or `vlc` - If you use VLC, two split plugin packages are also needed on Arch-family distros — the base `vlc` package does not pull them in: - `vlc-plugin-dvb` — provides the MPEG-TS demuxer (`libts_plugin.so`). Without it, VLC can't parse the container. - `vlc-plugin-ffmpeg` — provides the H.264 decoder (`libavcodec_plugin.so`). Without it, VLC parses the container, identifies the codec as H.264, then errors with `Codec h264 ... is not supported`. mpv ships its own decoder stack and doesn't share either dependency. - PipeWire (for screencast portal + audio capture) On Arch / CachyOS / EndeavourOS: ```sh sudo pacman -S gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad \ gst-plugins-ugly gst-libav gst-plugin-va gst-plugin-pipewire \ libva-utils mpv # plus your GPU's VAAPI driver # plus, if you want to use VLC instead of mpv: sudo pacman -S vlc vlc-plugin-dvb vlc-plugin-ffmpeg ``` If the viewer is running on battery, set the CPU governor to performance or balanced — power-saver can choke even hardware-decoded 1080p H.264. ## Build ```sh cargo build --release ./target/release/pixelpass --help ``` `rustc` 1.95+ / edition 2024. ## How it works ``` Host Viewer ──── ────── Wayland portal (ashpd) ──> PipeWire fd ─┐ (X11: ximagesrc, no portal) │ ▼ gst-launch: -> videorate -> vah264enc/x264enc -> h264parse -> mpegtsmux (audio: pulsesrc .monitor -> avenc_aac -> aacparse ─┘) │ stdout ▼ tokio HTTP server (in-process, ~30 lines) │ ▼ iroh QUIC bi-stream (ALPN pixelpass/0) ◄══════════► │ ▼ tokio TcpListener on 127.0.0.1:rand │ ▼ mpv / VLC HTTP client ``` The viewer's player connects to a localhost HTTP server, which is just one end of the iroh tunnel. The host's HTTP server sits on the other end and streams GStreamer's stdout (an MPEG-TS containing H.264 + AAC) through with no demux or remux. iroh handles NAT traversal: direct UDP if hole-punching succeeds, relay path otherwise. Both have been verified end-to-end. ## Why these choices - **iroh over Holesail / dumbpipe / Tailscale**: single Rust dep, no Node runtime, no signup, no daemon — fits the "one self-contained binary" goal. - **GStreamer for capture/encode, not ffmpeg**: stride/format pitfalls when bridging raw video between processes; one in-process pipeline sidesteps them. - **In-process Rust HTTP server, not ffmpeg-as-server**: ffmpeg's `-listen 1` is one-shot and probe-budget-sensitive; the Rust task is pure passthrough with no codec assumptions. - **MPEG-TS over fragmented MP4**: every player on Linux handles it out of the box. AV1-in-MPEG-TS was tried and is unworkable through libavformat — if AV1 ever comes back, it has to ride a different container. - **VAAPI H.264 over x264**: ~5% of one CPU core instead of ~50% on the host's hardware. ## Configuration `pixelpass` keeps a small TOML config at `~/.config/pixelpass/config.toml` (or the XDG equivalent). Right now it only stores the result of the bandwidth pre-flight: ```toml [bandwidth] status = "measured" # measured | skipped | failed | unmeasured upstream_mbps = 8.78 # safe estimate (raw * 0.8) measured_at = "2026-05-21T20:41:16Z" ``` - `pixelpass --reconfigure` re-runs the test (e.g. after an ISP change). - Deleting the file resets pixelpass to first-run state. - Skip is sticky — once you skip the test, pixelpass won't ask again unless you reconfigure. ## Audio By default pixelpass captures the default sink's monitor — the viewer hears whatever the host hears. `--app ` narrows that to a single application: pixelpass creates a per-PID null-sink and uses libpipewire to reroute matching `Stream/Output/Audio` nodes (by `application.name`) into it, so the viewer hears just that app instead of the whole desktop. In the interactive menu you can pick the app from a list of what's currently playing. Microphone capture is intentionally out of scope — pixelpass is a screen-share tool meant to be paired with a dedicated voice app (Mumble, TeamSpeak, Discord, …) for two-way talk. If a host crashes mid-session it can leave orphaned `pixelpass_capture_*` null-sinks and their paired loopbacks loaded in PipeWire. Run `pixelpass --repair` to unload them and exit. ## Display server The capture backend is autodetected from the environment (`WAYLAND_DISPLAY` → Wayland, else `DISPLAY` → X11, else `XDG_SESSION_TYPE`). Override it with `--display-server wayland|x11` — for example to force the X11 path while running inside a Wayland session (an Xwayland or Xephyr `DISPLAY`). - **Wayland** goes through the screencast portal: a "Share Screen?" dialog appears when the first viewer connects, and you pick the monitor (or window, with `--window`) there. - **X11** uses `ximagesrc` and starts silently when the first viewer connects — the ticket is the access control, there's no portal gate. `--window` runs an `xwininfo` picker (click the window you want to share); without it the whole root window is captured. Encoding is hardware VAAPI (`vah264enc`) by default. `--no-hwencode` switches to software x264 (`x264enc tune=zerolatency`) for hosts without a working VAAPI H.264 entrypoint — higher CPU, no GPU needed. This applies to both backends. ## Multi-viewer One gst capture pipeline fans out to N concurrent viewers via a `tokio::sync::broadcast` channel. The same ticket is reusable: as long as a viewer is connected, capture stays alive; when the last one leaves, the pipeline tears down and the portal stops streaming. A new viewer connecting after that re-triggers the portal dialog. Capacity is bounded by upstream bandwidth (each viewer is its own encrypted egress). The default cap comes from the bandwidth pre-flight result; `--max-viewers ` overrides it. When the cap is hit, additional connections are politely refused with a "host is full" message and the host keeps running. For more viewers, drop the per-viewer bitrate: e.g. `pixelpass --bitrate 2500 --max-viewers 4` fits four 2.5 Mbps streams in roughly 12 Mbps of upstream. ## Known limitations and gotchas - **VLC needs `vlc-plugin-dvb` and `vlc-plugin-ffmpeg`** on Arch-family distros — the base `vlc` package doesn't pull these in, and missing either one breaks playback (the first kills the demuxer, the second kills the H.264 decoder). pixelpass warns at player-launch time if either plugin isn't on disk. mpv doesn't share these dependencies. - **Audio echo** if the host plays the stream through speakers and captures system audio — expected, the mic / monitor picks up the playback. Headphones bypass it. - **Late joiners see ~2 s of garbage** before the next keyframe lets their decoder lock. Expected behavior, not a bug. - **VAAPI driver must be package-tracked**, not an orphaned `.so` on disk. mpv's `--hwdec=auto` silently falls back to software decode otherwise, which then chokes on a low-power viewer. ## License MIT OR Apache-2.0, your pick.