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:
+213
-9
@@ -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<PwEvent>,
|
||||
/// PipeWire 捕获线程的 JoinHandle,drop 时等待线程退出
|
||||
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 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)]
|
||||
|
||||
Reference in New Issue
Block a user