Files
pixelpass/src/common/deps.rs
T
mollusk c1b21b32c7 Add pixelpass --doctor environment diagnostic
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>
2026-07-04 03:19:06 -04:00

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
}