diff --git a/src/cap_portal.rs b/src/cap_portal.rs index b31b0c7..33d3899 100644 --- a/src/cap_portal.rs +++ b/src/cap_portal.rs @@ -1,3 +1,16 @@ +// cap_portal.rs — 通过 XDG Desktop Portal 的 ScreenCast 接口捕获屏幕帧 +// +// 整体架构: +// 1. CapPortal::new() 在主线程创建,内部启动一个专用的 PipeWire 捕获线程 +// 2. PipeWire 线程通过 Portal 获取的 fd 和 node_id 连接到 PipeWire,接收 DMA-BUF 帧 +// 3. 帧数据通过 crossbeam channel 从 PipeWire 线程传递给消费者 +// 4. 关闭时通过 eventfd 通知 PipeWire 线程退出,避免 UAF (Use-After-Free) +// +// 关键依赖: +// - ashpd: XDG Desktop Portal 的 Rust 绑定,用于请求屏幕录制权限 +// - pipewire / libspa: PipeWire 的 Rust 绑定,用于接收视频流 +// - crossbeam-channel: 高性能有界通道,用于线程间帧传递 + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; use std::sync::atomic::{AtomicU64, Ordering}; use std::thread::{self, JoinHandle}; @@ -8,53 +21,109 @@ use tokio::runtime::Runtime; use crate::args::Args; +/// PipeWire DMA-BUF 帧数据 +/// +/// 表示从 PipeWire 流中接收到的一帧视频数据。 +/// 帧的像素数据存储在 DMA-BUF(Linux 的零拷贝 buffer 共享机制)中, +/// 通过文件描述符 (fd) 引用,消费者通过 mmap 或 DRM 导入来访问像素数据。 pub struct PwDmaBufFrame { + /// DMA-BUF 文件描述符,指向 GPU 显存中的帧缓冲区 pub fd: OwnedFd, + /// 帧数据在 DMA-BUF 中的字节偏移量 pub offset: u64, + /// 每行像素的字节跨度(可能大于 width * bpp,因为可能有对齐填充) pub stride: u32, + /// DRM 格式修饰符,描述 buffer 的内存布局(如线性布局、tiling 等) pub modifier: u64, + /// 帧宽度(像素) pub width: u32, + /// 帧高度(像素) pub height: u32, + /// DRM FourCC 格式标识符(如 BGRA、RGBA 等) pub format: u32, + /// 显示时间戳 (PTS, Presentation Time Stamp),单位为纳秒 pub pts: i64, } +/// PipeWire 事件枚举 +/// +/// 从 PipeWire 捕获线程发送给消费者的事件类型。 +/// 消费者通过 frame_receiver() 获取的 Receiver 接收这些事件。 pub enum PwEvent { + /// 收到一帧新的 DMA-BUF 视频帧 Frame(PwDmaBufFrame), + /// 流已结束(PipeWire 流断开连接或进入错误状态) StreamEnded, + /// 发生错误,包含错误描述信息 Error(String), } +/// 屏幕捕获门户(Portal)封装 +/// +/// 通过 XDG Desktop Portal 的 ScreenCast 接口实现屏幕捕获。 +/// 内部管理一个 PipeWire 捕获线程,通过 channel 异步提供帧数据。 +/// +/// 生命周期: +/// 1. new() — 建立 Portal 会话,启动 PipeWire 线程 +/// 2. frame_receiver() — 获取帧接收端,供消费者轮询 +/// 3. Drop — 通过 eventfd 通知 PipeWire 线程安全退出 pub struct CapPortal { + /// eventfd 的写入端,用于在 drop 时通知 PipeWire 线程退出 shutdown_fd: OwnedFd, + /// 帧事件接收端,消费者通过此 Receiver 获取帧数据 frame_rx: Receiver, + /// PipeWire 捕获线程的 JoinHandle,drop 时等待线程退出 pw_thread: Option>, + /// Tokio 运行时,仅用于 setup_portal() 中的异步 Portal 调用 rt: Runtime, } +/// PipeWire 捕获线程的上下文数据 +/// +/// 从主线程传递给 PipeWire 捕获线程的所有必要资源。 +/// 该结构体在线程创建时一次性 move 到线程中使用。 struct PwThreadCtx { + /// 帧事件发送端,用于向消费者线程发送帧数据或错误/结束事件 frame_tx: Sender, + /// 已丢弃帧的计数器(原子操作),用于统计因通道满而丢弃的帧数 dropped: AtomicU64, + /// eventfd 的读取端,注册到 PipeWire 事件循环中,用于接收关闭信号 shutdown_read: OwnedFd, + /// Portal 返回的 PipeWire 远程连接文件描述符 pw_fd: OwnedFd, + /// Portal 返回的 PipeWire 节点 ID,标识要捕获的屏幕流 node_id: u32, + /// 目标帧率(当前保留,未直接用于 PipeWire 协商) fps: u32, } impl CapPortal { + /// 创建屏幕捕获实例 + /// + /// 执行流程: + /// 1. 创建 Tokio 运行时(用于异步 Portal 调用) + /// 2. 通过 XDG Desktop Portal 请求屏幕录制权限,获取 PipeWire fd 和 node_id + /// 3. 创建有界通道(容量 3)用于帧传递 + /// 4. 创建 eventfd 对,用于线程安全的关闭信号传递 + /// 5. 启动 PipeWire 捕获线程 pub fn new(args: &Args) -> Result { + // 创建独立的 Tokio 运行时,仅用于 setup_portal 中的异步 Portal D-Bus 调用 let rt = Runtime::new()?; + // 通过 Portal 获取 PipeWire 连接 fd 和节点 ID + // block_on 在此处同步等待异步 Portal 调用完成 let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal().await })?; + // 创建有界通道,容量为 3 帧 + // 使用有界通道实现背压:当消费者处理不过来时,生产者会丢弃帧而非无限堆积 let (frame_tx, frame_rx) = bounded(3); - // Create eventfd pair for thread-safe shutdown signaling. - // The write end lives in CapPortal (main thread), the read end is - // registered on the PipeWire loop so quit() happens on the loop thread - // where mainloop is guaranteed alive. + // 创建 eventfd 对,用于线程安全的关闭信号传递 + // eventfd 是 Linux 内核提供的轻量级进程/线程间通知机制 + // 写入端保存在 CapPortal(主线程),读取端注册到 PipeWire 事件循环中 + // 这样 CapPortal drop 时可以安全地通知 PipeWire 线程退出 let efd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) }; if efd < 0 { return Err(anyhow::anyhow!( @@ -62,6 +131,8 @@ impl CapPortal { std::io::Error::last_os_error() )); } + // 复制 eventfd 得到写入端,原始 fd 作为读取端 + // 需要 dup 是因为读取端和写入端需要各自独立的 OwnedFd 所有权 let write_fd = unsafe { libc::dup(efd) }; if write_fd < 0 { let err = std::io::Error::last_os_error(); @@ -69,6 +140,7 @@ impl CapPortal { return Err(anyhow::anyhow!("dup eventfd failed: {err}")); } + // 构建 PipeWire 线程上下文,将所有必要资源 move 进去 let ctx = PwThreadCtx { frame_tx, dropped: AtomicU64::new(0), @@ -78,6 +150,7 @@ impl CapPortal { fps: args.fps, }; + // 启动 PipeWire 捕获线程,命名便于调试和性能分析 let pw_thread = thread::Builder::new() .name("pipewire-capture".into()) .spawn(move || { @@ -92,25 +165,46 @@ impl CapPortal { }) } + /// 获取帧事件接收端的引用 + /// + /// 消费者通过此方法获取 Receiver,然后不断接收 PwEvent 事件来获取帧数据。 pub fn frame_receiver(&self) -> &Receiver { &self.frame_rx } + /// 通过 XDG Desktop Portal 建立屏幕录制会话 + /// + /// 与桌面环境的 D-Bus 服务交互,请求用户授权屏幕录制。 + /// 流程: + /// 1. 创建 Screencast 代理(D-Bus 代理) + /// 2. 创建 ScreenCast 会话 + /// 3. 配置源选择参数(光标模式、显示器源、不持久化会话) + /// 4. 启动录制,获取流信息(包含 PipeWire node_id) + /// 5. 打开 PipeWire 远程连接,获取文件描述符 + /// + /// 返回 (PipeWire fd, node_id),供 PipeWire 线程连接使用 async fn setup_portal() -> Result<(OwnedFd, u32)> { use ashpd::desktop::screencast::{ CursorMode, Screencast, SelectSourcesOptions, SourceType, }; use ashpd::desktop::PersistMode; + // 创建 Screencast D-Bus 代理,与桌面环境的 Portal 服务通信 let proxy = Screencast::new().await.map_err(|e| { anyhow::anyhow!("Failed to create Screencast proxy: {e}") })?; + // 创建 ScreenCast 会话(每个会话对应一次屏幕录制请求) let session = proxy .create_session(Default::default()) .await .map_err(|e| anyhow::anyhow!("Failed to create ScreenCast session: {e}"))?; + // 配置录制源选择参数: + // - CursorMode::Embedded: 光标嵌入到帧数据中(而非单独的元数据) + // - SourceType::Monitor: 仅捕获显示器(不捕获窗口) + // - multiple: false: 不允许多源选择 + // - PersistMode::DoNot: 不持久化会话(每次需要重新授权) proxy .select_sources( &session, @@ -125,6 +219,8 @@ impl CapPortal { anyhow::anyhow!("屏幕共享权限被拒绝 / Screen sharing permission denied: {e}") })?; + // 启动录制会话,此时桌面环境会弹出权限确认对话框 + // 用户确认后返回包含 PipeWire 流信息的响应 let response = proxy .start(&session, None, Default::default()) .await @@ -132,13 +228,18 @@ impl CapPortal { .response() .map_err(|e| anyhow::anyhow!("ScreenCast response error: {e}"))?; + // 获取返回的第一个(也是唯一的)视频流 + // 每个流对应一个 PipeWire 节点 let stream = response .streams() .first() .ok_or_else(|| anyhow::anyhow!("No streams returned from ScreenCast"))?; + // 提取 PipeWire 节点 ID,用于后续连接到该节点的视频流 let node_id = stream.pipe_wire_node_id(); + // 打开 PipeWire 远程连接,获取文件描述符 + // 这个 fd 允许直接与 PipeWire 守护进程通信 let fd = proxy .open_pipe_wire_remote(&session, Default::default()) .await @@ -151,6 +252,13 @@ impl CapPortal { } impl Drop for CapPortal { + /// 析构时安全关闭 PipeWire 线程 + /// + /// 通过向 eventfd 写入值来唤醒 PipeWire 事件循环,触发其退出。 + /// 然后等待 PipeWire 线程的 JoinHandle,确保线程完全退出后才返回。 + /// 这种基于 eventfd 的关闭机制避免了以下竞态条件: + /// - 直接调用 mainloop.quit() 可能在 mainloop 已经销毁后触发(UAF) + /// - eventfd 回调在 mainloop.run() 的上下文中执行,保证 mainloop 存活 fn drop(&mut self) { // Signal the PipeWire loop to quit via eventfd. // eventfd write is a kernel syscall — thread-safe and lock-free. @@ -163,12 +271,28 @@ impl Drop for CapPortal { ) }; + // 等待 PipeWire 线程完全退出 + // 这确保 PipeWire 资源在线程中被正确清理后,主线程才继续 if let Some(handle) = self.pw_thread.take() { let _ = handle.join(); } } } +/// PipeWire 捕获线程主函数 +/// +/// 在独立线程中运行 PipeWire 事件循环,接收来自 Portal 的屏幕捕获帧。 +/// 整体流程: +/// 1. 初始化 PipeWire 库 (pw::init) +/// 2. 创建 MainLoop(事件循环)、Context、Core(连接) +/// 3. 使用 Portal 提供的 fd 和 node_id 创建并连接视频流 +/// 4. 注册事件监听器(状态变化、格式协商、帧处理) +/// 5. 将 shutdown eventfd 注册到事件循环,实现安全退出 +/// 6. 运行事件循环,直到收到关闭信号 +/// 7. 清理资源,调用 pw::deinit() +/// +/// 注意: 此函数使用 Rc> 而非 Arc>,因为 PipeWire 的回调 +/// 都在同一个线程中执行,无需跨线程同步。 fn pipewire_thread(ctx: PwThreadCtx) { use pipewire as pw; use pw::properties::properties; @@ -177,8 +301,11 @@ fn pipewire_thread(ctx: PwThreadCtx) { use std::rc::Rc; use pw::spa::param::video::VideoInfoRaw; + // 初始化 PipeWire 库,必须在任何 PipeWire 操作之前调用 pw::init(); + // 解构上下文,取出所有必要资源 + // fps 重命名为 _fps 表示当前未使用(保留供将来帧率控制使用) let PwThreadCtx { frame_tx, dropped, @@ -188,6 +315,8 @@ fn pipewire_thread(ctx: PwThreadCtx) { fps: _fps, } = ctx; + // 创建 PipeWire MainLoop(主事件循环) + // MainLoopBox 是栈分配的 PipeWire 主循环封装 let mainloop = match pw::main_loop::MainLoopBox::new(None) { Ok(ml) => ml, Err(e) => { @@ -196,6 +325,7 @@ fn pipewire_thread(ctx: PwThreadCtx) { } }; + // 创建 PipeWire Context,用于管理核心对象和协议处理 let context = match pw::context::ContextBox::new(mainloop.loop_(), None) { Ok(c) => c, Err(e) => { @@ -204,6 +334,8 @@ fn pipewire_thread(ctx: PwThreadCtx) { } }; + // 使用 Portal 提供的 fd 连接到 PipeWire 核心守护进程 + // connect_fd 接管该 fd 的所有权(通过 dup),不关闭原始 fd let core = match context.connect_fd(pw_fd, None) { Ok(c) => c, Err(e) => { @@ -214,6 +346,11 @@ fn pipewire_thread(ctx: PwThreadCtx) { } }; + // 创建 PipeWire 视频流 + // 属性配置: + // - MEDIA_TYPE = "Video": 媒体类型为视频 + // - MEDIA_CATEGORY = "Capture": 类别为捕获(而非回放) + // - MEDIA_ROLE = "Screen": 角色为屏幕(用于策略管理) let stream = match StreamBox::new( &core, "wl-webrtc", @@ -230,13 +367,22 @@ fn pipewire_thread(ctx: PwThreadCtx) { } }; - // Shared format state: (width, height, drm_fourcc, modifier) + // 共享的格式状态: (宽度, 高度, DRM FourCC 格式, 修饰符) + // 使用 Rc> 因为 PipeWire 回调在同一个线程内执行,无需跨线程同步 + // Cell> 允许在不可变引用中修改值(内部可变性) + // format_info 在 param_changed 回调中设置,在 process 回调中读取 let format_info: Rc>> = Rc::new(Cell::new(None)); let frame_tx_clone = frame_tx.clone(); + // 注册流事件监听器,包含三个回调: + // - state_changed: 流状态变化通知 + // - param_changed: 格式协商完成通知 + // - process: 每帧数据处理 let _listener = stream .add_local_listener::<()>() + // 流状态变化回调 + // 当流进入 Error 或 Unconnected 状态时,通知消费者流已结束 .state_changed(move |_, _, old, new| { tracing::debug!("PipeWire stream state: {old:?} -> {new:?}"); match new { @@ -247,13 +393,18 @@ fn pipewire_thread(ctx: PwThreadCtx) { _ => {} } }) + // 参数变化回调(格式协商) + // PipeWire 在流格式协商完成后触发此回调 + // id 为参数类型,param 包含具体的格式参数(分辨率、像素格式等) .param_changed({ let format_info = format_info.clone(); move |_, _, id, param| { + // 仅处理 Format 类型的参数变化 let Some(param) = param else { return }; if id != pw::spa::param::ParamType::Format.as_raw() { return; } + // 解析视频格式信息(分辨率、像素格式、修饰符等) let mut info = VideoInfoRaw::new(); if let Err(e) = info.parse(param) { tracing::warn!("Failed to parse video format: {e}"); @@ -261,8 +412,11 @@ fn pipewire_thread(ctx: PwThreadCtx) { } let width = info.size().width; let height = info.size().height; + // 将 SPA 视频格式转换为 DRM FourCC 格式标识符 let drm_format = spa_to_drm_fourcc(info.format()); + // 获取 DRM 修饰符,描述 GPU buffer 的内存布局(如 tiling 模式) let modifier = info.modifier(); + // 保存协商后的格式信息,供 process 回调读取 format_info.set(Some((width, height, drm_format, modifier))); tracing::info!( "PipeWire format negotiated: {width}x{height}, \ @@ -270,22 +424,29 @@ fn pipewire_thread(ctx: PwThreadCtx) { ); } }) + // 帧处理回调 —— 这是核心的数据路径 + // 每当 PipeWire 有新的帧数据可用时触发 + // 关键操作: 从 buffer 中提取 DMA-BUF fd,dup 后通过 channel 发送给消费者 .process({ let format_info = format_info.clone(); let frame_tx = frame_tx.clone(); let dropped = dropped; move |stream, _| { + // 从流中出队原始 buffer(包含帧数据的元信息) let raw_buf = unsafe { stream.dequeue_raw_buffer() }; if raw_buf.is_null() { return; } + // 获取 SPA buffer 结构体,包含数据数组、元数据等 let spa_buf = unsafe { (*raw_buf).buffer }; if spa_buf.is_null() { unsafe { stream.queue_raw_buffer(raw_buf) }; return; } + // 获取 buffer 中的数据项数量和数据指针 + // 对于 DMA-BUF 帧,通常只有 1 个数据项(包含 fd) let n_datas = unsafe { (*spa_buf).n_datas }; let datas_ptr = unsafe { (*spa_buf).datas }; if n_datas == 0 || datas_ptr.is_null() { @@ -293,7 +454,8 @@ fn pipewire_thread(ctx: PwThreadCtx) { return; } - // Access first data item through libspa Data wrapper + // 从第一个数据项中获取 DMA-BUF 文件描述符 + // 通过 libspa 的 Data 包装类型安全地访问 SPA 数据结构 let data_ref: &pw::spa::buffer::Data = unsafe { &*(datas_ptr as *const pw::spa::buffer::Data) }; let fd = data_ref.fd(); if fd < 0 { @@ -301,11 +463,14 @@ fn pipewire_thread(ctx: PwThreadCtx) { return; } + // 获取 chunk 信息,包含帧数据在 DMA-BUF 中的偏移量和行跨度 let chunk = data_ref.chunk(); let offset = chunk.offset() as u64; let stride = chunk.stride() as u32; - // Get PTS from SPA_META_Header metadata + // 从 SPA_META_Header 元数据中提取 PTS (显示时间戳) + // 遍历 buffer 的所有元数据项,查找 Header 类型的元数据 + // PTS 可用于音视频同步和帧率控制 let pts: i64 = unsafe { let mut pts_val: i64 = 0; let n_metas = (*spa_buf).n_metas; @@ -326,6 +491,7 @@ fn pipewire_thread(ctx: PwThreadCtx) { pts_val }; + // 验证格式信息已协商完成,且分辨率和格式有效 let Some((width, height, format, modifier)) = format_info.get() else { unsafe { stream.queue_raw_buffer(raw_buf) }; return; @@ -335,12 +501,16 @@ fn pipewire_thread(ctx: PwThreadCtx) { return; } + // 复制 DMA-BUF 文件描述符 + // 必须 dup,因为原始 fd 由 PipeWire 管理,我们不能持有它 + // dup 后的 fd 由 PwDmaBufFrame 持有,生命周期独立于 PipeWire buffer let dup_fd = unsafe { libc::dup(fd) }; if dup_fd < 0 { unsafe { stream.queue_raw_buffer(raw_buf) }; return; } + // 构建帧数据对象,所有必要的帧信息已收集完毕 let frame = PwDmaBufFrame { fd: unsafe { OwnedFd::from_raw_fd(dup_fd) }, offset, @@ -352,6 +522,9 @@ fn pipewire_thread(ctx: PwThreadCtx) { pts, }; + // 尝试非阻塞发送帧到通道 + // 如果通道已满(消费者处理不过来),丢弃该帧并增加丢弃计数 + // 每 30 帧丢弃时输出一条警告日志,避免日志洪泛 if let Err(crossbeam_channel::TrySendError::Full(_)) = frame_tx.try_send(PwEvent::Frame(frame)) { @@ -360,13 +533,20 @@ fn pipewire_thread(ctx: PwThreadCtx) { tracing::warn!("dropped {prev} frames total: encoder backlog"); } } + // 无论是否成功发送帧,都必须将 buffer 重新入队 + // PipeWire 会复用这些 buffer,不入队会导致 buffer 泄漏 unsafe { stream.queue_raw_buffer(raw_buf) }; } }) .register(); + // 空的参数数组,不主动请求特定格式(由 PipeWire 和源端协商决定) let mut params: [&pw::spa::pod::Pod; 0] = []; + // 连接到指定的 PipeWire 节点 + // Direction::Input: 作为消费者(输入方向接收数据) + // AUTOCONNECT: 允许 PipeWire 自动连接源和消费者 + // MAP_BUFFERS: 映射 buffer 到用户空间(DMA-BUF 模式下必须设置) if let Err(e) = stream.connect( pw::spa::utils::Direction::Input, Some(node_id), @@ -378,6 +558,8 @@ fn pipewire_thread(ctx: PwThreadCtx) { } let loop_ = mainloop.loop_(); + // 注册信号处理(空回调),阻止 SIGINT/SIGTERM 默认行为终止线程 + // 真正的退出通过 shutdown eventfd 控制 loop_.add_signal_local( pw::loop_::Signal::SIGINT, Box::new(|| {}), @@ -394,6 +576,8 @@ fn pipewire_thread(ctx: PwThreadCtx) { // only fires while mainloop.run() is blocking this thread, mainloop // is guaranteed alive — eliminating the UAF that existed with the // previous detached helper thread approach. + // 保存 mainloop 的原始指针,用于在 shutdown 回调中调用 pw_main_loop_quit + // 这是安全的,因为回调只在 mainloop.run() 阻塞期间执行 let mainloop_ptr = mainloop.as_raw_ptr(); let _shutdown_source = loop_.add_io( @@ -415,6 +599,9 @@ fn pipewire_thread(ctx: PwThreadCtx) { }, ); + // 启动 PipeWire 主事件循环 + // 此调用会阻塞当前线程,直到 mainloop.quit() 被调用 + // quit() 由 shutdown eventfd 的 IO 回调触发 mainloop.run(); // run() returned — _shutdown_source drops first (reverse declaration order), @@ -426,10 +613,27 @@ fn pipewire_thread(ctx: PwThreadCtx) { unsafe { pw::deinit() }; } +/// 将四个 ASCII 字符编码为 32 位 FourCC (Four Character Code) 标识符 +/// +/// FourCC 是多媒体领域中广泛使用的像素格式标识方式。 +/// 编码规则: 第一个字符在最低 8 位,依次向高位排列。 +/// 例如: "BGRA" → 0x41524742 (小端序存储为 'B','G','R','A') const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 { (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24) } +/// 将 PipeWire SPA 视频格式转换为 DRM FourCC 格式 +/// +/// PipeWire 使用自己的 VideoFormat 枚举,而 DRM/KMS 使用 FourCC 格式标识。 +/// 此函数建立了两者之间的映射关系。 +/// +/// 支持的格式: +/// - BGRA/BGRx: 蓝绿红(Alpha/X) 32位格式 +/// - RGBA/RGBx: 红绿蓝(Alpha/X) 32位格式 +/// - ARGB/xRGB: Alpha/X-红绿蓝 32位格式 (映射为 AR24/XR24) +/// - ABGR/xBGR: Alpha/X-蓝绿红 32位格式 (映射为 AB24/XB24) +/// +/// 不支持的格式返回 0 fn spa_to_drm_fourcc(format: libspa::param::video::VideoFormat) -> u32 { use libspa::param::video::VideoFormat; match format { @@ -441,8 +645,8 @@ fn spa_to_drm_fourcc(format: libspa::param::video::VideoFormat) -> u32 { VideoFormat::xRGB => fourcc(b'X', b'R', b'2', b'4'), VideoFormat::ABGR => fourcc(b'A', b'B', b'2', b'4'), VideoFormat::xBGR => fourcc(b'X', b'B', b'2', b'4'), - _ => 0, - } + // 不支持的格式返回 0,调用者应检查此值 + _ => 0, } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index 9b3aba0..8209b13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +// 获取 Unix 原始文件描述符所需的 trait use std::os::unix::io::AsRawFd; use anyhow::Result; @@ -7,27 +8,41 @@ use mio::{Events, Interest, Poll, Token}; use wayland_client::globals::registry_queue_init; use wayland_client::Connection; -mod args; -mod avhw; -mod backend_detect; -mod cap_portal; -mod cap_wlr_screencopy; -mod fps_limit; -mod state; -mod state_portal; -mod transform; +// 各功能模块声明 +mod args; // 命令行参数解析 +mod avhw; // 音视频硬件加速 +mod backend_detect; // 截屏后端自动检测(wlroots vs Portal/PipeWire) +mod cap_portal; // XDG Portal 屏幕捕获 +mod cap_wlr_screencopy; // wlroots wlr-screencopy 截屏协议 +mod fps_limit; // 帧率限制器 +mod state; // wlr-screencopy 后端的主状态机 +mod state_portal; // Portal/PipeWire 后端的主状态机 +mod transform; // 图像变换(旋转/翻转) use crate::args::Args; use crate::cap_wlr_screencopy::CapWlrScreencopy; use crate::state::EncConstructionStage; use crate::state::State; +// mio 事件循环的 Token 标识:0 = Wayland 合成器事件,1 = 退出信号 const TOKEN_WAYLAND: Token = Token(0); const TOKEN_QUIT: Token = Token(1); +/// 程序入口:解析参数 → 初始化日志 → 检测后端 → 启动对应的事件循环 +/// +/// 整体流程: +/// 1. 解析命令行参数(分辨率、编码格式、帧率等) +/// 2. 初始化日志系统(verbose 模式输出 DEBUG 级别,否则 INFO) +/// 3. 检查编码格式(MVP 阶段仅支持 H.264) +/// 4. 自动检测当前桌面环境支持的截屏后端 +/// 5. 根据检测结果启动对应的事件循环: +/// - wlroots 合成器(Sway/Hyprland)→ run_wlr_screencopy +/// - GNOME/KDE 等 → run_portal_pipewire fn main() -> Result<()> { + // 解析命令行参数 let args = Args::parse(); + // 根据是否启用 verbose 模式设置日志级别 tracing_subscriber::fmt() .with_max_level(if args.verbose { tracing::Level::DEBUG @@ -39,12 +54,16 @@ fn main() -> Result<()> { tracing::info!("wl-webrtc starting"); tracing::debug!("Args: {:?}", args); + // MVP 阶段仅支持 H.264 编码,不支持 HEVC if args.codec != "h264" { anyhow::bail!("HEVC not supported in MVP. Use --codec h264"); } + // 自动检测当前桌面环境可用的截屏后端 + // 会尝试列举 Wayland 全局对象,判断合成器是否支持 wlr-screencopy 协议 let backend = crate::backend_detect::detect_backend(&args)?; + // 根据检测结果进入对应的事件循环 match backend { crate::backend_detect::CaptureBackend::WlrScreencopy => { run_wlr_screencopy(args) @@ -55,37 +74,66 @@ fn main() -> Result<()> { } } +/// 使用 wlroots wlr-screencopy 协议的事件循环(适用于 Sway、Hyprland 等 wlroots 合成器) +/// +/// 完整工作流程: +/// 1. 连接 Wayland 合成器,获取全局注册表 +/// 2. 初始化 wlr-screencopy 截屏状态机 +/// 3. 获取 Wayland socket 的文件描述符 +/// 4. 使用 mio 注册 fd 监听(Wayland 事件 + Unix 信号) +/// 5. 进入主事件循环: +/// - 监听合成器事件(帧就绪通知、输出信息等) +/// - 定时请求截屏帧 +/// - 将截取的帧编码为 H.264 并推流 +/// 6. 收到退出信号或发生错误时,刷新编码器并退出 fn run_wlr_screencopy(args: Args) -> Result<()> { // Connect to Wayland compositor + // 建立 Wayland 连接并初始化全局注册表 + // 通过环境变量 $WAYLAND_DISPLAY 找到合成器的 Unix socket let conn = Connection::connect_to_env()?; + // registry_queue_init 会绑定全局注册表回调, + // 当合成器广播其全局对象(输出、截屏管理器等)时,State 会收到通知 let (gm, mut queue) = registry_queue_init::>(&conn)?; let qhandle = queue.handle(); + // State 是 wlr-screencopy 后端的核心状态机, + // 内部管理输出探测、截屏请求、编码器构建、帧采集等阶段 let mut state = State::new(gm, args, qhandle); // Extract the Wayland fd and consume any immediately-available events. // prepare_read() flushes outgoing requests; read() pulls whatever the // compositor has already sent (may be EAGAIN if nothing yet). + // 获取 Wayland socket 的文件描述符,并消费合成器已发送的事件 + // 这个 fd 是后续 mio epoll 监听的对象,当合成器写入数据时变为可读 let wayland_fd = { let guard = queue .prepare_read() .ok_or_else(|| anyhow::anyhow!("Failed to prepare Wayland read"))?; + // 从 prepare_read 的 guard 中获取底层 socket 的原始文件描述符 let fd = guard.connection_fd().as_raw_fd(); + // 尝试非阻塞读取合成器已发送但尚未消费的数据 + // 如果没有数据会返回 EAGAIN,这里用 let _ 忽略 let _ = guard.read(); fd }; + // 处理队列中的待处理事件 + // 在进入主循环前,先处理注册表广播等初始化事件 + // 此时状态机应进入 ProbingOutputs 阶段,正在探测可用的显示输出 queue.dispatch_pending(&mut state)?; tracing::info!( "Initial dispatch done, stage is ProbingOutputs: {}", matches!(state.stage, EncConstructionStage::ProbingOutputs { .. }) ); + // 调试用:对 Wayland fd 做一次原始 poll,确认 fd 可读性 + // 使用 libc 底层 poll 而非 mio,纯粹用于诊断初始化阶段的 fd 状态 { let mut pfd = libc::pollfd { fd: wayland_fd, - events: libc::POLLIN, + events: libc::POLLIN, // 监听可读事件 revents: 0, }; + // timeout=0 表示非阻塞,立即返回当前 fd 状态 let ret = unsafe { libc::poll(&mut pfd, 1, 0) }; tracing::info!( "Raw poll on wayland fd={wayland_fd}: ret={ret}, revents={}", @@ -94,18 +142,26 @@ fn run_wlr_screencopy(args: Args) -> Result<()> { } // Set up mio event loop + // 使用 mio 创建事件循环,注册 Wayland fd 和 Unix 信号 + // mio 底层在 Linux 上使用 epoll,macOS 上使用 kqueue let mut poll = Poll::new()?; + // 事件缓冲区,容量 8 足够(实际只会同时处理 Wayland 事件和信号两种) let mut events = Events::with_capacity(8); + // 将 Wayland socket fd 注册为可读监听 + // 当合成器发送消息(如帧完成通知、配置变化)时,epoll 会唤醒 poll.registry().register( &mut SourceFd(&wayland_fd), TOKEN_WAYLAND, Interest::READABLE, )?; + // 注册 SIGINT / SIGTERM 信号用于优雅退出 + // signal_hook_mio 将 Unix 信号转换为 fd 可读事件, + // 这样信号也可以通过 epoll 统一监听,不需要单独的信号处理器 let mut signals = signal_hook_mio::v1_0::Signals::new(&[ - signal_hook::consts::SIGINT, - signal_hook::consts::SIGTERM, + signal_hook::consts::SIGINT, // Ctrl+C + signal_hook::consts::SIGTERM, // kill 命令默认信号 ])?; poll.registry() .register(&mut signals, TOKEN_QUIT, Interest::READABLE)?; @@ -113,18 +169,40 @@ fn run_wlr_screencopy(args: Args) -> Result<()> { tracing::info!("Event loop started"); // Flush outgoing before first poll iteration + // 在首次 poll 前刷新所有待发送的 Wayland 请求 + // 确保合成器能收到我们的初始化请求(如绑定全局对象、请求截屏等) conn.flush()?; + // 主事件循环 + // 这是 wlr-screencopy 后端的核心运行循环,负责: + // - 接收合成器事件(截屏帧就绪、输出变化) + // - 定时触发帧采集和编码 + // - 响应退出信号 let mut running = true; while running { + // 准备读取 Wayland 事件(非阻塞) + // prepare_read() 会先刷出所有待发送的请求, + // 然后进入"准备读取"状态,告诉合成器我们已准备好接收数据 let read_guard = queue.prepare_read(); + // 如果无法 prepare_read(有待处理数据),先分发 + // 返回 None 说明队列中已有待处理的合成器事件, + // 需要先 dispatch 掉,否则新事件无法进入队列 if read_guard.is_none() { queue.dispatch_pending(&mut state)?; } + // 阻塞等待事件,超时 100ms(用于帧率控制) + // poll 会阻塞当前线程,直到以下任一条件满足: + // 1. Wayland fd 可读(合成器发来了消息) + // 2. 信号 fd 可读(收到了 SIGINT/SIGTERM) + // 3. 超过 100ms 没有任何事件(超时返回,触发下一帧采集) + // 100ms 超时 ≈ 10 FPS 的帧率上限 poll.poll(&mut events, Some(std::time::Duration::from_millis(100))) .unwrap_or_else(|e| { + // EINTR 是信号中断,属于正常情况,继续循环 + // 当进程收到信号时,阻塞中的 poll 会被中断并返回 EINTR, + // 这不是错误,下一轮循环会继续正常 poll if e.kind() == std::io::ErrorKind::Interrupted { return; } @@ -132,6 +210,7 @@ fn run_wlr_screencopy(args: Args) -> Result<()> { running = false; }); + // 检查是否收到退出信号 for event in &events { if event.token() == TOKEN_QUIT { tracing::info!("Received quit signal"); @@ -139,10 +218,14 @@ fn run_wlr_screencopy(args: Args) -> Result<()> { } } + // Wayland fd 可读时,读取并分发合成器事件 + // 合成器可能发来多种事件:帧数据就绪、输出信息变化、协议错误等 if events.iter().any(|e| e.token() == TOKEN_WAYLAND) { if let Some(guard) = read_guard { match guard.read() { Ok(_) => { + // 读取成功后,dispatch_pending 会将合成器事件 + // 分发给 State 的对应回调方法处理 queue.dispatch_pending(&mut state)?; } Err(e) => { @@ -153,18 +236,30 @@ fn run_wlr_screencopy(args: Args) -> Result<()> { } } + // 请求状态机分配并编码一帧 + // queue_alloc_frame 会根据当前状态机阶段执行不同操作: + // - ProbingOutputs: 还在探测输出,跳过 + // - AwaitingFrame: 已请求截屏,等待合成器回调 + // - FrameReady: 有帧就绪,执行 DMA-BUF → H.264 编码 → 推流 + // - Streaming: 正常采集中,请求下一帧 state.queue_alloc_frame(); + // 状态机遇到致命错误时退出 if state.errored { tracing::error!("Fatal error in state machine, exiting"); running = false; } + // 每轮循环结束前刷新 Wayland 发送缓冲区 + // 将本轮回合中产生的所有 Wayland 请求(如截屏请求)发送给合成器 conn.flush()?; } + // 关闭前刷新编码器,确保所有帧数据已写出 + // 先通知帧率限制器停止,再刷新编码器缓冲区中残余的帧数据 tracing::info!("Shutting down, flushing encoder..."); state.fps_limit.flush(); + // 仅在编码器已构建完成(Streaming 阶段)时才需要刷新 if let crate::state::EncConstructionStage::Streaming { enc, .. } = &mut state.stage { if let Err(e) = enc.flush() { tracing::error!("Failed to flush encoder: {e}"); @@ -175,14 +270,30 @@ fn run_wlr_screencopy(args: Args) -> Result<()> { Ok(()) } +/// 使用 XDG Portal / PipeWire 后端的事件循环(适用于 KWin、KDE、GNOME 等桌面环境) +/// +/// 完整工作流程: +/// 1. 初始化 Portal 状态机(内部通过 D-Bus 与 XDG Portal 通信) +/// 2. 仅注册 Unix 信号监听(不需要监听 Wayland fd,帧数据由 PipeWire 投递) +/// 3. 进入主事件循环: +/// - 每 10ms 轮询一次 PipeWire 缓冲区 +/// - 有帧数据时,取出并编码为 H.264 推流 +/// - 收到退出信号时停止 +/// 4. 退出时关闭 Portal 连接并释放 PipeWire 资源 fn run_portal_pipewire(args: Args) -> Result<()> { use crate::state_portal::StatePortal; tracing::info!("Using Portal/PipeWire backend (KWin/KDE/GNOME)"); + // StatePortal 初始化时会: + // 1. 通过 D-Bus 连接到 XDG Portal 的 ScreenCast 接口 + // 2. 请求用户授权屏幕录制权限 + // 3. 建立 PipeWire 流连接,准备接收帧数据 let mut state = StatePortal::new(args)?; // Set up signal handling only (no Wayland fd needed) + // Portal 后端不需要监听 Wayland fd,只需处理 Unix 信号 + // 因为帧数据是通过 PipeWire 独立投递的,不走 Wayland 协议 let mut signals = signal_hook_mio::v1_0::Signals::new(&[ signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM, @@ -191,14 +302,23 @@ fn run_portal_pipewire(args: Args) -> Result<()> { let mut poll = mio::Poll::new()?; let mut events = mio::Events::with_capacity(8); + // 只注册信号 fd,没有 Wayland fd + // 所以 poll.poll 在这里只负责检测 SIGINT/SIGTERM + // 实际的帧采集完全依赖 poll_and_encode 的轮询 poll.registry().register( &mut signals, mio::Token(1), mio::Interest::READABLE, )?; + // 主事件循环(超时 10ms,比 wlr-screencopy 更短,因为不依赖 Wayland fd 唤醒) + // 10ms 超时的作用是让循环高频转动,以便及时处理 PipeWire 投递的帧 + // 如果没有信号,poll 最多阻塞 10ms 就会超时返回 let mut running = true; while running { + // poll 在此循环中只监听信号 fd,所以: + // - 收到 SIGINT/SIGTERM → 事件触发,设置 running=false + // - 超时 10ms → 事件为空,继续执行 poll_and_encode poll.poll(&mut events, Some(std::time::Duration::from_millis(10))) .unwrap_or_else(|e| { if e.kind() == std::io::ErrorKind::Interrupted { @@ -208,6 +328,7 @@ fn run_portal_pipewire(args: Args) -> Result<()> { running = false; }); + // 遍历事件,检查是否收到退出信号 for event in &events { if event.token() == mio::Token(1) { tracing::info!("Received quit signal"); @@ -216,8 +337,13 @@ fn run_portal_pipewire(args: Args) -> Result<()> { } // Process all available PipeWire frames + // 处理所有可用的 PipeWire 帧数据 + // poll_and_encode 会从 PipeWire 缓冲区取出帧, + // 编码为 H.264 并推送。返回 true 表示还有更多帧待处理, + // 返回 false 表示当前没有帧了,while 循环退出等待下一轮 poll while state.poll_and_encode()? {} + // Portal 状态机遇到致命错误时退出 if state.is_errored() { tracing::error!("Fatal error in portal state machine, exiting"); running = false; @@ -225,6 +351,7 @@ fn run_portal_pipewire(args: Args) -> Result<()> { } tracing::info!("Shutting down..."); + // 关闭 Portal 连接,释放 PipeWire 流和编码器资源 state.shutdown(); tracing::info!("Done"); Ok(()) diff --git a/src/state_portal.rs b/src/state_portal.rs index 06ffd0d..7f718ba 100644 --- a/src/state_portal.rs +++ b/src/state_portal.rs @@ -1,3 +1,4 @@ +// 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码 use std::mem; use std::path::PathBuf; @@ -11,24 +12,43 @@ use crate::cap_portal::{CapPortal, PwDmaBufFrame, PwEvent}; use crate::fps_limit::FpsLimit; use crate::transform::Transform; +/// 门户采集的阶段状态 +/// - WaitingForFormat: 等待接收到第一帧 DMA-BUF 以确定视频格式参数 +/// - Streaming: 已完成初始化,正在持续编码流 enum PortalStage { WaitingForFormat, Streaming, } +/// 门户模式的主状态机 +/// +/// 负责管理从 PipeWire 采集屏幕帧、通过 VAAPI 硬件编码的完整生命周期。 +/// 工作流程:等待第一帧 → 创建编码器 → 持续编码帧数据。 pub struct StatePortal { + /// 当前采集阶段 stage: PortalStage, + /// 硬件编码器状态(第一帧到达后才初始化) enc: Option, + /// 帧率限制器 fps_limit: FpsLimit<()>, + /// PipeWire 屏幕采集端点 cap: CapPortal, + /// 命令行参数 args: Args, + /// 是否遇到错误 errored: bool, + /// 是否为第一帧(首帧跳过帧率限制) first_frame: bool, + /// DRM 渲染设备路径(如 /dev/dri/renderD128) drm_device: PathBuf, + /// 第一帧的时间戳(纳秒),用于计算相对 PTS first_pts_ns: Option, } impl StatePortal { + /// 创建门户状态实例 + /// + /// 初始化 DRM 设备路径和 PipeWire 采集端点,编码器延迟到第一帧到达时创建。 pub fn new(args: Args) -> Result { let drm_device = resolve_drm_device(&args)?; tracing::info!("Using DRM device: {}", drm_device.display()); @@ -48,6 +68,10 @@ impl StatePortal { }) } + /// 轮询 PipeWire 事件并编码帧 + /// + /// 尝试从采集端点接收一帧事件。返回 `Ok(true)` 表示已处理事件, + /// `Ok(false)` 表示暂无数据。内部根据当前阶段(等待格式/流式)分发处理。 pub fn poll_and_encode(&mut self) -> Result { let event = match self.cap.frame_receiver().try_recv() { Ok(event) => event, @@ -58,6 +82,7 @@ impl StatePortal { PwEvent::Frame(frame) => { match self.stage { PortalStage::WaitingForFormat => { + // 第一帧到达:记录格式信息并用该分辨率创建编码器 tracing::info!( "First DMA-BUF frame: {}x{} format=0x{:08X} stride={} modifier=0x{:X}", frame.width, @@ -83,15 +108,18 @@ impl StatePortal { drop(frame); } PortalStage::Streaming => { + // 流式阶段:处理每一帧 DMA-BUF 数据 self.handle_pw_frame(frame)?; } } } PwEvent::StreamEnded => { + // PipeWire 流结束(如用户停止了屏幕共享) tracing::warn!("PipeWire stream ended"); self.errored = true; } PwEvent::Error(e) => { + // PipeWire 返回错误 tracing::error!("PipeWire error: {e}"); self.errored = true; } @@ -100,8 +128,20 @@ impl StatePortal { Ok(true) } + /// 处理单帧 DMA-BUF 数据 + /// + /// 完整的帧处理流水线: + /// 1. 帧率限制(首帧跳过) + /// 2. 构建 DRM 描述符 + /// 3. 分配 DRM_PRIME 源帧 + /// 4. 分配 VAAPI 硬件目标帧 + /// 5. 通过 DMA-BUF 导入将帧数据导入 VAAPI + /// 6. 计算 PTS 时间戳 + /// 7. 回收 DRM 描述符内存 + /// 8. 编码输出 fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> { // 1. FPS limiting (first frame bypasses) + // 帧率限制(首帧跳过限制,确保立即编码) if self.first_frame { self.first_frame = false; } else { @@ -112,10 +152,12 @@ impl StatePortal { } // 2. Build DRM descriptor for DMA-BUF import + // 根据 DMA-BUF 帧信息构建 FFmpeg DRM 描述符 let desc = build_drm_descriptor(&frame); let desc_box = Box::new(desc); // 3. Allocate raw DRM_PRIME source frame using Video wrapper + // 分配 DRM_PRIME 格式的源帧,将描述符指针挂载到 data[0] let mut raw_frame = ff::frame::Video::empty(); unsafe { let raw_ptr = raw_frame.as_mut_ptr(); @@ -126,10 +168,12 @@ impl StatePortal { } // 4. Get encoder reference + // 获取编码器引用 let enc = match self.enc.as_mut() { Some(e) => e, None => { // Recover the Box to prevent memory leak of the descriptor + // 编码器未初始化时回收描述符以防止内存泄漏 unsafe { let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; if !desc_ptr.is_null() { @@ -141,12 +185,14 @@ impl StatePortal { }; // 5. Allocate VAAPI hardware target frame + // 分配 VAAPI 硬件帧缓冲区 let mut hw_frame = ff::frame::Video::empty(); let ret = unsafe { ffi::av_hwframe_get_buffer(enc.frames_rgb().as_ptr(), hw_frame.as_mut_ptr(), 0) }; if ret < 0 { // Recover the Box to prevent memory leak of the descriptor + // 分配失败时回收描述符防止内存泄漏 unsafe { let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; if !desc_ptr.is_null() { @@ -157,10 +203,12 @@ impl StatePortal { } // 6. Import DMA-BUF into VAAPI via transfer_data + // 通过 DMA-BUF 导入将帧数据从 DRM 传输到 VAAPI 硬件表面 let ret = unsafe { ffi::av_hwframe_transfer_data(hw_frame.as_mut_ptr(), raw_frame.as_ptr(), 0) }; if ret < 0 { + // 传输失败时回收描述符防止内存泄漏 unsafe { let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; if !desc_ptr.is_null() { @@ -180,6 +228,10 @@ impl StatePortal { // PipeWire PTS is CLOCK_MONOTONIC in nanoseconds. // Encoder time_base = 1/fps, so PTS must be in frame numbers. // Use elapsed time since first frame to avoid i64 overflow on absolute timestamps. + // + // PTS 计算:将 PipeWire 的纳秒时间戳转换为编码器的帧号单位 + // PipeWire 使用 CLOCK_MONOTONIC 纳秒时间戳,编码器 time_base = 1/fps + // 使用相对时间避免绝对时间戳导致的 i64 溢出 let fps_i64 = self.args.fps as i64; let base_ns = *self.first_pts_ns.get_or_insert(frame.pts.max(0)); let elapsed_ns = (frame.pts.max(0) - base_ns).max(0); @@ -193,6 +245,10 @@ impl StatePortal { // VAAPI surface, so FFmpeg no longer references the descriptor struct. // Doing this before encode_frame ensures the descriptor is reclaimed // even if encode_frame returns early via `?`. + // + // 在编码前回收描述符内存。 + // 此时 DMA-BUF 数据已导入 VAAPI 表面,FFmpeg 不再引用描述符结构体。 + // 在 encode_frame 之前回收确保即使编码返回错误也能正确释放内存。 unsafe { let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor; if !desc_ptr.is_null() { @@ -201,12 +257,15 @@ impl StatePortal { } // 9. Encode — safe to early-return via `?` now that descriptor is recovered. + // 编码帧数据(此时描述符已回收,可安全通过 `?` 提前返回) enc.encode_frame(&hw_frame)?; - + // raw_frame and hw_frame drop here via Video::drop → av_frame_free + // raw_frame 和 hw_frame 在此处通过 Video::drop → av_frame_free 释放 Ok(()) } + /// 刷新编码器缓冲区,输出所有剩余帧 pub fn flush(&mut self) -> Result<()> { if let Some(enc) = &mut self.enc { enc.flush()?; @@ -214,6 +273,7 @@ impl StatePortal { Ok(()) } + /// 关闭状态:刷新编码器并清理资源 pub fn shutdown(&mut self) { if let Err(e) = self.flush() { tracing::error!("Flush error during shutdown: {e}"); @@ -221,19 +281,26 @@ impl StatePortal { tracing::info!("StatePortal shutdown complete"); } + /// 返回是否遇到不可恢复的错误 pub fn is_errored(&self) -> bool { self.errored } } +/// 根据 DMA-BUF 帧信息构建 FFmpeg DRM 帧描述符 +/// +/// 将 PipeWire 提供的 DMA-BUF 参数(fd、偏移量、步长、修饰符等) +/// 转换为 FFmpeg 的 AVDRMFrameDescriptor 结构体,用于零拷贝硬件导入。 fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffi::AVDRMFrameDescriptor { let mut desc: ffi::AVDRMFrameDescriptor = unsafe { mem::zeroed() }; + // DMA-BUF 对象层:一个 fd 对应一个内存对象 desc.nb_objects = 1; desc.objects[0].fd = frame.fd.as_raw_fd(); - desc.objects[0].size = 0; + desc.objects[0].size = 0; // 大小为 0 表示整个 fd desc.objects[0].format_modifier = frame.modifier; + // 像素格式层:单层单平面布局(如 XR24 格式) desc.nb_layers = 1; desc.layers[0].format = frame.format; desc.layers[0].nb_planes = 1; @@ -246,6 +313,10 @@ fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffi::AVDRMFrameDescriptor { use std::os::fd::AsRawFd; +/// 解析 DRM 渲染设备路径 +/// +/// 优先使用命令行指定的设备路径,否则依次尝试 +/// `/dev/dri/renderD128` 和 `/dev/dri/renderD129`。 fn resolve_drm_device(args: &Args) -> Result { if let Some(ref drm) = args.drm_device { return Ok(PathBuf::from(drm)); @@ -266,6 +337,7 @@ mod tests { use super::*; use std::os::fd::{FromRawFd, OwnedFd}; + /// 创建测试用的 DMA-BUF 帧数据(使用 stderr fd 的副本作为占位) fn make_test_frame() -> PwDmaBufFrame { // Create a dummy fd from stderr (always valid fd 2) let fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) }; @@ -281,6 +353,7 @@ mod tests { } } + /// 测试 DRM 描述符构建(单平面情况) #[test] fn build_drm_descriptor_single_plane() { let frame = make_test_frame(); @@ -296,6 +369,7 @@ mod tests { assert_eq!(desc.layers[0].planes[0].pitch, 1920 * 4); } + /// 测试显式指定 DRM 设备时的解析 #[test] fn resolve_drm_device_explicit() { let args = Args {