docs(sw_encode_bench): 中文注释软编码基准二进制
This commit is contained in:
@@ -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
|
||||
//
|
||||
// Benchmarks: Portal capture -> mmap DMA-BUF -> sws_scale BGR0->YUV420P -> libx264 encode
|
||||
//
|
||||
// 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::os::fd::AsRawFd;
|
||||
use std::path::Path;
|
||||
@@ -11,15 +42,24 @@ use std::ptr;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
// `clap::Parser` derive 宏:编译期生成 CLI 解析代码,等价于 Go 的 `flag` 包
|
||||
// 但支持子命令/类型转换/帮助文本自动生成。
|
||||
use clap::Parser;
|
||||
|
||||
// FFmpeg 绑定,使用 `ffmpeg_next` crate(社区维护的 next 分支)。`as ff` 别名
|
||||
// 缩短调用路径;`ffi` 子模块直接暴露 C ABI(裸指针、`AVFormatContext` 等)。
|
||||
use ffmpeg_next as ff;
|
||||
use ffmpeg_next::ffi;
|
||||
use ffmpeg_next::packet::Mut;
|
||||
|
||||
// 复用主程序的 `Args` 与 Portal 采集器:基准与主二进制共享同一采集代码路径,
|
||||
// 仅"消费方"不同(基准直接落盘,主程序走 WebRTC 推流)。
|
||||
use wl_webrtc::args::Args;
|
||||
use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent};
|
||||
|
||||
// 基准 CLI 参数定义。`#[derive(Parser, Debug)]` 让 clap 在编译期为 struct
|
||||
// 生成 `parse()` 方法;`#[command(...)]` 设置程序元信息。等价于 Go 程序的
|
||||
// `flag.StringVar(...)` 序列,但在 Rust 里完全声明式。
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "sw_encode_bench",
|
||||
@@ -39,6 +79,9 @@ struct BenchArgs {
|
||||
enc_height: u32,
|
||||
}
|
||||
|
||||
// 帧级耗时统计容器。每帧把 mmap/sws_scale/encode/total 的微秒数 push 进 Vec,
|
||||
// 结束后用 `avg_ms` 算平均值。这是"简单算术 + Vec"模式,比 streaming stats
|
||||
// 复杂但能保留分布信息(虽然本基准只打印均值)。Go 类似 `[]int64`。
|
||||
#[derive(Default)]
|
||||
struct FrameStats {
|
||||
mmap_us: Vec<u64>,
|
||||
@@ -48,7 +91,12 @@ struct FrameStats {
|
||||
mmap_failures: u32,
|
||||
}
|
||||
|
||||
// 关联函数(不是 method——没有 `&self`/`&mut self` receiver),类似 Go 的
|
||||
// package-level helper function。Rust 把它放在 `impl FrameStats` 内是组织习惯,
|
||||
// 也可以写成自由函数 `fn avg_ms(...)`。
|
||||
impl FrameStats {
|
||||
// 把 Vec<u64> 求和后除以元素数得到微秒均值,再除以 1000 转毫秒。空 Vec
|
||||
// 返回 0.0 避免除零。注意 Rust 这里 `as f64` 是显式转换(不像 Go 的隐式)。
|
||||
fn avg_ms(data: &[u64]) -> f64 {
|
||||
if data.is_empty() {
|
||||
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 {
|
||||
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> {
|
||||
loop {
|
||||
// `try_recv` 非阻塞地检查控制 channel 是否有事件(流结束/错误/格式变更)。
|
||||
if let Ok(ctrl) = cap.event_receiver().try_recv() {
|
||||
match ctrl {
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
// `recv_timeout` 阻塞最多 10s 等首帧。三路分支处理 Ok/Timeout/Disconnected。
|
||||
match cap
|
||||
.frame_receiver()
|
||||
.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<()> {
|
||||
// clap 生成的 `BenchArgs::parse()` 解析 argv;类型不符直接 panic 退出。
|
||||
let bench_args = BenchArgs::parse();
|
||||
|
||||
println!("=== Software Encode Benchmark ===");
|
||||
@@ -97,11 +158,14 @@ fn main() -> Result<()> {
|
||||
);
|
||||
println!();
|
||||
|
||||
// 初始化 FFmpeg 全局状态(注册编解码器、协议等)。`?` 在 Result 上传播错误。
|
||||
ff::init()?;
|
||||
|
||||
println!("[1/4] Requesting screen capture via XDG Portal...");
|
||||
println!(" (Select a screen to share in the portal dialog)");
|
||||
|
||||
// 复用主二进制的 `Args` struct 来构造 Portal 请求;hw_accel="vaapi" 只是为了
|
||||
// 走到 VAAPI 兼容的 DRM 设备路径(本基准并不会真正调用 VAAPI)。
|
||||
let portal_args = Args {
|
||||
output: Some(bench_args.output.clone()),
|
||||
output_name: None,
|
||||
@@ -119,12 +183,15 @@ fn main() -> Result<()> {
|
||||
stats: false,
|
||||
};
|
||||
|
||||
// `CapPortal::new` 会触发 XDG Portal 授权对话框(用户需要在屏幕共享对话框里选屏)。
|
||||
let cap = CapPortal::new(&portal_args)?;
|
||||
println!("[1/4] Portal connected, PipeWire stream active\n");
|
||||
|
||||
println!("[2/4] Waiting for first frame from PipeWire...");
|
||||
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_height = first_frame.height;
|
||||
let src_stride = first_frame.stride;
|
||||
@@ -142,6 +209,9 @@ fn main() -> Result<()> {
|
||||
|
||||
println!("[3/4] Testing mmap on DMA-BUF...");
|
||||
let mmap_size = (src_stride as usize) * (src_height as usize);
|
||||
// unsafe #1:调用 libc::mmap 把 DMA-BUF fd 映射到用户态地址空间。FFI 之所以
|
||||
// 必须 unsafe:mmap 接受 void* 返回 raw 指针,编译器无法验证其有效性;
|
||||
// 调用方必须保证 fd 真的是有效的 DMA-BUF 且 PROT_READ 权限匹配。
|
||||
let mmap_ptr = unsafe {
|
||||
libc::mmap(
|
||||
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 {
|
||||
let errno = std::io::Error::last_os_error();
|
||||
bail!(
|
||||
@@ -173,6 +245,8 @@ fn main() -> Result<()> {
|
||||
"[3/4] mmap SUCCESS — CPU can read DMA-BUF ({:.1} MB)\n",
|
||||
mmap_size as f64 / 1024.0 / 1024.0
|
||||
);
|
||||
// unsafe #2:解除映射。FFI 调用必须 unsafe——libc::munmap 接受 raw pointer,
|
||||
// 编译期无法保证 ptr 真的来自之前 mmap 的同一区域(不匹配会 UB)。
|
||||
unsafe {
|
||||
libc::munmap(mmap_ptr, mmap_size);
|
||||
}
|
||||
@@ -180,10 +254,15 @@ fn main() -> Result<()> {
|
||||
|
||||
// Set up libx264 encoder via FFI (same pattern as avhw.rs)
|
||||
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_cstr = CString::new(output_path.to_str().unwrap())?;
|
||||
|
||||
// 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")
|
||||
.or_else(|| ff::encoder::find_by_name("libopenh264"))
|
||||
.ok_or_else(|| {
|
||||
@@ -191,11 +270,13 @@ fn main() -> Result<()> {
|
||||
})?;
|
||||
println!("[4/4] Using encoder: {}\n", codec.name());
|
||||
|
||||
// 创建 FFmpeg 编码器 Context 并提取 video encoder 句柄。`enc.open()` 会在后面调用。
|
||||
let mut enc = {
|
||||
let ctx = ff::codec::Context::new_with_codec(codec);
|
||||
ctx.encoder().video()?
|
||||
};
|
||||
|
||||
// 编码器基础参数:分辨率/像素格式/时基/GOP。`time_base = 1/60` 表示一帧 = 1/60 秒。
|
||||
enc.set_width(enc_width);
|
||||
enc.set_height(enc_height);
|
||||
enc.set_format(ff::format::Pixel::YUV420P);
|
||||
@@ -205,6 +286,9 @@ fn main() -> Result<()> {
|
||||
|
||||
let codec_name = codec.name();
|
||||
if codec_name == "libx264" {
|
||||
// unsafe #3:调用 FFmpeg 的 `av_opt_set` 设置 libx264 的私有 preset/tune 选项。
|
||||
// FFI 必须 unsafe:接受 `*const c_char` 裸指针,编译期无法验证指针指向有效内存,
|
||||
// 也无法保证 priv_data 字段确实属于 libx264(其它编码器会 UB)。
|
||||
unsafe {
|
||||
let key = CString::new("preset").unwrap();
|
||||
let val = CString::new("veryfast").unwrap();
|
||||
@@ -219,7 +303,11 @@ fn main() -> Result<()> {
|
||||
let mut enc_video = opened.0;
|
||||
|
||||
// Create output format context via FFI
|
||||
// FFmpeg 输出格式上下文:根据文件扩展名(如 .mp4)自动推断容器。
|
||||
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 {
|
||||
ffi::avformat_alloc_output_context2(
|
||||
&mut fmt_ctx_ptr,
|
||||
@@ -232,21 +320,31 @@ fn main() -> Result<()> {
|
||||
bail!("Failed to allocate output format context: error {ret}");
|
||||
}
|
||||
|
||||
// unsafe #5:在 fmt_ctx 内创建一条新流(mp4 容器内的一条视频 track)。
|
||||
// 返回的 `stream_ptr` 是裸指针,调用方负责不 double-free(FFmpeg 内部托管)。
|
||||
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
|
||||
if stream_ptr.is_null() {
|
||||
bail!("Failed to create new stream");
|
||||
}
|
||||
|
||||
// unsafe #6:把编码器参数(分辨率/时基/像素格式)拷贝到流的 codecpar 字段。
|
||||
// FFmpeg C API 允许裸指针字段写入(`(*stream_ptr).codecpar`),编译期无法验证
|
||||
// 两个上下文确实兼容(同 codec、同 pixel format),调用方需自己保证。
|
||||
let ret =
|
||||
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
|
||||
if ret < 0 {
|
||||
bail!("Failed to copy encoder parameters: error {ret}");
|
||||
}
|
||||
|
||||
// unsafe #7:直接通过裸指针写字段:把编码器的 time_base 复制到流,避免后续
|
||||
// mux 时再 rescale。FFI 必须 unsafe——`(*stream_ptr).time_base = ...` 是 C 风格
|
||||
// 的指针解引用赋值,编译期无法验证 stream_ptr 仍存活。
|
||||
unsafe {
|
||||
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
||||
}
|
||||
|
||||
// unsafe #8:`avio_open` 打开输出文件的 IO 上下文。FFI 必须 unsafe:编译期
|
||||
// 无法验证 fmt_ctx_ptr->pb 字段可写、不能保证文件路径可写(运行时才报错)。
|
||||
let ret = unsafe {
|
||||
ffi::avio_open(
|
||||
&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()) };
|
||||
if ret < 0 {
|
||||
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) };
|
||||
|
||||
// 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 yuv420p_fmt = pix_fmt(ff::format::Pixel::YUV420P);
|
||||
|
||||
// unsafe #11:`sws_getContext` 创建转换器。FFI 必须 unsafe:返回 raw 指针,
|
||||
// 调用方负责后续 `sws_freeContext` 释放(cleanup 阶段会做)。
|
||||
let sws_ctx = unsafe {
|
||||
ffi::sws_getContext(
|
||||
src_width as i32,
|
||||
@@ -291,14 +399,20 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
// Allocate reusable YUV frame
|
||||
// 预分配一个 YUV420P 帧,循环里反复写入(避免每帧 malloc)。FFmpeg C API 要求
|
||||
// 显式 alloc/get_buffer/free 三步——Rust 端无法用 RAII 自动管理,必须 unsafe。
|
||||
let mut yuv_frame = unsafe {
|
||||
// unsafe #12:`av_frame_alloc` 只分配 struct 本体,不分配 data 缓冲区。
|
||||
let mut f = ffi::av_frame_alloc();
|
||||
if f.is_null() {
|
||||
bail!("av_frame_alloc failed");
|
||||
}
|
||||
// unsafe #13:通过裸指针写入 width/height/format 字段。
|
||||
(*f).width = enc_width as i32;
|
||||
(*f).height = enc_height 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);
|
||||
if ret < 0 {
|
||||
ffi::av_frame_free(&mut f);
|
||||
@@ -314,12 +428,16 @@ fn main() -> Result<()> {
|
||||
|
||||
println!("=== Encoding {} frames ===\n", bench_args.frames);
|
||||
|
||||
// 统计容器初始化。`Instant::now()` 是单调时钟(不受系统时间调整影响),
|
||||
// 类比 Go 的 `time.Now()`,但 Rust 的 Instant 设计上不允许"墙上时钟"用途。
|
||||
let mut stats = FrameStats::default();
|
||||
let total_start = Instant::now();
|
||||
let mut frames_encoded: u32 = 0;
|
||||
let mut pts: i64 = 0;
|
||||
|
||||
// 主采集编码循环:每帧从 PipeWire 拉帧 → mmap → sws_scale → send_frame → drain。
|
||||
while frames_encoded < bench_args.frames {
|
||||
// 控制通道优先检查(流结束/错误)。`try_recv` 非阻塞返回 Result<Option<T>>。
|
||||
if let Ok(ctrl) = cap.event_receiver().try_recv() {
|
||||
match ctrl {
|
||||
PwCtrlEvent::StreamEnded => {
|
||||
@@ -334,6 +452,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// 5s 超时拉帧。任何错误(超时/断开)都视为流终止,跳出循环。
|
||||
let frame = match cap
|
||||
.frame_receiver()
|
||||
.recv_timeout(std::time::Duration::from_secs(5))
|
||||
@@ -345,10 +464,14 @@ fn main() -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
// 帧级别计时:本轮 mmap/scale/encode 的总耗时统计锚点。
|
||||
let frame_start = Instant::now();
|
||||
|
||||
// ---- 第 1 段:mmap DMA-BUF 到用户态 ----
|
||||
let mmap_start = Instant::now();
|
||||
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 {
|
||||
libc::mmap(
|
||||
ptr::null_mut(),
|
||||
@@ -368,9 +491,16 @@ fn main() -> Result<()> {
|
||||
}
|
||||
stats.mmap_us.push(mmap_start.elapsed().as_micros() as u64);
|
||||
|
||||
// ---- 第 2 段:sws_scale BGR0 → YUV420P ----
|
||||
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) };
|
||||
|
||||
// unsafe #17:调用 FFmpeg 的 sws_scale 做颜色空间转换。三个 FFI 风险:
|
||||
// (1) 裸指针 src_ptr / src_linesize;(2) yuv_frame->data/linesize 数组
|
||||
// 必须有效;(3) sws_ctx 必须与 src/dst 像素格式匹配(不匹配会 UB)。
|
||||
unsafe {
|
||||
ffi::av_frame_make_writable(yuv_frame);
|
||||
|
||||
@@ -391,13 +521,18 @@ fn main() -> Result<()> {
|
||||
.scale_us
|
||||
.push(scale_start.elapsed().as_micros() as u64);
|
||||
|
||||
// unsafe #18:解除本帧的 mmap。FFI 必须 unsafe——ptr 必须仍是之前 mmap 的返回值。
|
||||
unsafe {
|
||||
libc::munmap(mmap_ptr, frame_size);
|
||||
}
|
||||
drop(frame);
|
||||
|
||||
// ---- 第 3 段:libx264 编码 ----
|
||||
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 {
|
||||
(*yuv_frame).pts = pts;
|
||||
pts += 1;
|
||||
@@ -431,6 +566,8 @@ fn main() -> Result<()> {
|
||||
let total_elapsed = total_start.elapsed();
|
||||
|
||||
println!("\nFlushing encoder...");
|
||||
// unsafe #20:发 NULL frame 表示"flush"——编码器吐出剩余的延迟帧(B-frame 等)。
|
||||
// 本基准 max_b_frames=0 所以没有延迟帧,但调用约定必须保留。
|
||||
unsafe {
|
||||
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}"))?;
|
||||
|
||||
// Cleanup
|
||||
// unsafe #21:手动释放 yuv_frame 与 sws_ctx。FFmpeg C API 不支持 RAII,
|
||||
// 必须显式 free,否则内存泄漏。`as *mut _` 是为了取 *mut *mut AVFrame 引用。
|
||||
unsafe {
|
||||
ffi::av_frame_free(&mut yuv_frame as *mut _);
|
||||
ffi::sws_freeContext(sws_ctx);
|
||||
@@ -448,6 +587,8 @@ fn main() -> Result<()> {
|
||||
drop(cap);
|
||||
|
||||
// Print results
|
||||
// 结果汇总:把 mmap/scale/encode 三段均值 + 总 FPS 打印成表格。Go 类比
|
||||
// `fmt.Printf`——Rust println! 是宏不是函数,编译期检查参数。
|
||||
let mmap_count = stats.mmap_us.len() as u32;
|
||||
let mmap_success_rate = if mmap_count + stats.mmap_failures > 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 avg_total_ms = FrameStats::avg_ms(&stats.total_us);
|
||||
// 最大理论 FPS = 1000ms / 每帧均耗时。avg_total_ms 为 0 时跳过避免除零。
|
||||
let max_fps = if avg_total_ms > 0.0 {
|
||||
1000.0 / avg_total_ms
|
||||
} else {
|
||||
@@ -520,14 +662,21 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 从编码器 drain(抽取)已经编码好的压缩包并写入输出容器。FFmpeg 编码 API 是
|
||||
// 异步的:`avcodec_send_frame` 入队原始帧,`avcodec_receive_packet` 出队 H.264
|
||||
// NAL;可能 send 一帧后 receive 多包(关键帧场景),也可能 receive 返回 EAGAIN
|
||||
// (编码器内部还在缓冲)。Go 类比:双 channel + select 循环,先收再吐。
|
||||
fn drain_encoder(
|
||||
enc_video: &mut ff::encoder::video::Video,
|
||||
octx: &mut ff::format::context::Output,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
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()) };
|
||||
if ret < 0 {
|
||||
// EAGAIN = 暂时没有更多包可吐(需要再 send);EOF = flush 完成。两者都退出。
|
||||
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
|
||||
break;
|
||||
}
|
||||
@@ -536,13 +685,19 @@ fn drain_encoder(
|
||||
}
|
||||
|
||||
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 streams = (*octx.as_ptr()).streams;
|
||||
let st = *streams.add(0);
|
||||
ff::Rational::from((*st).time_base)
|
||||
};
|
||||
// 把 PTS 从编码器时基 rescale 到流时基(mp4 容器要求)。Go 类比:单位换算。
|
||||
pkt.rescale_ts(enc_tb, stream_tb);
|
||||
pkt.set_stream(0);
|
||||
// `write_interleaved` 让 FFmpeg 自动处理 interleaving(音视频交错,避免 demuxer 卡)。
|
||||
pkt.write_interleaved(octx)
|
||||
.map_err(|e| anyhow::anyhow!("write packet failed: {e}"))?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user