VLC parses Content-Type before invoking the demuxer chain. With
video/mp2t it commits to demux="ts" by MIME alone, bypassing
byte-probing; when the ts demuxer's Open fails on the live HTTP stream
("no demux modules matched"), the input never opens. mpv probes
regardless of Content-Type.
Reproduced deterministically with a Python shim that mimics our
response headers byte-for-byte: only the Content-Type matters.
Changing it to application/octet-stream (or any non-video MIME, or
omitting the header) makes VLC fall back to byte-probing, which
finds the TS sync pattern and opens cleanly. mpv unaffected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pixelpass
P2P screen sharing CLI for Linux. Single binary, hole-punched over iroh — 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 <ticket>)
Not yet working:
- X11 capture (stubbed, returns an error)
- Per-app audio routing (
--app <name>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)
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
# 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) orintel-vaapi-driver(older) - NVIDIA:
libva-nvidia-driver(untested)
- AMD:
vainfofromlibva-utilsshould 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) orvlc - PipeWire (for screencast portal + audio capture)
On Arch / CachyOS:
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
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 1is 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
.soon disk. mpv's--hwdec=autosilently falls back to software decode otherwise, which then chokes on a low-power viewer.
License
MIT OR Apache-2.0, your pick.