Fixes #1 -- --port mode with wlr-screencopy backend caused panic at negotiate_format() because self.args.output is None and .expect() was called unconditionally. Changes: - Introduce StreamingEncoder enum wrapping EncState (MP4) and SwEncState (WebRTC) with unified frames_rgb/encode_frame/flush API - Add WebRTC fields to State<S> (webrtc, webrtc_tx, webrtc_rx, webrtc_frames_sent) matching Portal backend pattern - State::new() returns Result<Self> for clean WebRtcState init failure - negotiate_format() branches on webrtc_tx: WebRTC path uses SwEncState::new_webrtc(), MP4 path unchanged (hardware VAAPI) - Add poll_webrtc() method to drive signaling + channel drain - Event loop calls poll_webrtc() each iteration - Fix pre-existing test/bench Args construction (Option<String> output, missing no_persist field)
227 lines
7.9 KiB
Rust
227 lines
7.9 KiB
Rust
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.
|
||
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::<u32>("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<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,
|
||
gop_size: None,
|
||
verbose: false,
|
||
backend: backend.map(String::from),
|
||
port: 0,
|
||
no_persist: 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}"
|
||
);
|
||
}
|
||
}
|