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:
@@ -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)]
|
||||
|
||||
151
src/main.rs
151
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::<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 上使用 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(())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user