cd127a9704
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>
185 lines
7.8 KiB
Rust
185 lines
7.8 KiB
Rust
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, 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(())
|
|
}
|
|
|
|
fn require(bin: &str) -> Result<PathBuf> {
|
|
match which(bin) {
|
|
Some(p) => Ok(p),
|
|
None => bail!("`{bin}` not found on PATH.\n{}", install_hint_for_bin(bin)),
|
|
}
|
|
}
|
|
|
|
fn require_gst_element(name: &str) -> Result<()> {
|
|
let ok = Command::new("gst-inspect-1.0")
|
|
.args(["--exists", name])
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false);
|
|
if !ok {
|
|
bail!(
|
|
"GStreamer element `{name}` not available.\n{}",
|
|
install_hint_for_gst_element(name)
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn which(bin: &str) -> Option<PathBuf> {
|
|
let path = std::env::var_os("PATH")?;
|
|
for dir in std::env::split_paths(&path) {
|
|
let candidate = dir.join(bin);
|
|
if candidate.is_file() {
|
|
return Some(candidate);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn install_hint_for_bin(bin: &str) -> String {
|
|
let distro = detect_distro();
|
|
let pkg = match bin {
|
|
"gst-launch-1.0" | "gst-inspect-1.0" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gstreamer gst-plugins-base",
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-tools",
|
|
Some("fedora" | "nobara") => "gstreamer1 gstreamer1-plugins-base-tools",
|
|
_ => "gstreamer + tools",
|
|
},
|
|
"pactl" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "libpulse",
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "pulseaudio-utils",
|
|
Some("fedora" | "nobara") => "pulseaudio-utils",
|
|
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)
|
|
}
|
|
|
|
fn install_hint_for_gst_element(name: &str) -> String {
|
|
let distro = detect_distro();
|
|
let pkg = match name {
|
|
"pipewiresrc" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-plugin-pipewire",
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-pipewire",
|
|
Some("fedora" | "nobara") => "pipewire-gstreamer",
|
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "pipewire-gstreamer",
|
|
_ => "the GStreamer PipeWire plugin",
|
|
},
|
|
"vah264enc" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-plugin-va",
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-bad",
|
|
Some("fedora" | "nobara") => "gstreamer1-plugins-bad-free",
|
|
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",
|
|
Some("fedora" | "nobara") => "gstreamer1-plugins-bad-free",
|
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-bad",
|
|
_ => "the GStreamer plugins-bad set",
|
|
},
|
|
"pulsesrc" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-plugins-good",
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-pulseaudio",
|
|
Some("fedora" | "nobara") => "gstreamer1-plugins-good",
|
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-good",
|
|
_ => "the GStreamer PulseAudio plugin",
|
|
},
|
|
"avenc_aac" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => "gst-libav",
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-libav",
|
|
Some("fedora" | "nobara") => "gstreamer1-libav",
|
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-libav",
|
|
_ => "the GStreamer libav (avenc) plugin",
|
|
},
|
|
_ => name,
|
|
};
|
|
install_command(&distro, pkg)
|
|
}
|
|
|
|
fn install_command(distro: &Option<String>, pkg: &str) -> String {
|
|
let cmd = match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros") => format!("sudo pacman -S {pkg}"),
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => format!("sudo apt install {pkg}"),
|
|
Some("fedora" | "nobara") => format!("sudo dnf install {pkg}"),
|
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => format!("sudo zypper install {pkg}"),
|
|
_ => format!("install the `{pkg}` package via your distro's package manager"),
|
|
};
|
|
format!("Install hint: {cmd}")
|
|
}
|
|
|
|
fn detect_distro() -> Option<String> {
|
|
let contents = std::fs::read_to_string("/etc/os-release").ok()?;
|
|
for line in contents.lines() {
|
|
if let Some(rest) = line.strip_prefix("ID=") {
|
|
return Some(rest.trim_matches('"').to_lowercase());
|
|
}
|
|
}
|
|
None
|
|
}
|