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:
134
src/backend_detect.rs
Normal file
134
src/backend_detect.rs
Normal 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user