X11 full-desktop capture used `ximagesrc use-damage=false`, which copies
the whole root window every frame. On servers without working MIT-SHM
(and CPU-bound everywhere else) this collapses to ~1 fps — a field test
over an xlibre host played back at roughly one frame per minute. Default
to `use-damage=true` (XDamage re-grabs only changed regions); keep
`PIXELPASS_X11_NO_DAMAGE=1` as an escape hatch for driver artifacts.
Also drop `--untimed` from both mpv invocations (viewer banner + the
interactive launcher). `--untimed` displays each frame as it decodes and
ignores audio timestamps, which drifts a shared *video* progressively
out of sync with its audio. Pacing to the audio clock keeps A/V synced
at a negligible latency cost.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Found in a wider bug audit of the streaming/process-management code.
- Viewer ctrl-c/SIGINT was ignored mid-stream: viewer::run raced the
cancel token only against listener.accept(), not the bridge itself, so
once the local player connected nothing checked it. CLI needed a second
ctrl-c to quit and a GUI "Disconnect" only took effect via the child's 2s
SIGKILL backstop (and the host saw the viewer ~2s longer). Now races the
bridge against cancel, mirroring the host's handle_peer. (viewer/mod.rs)
- Wayland portal pipewire fd leaked on a capture-setup error: wayland::start
into_raw_fd'd the fd and relied on pipeline::spawn's after_spawn hook to
close it, but setup_audio/gst-spawn can ?-return before the hook runs,
leaking the fd per failed attempt. Now the OwnedFd is moved into the hook,
so it's closed whether the hook runs or (on early error) the unused closure
is dropped. (host/wayland.rs)
- Detached players (mpv/vlc) zombied under the long-lived GUI: spawn_detached
dropped the std Child, which has no orphan reaping, so each closed player
left a <defunct> entry until the GUI exited. Now a detached thread wait()s
it; the setsid'd player still survives a parent exit (init reaps it then).
A double-fork was avoided deliberately — fork(2) + non-trivial work in this
multithreaded process is unsound. (common/process.rs)
47 gui / 8 headless tests pass, clippy + fmt clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both host and viewer hardcoded presets::N0, pinning every session to the
bundled relays (which on iroh rc.0 are the canary-grade defaults). Add a
shared common::endpoint::bind() that keeps N0's DNS discovery + crypto but
swaps in a RelayMode::Custom single-relay map when --relay (or the
PIXELPASS_RELAY env var, so GUI children inherit it) is set.
Lets users point at a self-hosted relay or staging today; the production
relays (*.relay.iroh.network) speak a newer protocol that rc.0 rejects
("invalid iroh-relay version header"), so they only become usable — and
the default — after an iroh GA bump. Verified: override connects cleanly
through staging; bad URLs are rejected before any network work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
open_bi/bind/local_addr/accept all `?`-propagated straight out of run(),
skipping the endpoint.close().await at the end and leaking the iroh
Endpoint on any post-connect error. Wrap the post-connect body in one
block whose result is captured, then close unconditionally — matching
the explicit-close idiom of the connect-phase select arms.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
endpoint.connect() has no built-in deadline, so an offline host, a stale
share code, or an unreachable relay left the viewer spinning silently with
no feedback — surfacing in the GUI as a permanent "Connecting…" with no
error. Wrap the connect in a 15s tokio::time::timeout (matching the host's
online() cap) and race it against ctrl-c, bailing with an actionable
message. The error reaches stderr, so the GUI's ChildProc stderr-tail
path renders it on the viewer screen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds common/output.rs: a process-global JSON-lines emitter for
non-interactive front-ends. With --output json, host and viewer emit one
JSON object per line on stdout (ticket, host_info, viewer_count, capture
start/stop, viewer_refused, connected), flushed per line; the human banner
and tracing logs stay on stderr so the two never interleave. No-op when the
flag is absent, so call sites emit unconditionally.
This is the shell-out counterpart to an in-process event channel: the
upcoming --gui front-end re-execs this binary as `pixelpass --host
--output json` and parses these lines to drive its window. serde_json was
already in the tree from the bandwidth pre-flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Software H.264 decode at 1080p struggled on a low-power viewer (Intel
UHD 620 laptop on battery / power-save governor), surfacing as choppy
motion and A-V drift even though the host encode side was clean. mpv's
auto hwdec picks vaapi/nvdec/etc. when available and silently falls
back to software when not, so the default is strictly safer.
Applied to both the headless banner recipe and the interactive
player-picker spawn args.
Bare `pixelpass` now opens a dialoguer-driven Host/View menu instead of
going straight to host mode. Host path copies the ticket to the system
clipboard via arboard with silent print-only fallback. View path
prompts for the ticket, then after the local listener binds prompts
mpv-vs-VLC and spawns it detached (setsid + null stdio) so the player
survives pixelpass exiting.
Headless invocations (`pixelpass <ticket>`, `pixelpass --repair`)
unchanged. Per spec at ~/Documents/pixelpass-interactive-mode-spec.md.
Symptom: cross-machine streams drifted ~6-9 seconds per minute. mpv
showed continuous "Audio device underrun detected" with video packets
piling up in its demuxer queue while audio queue stayed at 0.
Root causes:
- pipewiresrc captured at the host monitor's refresh rate (180Hz),
not at the configured framerate. The encoder + mux produced 6x more
frames per wallclock second than mpv could consume at realtime,
burying audio packet density in mpv's demuxer queue.
- `--profile=low-latency` sets `audio-buffer=0`, which is too
aggressive — any sub-millisecond network jitter immediately
starves the audio device. Underruns slowed mpv's audio clock, and
with `video-sync=audio` (also from low-latency profile) video
followed.
Fixes:
- `videorate ! video/x-raw,framerate=<fps>/1` after pipewiresrc to
cap input to the requested rate deterministically.
- mpv command grows `--audio-buffer=0.2 --demuxer-max-bytes=2M
--demuxer-readahead-secs=0.5`: small audio buffer to absorb network
jitter, demuxer cap to prevent runaway buildup.
A leaky-queues attempt landed and was reverted in the same commit —
it removed backpressure without addressing the root cause and made
things worse.
Verified cross-machine 6m51s: drift held at ~1s floor, zero audio
underruns, perfect A-V sync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the Wayland host pipeline from plan §4.5:
ashpd ScreenCast portal
-> CreateSession + SelectSources + Start + OpenPipeWireRemote
-> (pipewire fd, node_id, width, height)
gst-launch-1.0 pipewiresrc fd=N path=NODE_ID ! videoconvert
! video/x-raw,format=NV12 ! fdsink fd=1
ffmpeg
-f rawvideo -pix_fmt nv12 -video_size WxH -i pipe:0
-f pulse -i default
-c:v libx264 -preset ultrafast -tune zerolatency
-c:a aac -f mpegts -listen 1 http://127.0.0.1:<rand>
Phase 1 ships software x264 per plan §7; VAAPI is Phase 2.
src/host/wayland.rs is the new module. capture.rs becomes a thin
dispatcher with a CaptureHandle enum (Wayland today, X11 next).
host/mod.rs swaps the 150ms sleep for a poll-until-listener-ready
helper, and calls handle.shutdown().await for an orderly SIGTERM /
1s grace / SIGKILL teardown. The Drop impl is the panic backstop.
The pipewire fd handoff clears CLOEXEC before gst-launch spawn and
closes the parent's copy of the raw fd after the child has it.
Also deletes the empty src/host/tunnel.rs and src/viewer/tunnel.rs
placeholder files — the generic bridge in common/tunnel.rs is doing
the work, and there's no host- or viewer-specific tunnel concern
worth a module yet.