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
+213 -9
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)]