docs: add Chinese documentation comments to core modules

Add comprehensive Chinese documentation comments to cap_portal,
main, and state_portal modules covering architecture, lifecycle,
and data flow for each component.
This commit is contained in:
dailz
2026-05-25 08:56:43 +08:00
parent 25110e8463
commit dcf8d1affb
3 changed files with 428 additions and 23 deletions

View File

@@ -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-BUFLinux 的零拷贝 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<PwEvent>,
/// PipeWire 捕获线程的 JoinHandledrop 时等待线程退出
pw_thread: Option<JoinHandle<()>>,
/// Tokio 运行时,仅用于 setup_portal() 中的异步 Portal 调用
rt: Runtime,
}
/// PipeWire 捕获线程的上下文数据
///
/// 从主线程传递给 PipeWire 捕获线程的所有必要资源。
/// 该结构体在线程创建时一次性 move 到线程中使用。
struct PwThreadCtx {
/// 帧事件发送端,用于向消费者线程发送帧数据或错误/结束事件
frame_tx: Sender<PwEvent>,
/// 已丢弃帧的计数器(原子操作),用于统计因通道满而丢弃的帧数
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<Self> {
// 创建独立的 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<PwEvent> {
&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<Cell<>> 而非 Arc<Mutex<>>,因为 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<Cell<>> 因为 PipeWire 回调在同一个线程内执行,无需跨线程同步
// Cell<Option<...>> 允许在不可变引用中修改值(内部可变性)
// format_info 在 param_changed 回调中设置,在 process 回调中读取
let format_info: Rc<Cell<Option<(u32, u32, u32, u64)>>> =
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 fddup 后通过 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)]

View File

@@ -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::<State<CapWlrScreencopy>>(&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 上使用 epollmacOS 上使用 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(())

View File

@@ -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<EncState>,
/// 帧率限制器
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<i64>,
}
impl StatePortal {
/// 创建门户状态实例
///
/// 初始化 DRM 设备路径和 PipeWire 采集端点,编码器延迟到第一帧到达时创建。
pub fn new(args: Args) -> Result<Self> {
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<bool> {
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<PathBuf> {
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 {