# 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) - VAAPI H.264 encode in GStreamer (RDNA3 confirmed; other VAAPI-capable GPUs should work) - Audio capture of the default sink's monitor - 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 `) Not yet working: - X11 capture (stubbed, returns an error) - Per-app audio routing (`--app ` is a flag stub) - Multi-viewer (single viewer per host by design right now) - VLC client renders the H.264 stream as a green screen — mpv works - `--repair` (PipeWire orphan cleanup) is a stub ## 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). 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 session for now; X11 stubbed) - 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` - PipeWire (for screencast portal + audio capture) On Arch / CachyOS: ```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 ``` 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 │ ▼ gst-launch: pipewiresrc -> videorate -> vah264enc -> 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 hardware-encoded 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. ## Known limitations and gotchas - **VLC shows a green screen** against a stream mpv handles correctly. Likely a VLC-specific PCR / PMT / alignment expectation; not yet diagnosed. Use mpv until this is fixed. - **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. - **Single viewer per host** by design right now. Restarting the player against the same URL fails with "connection refused"; restart the viewer too. - **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.