c1b21b32c7
Screen-share failures are usually environment gaps, not pixelpass bugs — most often a GPU/driver with no working VA-API H.264 encoder, so the default vah264enc pipeline produces no video and the viewer "can't connect." doctor probes the whole chain and prints one actionable report so a remote tester can read it over a call instead of us guessing from logs, and it validates any X11/Wayland test environment we stand up. Checks (each a ✓/!/✗ line with a distro-aware install hint): - display server (Wayland/X11 + session env), and the X server vendor/ version so an xlibre server is distinguishable from stock Xorg - capture: gst tools + the backend's source element (pipewiresrc/ximagesrc) - encode: hardware H.264 (vah264enc + DRM render node + a VA-API H.264 *encode* entrypoint parsed from vainfo) and the software x264 fallback - mux/audio tail + pactl - viewer player (mpv/vlc) - network: binds a real endpoint and checks relay reachability Unlike deps::check_host_binaries (bails on first miss), doctor runs every check and reports them together. Closes with a specific hosting verdict and exits non-zero on any hard failure so scripts/CI can gate. Pure seams (vainfo entrypoint parse, summary tally, hosting verdict) are unit-tested; deps.rs gained pub(crate) which/gst_element_exists/install-hint/distro helpers so doctor reuses the same package-name knowledge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
227 lines
9.4 KiB
Rust
227 lines
9.4 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")?;
|
|
// videoscale (downscale for the quality presets) lives in plugins-base,
|
|
// the same package the gst tools need, so this rarely fails on its own —
|
|
// but check it for a clear error if a partial install is missing it.
|
|
require_gst_element("videoscale")?;
|
|
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<()> {
|
|
if !gst_element_exists(name) {
|
|
bail!(
|
|
"GStreamer element `{name}` not available.\n{}",
|
|
install_hint_for_gst_element(name)
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Whether a GStreamer element is registered, via `gst-inspect-1.0 --exists`.
|
|
/// Non-bailing counterpart to [`require_gst_element`] for the `doctor` report.
|
|
pub(crate) fn gst_element_exists(name: &str) -> bool {
|
|
Command::new("gst-inspect-1.0")
|
|
.args(["--exists", name])
|
|
.status()
|
|
.map(|s| s.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub(crate) 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
|
|
}
|
|
|
|
pub(crate) 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" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => "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" | "artix" | "garuda") => {
|
|
"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)
|
|
}
|
|
|
|
pub(crate) 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" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => {
|
|
"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)",
|
|
},
|
|
"videoscale" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
|
"gst-plugins-base"
|
|
}
|
|
Some("debian" | "ubuntu" | "pop" | "linuxmint") => "gstreamer1.0-plugins-base",
|
|
Some("fedora" | "nobara") => "gstreamer1-plugins-base",
|
|
Some("opensuse" | "opensuse-tumbleweed" | "opensuse-leap") => "gstreamer-plugins-base",
|
|
_ => "the GStreamer plugins-base set",
|
|
},
|
|
"h264parse" | "mpegtsmux" | "aacparse" => match distro.as_deref() {
|
|
Some("arch" | "cachyos" | "manjaro" | "endeavouros" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => {
|
|
"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" | "artix" | "garuda") => {
|
|
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}")
|
|
}
|
|
|
|
pub(crate) 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
|
|
}
|