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.
273 lines
9.8 KiB
Rust
273 lines
9.8 KiB
Rust
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}"
|
||
);
|
||
}
|
||
}
|