Files
mollusk 45e5d7ef37 feat(cli): remove --mic (microphone is 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 — it never
mixes a mic. The --mic flag was declared, shown in the host banner, and
documented as working, but was never wired into the gst pipeline (a
no-op). Removed the flag from Cli + HostOpts + into_host_opts, dropped
it from the banner capture summary, and replaced the README's "--mic
mixes the mic" claim with an explicit out-of-scope note.

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

266 lines
11 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)
- 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 <name>`)
- `--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 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 <ticket>
# 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: <source> -> videorate -> vah264enc/x264enc ->
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
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 <name>` 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 <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.