Live medium-quality stream errored with "negotiation problem" on the host and rendered a squashed, garbled picture in the viewer. Two causes, both from inserting videoscale before videoconvert with PAR+range caps: - videoscale was scaling pipewiresrc's raw output directly. The portal source's format/memory (e.g. DMABuf) isn't something software videoscale negotiates — the original pipeline always fed pipewiresrc through videoconvert first. Move videoscale *after* videoconvert so it operates on system-memory NV12/I420. - `pixel-aspect-ratio=1/1` + a width range over-constrained negotiation and risked a non-square-PAR / distorted result. Instead compute an exact even WxH from the known source dimensions (Wayland: portal size; X11: root/window geometry), preserving aspect, and pin it fully in the caps. This is also downscale-only now — a source already at/below the target height is left native instead of upscaled. Unknown dims (rare X11 geometry failure) fall back to the height-only + square-pixel + even width-range negotiation. source_dims threaded through pipeline::spawn from both backends. Smoke test updated to mirror the new ordering (1920x1080 -> 852x480, videoscale after videoconvert) and still asserts an even sub-source width. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
104 lines
4.0 KiB
Rust
104 lines
4.0 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
|
|
}
|
|
};
|
|
|
|
let mut source_args = vec![
|
|
"ximagesrc".to_string(),
|
|
// Full frames (no damage regions) to avoid partial-update artifacts;
|
|
// use-damage=true is a later CPU optimization. show-pointer matches
|
|
// Wayland's CursorMode::Embedded.
|
|
"use-damage=false".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))
|
|
}
|
|
}
|
|
}
|