feat(host): X11 capture backend + shared pipeline extraction

Extract the display-agnostic encode/mux tail out of wayland.rs into a new
host/pipeline.rs: CaptureHandle + lifecycle, audio routing setup, the gst
arg builder, the spawn, and Serve::bind now live there. Backends supply
only their video-source element args plus a post-spawn hook (Wayland uses
it to close its leaked pipewire fd; X11 passes a no-op). capture.rs
collapses to a thin dispatcher; its CaptureHandle enum is gone.

Add host/x11.rs: ximagesrc (use-damage=false show-pointer=true), whole
root window by default or a single window via --window (xwininfo
click-picker → xid). x11rb reads geometry for an info log, justifying the
previously-vestigial dep. No portal, no fd dance — capture starts
silently when the first viewer connects (the ticket is the access
control). Viewer is display-agnostic and unchanged.

Wire --no-hwencode for real (was a no-op): the shared tail now selects
x264enc(tune=zerolatency,ultrafast)/I420 vs vah264enc/NV12 and switches
the videoconvert target format to match. Applies to both backends.

deps.rs: check_host_binaries now takes &HostOpts and checks shared
elements for both backends, encoder by --no-hwencode, source per backend
(pipewiresrc/ximagesrc), and xwininfo only when X11 + --window. Install
hints added for x264enc, ximagesrc, xwininfo.

Verified: warning-free build; smoke test still passes (tail unchanged);
ximagesrc + both encoder tails produce mpv-decodable H.264 against an
Xwayland root. Interactive cross-machine end-to-end pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:39:16 -04:00
parent 0c9d8eb9f9
commit cd127a9704
7 changed files with 474 additions and 247 deletions
+57 -12
View File
@@ -2,21 +2,45 @@ use anyhow::{Result, bail};
use std::path::PathBuf;
use std::process::Command;
use crate::cli::HostOpts;
use crate::common::display::DisplayServer;
pub fn check_host_binaries(display: DisplayServer) -> Result<()> {
if display == DisplayServer::Wayland {
require("gst-launch-1.0")?;
require("gst-inspect-1.0")?;
require("pactl")?;
require_gst_element("pipewiresrc")?;
require_gst_element("vah264enc")?;
require_gst_element("h264parse")?;
require_gst_element("mpegtsmux")?;
require_gst_element("pulsesrc")?;
require_gst_element("avenc_aac")?;
require_gst_element("aacparse")?;
pub fn check_host_binaries(display: DisplayServer, opts: &HostOpts) -> Result<()> {
// Unknown is handled (and rejected) by the caller; nothing to check here.
if display == DisplayServer::Unknown {
return Ok(());
}
// Shared across both backends: the gst tools, audio routing, and the
// encode/mux tail elements.
require("gst-launch-1.0")?;
require("gst-inspect-1.0")?;
require("pactl")?;
require_gst_element("h264parse")?;
require_gst_element("mpegtsmux")?;
require_gst_element("pulsesrc")?;
require_gst_element("avenc_aac")?;
require_gst_element("aacparse")?;
// Encoder depends on --no-hwencode (software x264 vs hardware VAAPI).
if opts.no_hwencode {
require_gst_element("x264enc")?;
} else {
require_gst_element("vah264enc")?;
}
// Per-backend video source, plus the X11 window-picker when --window is set.
match display {
DisplayServer::Wayland => require_gst_element("pipewiresrc")?,
DisplayServer::X11 => {
require_gst_element("ximagesrc")?;
if opts.window {
require("xwininfo")?;
}
}
DisplayServer::Unknown => unreachable!("early-returned above"),
}
Ok(())
}
@@ -69,6 +93,13 @@ fn install_hint_for_bin(bin: &str) -> String {
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "pulseaudio-utils",
_ => "pulseaudio-utils (provides `pactl`)",
},
"xwininfo" => match distro.as_deref() {
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "xorg-xwininfo",
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "x11-utils",
Some("fedora" | "nobara") => "xorg-x11-utils",
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "xwininfo",
_ => "xwininfo (X11 window-info utility)",
},
_ => bin,
};
install_command(&distro, pkg)
@@ -91,6 +122,20 @@ fn install_hint_for_gst_element(name: &str) -> String {
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-bad",
_ => "the GStreamer VA-API plugin (requires an H.264-capable GPU; almost all modern GPUs)",
},
"x264enc" => match distro.as_deref() {
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-plugins-ugly",
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-ugly",
Some("fedora" | "nobara") => "gstreamer1-plugins-ugly",
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-ugly",
_ => "the GStreamer x264 plugin (plugins-ugly)",
},
"ximagesrc" => match distro.as_deref() {
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-plugins-good",
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-good",
Some("fedora" | "nobara") => "gstreamer1-plugins-good",
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-good",
_ => "the GStreamer X11 plugin (plugins-good)",
},
"h264parse" | "mpegtsmux" | "aacparse" => match distro.as_deref() {
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-plugins-bad",
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-bad",