docs(sw_encode_bench): 中文注释软编码基准二进制

This commit is contained in:
dailz
2026-06-22 17:49:25 +08:00
parent 895b9aeb32
commit d94431bd1e
+155
View File
@@ -1,9 +1,40 @@
//! 软件编码流水线性能基准(独立二进制 `sw_encode_bench`)。
//!
//! ## 用途
//!
//! 测量"纯 CPU"屏幕采集编码流水线的端到端耗时,作为对照参考与 VAAPI 硬件编码
//! 基准 `vaapi_import_bench``src/bin/vaapi_import_bench.rs`)形成对比:
//! - 本文件:Portal 采集 → `mmap` 把 DMA-BUF 映射到用户态 → `sws_scale` 在 CPU
//! 上做 BGR0→YUV420P 颜色空间/缩放转换 → libx264/openh264 软件编码。
//! - 对照 `vaapi_import_bench.rs`Portal 采集 → `av_hwframe_map` 在 GPU 上做
//! 零拷贝格式转换 → VAAPI 硬件编码(GPU)。
//!
//! ## 输出
//!
//! 打印 mmap / sws_scale / encode 三段每帧平均耗时与总体 FPS,便于判断"软件路径"
//! 在当前硬件上能否达到 30 FPS 目标。AMD GPU 在某些驱动下不允许 CPU 读取 DMA-BUF
//! `mmap` 会失败——这正是 `vaapi_import_bench` 存在的意义。
//!
//! ## Rust ↔ Go 对照
//!
//! - `clap::Parser` derive 宏:类似 Go 的 `flag` 包,但在编译期生成解析代码。
//! - `std::time::Instant`:高精度单调时钟,等价于 Go 的 `time.Now()` + `time.Since()`。
//! - `crossbeam_channel::recv_timeout`:等价于 Go 的 `select { case <-time.After(): }`。
//! - 本文件大量使用裸 `unsafe` FFI 调用 FFmpeg C API;现有 21 处 unsafe 块均
//! 未标注 `// SAFETY:`,本任务也不补充,仅在每个 unsafe 块上方加普通 `//`
//! 中文概述,说明"为什么必须 unsafe"。
//!
//! 用法:`cargo run --bin sw_encode_bench -- --output /tmp/bench_test.mp4`
// sw_encode_bench.rs — Software encoding pipeline benchmark for screen capture // sw_encode_bench.rs — Software encoding pipeline benchmark for screen capture
// //
// Benchmarks: Portal capture -> mmap DMA-BUF -> sws_scale BGR0->YUV420P -> libx264 encode // Benchmarks: Portal capture -> mmap DMA-BUF -> sws_scale BGR0->YUV420P -> libx264 encode
// //
// Usage: cargo run --bin sw_encode_bench -- --output /tmp/bench_test.mp4 // Usage: cargo run --bin sw_encode_bench -- --output /tmp/bench_test.mp4
// 以下 `use` 语句分组:FFI 字符串/裸 fd 转换/路径/指针/计时 → anyhow/clap →
// ffmpeg_next 别名与 ffi → crate 内 Portal 采集器。Rust 没有 Go 的 "package"
// 概念,每个外部 crate 都要显式 `use`。
use std::ffi::CString; use std::ffi::CString;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use std::path::Path; use std::path::Path;
@@ -11,15 +42,24 @@ use std::ptr;
use std::time::Instant; use std::time::Instant;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
// `clap::Parser` derive 宏:编译期生成 CLI 解析代码,等价于 Go 的 `flag` 包
// 但支持子命令/类型转换/帮助文本自动生成。
use clap::Parser; use clap::Parser;
// FFmpeg 绑定,使用 `ffmpeg_next` crate(社区维护的 next 分支)。`as ff` 别名
// 缩短调用路径;`ffi` 子模块直接暴露 C ABI(裸指针、`AVFormatContext` 等)。
use ffmpeg_next as ff; use ffmpeg_next as ff;
use ffmpeg_next::ffi; use ffmpeg_next::ffi;
use ffmpeg_next::packet::Mut; use ffmpeg_next::packet::Mut;
// 复用主程序的 `Args` 与 Portal 采集器:基准与主二进制共享同一采集代码路径,
// 仅"消费方"不同(基准直接落盘,主程序走 WebRTC 推流)。
use wl_webrtc::args::Args; use wl_webrtc::args::Args;
use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent}; use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent};
// 基准 CLI 参数定义。`#[derive(Parser, Debug)]` 让 clap 在编译期为 struct
// 生成 `parse()` 方法;`#[command(...)]` 设置程序元信息。等价于 Go 程序的
// `flag.StringVar(...)` 序列,但在 Rust 里完全声明式。
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command( #[command(
name = "sw_encode_bench", name = "sw_encode_bench",
@@ -39,6 +79,9 @@ struct BenchArgs {
enc_height: u32, enc_height: u32,
} }
// 帧级耗时统计容器。每帧把 mmap/sws_scale/encode/total 的微秒数 push 进 Vec
// 结束后用 `avg_ms` 算平均值。这是"简单算术 + Vec"模式,比 streaming stats
// 复杂但能保留分布信息(虽然本基准只打印均值)。Go 类似 `[]int64`。
#[derive(Default)] #[derive(Default)]
struct FrameStats { struct FrameStats {
mmap_us: Vec<u64>, mmap_us: Vec<u64>,
@@ -48,7 +91,12 @@ struct FrameStats {
mmap_failures: u32, mmap_failures: u32,
} }
// 关联函数(不是 method——没有 `&self`/`&mut self` receiver),类似 Go 的
// package-level helper function。Rust 把它放在 `impl FrameStats` 内是组织习惯,
// 也可以写成自由函数 `fn avg_ms(...)`。
impl FrameStats { impl FrameStats {
// 把 Vec<u64> 求和后除以元素数得到微秒均值,再除以 1000 转毫秒。空 Vec
// 返回 0.0 避免除零。注意 Rust 这里 `as f64` 是显式转换(不像 Go 的隐式)。
fn avg_ms(data: &[u64]) -> f64 { fn avg_ms(data: &[u64]) -> f64 {
if data.is_empty() { if data.is_empty() {
return 0.0; return 0.0;
@@ -57,12 +105,18 @@ impl FrameStats {
} }
} }
// 把 `ffmpeg_next` 的高级 Pixel 枚举转换为 FFmpeg C API 期望的原始
// `AVPixelFormat`i32 别名)。`Into::into` 在此处零成本——编译期已知映射。
fn pix_fmt(p: ff::format::Pixel) -> ffi::AVPixelFormat { fn pix_fmt(p: ff::format::Pixel) -> ffi::AVPixelFormat {
Into::<ffi::AVPixelFormat>::into(p) Into::<ffi::AVPixelFormat>::into(p)
} }
// 从 Portal channel 拉取首帧:阻塞等待 PipeWire 推送 DMA-BUF。
// 同时监控控制 channel(流结束/格式变更/错误)。Go 类比:
// `for { select { case f := <-frameCh: return f; case <-time.After(10*time.Second): ... } }`
fn receive_first_frame(cap: &CapPortal) -> Result<wl_webrtc::cap_portal::PwDmaBufFrame> { fn receive_first_frame(cap: &CapPortal) -> Result<wl_webrtc::cap_portal::PwDmaBufFrame> {
loop { loop {
// `try_recv` 非阻塞地检查控制 channel 是否有事件(流结束/错误/格式变更)。
if let Ok(ctrl) = cap.event_receiver().try_recv() { if let Ok(ctrl) = cap.event_receiver().try_recv() {
match ctrl { match ctrl {
PwCtrlEvent::StreamEnded => bail!("PipeWire stream ended before first frame"), PwCtrlEvent::StreamEnded => bail!("PipeWire stream ended before first frame"),
@@ -70,6 +124,7 @@ fn receive_first_frame(cap: &CapPortal) -> Result<wl_webrtc::cap_portal::PwDmaBu
PwCtrlEvent::Error(e) => bail!("PipeWire error: {e}"), PwCtrlEvent::Error(e) => bail!("PipeWire error: {e}"),
} }
} }
// `recv_timeout` 阻塞最多 10s 等首帧。三路分支处理 Ok/Timeout/Disconnected。
match cap match cap
.frame_receiver() .frame_receiver()
.recv_timeout(std::time::Duration::from_secs(10)) .recv_timeout(std::time::Duration::from_secs(10))
@@ -85,7 +140,13 @@ fn receive_first_frame(cap: &CapPortal) -> Result<wl_webrtc::cap_portal::PwDmaBu
} }
} }
// 程序入口。流程四阶段:[1/4] 申请 Portal 授权并连接 PipeWire[2/4] 等首帧
// 拿到 DMA-BUF 元数据(宽高/stride/fd);[3/4] 试 mmap 一帧验证 CPU 可读;
// [4/4] 配置 libx264 编码器 + FFmpeg 输出格式上下文,进入主采集编码循环并打印统计。
// `anyhow::Result<()>` 把所有错误用 `?` 传播到 main 顶层——Rust 的 main 可以返回
// Result,运行时打印错误并退出码非零,类似 Go 1.0 时代 `log.Fatal` 的现代等价物。
fn main() -> Result<()> { fn main() -> Result<()> {
// clap 生成的 `BenchArgs::parse()` 解析 argv;类型不符直接 panic 退出。
let bench_args = BenchArgs::parse(); let bench_args = BenchArgs::parse();
println!("=== Software Encode Benchmark ==="); println!("=== Software Encode Benchmark ===");
@@ -97,11 +158,14 @@ fn main() -> Result<()> {
); );
println!(); println!();
// 初始化 FFmpeg 全局状态(注册编解码器、协议等)。`?` 在 Result 上传播错误。
ff::init()?; ff::init()?;
println!("[1/4] Requesting screen capture via XDG Portal..."); println!("[1/4] Requesting screen capture via XDG Portal...");
println!(" (Select a screen to share in the portal dialog)"); println!(" (Select a screen to share in the portal dialog)");
// 复用主二进制的 `Args` struct 来构造 Portal 请求;hw_accel="vaapi" 只是为了
// 走到 VAAPI 兼容的 DRM 设备路径(本基准并不会真正调用 VAAPI)。
let portal_args = Args { let portal_args = Args {
output: Some(bench_args.output.clone()), output: Some(bench_args.output.clone()),
output_name: None, output_name: None,
@@ -119,12 +183,15 @@ fn main() -> Result<()> {
stats: false, stats: false,
}; };
// `CapPortal::new` 会触发 XDG Portal 授权对话框(用户需要在屏幕共享对话框里选屏)。
let cap = CapPortal::new(&portal_args)?; let cap = CapPortal::new(&portal_args)?;
println!("[1/4] Portal connected, PipeWire stream active\n"); println!("[1/4] Portal connected, PipeWire stream active\n");
println!("[2/4] Waiting for first frame from PipeWire..."); println!("[2/4] Waiting for first frame from PipeWire...");
let first_frame = receive_first_frame(&cap)?; let first_frame = receive_first_frame(&cap)?;
// PipeWire 推来的首帧携带了 DMA-BUF 的元数据:fd(文件描述符)+ offset
// + stride(每行字节数)+ width/height/format。后续 mmap 就靠这些。
let src_width = first_frame.width; let src_width = first_frame.width;
let src_height = first_frame.height; let src_height = first_frame.height;
let src_stride = first_frame.stride; let src_stride = first_frame.stride;
@@ -142,6 +209,9 @@ fn main() -> Result<()> {
println!("[3/4] Testing mmap on DMA-BUF..."); println!("[3/4] Testing mmap on DMA-BUF...");
let mmap_size = (src_stride as usize) * (src_height as usize); let mmap_size = (src_stride as usize) * (src_height as usize);
// unsafe #1:调用 libc::mmap 把 DMA-BUF fd 映射到用户态地址空间。FFI 之所以
// 必须 unsafemmap 接受 void* 返回 raw 指针,编译器无法验证其有效性;
// 调用方必须保证 fd 真的是有效的 DMA-BUF 且 PROT_READ 权限匹配。
let mmap_ptr = unsafe { let mmap_ptr = unsafe {
libc::mmap( libc::mmap(
ptr::null_mut(), ptr::null_mut(),
@@ -153,6 +223,8 @@ fn main() -> Result<()> {
) )
}; };
// `MAP_FAILED` 是 mmap 失败的哨兵值(不是 NULL)。AMD 某些驱动禁止 CPU 读
// DMA-BUF,必须改用 VAAPI 硬件路径——这就是 `vaapi_import_bench.rs` 的意义。
if mmap_ptr == libc::MAP_FAILED { if mmap_ptr == libc::MAP_FAILED {
let errno = std::io::Error::last_os_error(); let errno = std::io::Error::last_os_error();
bail!( bail!(
@@ -173,6 +245,8 @@ fn main() -> Result<()> {
"[3/4] mmap SUCCESS — CPU can read DMA-BUF ({:.1} MB)\n", "[3/4] mmap SUCCESS — CPU can read DMA-BUF ({:.1} MB)\n",
mmap_size as f64 / 1024.0 / 1024.0 mmap_size as f64 / 1024.0 / 1024.0
); );
// unsafe #2:解除映射。FFI 调用必须 unsafe——libc::munmap 接受 raw pointer
// 编译期无法保证 ptr 真的来自之前 mmap 的同一区域(不匹配会 UB)。
unsafe { unsafe {
libc::munmap(mmap_ptr, mmap_size); libc::munmap(mmap_ptr, mmap_size);
} }
@@ -180,10 +254,15 @@ fn main() -> Result<()> {
// Set up libx264 encoder via FFI (same pattern as avhw.rs) // Set up libx264 encoder via FFI (same pattern as avhw.rs)
println!("[4/4] Setting up libx264 encoder..."); println!("[4/4] Setting up libx264 encoder...");
// 输出路径转 C 字符串(FFmpeg C API 期望 `const char*`,不接受 Rust &str)。
// CString 保证结尾有 NUL 字节,调用方必须保证字符串内部不含 NUL。
let output_path = Path::new(&bench_args.output); let output_path = Path::new(&bench_args.output);
let output_cstr = CString::new(output_path.to_str().unwrap())?; let output_cstr = CString::new(output_path.to_str().unwrap())?;
// Try libx264 first (best quality/speed), fall back to openh264 // Try libx264 first (best quality/speed), fall back to openh264
// 查找软件 H.264 编码器:优先 libx264(最快/质量最好),缺失则 fallback openh264。
// Rust 的 `or_else` + `ok_or_else` 是 Result/Option 链式习惯,类似 Go 的
// 多次 if err != nil 但不嵌套。
let codec = ff::encoder::find_by_name("libx264") let codec = ff::encoder::find_by_name("libx264")
.or_else(|| ff::encoder::find_by_name("libopenh264")) .or_else(|| ff::encoder::find_by_name("libopenh264"))
.ok_or_else(|| { .ok_or_else(|| {
@@ -191,11 +270,13 @@ fn main() -> Result<()> {
})?; })?;
println!("[4/4] Using encoder: {}\n", codec.name()); println!("[4/4] Using encoder: {}\n", codec.name());
// 创建 FFmpeg 编码器 Context 并提取 video encoder 句柄。`enc.open()` 会在后面调用。
let mut enc = { let mut enc = {
let ctx = ff::codec::Context::new_with_codec(codec); let ctx = ff::codec::Context::new_with_codec(codec);
ctx.encoder().video()? ctx.encoder().video()?
}; };
// 编码器基础参数:分辨率/像素格式/时基/GOP。`time_base = 1/60` 表示一帧 = 1/60 秒。
enc.set_width(enc_width); enc.set_width(enc_width);
enc.set_height(enc_height); enc.set_height(enc_height);
enc.set_format(ff::format::Pixel::YUV420P); enc.set_format(ff::format::Pixel::YUV420P);
@@ -205,6 +286,9 @@ fn main() -> Result<()> {
let codec_name = codec.name(); let codec_name = codec.name();
if codec_name == "libx264" { if codec_name == "libx264" {
// unsafe #3:调用 FFmpeg 的 `av_opt_set` 设置 libx264 的私有 preset/tune 选项。
// FFI 必须 unsafe:接受 `*const c_char` 裸指针,编译期无法验证指针指向有效内存,
// 也无法保证 priv_data 字段确实属于 libx264(其它编码器会 UB)。
unsafe { unsafe {
let key = CString::new("preset").unwrap(); let key = CString::new("preset").unwrap();
let val = CString::new("veryfast").unwrap(); let val = CString::new("veryfast").unwrap();
@@ -219,7 +303,11 @@ fn main() -> Result<()> {
let mut enc_video = opened.0; let mut enc_video = opened.0;
// Create output format context via FFI // Create output format context via FFI
// FFmpeg 输出格式上下文:根据文件扩展名(如 .mp4)自动推断容器。
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut(); let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
// unsafe #4`avformat_alloc_output_context2` 接受 out-pointer 模式(C 风格返回
// 指针的指针)。FFI 必须 unsafe:编译期无法验证 fmt_ctx_ptr 可写、不能保证
// 调用方传入了正确的容器格式猜测。
let ret = unsafe { let ret = unsafe {
ffi::avformat_alloc_output_context2( ffi::avformat_alloc_output_context2(
&mut fmt_ctx_ptr, &mut fmt_ctx_ptr,
@@ -232,21 +320,31 @@ fn main() -> Result<()> {
bail!("Failed to allocate output format context: error {ret}"); bail!("Failed to allocate output format context: error {ret}");
} }
// unsafe #5:在 fmt_ctx 内创建一条新流(mp4 容器内的一条视频 track)。
// 返回的 `stream_ptr` 是裸指针,调用方负责不 double-freeFFmpeg 内部托管)。
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) }; let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
if stream_ptr.is_null() { if stream_ptr.is_null() {
bail!("Failed to create new stream"); bail!("Failed to create new stream");
} }
// unsafe #6:把编码器参数(分辨率/时基/像素格式)拷贝到流的 codecpar 字段。
// FFmpeg C API 允许裸指针字段写入(`(*stream_ptr).codecpar`),编译期无法验证
// 两个上下文确实兼容(同 codec、同 pixel format),调用方需自己保证。
let ret = let ret =
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) }; unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
if ret < 0 { if ret < 0 {
bail!("Failed to copy encoder parameters: error {ret}"); bail!("Failed to copy encoder parameters: error {ret}");
} }
// unsafe #7:直接通过裸指针写字段:把编码器的 time_base 复制到流,避免后续
// mux 时再 rescale。FFI 必须 unsafe——`(*stream_ptr).time_base = ...` 是 C 风格
// 的指针解引用赋值,编译期无法验证 stream_ptr 仍存活。
unsafe { unsafe {
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base; (*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
} }
// unsafe #8`avio_open` 打开输出文件的 IO 上下文。FFI 必须 unsafe:编译期
// 无法验证 fmt_ctx_ptr->pb 字段可写、不能保证文件路径可写(运行时才报错)。
let ret = unsafe { let ret = unsafe {
ffi::avio_open( ffi::avio_open(
&mut (*fmt_ctx_ptr).pb, &mut (*fmt_ctx_ptr).pb,
@@ -261,17 +359,27 @@ fn main() -> Result<()> {
); );
} }
// unsafe #9:写容器头(mp4 的 ftyp box 等)。FFI 必须 unsafe:调用顺序约束
// (必须在 avio_open 之后、第一帧之前)由调用方维护,编译期不验证。
let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) }; let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) };
if ret < 0 { if ret < 0 {
bail!("Failed to write header: error {ret}"); bail!("Failed to write header: error {ret}");
} }
// unsafe #10`Output::wrap` 把 C 指针包装成 Rust 类型——FFI 边界。
// unsafe 必须:调用方保证 fmt_ctx_ptr 在此后由 Rust 独占管理(FFmpeg C 代码
// 不能再 free 它,否则 double-free)。这是 `unsafe impl Send` 在 avhw.rs 中
// 同款的"独占所有权"约定。
let mut octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) }; let mut octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
// Create sws_scale context: BGRZ (BGR0) -> YUV420P // Create sws_scale context: BGRZ (BGR0) -> YUV420P
// sws_scale 是 FFmpeg 的颜色空间转换器(CPU 软件)。本基准的"软件路径"核心:
// 把 DMA-BUF 的 BGR0 像素数据转成 libx264 期望的 YUV420P planar 格式。
let bgr0_fmt = pix_fmt(ff::format::Pixel::BGRZ); let bgr0_fmt = pix_fmt(ff::format::Pixel::BGRZ);
let yuv420p_fmt = pix_fmt(ff::format::Pixel::YUV420P); let yuv420p_fmt = pix_fmt(ff::format::Pixel::YUV420P);
// unsafe #11`sws_getContext` 创建转换器。FFI 必须 unsafe:返回 raw 指针,
// 调用方负责后续 `sws_freeContext` 释放(cleanup 阶段会做)。
let sws_ctx = unsafe { let sws_ctx = unsafe {
ffi::sws_getContext( ffi::sws_getContext(
src_width as i32, src_width as i32,
@@ -291,14 +399,20 @@ fn main() -> Result<()> {
} }
// Allocate reusable YUV frame // Allocate reusable YUV frame
// 预分配一个 YUV420P 帧,循环里反复写入(避免每帧 malloc)。FFmpeg C API 要求
// 显式 alloc/get_buffer/free 三步——Rust 端无法用 RAII 自动管理,必须 unsafe。
let mut yuv_frame = unsafe { let mut yuv_frame = unsafe {
// unsafe #12`av_frame_alloc` 只分配 struct 本体,不分配 data 缓冲区。
let mut f = ffi::av_frame_alloc(); let mut f = ffi::av_frame_alloc();
if f.is_null() { if f.is_null() {
bail!("av_frame_alloc failed"); bail!("av_frame_alloc failed");
} }
// unsafe #13:通过裸指针写入 width/height/format 字段。
(*f).width = enc_width as i32; (*f).width = enc_width as i32;
(*f).height = enc_height as i32; (*f).height = enc_height as i32;
(*f).format = yuv420p_fmt as i32; (*f).format = yuv420p_fmt as i32;
// unsafe #14`av_frame_get_buffer` 根据 width/height/format 分配实际像素缓冲区。
// 失败时必须 free 已分配的 struct(避免泄漏)。
let ret = ffi::av_frame_get_buffer(f, 0); let ret = ffi::av_frame_get_buffer(f, 0);
if ret < 0 { if ret < 0 {
ffi::av_frame_free(&mut f); ffi::av_frame_free(&mut f);
@@ -314,12 +428,16 @@ fn main() -> Result<()> {
println!("=== Encoding {} frames ===\n", bench_args.frames); println!("=== Encoding {} frames ===\n", bench_args.frames);
// 统计容器初始化。`Instant::now()` 是单调时钟(不受系统时间调整影响),
// 类比 Go 的 `time.Now()`,但 Rust 的 Instant 设计上不允许"墙上时钟"用途。
let mut stats = FrameStats::default(); let mut stats = FrameStats::default();
let total_start = Instant::now(); let total_start = Instant::now();
let mut frames_encoded: u32 = 0; let mut frames_encoded: u32 = 0;
let mut pts: i64 = 0; let mut pts: i64 = 0;
// 主采集编码循环:每帧从 PipeWire 拉帧 → mmap → sws_scale → send_frame → drain。
while frames_encoded < bench_args.frames { while frames_encoded < bench_args.frames {
// 控制通道优先检查(流结束/错误)。`try_recv` 非阻塞返回 Result<Option<T>>。
if let Ok(ctrl) = cap.event_receiver().try_recv() { if let Ok(ctrl) = cap.event_receiver().try_recv() {
match ctrl { match ctrl {
PwCtrlEvent::StreamEnded => { PwCtrlEvent::StreamEnded => {
@@ -334,6 +452,7 @@ fn main() -> Result<()> {
} }
} }
// 5s 超时拉帧。任何错误(超时/断开)都视为流终止,跳出循环。
let frame = match cap let frame = match cap
.frame_receiver() .frame_receiver()
.recv_timeout(std::time::Duration::from_secs(5)) .recv_timeout(std::time::Duration::from_secs(5))
@@ -345,10 +464,14 @@ fn main() -> Result<()> {
} }
}; };
// 帧级别计时:本轮 mmap/scale/encode 的总耗时统计锚点。
let frame_start = Instant::now(); let frame_start = Instant::now();
// ---- 第 1 段:mmap DMA-BUF 到用户态 ----
let mmap_start = Instant::now(); let mmap_start = Instant::now();
let frame_size = (frame.stride as usize) * (frame.height as usize); let frame_size = (frame.stride as usize) * (frame.height as usize);
// unsafe #15:与首帧的 mmap 同语义——把 PipeWire 推来的 DMA-BUF fd 映射到
// 用户态。每帧都重新 mmap 是因为 fd 可能切换(Portal 可能用 buffer pool)。
let mmap_ptr = unsafe { let mmap_ptr = unsafe {
libc::mmap( libc::mmap(
ptr::null_mut(), ptr::null_mut(),
@@ -368,9 +491,16 @@ fn main() -> Result<()> {
} }
stats.mmap_us.push(mmap_start.elapsed().as_micros() as u64); stats.mmap_us.push(mmap_start.elapsed().as_micros() as u64);
// ---- 第 2 段:sws_scale BGR0 → YUV420P ----
let scale_start = Instant::now(); let scale_start = Instant::now();
// unsafe #16`slice::from_raw_parts` 把裸指针+长度包成 Rust slice。
// 这是 Rust 最危险的 unsafe 之一:编译期无法验证 (ptr, len) 真的指向
// 有效内存、对齐正确、与 aliasing 规则兼容(不允许其它 &mut 同时存活)。
let src_data = unsafe { std::slice::from_raw_parts(mmap_ptr as *const u8, frame_size) }; let src_data = unsafe { std::slice::from_raw_parts(mmap_ptr as *const u8, frame_size) };
// unsafe #17:调用 FFmpeg 的 sws_scale 做颜色空间转换。三个 FFI 风险:
// (1) 裸指针 src_ptr / src_linesize(2) yuv_frame->data/linesize 数组
// 必须有效;(3) sws_ctx 必须与 src/dst 像素格式匹配(不匹配会 UB)。
unsafe { unsafe {
ffi::av_frame_make_writable(yuv_frame); ffi::av_frame_make_writable(yuv_frame);
@@ -391,13 +521,18 @@ fn main() -> Result<()> {
.scale_us .scale_us
.push(scale_start.elapsed().as_micros() as u64); .push(scale_start.elapsed().as_micros() as u64);
// unsafe #18:解除本帧的 mmap。FFI 必须 unsafe——ptr 必须仍是之前 mmap 的返回值。
unsafe { unsafe {
libc::munmap(mmap_ptr, frame_size); libc::munmap(mmap_ptr, frame_size);
} }
drop(frame); drop(frame);
// ---- 第 3 段:libx264 编码 ----
let encode_start = Instant::now(); let encode_start = Instant::now();
// unsafe #19`avcodec_send_frame` 把一帧 YUV 喂给编码器(异步:内部入队)。
// FFI 必须 unsafe:裸指针 enc_video.as_mut_ptr()/yuv_frame;编译期无法
// 验证 enc 已 open、yuv_frame 的 width/height/format 与编码器配置一致。
unsafe { unsafe {
(*yuv_frame).pts = pts; (*yuv_frame).pts = pts;
pts += 1; pts += 1;
@@ -431,6 +566,8 @@ fn main() -> Result<()> {
let total_elapsed = total_start.elapsed(); let total_elapsed = total_start.elapsed();
println!("\nFlushing encoder..."); println!("\nFlushing encoder...");
// unsafe #20:发 NULL frame 表示"flush"——编码器吐出剩余的延迟帧(B-frame 等)。
// 本基准 max_b_frames=0 所以没有延迟帧,但调用约定必须保留。
unsafe { unsafe {
ffi::avcodec_send_frame(enc_video.as_mut_ptr(), ptr::null()); ffi::avcodec_send_frame(enc_video.as_mut_ptr(), ptr::null());
} }
@@ -440,6 +577,8 @@ fn main() -> Result<()> {
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?; .map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
// Cleanup // Cleanup
// unsafe #21:手动释放 yuv_frame 与 sws_ctx。FFmpeg C API 不支持 RAII
// 必须显式 free,否则内存泄漏。`as *mut _` 是为了取 *mut *mut AVFrame 引用。
unsafe { unsafe {
ffi::av_frame_free(&mut yuv_frame as *mut _); ffi::av_frame_free(&mut yuv_frame as *mut _);
ffi::sws_freeContext(sws_ctx); ffi::sws_freeContext(sws_ctx);
@@ -448,6 +587,8 @@ fn main() -> Result<()> {
drop(cap); drop(cap);
// Print results // Print results
// 结果汇总:把 mmap/scale/encode 三段均值 + 总 FPS 打印成表格。Go 类比
// `fmt.Printf`——Rust println! 是宏不是函数,编译期检查参数。
let mmap_count = stats.mmap_us.len() as u32; let mmap_count = stats.mmap_us.len() as u32;
let mmap_success_rate = if mmap_count + stats.mmap_failures > 0 { let mmap_success_rate = if mmap_count + stats.mmap_failures > 0 {
mmap_count as f64 / (mmap_count + stats.mmap_failures) as f64 * 100.0 mmap_count as f64 / (mmap_count + stats.mmap_failures) as f64 * 100.0
@@ -456,6 +597,7 @@ fn main() -> Result<()> {
}; };
let total_fps = frames_encoded as f64 / total_elapsed.as_secs_f64(); let total_fps = frames_encoded as f64 / total_elapsed.as_secs_f64();
let avg_total_ms = FrameStats::avg_ms(&stats.total_us); let avg_total_ms = FrameStats::avg_ms(&stats.total_us);
// 最大理论 FPS = 1000ms / 每帧均耗时。avg_total_ms 为 0 时跳过避免除零。
let max_fps = if avg_total_ms > 0.0 { let max_fps = if avg_total_ms > 0.0 {
1000.0 / avg_total_ms 1000.0 / avg_total_ms
} else { } else {
@@ -520,14 +662,21 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
// 从编码器 drain(抽取)已经编码好的压缩包并写入输出容器。FFmpeg 编码 API 是
// 异步的:`avcodec_send_frame` 入队原始帧,`avcodec_receive_packet` 出队 H.264
// NAL;可能 send 一帧后 receive 多包(关键帧场景),也可能 receive 返回 EAGAIN
// (编码器内部还在缓冲)。Go 类比:双 channel + select 循环,先收再吐。
fn drain_encoder( fn drain_encoder(
enc_video: &mut ff::encoder::video::Video, enc_video: &mut ff::encoder::video::Video,
octx: &mut ff::format::context::Output, octx: &mut ff::format::context::Output,
) -> Result<()> { ) -> Result<()> {
loop { loop {
let mut pkt = ff::Packet::empty(); let mut pkt = ff::Packet::empty();
// unsafe #22`avcodec_receive_packet` 出队一个 H.264 压缩包到 pkt。FFI 必须
// unsafe:编译期无法验证 enc_video 已 open、pkt.as_mut_ptr() 真指向空 packet。
let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) }; let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) };
if ret < 0 { if ret < 0 {
// EAGAIN = 暂时没有更多包可吐(需要再 send);EOF = flush 完成。两者都退出。
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF { if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
break; break;
} }
@@ -536,13 +685,19 @@ fn drain_encoder(
} }
let enc_tb = enc_video.time_base(); let enc_tb = enc_video.time_base();
// unsafe #23:从 `(*octx.as_ptr()).streams` 取第一条流的 time_base,用于
// rescale 时间戳。FFI 必须 unsafe——裸指针 + `*streams.add(0)` 假定 streams
// 数组至少有一项(fmt_ctx 已注册至少一条流,否则前面 avformat_new_stream
// 就 bail 了)。
let stream_tb = unsafe { let stream_tb = unsafe {
let streams = (*octx.as_ptr()).streams; let streams = (*octx.as_ptr()).streams;
let st = *streams.add(0); let st = *streams.add(0);
ff::Rational::from((*st).time_base) ff::Rational::from((*st).time_base)
}; };
// 把 PTS 从编码器时基 rescale 到流时基(mp4 容器要求)。Go 类比:单位换算。
pkt.rescale_ts(enc_tb, stream_tb); pkt.rescale_ts(enc_tb, stream_tb);
pkt.set_stream(0); pkt.set_stream(0);
// `write_interleaved` 让 FFmpeg 自动处理 interleaving(音视频交错,避免 demuxer 卡)。
pkt.write_interleaved(octx) pkt.write_interleaved(octx)
.map_err(|e| anyhow::anyhow!("write packet failed: {e}"))?; .map_err(|e| anyhow::anyhow!("write packet failed: {e}"))?;
} }