feat(host): X11 capture backend + shared pipeline extraction

Extract the display-agnostic encode/mux tail out of wayland.rs into a new
host/pipeline.rs: CaptureHandle + lifecycle, audio routing setup, the gst
arg builder, the spawn, and Serve::bind now live there. Backends supply
only their video-source element args plus a post-spawn hook (Wayland uses
it to close its leaked pipewire fd; X11 passes a no-op). capture.rs
collapses to a thin dispatcher; its CaptureHandle enum is gone.

Add host/x11.rs: ximagesrc (use-damage=false show-pointer=true), whole
root window by default or a single window via --window (xwininfo
click-picker → xid). x11rb reads geometry for an info log, justifying the
previously-vestigial dep. No portal, no fd dance — capture starts
silently when the first viewer connects (the ticket is the access
control). Viewer is display-agnostic and unchanged.

Wire --no-hwencode for real (was a no-op): the shared tail now selects
x264enc(tune=zerolatency,ultrafast)/I420 vs vah264enc/NV12 and switches
the videoconvert target format to match. Applies to both backends.

deps.rs: check_host_binaries now takes &HostOpts and checks shared
elements for both backends, encoder by --no-hwencode, source per backend
(pipewiresrc/ximagesrc), and xwininfo only when X11 + --window. Install
hints added for x264enc, ximagesrc, xwininfo.

Verified: warning-free build; smoke test still passes (tail unchanged);
ximagesrc + both encoder tails produce mpv-decodable H.264 against an
Xwayland root. Interactive cross-machine end-to-end pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 20:48:50 -04:00
co-authored by Claude Opus 4.7
parent 0c9d8eb9f9
commit cd127a9704
7 changed files with 474 additions and 247 deletions
+10 -30
View File
@@ -1,40 +1,20 @@
//! Capture dispatcher. Selects the per-display-server pipeline and returns its
//! [`CaptureHandle`]. Each backend owns its own children and tears them down
//! via its own Drop / shutdown.
//! Capture dispatcher. Picks the per-display-server source backend and returns
//! the shared [`pipeline::CaptureHandle`]. The handle itself, its teardown, and
//! the whole encode/serve tail are display-agnostic and live in
//! [`super::pipeline`]; the backends differ only in how they obtain a video
//! source element.
use anyhow::{Result, bail};
use anyhow::Result;
use crate::cli::HostOpts;
use crate::common::display::DisplayServer;
use crate::host::wayland;
pub enum CaptureHandle {
Wayland(wayland::CaptureHandle),
}
impl CaptureHandle {
pub fn local_port(&self) -> u16 {
match self {
CaptureHandle::Wayland(h) => h.local_port(),
}
}
pub async fn shutdown(self) {
match self {
CaptureHandle::Wayland(h) => h.shutdown().await,
}
}
}
use crate::host::pipeline::CaptureHandle;
use crate::host::{wayland, x11};
pub async fn spawn(display: DisplayServer, opts: &HostOpts) -> Result<CaptureHandle> {
match display {
DisplayServer::Wayland => {
let h = wayland::start(opts).await?;
Ok(CaptureHandle::Wayland(h))
}
DisplayServer::X11 => {
bail!("X11 capture pipeline not yet implemented (Phase 2 follow-up)");
}
DisplayServer::Wayland => wayland::start(opts).await,
DisplayServer::X11 => x11::start(opts).await,
DisplayServer::Unknown => unreachable!("caller guarantees display != Unknown"),
}
}