X11 full-desktop capture used `ximagesrc use-damage=false`, which copies the whole root window every frame. On servers without working MIT-SHM (and CPU-bound everywhere else) this collapses to ~1 fps — a field test over an xlibre host played back at roughly one frame per minute. Default to `use-damage=true` (XDamage re-grabs only changed regions); keep `PIXELPASS_X11_NO_DAMAGE=1` as an escape hatch for driver artifacts. Also drop `--untimed` from both mpv invocations (viewer banner + the interactive launcher). `--untimed` displays each frame as it decodes and ignores audio timestamps, which drifts a shared *video* progressively out of sync with its audio. Pacing to the audio clock keeps A/V synced at a negligible latency cost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
4.5 KiB
Rust
114 lines
4.5 KiB
Rust
//! X11 capture: `ximagesrc` → the shared encode/mux tail in [`super::pipeline`].
|
|
//! Unlike Wayland there's no portal and no fd hand-off — `ximagesrc` opens its
|
|
//! own X connection from `$DISPLAY`. The whole root window is captured by
|
|
//! default; `--window` resolves a single window's XID via an `xwininfo`
|
|
//! click-picker. The ticket is the access control, so capture starts silently
|
|
//! when the first viewer connects (no host-side consent prompt).
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use tokio::process::Command;
|
|
use x11rb::connection::Connection;
|
|
use x11rb::protocol::xproto::ConnectionExt;
|
|
|
|
use super::pipeline::{self, CaptureHandle};
|
|
use super::quality::EffectiveQuality;
|
|
use crate::cli::HostOpts;
|
|
|
|
pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result<CaptureHandle> {
|
|
let xid = if opts.window {
|
|
Some(pick_window().await?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Geometry mirrors Wayland's portal-handshake log line and feeds the
|
|
// downscale presets (so they can compute an exact target size). A failure
|
|
// here shouldn't abort capture — ximagesrc will surface a real error if the
|
|
// X connection is genuinely unusable, and the scaler falls back to a
|
|
// height-only negotiation when dims are unknown.
|
|
let source_dims = match read_geometry(xid) {
|
|
Ok((w, h)) => {
|
|
tracing::info!(width = w, height = h, xid = ?xid, "X11 capture geometry");
|
|
Some((w as u32, h as u32))
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("could not read X11 geometry (capture will still try): {e:#}");
|
|
None
|
|
}
|
|
};
|
|
|
|
// XDamage capture (`use-damage=true`) only re-grabs changed screen
|
|
// regions instead of copying the whole root window every frame. On a busy
|
|
// desktop that is the difference between a usable framerate and ~1 fps —
|
|
// `use-damage=false` does a full XGetImage per frame, which collapses on
|
|
// servers without working MIT-SHM (and pins the CPU everywhere else).
|
|
// Kept as the default; `PIXELPASS_X11_NO_DAMAGE=1` restores full-frame
|
|
// capture if a driver produces partial-update artifacts with damage on.
|
|
let use_damage = if std::env::var_os("PIXELPASS_X11_NO_DAMAGE").is_some() {
|
|
"use-damage=false"
|
|
} else {
|
|
"use-damage=true"
|
|
};
|
|
let mut source_args = vec![
|
|
"ximagesrc".to_string(),
|
|
// show-pointer matches Wayland's CursorMode::Embedded.
|
|
use_damage.to_string(),
|
|
"show-pointer=true".to_string(),
|
|
];
|
|
if let Some(xid) = xid {
|
|
source_args.push(format!("xid={xid}"));
|
|
}
|
|
|
|
// X11 has no leaked fd to clean up, so the post-spawn hook is a no-op.
|
|
pipeline::spawn(opts, quality, source_dims, source_args, || {}).await
|
|
}
|
|
|
|
/// Run `xwininfo` and let the user click the window they want to share, then
|
|
/// parse the `Window id: 0x…` line out of its output. Returns the numeric XID.
|
|
async fn pick_window() -> Result<u32> {
|
|
eprintln!("[pixelpass] click the window you want to share…");
|
|
let output = Command::new("xwininfo")
|
|
.output()
|
|
.await
|
|
.context("failed to run `xwininfo` (install xorg-xwininfo)")?;
|
|
if !output.status.success() {
|
|
bail!(
|
|
"xwininfo failed: {}",
|
|
String::from_utf8_lossy(&output.stderr).trim()
|
|
);
|
|
}
|
|
let text = String::from_utf8_lossy(&output.stdout);
|
|
for line in text.lines() {
|
|
// e.g. "xwininfo: Window id: 0x3a00007 \"xterm\""
|
|
if let Some((_, rest)) = line.split_once("Window id: ") {
|
|
let token = rest.split_whitespace().next().unwrap_or("");
|
|
let hex = token.strip_prefix("0x").unwrap_or(token);
|
|
if let Ok(xid) = u32::from_str_radix(hex, 16) {
|
|
return Ok(xid);
|
|
}
|
|
}
|
|
}
|
|
bail!("could not parse a window id from xwininfo output");
|
|
}
|
|
|
|
/// Read pixel dimensions: the selected window's geometry when `--window` was
|
|
/// used, otherwise the root window of the screen named by `$DISPLAY`.
|
|
fn read_geometry(xid: Option<u32>) -> Result<(u16, u16)> {
|
|
let (conn, screen_num) =
|
|
x11rb::connect(None).context("could not connect to the X server (is DISPLAY set?)")?;
|
|
match xid {
|
|
Some(id) => {
|
|
let geo = conn
|
|
.get_geometry(id)
|
|
.context("GetGeometry request failed")?
|
|
.reply()
|
|
.context("GetGeometry reply failed")?;
|
|
Ok((geo.width, geo.height))
|
|
}
|
|
None => {
|
|
let screen = &conn.setup().roots[screen_num];
|
|
Ok((screen.width_in_pixels, screen.height_in_pixels))
|
|
}
|
|
}
|
|
}
|