feat(backend_detect): add portal/screencopy availability pre-checks

Add check_portal_available() and check_screencopy_available() to
probe each backend independently before committing. This enables
smarter fallback logic and better diagnostics when no backend is
found. Includes Chinese documentation comments.
This commit is contained in:
dailz
2026-05-25 08:56:33 +08:00
parent 14d1cf173a
commit 25110e8463

View File

@@ -6,18 +6,25 @@ use wayland_client::{Connection, Dispatch, QueueHandle};
use crate::args::Args; use crate::args::Args;
// 屏幕捕获后端类型
/// Capture backend to use for screen capture. /// Capture backend to use for screen capture.
/// 屏幕捕获后端枚举
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureBackend { pub enum CaptureBackend {
/// wlroots wlr-screencopy protocol (Sway, Hyprland, etc.) /// wlroots wlr-screencopy protocol (Sway, Hyprland, etc.)
/// wlroots 合成器的 wlr-screencopy 协议(适用于 Sway、Hyprland 等)
WlrScreencopy, WlrScreencopy,
/// xdg-desktop-portal with PipeWire (KWin/KDE, GNOME, etc.) /// xdg-desktop-portal with PipeWire (KWin/KDE, GNOME, etc.)
/// XDG 桌面门户 + PipeWire 方式(适用于 KWin/KDE、GNOME 等)
PortalPipeWire, PortalPipeWire,
} }
/// Minimal dispatch type for listing Wayland globals during backend detection. /// Minimal dispatch type for listing Wayland globals during backend detection.
/// 用于后端检测期间列举 Wayland 全局对象的最小化分发类型(无需实际处理事件)
struct RegistryLs; struct RegistryLs;
// 为 RegistryLs 实现 Wayland 注册表事件分发(空实现,仅需类型满足 trait 约束)
impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs { impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
fn event( fn event(
_state: &mut Self, _state: &mut Self,
@@ -30,37 +37,49 @@ impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
} }
} }
/// Detect which capture backend to use. // 通过 D-Bus 检测 XDG Desktop Portal 的 ScreenCast 接口是否可用
/// // 尝试创建 Screencast proxy如果 Portal 服务未运行则返回 false
/// Priority: fn check_portal_available() -> bool {
/// 1. Explicit `--backend` override from CLI args use ashpd::desktop::screencast::Screencast;
/// 2. Auto-detect by checking for `zwlr_screencopy_manager_v1` in Wayland globals
/// let rt = match tokio::runtime::Runtime::new() {
/// The detection Wayland connection is dropped before returning so the actual Ok(rt) => rt,
/// capture backend can create its own connection without holding two simultaneously. Err(e) => {
pub fn detect_backend(args: &Args) -> Result<CaptureBackend> { tracing::warn!("Failed to create tokio runtime for portal check: {e}");
// 1. Check explicit override return false;
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 rt.block_on(async {
tracing::info!("Auto-detecting capture backend..."); let proxy = match Screencast::new().await {
Ok(p) => p,
Err(e) => {
tracing::info!("Portal not available: {e}");
return false;
}
};
// Verify the portal actually exposes ScreenCast capabilities,
// not just that the D-Bus service is running.
match proxy.available_source_types().await {
Ok(types) if !types.is_empty() => {
tracing::info!("Portal ScreenCast available (source types: {types:?})");
true
}
Ok(types) => {
tracing::info!("Portal ScreenCast proxy exists but no source types available ({types:?})");
false
}
Err(e) => {
tracing::info!("Portal ScreenCast available_source_types query failed: {e}");
false
}
}
})
}
// 通过 Wayland globals 检测 wlr-screencopy 协议是否可用
fn check_screencopy_available() -> Result<bool> {
let conn = Connection::connect_to_env()?; let conn = Connection::connect_to_env()?;
let (globals, _queue) = registry_queue_init::<RegistryLs>(&conn)?; let (globals, _queue) = registry_queue_init::<RegistryLs>(&conn)?;
@@ -73,21 +92,81 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
// Drop the Wayland connection explicitly before returning. // Drop the Wayland connection explicitly before returning.
// The screencopy path creates its own connection. Holding two connections // The screencopy path creates its own connection. Holding two connections
// simultaneously is wasteful and may cause issues on some compositors. // simultaneously is wasteful and may cause issues on some compositors.
// 显式释放检测用的 Wayland 连接,避免与后续捕获后端同时占用两个连接
drop(conn); drop(conn);
if has_screencopy { 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<CaptureBackend> {
// 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"); tracing::info!("Detected wlr-screencopy support → using WlrScreencopy backend");
Ok(CaptureBackend::WlrScreencopy) Ok(CaptureBackend::WlrScreencopy)
} else { }
tracing::info!("No wlr-screencopy support → using Portal/PipeWire backend"); (false, true) => {
tracing::info!("No wlr-screencopy, Portal available → using Portal/PipeWire backend");
Ok(CaptureBackend::PortalPipeWire) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
// 测试辅助函数:构造指定后端参数的 Args 实例
fn make_args(backend: Option<&str>) -> Args { fn make_args(backend: Option<&str>) -> Args {
Args { Args {
output: "test.mp4".to_string(), output: "test.mp4".to_string(),
@@ -104,6 +183,7 @@ mod tests {
} }
} }
// 测试:显式指定 portal 后端
#[test] #[test]
fn explicit_portal_backend() { fn explicit_portal_backend() {
let args = make_args(Some("portal")); let args = make_args(Some("portal"));
@@ -112,6 +192,7 @@ mod tests {
assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire); assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire);
} }
// 测试:显式指定 screencopy 后端
#[test] #[test]
fn explicit_screencopy_backend() { fn explicit_screencopy_backend() {
let args = make_args(Some("screencopy")); let args = make_args(Some("screencopy"));
@@ -120,6 +201,7 @@ mod tests {
assert_eq!(result.unwrap(), CaptureBackend::WlrScreencopy); assert_eq!(result.unwrap(), CaptureBackend::WlrScreencopy);
} }
// 测试:无效的后端名称应返回错误
#[test] #[test]
fn invalid_backend_name_returns_error() { fn invalid_backend_name_returns_error() {
let args = make_args(Some("magic")); let args = make_args(Some("magic"));