From 895b9aeb32e146b631e7dcbcdd5d69732557fd7e Mon Sep 17 00:00:00 2001 From: dailz Date: Mon, 22 Jun 2026 17:48:59 +0800 Subject: [PATCH] =?UTF-8?q?docs(vaapi=5Fimport=5Fbench):=20[1/2]=20?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E6=B3=A8=E9=87=8A=E5=9F=BA=E5=87=86=20setup?= =?UTF-8?q?=20=E4=B8=8E=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bin/vaapi_import_bench.rs | 168 ++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/src/bin/vaapi_import_bench.rs b/src/bin/vaapi_import_bench.rs index 675a05e..14ffd33 100644 --- a/src/bin/vaapi_import_bench.rs +++ b/src/bin/vaapi_import_bench.rs @@ -1,48 +1,99 @@ +//! # vaapi_import_bench — VAAPI DMA-BUF 导入性能基准 +//! +//! 本文件是 wl-webrtc 项目下的**独立可执行二进制**(位于 `src/bin/`),用于离线 +//! 测量 "Portal 屏幕捕获 → DMA-BUF 导入到 VAAPI 硬件帧 → GPU 下采样 → 编码" 这条 +//! 关键流水线的端到端耗时,并与 "CPU 软编" 路径作对比,输出每阶段平均毫秒数与 FPS。 +//! +//! ## 流水线 +//! +//! - **CPU 路径**:PipeWire BGRA 帧 → `sws_scale` 缩放 → libx264/libopenh264 软编 +//! - **GPU 路径**:PipeWire DMA-BUF → `av_hwframe_map` → `scale_vaapi` 滤镜 → VAAPI H.264 +//! +//! ## 与 Go benchmark 的类比 +//! +//! 类似 Go 的 `testing.B`:先跑预热帧,再用 `Instant::now()` / `Duration::as_micros()` +//! 采集每个阶段的耗时(导入、缩放、传输、编码),最后输出 `FrameStats` 平均值。 +//! +//! ## 用法 +//! +//! ```bash +//! cargo run --bin vaapi_import_bench -- --output /tmp/vaapi_bench.mp4 +//! cargo run --bin vaapi_import_bench -- --output /dev/null --mode gpu +//! cargo run --bin vaapi_import_bench -- --output /tmp/cpu.mp4 --mode cpu --frames 120 +//! ``` +//! +//! 详见 `AGENTS.md` 的 "Useful manual commands" 章节。 + // vaapi_import_bench.rs — VAAPI DMA-BUF import + GPU-side downscale benchmark // // Tests: Portal capture -> av_hwframe_map (ARGB sw_format) -> transfer -> sw encode // // Usage: cargo run --bin vaapi_import_bench -- --output /tmp/vaapi_bench.mp4 +// ===== 标准库导入 ===== +// CString:FFI 传递给 C 函数的 NUL 结尾字符串;类比 Go 中显式末尾 0 的 []byte +// AsRawFd trait:把 Rust 的 OwnedFd 暴露为原始 int fd(用于 DMA-BUF 导入) +// Path:跨平台路径类型;类比 Go filepath +// ptr:FFI 裸指针工具(ptr::null_mut()、ptr::null()),类比 Go unsafe.Pointer(nil) +// Instant:高精度单调时钟;类比 Go time.Now(),用 elapsed() 取差值 use std::ffi::CString; use std::os::fd::AsRawFd; use std::path::Path; use std::ptr; use std::time::Instant; +// ===== 第三方 crate ===== +// anyhow:Result = Result;bail! 宏提前返回 Err;类比 Go (T, error) +// clap:CLI 参数解析(Derive 宏);本文件 BenchArgs 与 args.rs Args 都用此模式 use anyhow::{bail, Result}; use clap::{Parser, ValueEnum}; +// ffmpeg_next:FFmpeg 绑定。ffi 子模块是 raw C FFI(含 unsafe),其余为高层封装 +// packet::Mut trait:提供 as_mut_ptr(),用于拿到 AVPacket* 喂给 C API use ffmpeg_next as ff; use ffmpeg_next::ffi; use ffmpeg_next::packet::Mut; +// 从本 crate (wl-webrtc) 复用:CLI Args、VAAPI 上下文、Portal 捕获 use wl_webrtc::args::Args; use wl_webrtc::avhw::{import_dma_buf_to_vaapi, AvHwDevCtx, AvHwFrameCtx}; use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent}; +/// 基准测试的 CLI 参数。Derive `Parser` 后 `BenchArgs::parse()` 即可从 argv 解析; +/// 类比 Go 中 `flag.StringVar` + `flag.Parse()`,但 Rust 用编译期宏生成代码。 +/// +/// 注意:与生产二进制 `wl-webrtc` 的 `Args`(见 `src/args.rs`)不同——这里是基准专用 +/// 参数集(更细粒度的 enc_width/enc_height/mode),不复用 `Args`。 #[derive(Parser, Debug)] #[command(name = "vaapi_import_bench", about = "VAAPI DMA-BUF import benchmark")] struct BenchArgs { + // 输出文件路径。如果包含 "null" 子串则使用 FFmpeg 的 null muxer(不写盘,只测编码耗时) #[arg(short, long)] output: String, + // 总编码帧数;类比 Go benchmark 的 b.N,但这里是固定值(默认 60 帧) #[arg(long, default_value_t = 60)] frames: u32, + // 编码器输出宽(GPU 路径会下采样到该尺寸) #[arg(long, default_value_t = 2560)] enc_width: u32, + // 编码器输出高 #[arg(long, default_value_t = 1440)] enc_height: u32, + // DRM 渲染节点路径;VAAPI 上下文绑定到此设备(Intel iGPU 通常是 renderD128) #[arg(long, default_value = "/dev/dri/renderD128")] drm_device: String, + // 流水线模式:cpu 只跑软编;gpu 只跑 VAAPI;both 两条路径都跑并对比 #[arg(long, value_enum, default_value_t = PipelineMode::Both)] mode: PipelineMode, } +/// 流水线模式选择。Derive `ValueEnum` 后 clap 自动把 "cpu"/"gpu"/"both" 字符串 +/// 映射到枚举值;Derive `Copy` 让它在 match 时按值复制(无需 & 引用)。 #[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] enum PipelineMode { Cpu, @@ -50,34 +101,54 @@ enum PipelineMode { Both, } +/// 单条流水线(CPU 或 GPU)运行结束后的统计聚合。每个 `Vec` 保存每帧的耗时(微秒)。 +/// +/// 类比 Go 的 `BenchmarkResult`:把 N 帧的逐次耗时收集起来,最后统一计算均值。 +/// 用 `Vec` 而非流式累积是为了支持后续可能的中位数/分位数扩展。 #[derive(Default)] struct FrameStats { + // DMA-BUF 导入耗时(仅 GPU 路径有,CPU 路径为空) import_us: Vec, + // GPU 滤镜图耗时(仅 GPU 路径有) filter_us: Vec, + // CPU 路径的 sws_scale 耗时 transfer_us: Vec, + // 预留:缩放耗时单独拆分(当前与 filter_us/transfer_us 重叠) scale_us: Vec, + // 像素格式转换耗时(BGRA → YUV420P) format_us: Vec, + // 编码器 send_frame + drain_packet 总耗时 encode_us: Vec, + // 单帧总耗时(capture_start → encode_done),用于理论 FPS total_us: Vec, + // 导入失败的次数(DMA-BUF fd 失效等) import_failures: u32, + // 实际成功编码的帧数 frames_encoded: u32, + // 端到端墙钟耗时(从首帧到末帧),用于实测 FPS elapsed_secs: f64, + // 编码器名称(libx264 / libopenh264 / h264_vaapi) codec_name: String, + // 输出路径(区分 cpu / gpu 文件名) output_path: String, } impl FrameStats { + // 计算每帧耗时的均值(微秒 → 毫秒);空 Vec 返回 0.0 避免除零 fn avg_ms(data: &[u64]) -> f64 { if data.is_empty() { return 0.0; } + // sum::() 显式指定求和类型,避免类型推导失败;类比 Go 的 for-range 累加 data.iter().sum::() as f64 / data.len() as f64 / 1000.0 } + // 单帧总耗时的均值(毫秒),用于报告 "平均每帧 X ms" fn avg_total_ms(&self) -> f64 { Self::avg_ms(&self.total_us) } + // 实测 FPS = 成功编码帧数 / 墙钟耗时;避免零除返回 0.0 fn achieved_fps(&self) -> f64 { if self.frames_encoded > 0 && self.elapsed_secs > 0.0 { self.frames_encoded as f64 / self.elapsed_secs @@ -86,6 +157,7 @@ impl FrameStats { } } + // 理论 FPS = 1000 / 平均单帧总耗时(仅编码侧上限,不含 PipeWire 等待) fn theoretical_fps(&self) -> f64 { let avg = self.avg_total_ms(); if avg > 0.0 { @@ -96,6 +168,11 @@ impl FrameStats { } } +/// CPU 软编路径的状态聚合体:编码器、输出容器、可复用的 YUV 帧。 +/// +/// 字段 `yuv_frame` 是裸指针 `*mut ffi::AVFrame`——因为 FFmpeg C API 要求长生命周期 +/// 的可变指针,且需要 Drop 时显式释放。裸指针 `*mut T` 默认非 Send/Sync,但本结构体 +/// 只在主线程使用,无需跨线程传递,因此无需手动 impl Send。 struct SoftwareEncoder { enc_video: ff::codec::encoder::video::Video, octx: ff::format::context::Output, @@ -103,8 +180,11 @@ struct SoftwareEncoder { codec_name: String, } +// Drop trait 类比 Go 的 `defer cleanup()`:结构体析构时由 Rust 自动调用, +// 避免裸指针 yuv_frame 泄漏。注意 Drop 内不能再使用 self.yuv_frame,只能释放 impl Drop for SoftwareEncoder { fn drop(&mut self) { + // Drop trait 类比 Go 的 `defer cleanup()`:结构体析构时自动调用 // SAFETY: yuv_frame is allocated by av_frame_alloc in create_software_encoder and // owned exclusively by this SoftwareEncoder. unsafe { @@ -113,10 +193,14 @@ impl Drop for SoftwareEncoder { } } +/// FFmpeg `sws_scale` 上下文的拥有型包装。Newtype 模式(tuple struct 单字段)让 +/// Rust 类型系统追踪 C 资源的所有权,并通过 Drop 自动释放;类比 Go 中 +/// `type SwsContext struct{ p *C.SwsContext }` + `func (s *SwsContext) Close()`。 struct SwsContext(*mut ffi::SwsContext); impl Drop for SwsContext { fn drop(&mut self) { + // sws_freeContext 接受 NULL 是安全的(C 规范),无需额外判空 // SAFETY: Context is either null or returned by sws_getContext and owned here. unsafe { ffi::sws_freeContext(self.0); @@ -124,17 +208,28 @@ impl Drop for SwsContext { } } +/// 把 FFmpeg 错误码(负数)翻译成人类可读字符串。FFmpeg 的错误码没有官方码表, +/// 必须通过 `av_strerror` 拿到文本;类比 Go 中 `errno.String()` 或 `os.PathError.Err`。 fn av_err_to_string(ret: i32) -> String { + // 准备 128 字节缓冲区(FFmpeg 习惯用 128),由 av_strerror 写入 NUL 结尾的 C 字符串 let mut buf = vec![0u8; 128]; + // SAFETY: av_strerror 最多写 128 字节并以 NUL 结尾;buf 是独占的可变 Vec, + // as_mut_ptr 把缓冲区首字节暴露给 C,借用仅在这次调用期间有效。 unsafe { ffi::av_strerror(ret, buf.as_mut_ptr() as *mut i8, buf.len()); } + // 找到首个 NUL 字节作为字符串末尾,再 from_utf8_lossy 容错转 String let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); String::from_utf8_lossy(&buf[..end]).to_string() } +/// 阻塞等待第一帧 PipeWire DMA-BUF 到达;类比 Go 的 `chan.Recv()` 配 `select`。 +/// +/// 同时监听控制通道(StreamEnded / FormatChanged / Error),任何错误都立即 `bail!`。 +/// 超时 10 秒防止 GPU/驱动卡死导致基准测试无限挂起。 fn receive_first_frame(cap: &CapPortal) -> Result { loop { + // 控制通道:非阻塞 try_recv(类比 Go `select { case e := <-ctrl: ... default: }`) if let Ok(ctrl) = cap.event_receiver().try_recv() { match ctrl { PwCtrlEvent::StreamEnded => bail!("PipeWire stream ended before first frame"), @@ -142,6 +237,8 @@ fn receive_first_frame(cap: &CapPortal) -> Result bail!("PipeWire error: {e}"), } } + // 帧通道:阻塞等待最多 10 秒 + // 类比 Go `select { case f := <-frame: ... case <-time.After(10*time.Second): bail! }` match cap .frame_receiver() .recv_timeout(std::time::Duration::from_secs(10)) @@ -157,21 +254,31 @@ fn receive_first_frame(cap: &CapPortal) -> Result Result<()> { loop { let mut pkt = ff::Packet::empty(); + // SAFETY: enc_video.as_mut_ptr() 指向已打开的编码器上下文;pkt.as_mut_ptr() + // 指向空 packet,FFmpeg 会在此调用中分配 packet 数据。 let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) }; if ret < 0 { + // EAGAIN = 编码器还需要更多输入帧;EOF = 已 flush;两者都是正常终止 if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF { break; } eprintln!("avcodec_receive_packet failed: {ret}"); break; } + // 把 PTS 从编码器时间基重缩放为输出流的时间基(视频流可有不同 time_base) let enc_tb = enc_video.time_base(); + // SAFETY: octx.as_ptr() 指向有效的 AVFormatContext;streams 数组至少有一个流 + // (在 create_software_encoder 中由 avformat_new_stream 创建)。 let stream_tb = unsafe { let streams = (*octx.as_ptr()).streams; let st = *streams.add(0); @@ -179,14 +286,28 @@ fn drain_encoder( }; pkt.rescale_ts(enc_tb, stream_tb); pkt.set_stream(0); + // write_interleaved 让 FFmpeg 自动按 DTS 排序,避免手动管理 PTS/DTS pkt.write_interleaved(octx) .map_err(|e| anyhow::anyhow!("write packet failed: {e}"))?; } Ok(()) } +/// 初始化 libx264/libopenh264 软编码器 + 输出容器(MP4/null muxer)+ 可复用 YUV420P 帧。 +/// +/// 这是基准 CPU 路径的核心装配函数,步骤依次为: +/// 1. 寻找 codec(libx264 优先,libopenh264 回退) +/// 2. 创建 encoder context(builder 模式) +/// 3. 设置 width/height/fps/time_base/GOP +/// 4. (libx264 专属)设置 preset/tune +/// 5. 打开编码器 +/// 6. 分配 AVFormatContext + 创建流 + 复制 codec parameters +/// 7. 打开输出文件(除非 null muxer)+ 写文件头 +/// 8. 分配可复用的 YUV420P 帧(在每帧 encode 时复用) fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Result { + // CString 必须在 unsafe 块外构造,确保 NUL 结尾的字符串生命周期覆盖下面的 FFI 调用 let output_cstr = CString::new(output_path.to_str().unwrap())?; + // 优先 libx264(性能最好,GPL 协议),其次 libopenh264(BSD,回退方案) let codec = ff::encoder::find_by_name("libx264") .or_else(|| ff::encoder::find_by_name("libopenh264")) .ok_or_else(|| { @@ -194,18 +315,24 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul })?; let codec_name = codec.name().to_string(); + // 两阶段构造:先 Context::new_with_codec 拿到 builder,再 .encoder().video()? 切到视频编码视图 let mut enc = { let ctx = ff::codec::Context::new_with_codec(codec); ctx.encoder().video()? }; + // 编码器参数:分辨率、像素格式、时基、GOP 结构 enc.set_width(width); enc.set_height(height); enc.set_format(ff::format::Pixel::YUV420P); + // time_base = 1/60,与基准测试默认 60 FPS 对齐;生产代码里通常从源流继承 enc.set_time_base(ff::Rational::new(1, 60)); + // 关闭 B 帧以降低延迟(基准不追求压缩率) enc.set_max_b_frames(0); + // GOP = 60:每 60 帧一个 I 帧(与 60 FPS 对齐 = 每秒一个 IDR 帧) enc.set_gop(60); + // libx264 的私有参数 preset/tune 必须在 encoder 打开前通过 av_opt_set 设置到 priv_data if codec_name == "libx264" { // SAFETY: priv_data belongs to the not-yet-opened encoder context. Option strings are // valid NUL-terminated C strings for the duration of each av_opt_set call. @@ -219,9 +346,11 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul } } + // 真正打开编码器(前面只是 builder 状态),此后 enc_video 进入 ready 状态 let opened = enc.open()?; let enc_video = opened.0; + // 输出文件名含 "null" → 用 FFmpeg 内置 null muxer(不写盘),适合纯 CPU 基准 let use_null_muxer = output_path .to_str() .map(|s| s.contains("null")) @@ -266,6 +395,7 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul } // SAFETY: fmt_ctx_ptr is valid; pb is initialized for non-NOFILE muxers. + // AVFMT_NOFILE 表示该 muxer 不需要物理文件(如 null muxer),跳过 avio_open unsafe { if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 { let ret = ffi::avio_open( @@ -286,9 +416,11 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul } // SAFETY: ownership of fmt_ctx_ptr transfers into ffmpeg-next Output wrapper. + // 此后 octx 拥有 fmt_ctx_ptr,会在 Drop 时调用 avformat_free_context let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) }; // SAFETY: Allocate and configure an owned writable YUV420P frame for encoder input. + // 这个 yuv_frame 在每次 encode_yuv_frame 中复用(不重新分配),由 SoftwareEncoder::drop 释放 let yuv_frame = unsafe { let mut f = ffi::av_frame_alloc(); if f.is_null() { @@ -313,28 +445,41 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul }) } +/// 根据 `PipelineMode` 在文件名中插入 `cpu` 或 `gpu` 后缀,让 both 模式下两条路径不互相覆盖。 +/// +/// 例:`/tmp/out.mp4` + `PipelineMode::Cpu` → `/tmp/out.cpu.mp4`。 +/// `split=false` 或文件名含 "null" 时直接返回原路径(null muxer 不需要分裂)。 fn output_for_mode(base: &str, mode: PipelineMode, split: bool) -> String { if !split || base.contains("null") { return base.to_string(); } let path = Path::new(base); + // match 在 Rust 中默认是穷尽的(编译器强制覆盖所有 enum 变体); + // 这里 Both 在调用前已被外层排除,用 unreachable!() 标记 let suffix = match mode { PipelineMode::Cpu => "cpu", PipelineMode::Gpu => "gpu", PipelineMode::Both => unreachable!(), }; + // file_name 返回 Option<&OsStr>,and_then + to_str 链式处理 None 情况 let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or(base); + // rsplit_once 类比 Go 的 strings.Cut:从右侧切分一次扩展名(保留 "a.b.c" 中的 "a.b" 与 "c") let split_name = if let Some((stem, ext)) = file_name.rsplit_once('.') { format!("{stem}.{suffix}.{ext}") } else { format!("{file_name}.{suffix}") }; + // with_file_name 保留父目录,只替换末尾文件名;to_string_lossy 容错 OsStr → &str path.with_file_name(split_name) .to_string_lossy() .into_owned() } +/// 创建 BGRA→YUV420P 的 swscale 上下文。`SwsContext` 是 CPU 路径的颜色空间/尺寸转换核心。 +/// +/// 第 7 个参数 `2` = bicubic 算法;FFmpeg 还提供 fast_bilinear(1) / bilinear(2) / +/// lanczos(16) 等。基准选 bicubic 是平衡速度与质量。 fn create_sws_context( src_width: u32, src_height: u32, @@ -343,6 +488,7 @@ fn create_sws_context( dst_height: u32, ) -> Result { // SAFETY: sws_getContext creates an owned scaler context for the provided dimensions/formats. + // 返回的 *mut SwsContext 由 SwsContext 包装并在 Drop 中通过 sws_freeContext 释放。 let ctx = unsafe { ffi::sws_getContext( src_width as i32, @@ -363,11 +509,15 @@ fn create_sws_context( Ok(SwsContext(ctx)) } +/// 把已填好 YUV420P 数据的 `encoder.yuv_frame` 送入编码器,并 drain 已编码 packet。 +/// 返回编码阶段的耗时(微秒),用于 `FrameStats::encode_us` 统计。 fn encode_yuv_frame(encoder: &mut SoftwareEncoder, pts: &mut i64) -> Result { + // 类比 Go time.Now();用 as_micros() as u64 转 u64(u128 截断不影响 60s 量级基准) let t_encode = Instant::now(); // SAFETY: yuv_frame is allocated, writable, and formatted as the encoder's configured // YUV420P input frame. FFmpeg consumes but does not take ownership. unsafe { + // 单调递增的 PTS;FFmpeg 要求 PTS 必须按 time_base 单位递增,否则丢帧 (*encoder.yuv_frame).pts = *pts; *pts += 1; let r = ffi::avcodec_send_frame(encoder.enc_video.as_mut_ptr(), encoder.yuv_frame); @@ -375,10 +525,14 @@ fn encode_yuv_frame(encoder: &mut SoftwareEncoder, pts: &mut i64) -> Result bail!("avcodec_send_frame failed: {r}"); } } + // drain 编码器缓冲区(必须,否则下一帧 send_frame 会 EAGAIN) drain_encoder(&mut encoder.enc_video, &mut encoder.octx)?; Ok(t_encode.elapsed().as_micros() as u64) } +/// 编码结束:发送 NULL frame 触发编码器 flush,drain 残余 packet,写入文件尾(trailer)。 +/// +/// 类比 Go 中 `io.Closer`:必须按顺序 (flush → drain → trailer) 才能产出可播放的文件。 fn finish_encoder(mut encoder: SoftwareEncoder) -> Result<()> { // SAFETY: Sending a null frame flushes the encoder; context remains owned by encoder. unsafe { @@ -392,6 +546,8 @@ fn finish_encoder(mut encoder: SoftwareEncoder) -> Result<()> { Ok(()) } +/// 把 PipeWire 给的 DMA-BUF 帧导入 VAAPI 硬件帧上下文,返回 `ff::frame::Video`(GPU 帧)。 +/// 这是 GPU 路径的入口;耗时由 `FrameStats::import_us` 统计。 fn import_frame( frames_ctx: &AvHwFrameCtx, frame: &wl_webrtc::cap_portal::PwDmaBufFrame, @@ -412,6 +568,10 @@ fn import_frame( } } +/// 构建 GPU 路径的 FFmpeg 滤镜图:`buffer`(CPU 入口)→ `scale_vaapi`(GPU 缩放+格式转换)→ `buffersink`。 +/// +/// 关键点:buffer 滤镜不能用 pix_fmt=VAAPI 直接初始化(FFmpeg 8+ 会拒绝), +/// 必须用 `av_buffersrc_parameters_set` 注入 hw_frames_ctx 才能让后续 VAAPI 滤镜识别。 fn build_gpu_filter_graph( hw_dev: &AvHwDevCtx, frames_rgb: &AvHwFrameCtx, @@ -421,10 +581,13 @@ fn build_gpu_filter_graph( enc_height: u32, ) -> Result { let mut graph = ff::filter::Graph::new(); + // buffer = 滤镜图入口,从 AVFrame 注入数据 let buffersrc = ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?; + // buffersink = 滤镜图出口,取出处理后的 AVFrame let buffersink = ff::filter::find("buffersink") .ok_or_else(|| anyhow::anyhow!("filter 'buffersink' not found"))?; + // scale_vaapi = VAAPI 硬件缩放 + 格式转换(BGRA→NV12) let scale_vaapi = ff::filter::find("scale_vaapi") .ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?; @@ -449,14 +612,18 @@ fn build_gpu_filter_graph( (*par).width = width as i32; (*par).height = height as i32; (*par).time_base = ffi::AVRational { num: 1, den: 60 }; + // ref_clone 增加引用计数(AVBufferRef 共享底层 AVHWFramesContext), + // FFmpeg 内部会持有这个引用直到 buffersrc 释放 (*par).hw_frames_ctx = frames_rgb.ref_clone(); let ret = ffi::av_buffersrc_parameters_set(src_ctx.as_mut_ptr(), par); + // 只释放参数结构体本身;AVBufferRef 的引用由 buffersrc 持有,不能在这里 free ffi::av_free(par as *mut _); if ret < 0 { bail!("av_buffersrc_parameters_set failed: error {ret}"); } } + // scale_vaapi 滤镜参数:缩放到 enc_width×enc_height,输出 NV12(VAAPI H.264 要求的输入格式) let mut scale_ctx = graph.add( &scale_vaapi, "scale", @@ -468,6 +635,7 @@ fn build_gpu_filter_graph( } let mut sink_ctx = graph.add(&buffersink, "out", "")?; + // 链接:in[0] → scale[0] → out[0],pad index 0 是默认输入/输出口 src_ctx.link(0, &mut scale_ctx, 0); scale_ctx.link(0, &mut sink_ctx, 0); graph