feat: add KWin/KDE Plasma screen capture via xdg-desktop-portal ScreenCast + PipeWire

Add a second capture backend for compositors without wlr-screencopy
(KWin, GNOME, etc.) using the xdg-desktop-portal ScreenCast interface
and PipeWire DMA-BUF streaming.

New files:
- src/backend_detect.rs: auto-detect wlr-screencopy vs portal backend
- src/cap_portal.rs: Portal session setup + PipeWire DMA-BUF thread
- src/state_portal.rs: StatePortal encoder pipeline (DMA-BUF → VAAPI)

Changes:
- Cargo.toml: add ashpd 0.13, tokio 1, pipewire 0.9, libspa 0.9,
  crossbeam-channel 0.5
- src/args.rs: add --backend CLI flag
- src/avhw.rs: extract create_encoder() from inline State code
- src/main.rs: route to portal or wlr-screencopy based on backend
- src/state.rs: fix params.destroy() on dup failure, cleanup
  in_flight_surface on copy fail, use create_encoder()
- tests/integration_test.rs: add --backend flag tests
This commit is contained in:
dailz
2026-05-11 08:49:08 +08:00
parent 2972216a02
commit d7fbb5256c
12 changed files with 2198 additions and 79 deletions

134
src/backend_detect.rs Normal file
View File

@@ -0,0 +1,134 @@
use anyhow::Result;
use wayland_client::globals::registry_queue_init;
use wayland_client::globals::GlobalListContents;
use wayland_client::protocol::wl_registry::{Event, WlRegistry};
use wayland_client::{Connection, Dispatch, QueueHandle};
use crate::args::Args;
/// Capture backend to use for screen capture.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureBackend {
/// wlroots wlr-screencopy protocol (Sway, Hyprland, etc.)
WlrScreencopy,
/// xdg-desktop-portal with PipeWire (KWin/KDE, GNOME, etc.)
PortalPipeWire,
}
/// Minimal dispatch type for listing Wayland globals during backend detection.
struct RegistryLs;
impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
fn event(
_state: &mut Self,
_registry: &WlRegistry,
_event: Event,
_data: &GlobalListContents,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
/// Detect which capture backend to use.
///
/// Priority:
/// 1. Explicit `--backend` override from CLI args
/// 2. Auto-detect by checking for `zwlr_screencopy_manager_v1` in Wayland globals
///
/// The detection Wayland connection is dropped before returning so the actual
/// capture backend can create its own connection without holding two simultaneously.
pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
// 1. Check explicit override
if let Some(ref backend) = args.backend {
return match backend.as_str() {
"portal" => {
tracing::info!("Backend override: Portal/PipeWire");
Ok(CaptureBackend::PortalPipeWire)
}
"screencopy" => {
tracing::info!("Backend override: wlr-screencopy");
Ok(CaptureBackend::WlrScreencopy)
}
other => {
anyhow::bail!(
"Unknown backend '{}'. Use 'screencopy' or 'portal'.",
other
);
}
};
}
// 2. Auto-detect: check if zwlr_screencopy_manager_v1 is available
tracing::info!("Auto-detecting capture backend...");
let conn = Connection::connect_to_env()?;
let (globals, _queue) = registry_queue_init::<RegistryLs>(&conn)?;
let has_screencopy = globals
.contents()
.clone_list()
.iter()
.any(|g| g.interface == "zwlr_screencopy_manager_v1");
// Drop the Wayland connection explicitly before returning.
// The screencopy path creates its own connection. Holding two connections
// simultaneously is wasteful and may cause issues on some compositors.
drop(conn);
if has_screencopy {
tracing::info!("Detected wlr-screencopy support → using WlrScreencopy backend");
Ok(CaptureBackend::WlrScreencopy)
} else {
tracing::info!("No wlr-screencopy support → using Portal/PipeWire backend");
Ok(CaptureBackend::PortalPipeWire)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_args(backend: Option<&str>) -> Args {
Args {
output: "test.mp4".to_string(),
output_name: None,
fps: 30,
codec: "h264".to_string(),
hw_accel: "vaapi".to_string(),
drm_device: None,
bitrate: None,
gop_size: None,
verbose: false,
backend: backend.map(String::from),
port: 0,
}
}
#[test]
fn explicit_portal_backend() {
let args = make_args(Some("portal"));
let result = detect_backend(&args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire);
}
#[test]
fn explicit_screencopy_backend() {
let args = make_args(Some("screencopy"));
let result = detect_backend(&args);
assert!(result.is_ok());
assert_eq!(result.unwrap(), CaptureBackend::WlrScreencopy);
}
#[test]
fn invalid_backend_name_returns_error() {
let args = make_args(Some("magic"));
let result = detect_backend(&args);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Unknown backend 'magic'"),
"Expected error about unknown backend, got: {err}"
);
}
}