Files
pixelpass/README.md
T
mollusk 8674f907f2 docs: sync README status with shipped audio + repair work
Per-app audio routing (--app), mic mixing (--mic), and --repair all
landed in recent commits but the README still listed the first and last
as stubs. Move them to Working, drop them from "Not yet working" (X11
capture is now the only remaining stub), and add an Audio section
documenting --app/--mic/--repair.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 15:37:23 -04:00

241 lines
9.6 KiB
Markdown

# 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, with optional per-app
routing (`--app <name>`) and microphone mixing (`--mic`)
- `--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 <ticket>`)
- 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 working:
- X11 capture (stubbed, returns an error — Phase 2 follow-up)
## 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 <ticket>
# 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`
- 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
gst-launch: pipewiresrc -> videorate -> vah264enc ->
h264parse -> mpegtsmux
(audio: pulsesrc <sink>.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.
## 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. Two flags adjust this:
- `--app <name>` routes only a single application's audio. 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.
- `--mic` mixes the default microphone source into the stream alongside
system audio.
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.
## 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 <N>` 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.