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.) /// wlroots 合成器的 wlr-screencopy 协议(适用于 Sway、Hyprland 等) WlrScreencopy, /// xdg-desktop-portal with PipeWire (KWin/KDE, GNOME, etc.) /// XDG 桌面门户 + PipeWire 方式(适用于 KWin/KDE、GNOME 等) PortalPipeWire, } /// Minimal dispatch type for listing Wayland globals during backend detection. /// 用于后端检测期间列举 Wayland 全局对象的最小化分发类型(无需实际处理事件) struct RegistryLs; // 为 RegistryLs 实现 Wayland 注册表事件分发(空实现,仅需类型满足 trait 约束) impl Dispatch for RegistryLs { fn event( _state: &mut Self, _registry: &WlRegistry, _event: Event, _data: &GlobalListContents, _conn: &Connection, _qhandle: &QueueHandle, ) { } } // CAUTION: must NOT use ashpd here — ashpd caches zbus::Connection in a global // OnceLock; if the tokio runtime owning that connection is dropped before // setup_portal() runs, the cached connection becomes dead and hangs forever. fn check_portal_available() -> bool { let rt = match tokio::runtime::Runtime::new() { Ok(rt) => rt, Err(e) => { tracing::warn!("Failed to create tokio runtime for portal check: {e}"); return false; } }; rt.block_on(async { let conn = match zbus::Connection::session().await { Ok(c) => c, Err(e) => { tracing::info!("D-Bus session bus unavailable: {e}"); return false; } }; let inner: zbus::Proxy = match zbus::proxy::Builder::new(&conn) .destination("org.freedesktop.portal.Desktop") .and_then(|b| b.path("/org/freedesktop/portal/desktop")) .and_then(|b| b.interface("org.freedesktop.portal.ScreenCast")) { Ok(b) => match b.build().await { Ok(p) => p, Err(e) => { tracing::info!("Portal ScreenCast interface not available: {e}"); return false; } }, Err(e) => { tracing::info!("Portal ScreenCast proxy build failed: {e}"); return false; } }; let version = match inner.get_property::("version").await { Ok(version) => { tracing::info!("Portal ScreenCast available (version: {version})"); true } Err(e) => { tracing::info!("Portal ScreenCast version query failed: {e}"); false } }; version }) } // 通过 Wayland globals 检测 wlr-screencopy 协议是否可用 fn check_screencopy_available() -> Result { let conn = Connection::connect_to_env()?; let (globals, _queue) = registry_queue_init::(&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. // 显式释放检测用的 Wayland 连接,避免与后续捕获后端同时占用两个连接 drop(conn); Ok(has_screencopy) } /// Detect which capture backend to use. /// 检测应使用哪种屏幕捕获后端 /// /// Priority: /// 优先级: /// 1. Explicit `--backend` override from CLI args /// 用户通过 `--backend` 命令行参数显式指定 /// 2. Auto-detect: check wlr-screencopy (Wayland) and Portal (D-Bus) respectively /// 自动检测:分别通过 Wayland globals 和 D-Bus 检测两个后端 /// /// Both backends are checked independently. If neither is available, returns an error. /// 两个后端独立检测,都不可用时返回错误。 pub fn detect_backend(args: &Args) -> Result { // 1. Check explicit override // 步骤 1:检查用户是否通过命令行参数显式指定了后端 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 both backends independently // 步骤 2:自动检测 — 分别检测两个后端的可用性 tracing::info!("Auto-detecting capture backend..."); // 检测 wlr-screencopy(通过 Wayland globals) let has_screencopy = check_screencopy_available()?; // 检测 Portal(通过 D-Bus) let has_portal = check_portal_available(); // 根据检测结果选择后端,screencopy 优先(性能更好、延迟更低) match (has_screencopy, has_portal) { (true, _) => { tracing::info!("Detected wlr-screencopy support → using WlrScreencopy backend"); Ok(CaptureBackend::WlrScreencopy) } (false, true) => { tracing::info!("No wlr-screencopy, Portal available → using Portal/PipeWire backend"); Ok(CaptureBackend::PortalPipeWire) } (false, false) => { anyhow::bail!( "No supported capture backend found. \ Install a wlroots compositor (for wlr-screencopy) \ or xdg-desktop-portal (for Portal/PipeWire)." ); } } } #[cfg(test)] mod tests { use super::*; // 测试辅助函数:构造指定后端参数的 Args 实例 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, } } // 测试:显式指定 portal 后端 #[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); } // 测试:显式指定 screencopy 后端 #[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}" ); } }