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;
// 屏幕捕获后端类型
/// 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<WlRegistry, GlobalListContents> for RegistryLs {
fn event(
_state: &mut Self,
@@ -30,37 +37,49 @@ impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
}
}
/// 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
);
// 通过 D-Bus 检测 XDG Desktop Portal 的 ScreenCast 接口是否可用
// 尝试创建 Screencast proxy如果 Portal 服务未运行则返回 false
fn check_portal_available() -> bool {
use ashpd::desktop::screencast::Screencast;
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 proxy = match Screencast::new().await {
Ok(p) => p,
Err(e) => {
tracing::info!("Portal not available: {e}");
return false;
}
};
}
// 2. Auto-detect: check if zwlr_screencopy_manager_v1 is available
tracing::info!("Auto-detecting capture backend...");
// 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 (globals, _queue) = registry_queue_init::<RegistryLs>(&conn)?;
@@ -73,14 +92,73 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
// 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);
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)
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");
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)."
);
}
}
}
@@ -88,6 +166,7 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
mod tests {
use super::*;
// 测试辅助函数:构造指定后端参数的 Args 实例
fn make_args(backend: Option<&str>) -> Args {
Args {
output: "test.mp4".to_string(),
@@ -104,6 +183,7 @@ mod tests {
}
}
// 测试:显式指定 portal 后端
#[test]
fn explicit_portal_backend() {
let args = make_args(Some("portal"));
@@ -112,6 +192,7 @@ mod tests {
assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire);
}
// 测试:显式指定 screencopy 后端
#[test]
fn explicit_screencopy_backend() {
let args = make_args(Some("screencopy"));
@@ -120,6 +201,7 @@ mod tests {
assert_eq!(result.unwrap(), CaptureBackend::WlrScreencopy);
}
// 测试:无效的后端名称应返回错误
#[test]
fn invalid_backend_name_returns_error() {
let args = make_args(Some("magic"));