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 { 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 { 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, 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 { 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 }