Files
wl-webrtc/src/backend_detect.rs
T
dailz 75ad4bba78 style: apply rustfmt to establish clean baseline before refactor
Pre-refactor baseline state:
- 79 lib tests + 3 integration tests pass (1 integration test #[ignore])
- cargo clippy --all-targets -- -D warnings clean
- cargo build --release clean

No semantic changes; only rustfmt drift correction across 6 files.
2026-07-13 15:40:54 +08:00

273 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::time::Duration;
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<WlRegistry, GlobalListContents> for RegistryLs {
fn event(
_state: &mut Self,
_registry: &WlRegistry,
_event: Event,
_data: &GlobalListContents,
_conn: &Connection,
_qhandle: &QueueHandle<Self>,
) {
}
}
// 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.
/// Per-operation D-Bus timeout for Portal backend detection.
/// Portal 后端检测期间每个 D-Bus 操作的超时时间。
const PORTAL_DBUS_TIMEOUT: Duration = Duration::from_secs(5);
fn log_portal_unresponsive(operation: &str) {
tracing::error!(
"Portal service did not respond within 5s while {operation}. \
This usually means xdg-desktop-portal or xdg-desktop-portal-kde is stuck. \
Try: systemctl --user restart xdg-desktop-portal xdg-desktop-portal-kde, \
then re-run wl-webrtc."
);
}
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 {
// Set method_timeout on the connection (bounds method replies) and wrap
// the build itself in tokio::time::timeout (bounds connection setup).
// 同时设置 method_timeout 与外层 tokio::time::timeout 双重保护。
let conn = match tokio::time::timeout(PORTAL_DBUS_TIMEOUT, async {
zbus::connection::Builder::session()
.expect("D-Bus session bus builder failed")
.method_timeout(PORTAL_DBUS_TIMEOUT)
.build()
.await
})
.await
{
Ok(Ok(c)) => c,
Ok(Err(e)) => {
tracing::info!("D-Bus session bus unavailable: {e}");
return false;
}
Err(_) => {
log_portal_unresponsive("connecting to D-Bus session bus");
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 tokio::time::timeout(PORTAL_DBUS_TIMEOUT, b.build()).await {
Ok(Ok(p)) => p,
Ok(Err(e)) => {
tracing::info!("Portal ScreenCast interface not available: {e}");
return false;
}
Err(_) => {
log_portal_unresponsive("building ScreenCast proxy");
return false;
}
},
Err(e) => {
tracing::info!("Portal ScreenCast proxy build failed: {e}");
return false;
}
};
// The most likely operation to hang — requires actual Portal-side work.
// 最可能卡住的操作,需要 Portal 端实际处理。
let version =
match tokio::time::timeout(PORTAL_DBUS_TIMEOUT, inner.get_property::<u32>("version"))
.await
{
Ok(Ok(version)) => {
tracing::info!("Portal ScreenCast available (version: {version})");
true
}
Ok(Err(e)) => {
tracing::info!("Portal ScreenCast version query failed: {e}");
false
}
Err(_) => {
log_portal_unresponsive("querying ScreenCast version");
false
}
};
version
})
}
// 通过 Wayland globals 检测 wlr-screencopy 协议是否可用
fn check_screencopy_available() -> Result<bool> {
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.
// 显式释放检测用的 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<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)."
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// 测试辅助函数:构造指定后端参数的 Args 实例
fn make_args(backend: Option<&str>) -> Args {
Args {
output: Some("test.mp4".to_string()),
output_name: None,
fps: 30,
codec: "h264".to_string(),
hw_accel: "vaapi".to_string(),
drm_device: None,
bitrate: None,
max_bitrate: 8_000_000,
gop_size: None,
verbose: false,
backend: backend.map(String::from),
port: 0,
no_persist: false,
stats: false,
}
}
// 测试:显式指定 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}"
);
}
}