mollusk ffe5a90686 multi-viewer: broadcast fanout + supervisor lifecycle
One gst capture pipeline now fans out to N concurrent viewers via a
tokio::sync::broadcast<Arc<Vec<u8>>>. The HTTP listener accepts forever;
each accepted connection spawns a sender task draining its own
broadcast::Receiver. Slow consumers see Lagged and skip ahead — MPEG-TS
resyncs at the next keyframe.

Host runtime is now lazy + sticky: a supervisor task owns the capture
handle and viewer count. First viewer triggers capture::spawn; last
viewer triggers shutdown. Subsequent reconnects re-trigger the portal
dialog as expected. --max-viewers (default 2) caps concurrent viewers;
additional connections get a "host is full" refusal and are dropped.

Banner updated to reflect the new lifecycle and viewer cap.

NOT YET RUNTIME-VERIFIED. cargo build is clean and the pipeline-level
smoke test still passes, but the multi-viewer behavior (cap enforcement,
lazy-sticky restart, concurrent fanout) requires manual end-to-end
testing with the portal dialog + multiple mpv instances.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:11:43 -04:00

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)
  • --repair (PipeWire orphan cleanup) is a stub

Quick start

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) 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:

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

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.

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.
  • 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.

S
Description
No description provided
Readme 368 KiB
Languages
Rust 97.6%
Shell 2.4%