//! # avhw — FFmpeg / VAAPI 硬件加速 FFI 绑定(FFI 最密集的文件) //! //! 本文件是整个 crate 中 `unsafe` 块密度最高的模块:直接调用 FFmpeg C API。 //! `ffmpeg-next` 是 ffmpeg-sys-next(C FFI 绑定)的薄包装;硬件加速相关 API //! (`AVBufferRef`/`av_hwframe_*`)仍是裸 C 接口。 //! //! ## FFmpeg 引用计数对象的生命周期 //! - `AVBufferRef`:FFmpeg 引用计数句柄,`av_buffer_ref` 加 1、`av_buffer_unref` 减 1。 //! `AvHwDevCtx`/`AvHwFrameCtx` 用 RAII 包装:`new` alloc,`Drop` unref。 //! - `AVFrame`:硬件帧(`AV_PIX_FMT_VAAPI`)的 `data[0]` 是 GPU 表面指针。 //! //! ## `unsafe impl Send` 的存在原因(AGENTS.md 明确警告) //! `*mut AVBufferRef` 默认 `!Send`;wrapper 显式声明 Send 的前提是**外部调用方 //! 保证 `&mut self` 独占访问**。AGENTS.md:不要跨线程移动这些 wrapper 而不重新 //! 检查 exclusivity 假设。 //! //! ## 与 Go cgo 的类比 //! - `extern "C" fn` ≈ Go `//export` C 回调; //! - `unsafe { ffi::av_*() }` ≈ Go `C.av_xxx()`; //! - RAII `Drop` ≈ Go `runtime.SetFinalizer`(但 Rust Drop 是确定性的)。 // std 导入:CString(C 字符串)、mem::zeroed(FFI 零初始化)、RawFd/AsRawFd(fd 桥), // c_void(C void 跨语言)、Path(DRM 设备路径)、ptr(null_mut 等裸指针工具), // slice(从裸指针构造切片)、AtomicBool/Ordering/Arc(跨线程暂停标志,T10b 用), // Instant(编码计时,T10b 用)。本 sub-todo(lines 1-330)仅部分使用。 use std::ffi::CString; use std::mem; use std::os::fd::{AsRawFd, RawFd}; use std::os::raw::c_void; use std::path::Path; use std::ptr; use std::slice; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; // anyhow 错误处理:bail!(提前返回 Err)、Result(错误传播)。 // ffmpeg-next:稍安全的 API(ff::frame::Video、ff::format::Pixel、ff::codec 等)。 // ffmpeg_next::ffi:FFmpeg C 头绑定的裸 API(AVBufferRef、av_hwdevice_ctx_create 等), // 所有 `unsafe` FFI 调用的入口。`packet::Mut as _`:导入 packet 的 Mut trait 但匿名, // 仅用于 trait method 解析,不污染命名空间。 use anyhow::{bail, Result}; use ffmpeg_next as ff; use ffmpeg_next::ffi; use ffmpeg_next::packet::Mut as _; // crate 内部模块:PwDmaBufFrame(PipeWire DMA-BUF 帧元数据:fd/width/height/stride/modifier), // Transform(Wayland wl_output 变换,决定 ROI 是否需要转置处理)。 use crate::cap_portal::PwDmaBufFrame; use crate::transform::{transpose_if_transform_transposed, Transform}; // --------------------------------------------------------------------------- // Bitrate feedback command (WebRTC BWE → SW encoder) // --------------------------------------------------------------------------- // 跨线程控制信令:WebRTC 线程根据 BWE(bandwidth estimation)估算的可用带宽, // 通过 MPSC channel 把 `BitrateCommand` 发给软件编码线程,由后者调整 x264 参数。 // 注意:硬件(VAAPI)路径目前不接受动态码率调整,只有软件(x264)路径消费这些命令。 // 类比 Go:`chan BitrateCommand` 单向 channel。 /// Commands sent from the WebRTC thread to the SW encoder when the /// bandwidth estimate changes significantly. pub enum BitrateCommand { UpdateBitrate { target_bps: u64 }, UpdateResolution { width: u32, height: u32 }, /// Force the next encoded frame to be an IDR. Sent by the WebRTC thread /// in response to str0m `Event::KeyframeRequest` or a resolution change. ForceKeyframe, } // 分辨率变更事件:WebRTC 线程根据对端能力(SVC/layered encoding 协商)建议切换分辨率, // 编码线程收到后重建 encoder(x264 不支持运行时改变 width/height,必须销毁重建)。 // `#[derive(Clone, Copy, Debug)]`:Copy 表示按位拷贝(小值类型,无需 clone 调用), // 类比 Go 的 `type ResolutionChange struct{...}`(Go 默认值语义即 Copy)。 #[derive(Clone, Copy, Debug)] pub struct ResolutionChange { pub width: u32, pub height: u32, } // 每帧编码耗时快照,由编码线程填好后通过 channel 发给统计线程(stats thread)。 // `#[derive(Default, Clone, Copy, Debug)]`:Default 允许 `SwEncodeTiming::default()` 全零初始化; // Copy/Clone 表示小值类型按位拷贝(无堆字段)。 // // 字段语义: // - sws_us:libswscale 把 NV12(VAAPI 输出)转 YUV420P(x264 输入)的耗时(微秒) // - encode_us:`avcodec_send_frame` + drain 接收所有 packet 的总耗时(微秒) // - output_bytes:本帧产出的 H.264 字节数(即使下游 WebRTC 因暂停丢帧也计入) // // 类比 Go:`type SwEncodeTiming struct{...}` + atomic store/load 跨 goroutine 传递。 /// Per-frame timing snapshot for the software encoder, consumed by the stats /// thread. `sws_us` measures NV12→YUV420P conversion, `encode_us` measures /// `avcodec_send_frame` + drain, and `output_bytes` counts encoded bytes /// produced by libavcodec (even if downstream delivery later drops them). #[derive(Default, Clone, Copy, Debug)] pub struct SwEncodeTiming { pub sws_us: u64, pub encode_us: u64, pub output_bytes: usize, } // `encode_cpu_frame` 的返回值,编码线程据此判断是否计 encoded_fps(只有 `Encoded` // 才算一次真实编码)。`#[derive(Debug, Clone, Copy, PartialEq, Eq)]`:PartialEq/Eq // 允许 `==` 比较(测试和 stats 聚合用),Copy 表示小值类型按位拷贝。 // 类比 Go:`type EncodeOutcome int` + const 枚举值。 /// Outcome of a single `encode_cpu_frame` call. Used by the encode thread /// to decide whether to report timing stats (only real encodes tick encoded_fps). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EncodeOutcome { /// Frame was actually encoded and produced output bytes. Encoded, /// Frame was dropped because WebRTC is paused (no client connected). SkippedPaused, /// Frame was dropped because the encoder is in disconnected state. SkippedDisconnected, /// Frame was dropped because its Y-plane hash matched the previous frame. SkippedDuplicate, } // --------------------------------------------------------------------------- // AvHwDevCtx // --------------------------------------------------------------------------- // VAAPI 设备上下文的 RAII 包装。`ptr` 是 FFmpeg 引用计数的 `AVBufferRef`, // 包装 `AVHWDeviceContext`(VAAPI 内部的 `VADisplay`)。`new_vaapi` 时 alloc, // `Drop` 时 `av_buffer_unref`;`ref_clone` 用于把内部 ref 传递给编码器的 // `AVCodecContext::hw_device_ctx`(FFmpeg 会再 `av_buffer_ref` 一次)。 // **独占访问假设**:`unsafe impl Send` 的前提是外部调用方保证 `&mut self`, // 不允许多线程共享 `&AvHwDevCtx` 同时调用 mut method(详见 AGENTS.md 警告)。 pub struct AvHwDevCtx { // FFmpeg 内部引用计数句柄,类型擦除为 `*mut AVBufferRef`。 // 永不为 null(除非在 Drop 中被置 null)。 ptr: *mut ffi::AVBufferRef, } // 中文概述:声明 `AvHwDevCtx` 可以跨线程 `Send`。前提是调用方用 `&mut self` // 独占访问(无并发可变借用)。VAAPI 的 `VADisplay` 在硬件驱动层线程安全, // FFmpeg 的 `AVHWDeviceContext` 内部状态在 send/receive 编码模式下也线程安全。 // 不修复:保留 AGENTS.md 文档的 exclusivity 假设(这是本 crate 的并发模型基础)。 // SAFETY: AvHwDevCtx wraps an FFmpeg AVBufferRef which is not Send by default, // but we guarantee exclusive access through &mut self. The underlying VAAPI // device context is thread-safe for the operations we perform. unsafe impl Send for AvHwDevCtx {} impl AvHwDevCtx { // 创建 VAAPI 设备上下文。`drm_device` 通常是 `/dev/dri/renderD128`。 // 失败原因:设备不存在、无 VAAPI 驱动、权限不足(用户不在 `video` 组)。 // 成功时返回 `AvHwDevCtx`,ptr 持有一个 ref(refcount=1)。 pub fn new_vaapi(drm_device: &Path) -> Result { // CString::new 在路径含内部 NUL 时返回 Err(FFmpeg C API 要求 NUL 结尾)。 // `to_str().unwrap()`:路径非 UTF-8 时 panic(Linux DRM 设备路径通常 ASCII)。 let device_cstr = CString::new(drm_device.to_str().unwrap())?; // 初始化为 null_mut:FFmpeg 的 out-pointer 约定(调用者置 null,被调者赋值)。 let mut p: *mut ffi::AVBufferRef = ptr::null_mut(); // 中文概述:调用 FFmpeg C API 创建 VAAPI `AVHWDeviceContext`,写入 `*p`。 // 失败返回负的 AVERROR;成功返回 0 且 `p` 指向新分配的 `AVBufferRef`(refcount=1)。 // SAFETY: device_cstr is a valid C string for the duration of the call; // p is a valid out-pointer that FFmpeg initializes on success. let ret = unsafe { ffi::av_hwdevice_ctx_create( &mut p, ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI, device_cstr.as_ptr(), ptr::null_mut(), 0, ) }; if ret < 0 { bail!( "Failed to create VAAPI device context from {}: {}", drm_device.display(), ff_err(ret) ); } Ok(Self { ptr: p }) } // 返回裸 `*mut AVBufferRef`,调用方负责管理引用计数。 // 通常用于传给 FFmpeg API 的 `hw_device_ctx` 字段(FFmpeg 内部会 av_buffer_ref)。 pub fn as_ptr(&self) -> *mut ffi::AVBufferRef { self.ptr } // 增加 refcount,返回新的 `*mut AVBufferRef`(不复制底层缓冲)。 // 用于把设备 ref 传给编码器的 `AVCodecContext::hw_device_ctx`,编码器 Drop 时 // 调用 `av_buffer_unref` 释放自己持有的 ref,不影响本 wrapper。 pub fn ref_clone(&self) -> *mut ffi::AVBufferRef { // 中文概述:FFmpeg 引用计数 +1,返回新 ref;线程安全(内部原子操作)。 // SAFETY: av_buffer_ref atomically increments refcount and returns a new ref. unsafe { ffi::av_buffer_ref(self.ptr) } } } // RAII 析构:减少 refcount。如果是最后一个 ref(refcount→0),FFmpeg 释放 // 底层的 `AVHWDeviceContext` 和 `VADisplay`。`is_null()` 守卫是为了应对 // `Drop` 被多次调用或 `new_vaapi` 失败后 ptr 仍为 null 的边缘情况(实际上 // Rust 的 Drop 不会被调用两次,但 FFmpeg 的 av_buffer_unref 接受 null 入参)。 impl Drop for AvHwDevCtx { fn drop(&mut self) { if !self.ptr.is_null() { // 中文概述:FFmpeg 引用计数 -1,refcount=0 时释放底层设备上下文。 // SAFETY: av_buffer_unref decrements refcount; frees the buffer when it hits zero. unsafe { ffi::av_buffer_unref(&mut self.ptr) }; } } } // --------------------------------------------------------------------------- // AvHwFrameCtx // --------------------------------------------------------------------------- // VAAPI 硬件帧池的 RAII 包装。`AVHWFramesContext` 是 FFmpeg 的硬件帧分配器, // 编码器通过它分配 `AV_PIX_FMT_VAAPI` 帧(GPU 表面)。`initial_pool_size=4` // 表示池大小为 4(足够 send/receive pipeline:1 帧 encode 中 + 1 帧 queue + 2 缓冲)。 // 字段配置(width/height/sw_format)必须在 `av_hwframe_ctx_init` 前设置; // `sw_fmt` 是 CPU 可读的软件格式(VAAPI 内部从该格式上传/下载)。 pub struct AvHwFrameCtx { ptr: *mut ffi::AVBufferRef, } // 中文概述:声明 `AvHwFrameCtx` 可以跨线程 `Send`。前提是 `&mut self` 独占访问, // 且底层硬件帧池在 send/receive 模式下线程安全。与 `AvHwDevCtx` 同样的 exclusivity 假设。 // 不修复:保留 AGENTS.md 文档的并发约束。 // SAFETY: AvHwFrameCtx wraps an FFmpeg AVBufferRef to an AVHWFramesContext. // It is only accessed through &mut self, ensuring no concurrent mutation. // The underlying hardware frames pool is thread-safe for the send/receive pattern. unsafe impl Send for AvHwFrameCtx {} impl AvHwFrameCtx { // 内部构造函数:分配 `AVHWFramesContext`、配置字段、调用 `av_hwframe_ctx_init`。 // 失败时(alloc 或 init 失败)返回 Err,确保 ptr 不泄漏。 fn new_inner(hw_dev: &AvHwDevCtx, w: u32, h: u32, sw_fmt: ff::format::Pixel) -> Result { // 中文概述:分配 `AVHWFramesContext`,refcount=1,关联到 hw_dev 的设备上下文。 // 返回的 ref 的 `data` 字段指向未初始化的 `AVHWFramesContext`,需手动配置。 // SAFETY: hw_dev is a live AVHWDeviceContext; FFmpeg returns either a valid // frames context ref or null (checked below). let mut p = unsafe { ffi::av_hwframe_ctx_alloc(hw_dev.as_ptr()) }; if p.is_null() { bail!("av_hwframe_ctx_alloc returned null"); } // 中文概述:通过 `(*p).data as *mut AVHWFramesContext` 访问 FFmpeg 内部类型擦除的 // 字段(`AVBufferRef::data` 是 `*mut u8`,实际指向具体 context 类型)。 // 直接赋值 `format`/`sw_format`/`width`/`height`/`initial_pool_size` 5 个字段。 // SAFETY: p is a valid AVBufferRef from av_hwframe_ctx_alloc. // Its .data field points to an AVHWFramesContext that we must configure. unsafe { let fc = (*p).data as *mut ffi::AVHWFramesContext; (*fc).format = ff::format::Pixel::VAAPI.into(); (*fc).sw_format = sw_fmt.into(); (*fc).width = w as i32; (*fc).height = h as i32; (*fc).initial_pool_size = 4; } // 中文概述:初始化帧池(实际分配 GPU 表面)。失败时必须手动 unref p 否则泄漏。 // SAFETY: p is a valid AVHWFramesContext ref configured above and not yet // transferred or freed. let ret = unsafe { ffi::av_hwframe_ctx_init(p) }; if ret < 0 { // 中文概述:init 失败的清理路径——p 仍有效但不可用,unref 释放。 // SAFETY: p is valid but init failed; clean up. unsafe { ffi::av_buffer_unref(&mut p) }; bail!("av_hwframe_ctx_init failed: {}", ff_err(ret)); } Ok(Self { ptr: p }) } // 为采集路径创建硬件帧池。`sw_fmt` 通常是 `BGRA`(PipeWire DMA-BUF 像素格式)。 pub fn for_capture( hw_dev: &AvHwDevCtx, w: u32, h: u32, sw_fmt: ff::format::Pixel, ) -> Result { Self::new_inner(hw_dev, w, h, sw_fmt) } pub fn as_ptr(&self) -> *mut ffi::AVBufferRef { self.ptr } pub fn ref_clone(&self) -> *mut ffi::AVBufferRef { // 中文概述:FFmpeg 引用计数 +1,返回新 ref;用于把帧池 ref 传给编码器。 // SAFETY: av_buffer_ref atomically increments refcount and returns a new ref. unsafe { ffi::av_buffer_ref(self.ptr) } } } // RAII 析构:与 AvHwDevCtx::Drop 同样的模式(unref 释放 ref)。 impl Drop for AvHwFrameCtx { fn drop(&mut self) { if !self.ptr.is_null() { // 中文概述:FFmpeg 引用计数 -1,refcount=0 时释放帧池和 GPU 表面。 // SAFETY: av_buffer_unref decrements refcount; frees when zero. unsafe { ffi::av_buffer_unref(&mut self.ptr) }; } } } // 启动时探针:测试 `drm_device` 能否通过 VAAPI 导入 PipeWire 给的 DMA-BUF 帧。 // 用于 backend_detect 决定走硬件(VAAPI)还是软件(x264)编码路径。 // 失败原因:硬件不支持给定格式/修饰符组合、DRM 设备无 VAAPI、内核 driver 限制。 // 成功只是"探针通过",不代表实际编码时一直可用(运行时仍可能因 OOM 等失败)。 /// Test whether `drm_device` can import the PipeWire DMA-BUF frame via VAAPI. pub fn test_dma_buf_import(drm_device: &Path, frame: &PwDmaBufFrame) -> Result<()> { let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?; let frames = AvHwFrameCtx::for_capture(&hw_dev, frame.width, frame.height, ff::format::Pixel::BGRA)?; // 中文概述:调用下面的 `import_dma_buf_to_vaapi`,用 `av_hwframe_map` 把 DMA-BUF // 映射到 VAAPI 表面(零拷贝)。`as_raw_fd()` 把 `OwnedFd` 转为裸 fd 传给 FFI。 // SAFETY: frames is a live VAAPI frames context; frame carries valid DMA-BUF metadata. unsafe { import_dma_buf_to_vaapi( frames.as_ptr(), frame.fd.as_raw_fd(), frame.width, frame.height, frame.format, frame.modifier, frame.stride, frame.offset, ) }?; Ok(()) } // 中文概述:把 PipeWire 的 DMA-BUF 帧零拷贝映射为 VAAPI `AVFrame`。 // DMA-BUF 是 Linux 内核的 GPU 缓冲共享机制(fd 指向 GPU 内存);`av_hwframe_map` // 在 VAAPI 内部通过 `vaCreateSurfaces` + `vaMapBuffer` 直接 GPU 映射,无需 CPU 拷贝。 // 类比 Go:Go 没有等价物——cgo + libavutil 才能实现同样的零拷贝路径。 /// Import a DMA-BUF into a VAAPI hardware frame via zero-copy `av_hwframe_map`. /// /// # Safety /// - `frames_ctx` must point to an initialized AVHWCramesContext for VAAPI /// - `raw_fd` must be a valid DMA-BUF file descriptor pub unsafe fn import_dma_buf_to_vaapi( frames_ctx: *mut ffi::AVBufferRef, raw_fd: RawFd, width: u32, height: u32, drm_format: u32, modifier: u64, stride: u32, offset: u64, ) -> Result { // `libc::dup` 复制 fd——`AVDRMFrameDescriptor` 持有 dup 后的 fd, // 当 descriptor 释放时 `cleanup_drm_descriptor` 会 `libc::close` 它; // 原始 `raw_fd` 不被本函数消费(仍由调用方拥有,通常是 PipeWire)。 let duped_fd = libc::dup(raw_fd); if duped_fd < 0 { bail!("dup(fd) failed: {}", std::io::Error::last_os_error()); } // `mem::zeroed()`:FFI 零初始化(所有字段置 0),类似 Go 的 `&drmDesc{}`。 // SAFETY 后续不再赘述——这是 `unsafe fn` 内部,调用契约由函数签名保证。 let mut desc: ffi::AVDRMFrameDescriptor = mem::zeroed(); desc.nb_objects = 1; desc.objects[0].fd = duped_fd; desc.objects[0].size = (height as usize) * (stride as usize); desc.objects[0].format_modifier = modifier; desc.nb_layers = 1; desc.layers[0].format = drm_format; desc.layers[0].nb_planes = 1; desc.layers[0].planes[0].object_index = 0; desc.layers[0].planes[0].offset = offset as isize; desc.layers[0].planes[0].pitch = stride as isize; // `Box::new(desc)` 把栈上的 descriptor 移到堆上;`Box::into_raw` 交出所有权给 FFmpeg。 // `av_buffer_create` 会接管这块内存:当 AVBufferRef refcount 归零时调用 // `cleanup_drm_descriptor`(自定义 free 回调)→ close fd + Box::from_raw 释放堆内存。 let desc_box = Box::new(desc); let desc_ptr = Box::into_raw(desc_box); // 创建 FFmpeg 自定义 AVBufferRef:data 指向 descriptor,free 回调为 cleanup_drm_descriptor。 // 失败时(极少)需要手动恢复 Box 并 close fd,否则泄漏。 let buf_ref = ffi::av_buffer_create( desc_ptr as *mut u8, std::mem::size_of::(), Some(cleanup_drm_descriptor), ptr::null_mut(), 0, ); if buf_ref.is_null() { let desc_box = Box::from_raw(desc_ptr); libc::close(desc_box.objects[0].fd); bail!("av_buffer_create returned null for DRM descriptor"); } // 构造 src AVFrame:格式为 `AV_PIX_FMT_DRM_PRIME`(FFmpeg 的 DMA-BUF 包装格式), // `data[0]` 指向 AVBufferRef 的 data(即 descriptor),`buf[0]` 持有 ref 防止提前释放。 let mut src = ff::frame::Video::empty(); { let sp = src.as_mut_ptr(); (*sp).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; (*sp).width = width as i32; (*sp).height = height as i32; (*sp).data[0] = (*buf_ref).data; (*sp).buf[0] = buf_ref; } // 构造 dst AVFrame:格式为 `AV_PIX_FMT_VAAPI`,挂上 `hw_frames_ctx` 让 FFmpeg 知道用哪个池。 let mut dst = ff::frame::Video::empty(); // 中文概述:把 dst 的 format 设为 VAAPI、`hw_frames_ctx` 设为新的 ref。 // `av_buffer_ref(frames_ctx)` 必须成功,否则 av_hwframe_map 无法定位帧池。 // SAFETY: frames_ctx is guaranteed by this unsafe function's contract to be a // valid initialized VAAPI frames context; we set format/hw_frames_ctx on a // freshly allocated dst frame. unsafe { let dp = dst.as_mut_ptr(); (*dp).format = ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32; (*dp).hw_frames_ctx = ffi::av_buffer_ref(frames_ctx); if (*dp).hw_frames_ctx.is_null() { bail!("av_buffer_ref(frames_ctx) returned null"); } } // 中文概述:核心 FFI 调用——FFmpeg 在内部调用 VAAPI `vaCreateSurfaces`+`vaMapBuffer` // 把 DMA-BUF 映射到 GPU 表面,写入 dst。零 CPU 拷贝。 // SAFETY: src and dst are initialized AVFrames; dst has a valid hw_frames_ctx // ref and av_hwframe_map fills dst from src. let ret = unsafe { ffi::av_hwframe_map( dst.as_mut_ptr(), src.as_ptr(), ffi::AV_HWFRAME_MAP_READ as i32, ) }; if ret < 0 { bail!("av_hwframe_map failed: {}", ff_err(ret)); } Ok(dst) } // `av_buffer_create` 的 free 回调:当自定义 AVBufferRef 的 refcount 归零时被 FFmpeg 调用。 // `unsafe extern "C" fn` 是因为 FFmpeg 通过 C 函数指针调用它(C ABI 兼容)。 // 职责:(1) close DMA-BUF 的 dup'd fd;(2) `Box::from_raw` 恢复堆内存所有权并立即 drop。 // `_opaque`:FFmpeg 的 AVBufferRef 允许附带 opaque 数据(本例未用,故 `_` 前缀)。 // 类比 Go:相当于 `runtime.SetFinalizer` 的回调,但是 FFmpeg 主动调用的(确定性)。 unsafe extern "C" fn cleanup_drm_descriptor(_opaque: *mut c_void, data: *mut u8) { let desc = data as *mut ffi::AVDRMFrameDescriptor; if !desc.is_null() && (*desc).nb_objects > 0 && (*desc).objects[0].fd >= 0 { libc::close((*desc).objects[0].fd); } let _ = Box::from_raw(data as *mut ffi::AVDRMFrameDescriptor); } // 把 FFmpeg 的负数错误码(AVERROR)转成人类可读字符串。 // `av_strerror` 是 FFmpeg 的线程安全 strerror 等价物,写入调用方提供的缓冲区。 // `pub(crate)` 可见性:仅供本 crate 内部错误格式化使用(不暴露给外部)。 /// Convert an FFmpeg error code to a human-readable string. pub(crate) fn av_err_to_string(err: i32) -> String { // 128 字节足够 AVERROR 描述(FFmpeg 内部字符串都很短)。 let mut buf = vec![0u8; 128]; // 中文概述:调用 FFmpeg `av_strerror`,把错误码描述写入 buf。 // 失败(无效错误码)时 buf 仍为全 0,trim 后为空字符串。 // SAFETY: buf points to 128 writable bytes and lives for the duration of // av_strerror. unsafe { ffi::av_strerror(err, buf.as_mut_ptr() as *mut i8, buf.len()); } // `from_utf8_lossy` 容忍非 UTF-8 字节;`trim_end_matches('\0')` 去掉 C 字符串结尾的 NUL。 String::from_utf8_lossy(&buf) .trim_end_matches('\0') .to_string() } // 带数字码的错误格式化:`"error -22 (Invalid argument)"`。 // 数字码很重要——AVERROR 是负数,但有些 FFmpeg 错误是宏(如 AVERROR(EAGAIN)), // 看数字能区分底层 errno 还是 FFmpeg 自定义错误。 /// Format an FFmpeg error code with both numeric value and description. /// Example output: "error -22 (Invalid argument)" pub(crate) fn ff_err(ret: i32) -> String { format!("error {ret} ({})", av_err_to_string(ret)) } // --------------------------------------------------------------------------- // EncState // --------------------------------------------------------------------------- // 编码状态机:把 VAAPI 硬件帧 → H.264 ES 流 → MP4 文件的"主循环编排者"。 // // 字段角色(按数据流顺序): // - enc_video : 已打开的 h264_vaapi 编码器句柄(FFmpeg AVCodecContext 包装) // - video_filter : scale/crop/format 过滤图(BGRA→NV12 + VAAPI 像素格式上传) // - frames_rgb : 捕获侧 hw frames 池(与 import_dma_buf_to_vaapi 共享) // - hw_device_ctx : VAAPI 设备上下文(DRM render node 句柄) // - octx : 输出 muxer 上下文(MP4 文件) // - starting_timestamp : 首帧 PTS 锚点,用于把绝对时间戳归零(避免 MP4 起始时间漂移) // - frames_written : 是否已写出过帧(用于决定 flush 时是否写 trailer) // // 跨线程约束:state.rs/state_portal.rs 的设计是"单线程驱动"——`&mut EncState` // 由外层 main 串行化访问,因此 `enc_video` / `octx` 等 FFmpeg 上下文不需要锁。 // `unsafe impl Send` 仅表达"类型上可以跨线程移动",不代表"可以并发访问"。 pub struct EncState { enc_video: ff::codec::encoder::video::Video, frames_rgb: AvHwFrameCtx, video_filter: ff::filter::Graph, hw_device_ctx: AvHwDevCtx, octx: ff::format::context::Output, starting_timestamp: Option, frames_written: bool, } // 安全说明:FFmpeg 的 AVCodecContext/AVFormatContext 等 C 对象不是自动 Sync 的, // 但本工程的并发模型保证 EncState 只在单一编码线程内被 `&mut` 访问,因此标记 // Send(跨线程移动)是安全的;若未来引入并发编码必须重新审视(参见 AGENTS.md // 关于 avhw.rs 显式 `unsafe impl Send` 与独占性假设的说明)。 unsafe impl Send for EncState {} // impl EncState:编码主循环对外暴露的方法集合。 // // 方法职责(按数据流顺序): // - new : 构造 VAAPI 设备/帧池/filter 图/编码器/muxer 6 步流水线(FFmpeg 严格顺序) // - frames_rgb : 暴露内部 hw frames 池给捕获侧 import_dma_buf_to_vaapi 共享 // - encode_frame : 单帧驱动:送入 filter → 拉过滤后帧 → avcodec_send_frame → drain_encoder // - flush : EOF 处理:filter drain + 空帧 avcodec_send_frame 触发 encoder flush + write_trailer // - drain_encoder: 内部辅助:循环 avcodec_receive_packet 直到 EAGAIN/EOF,重缩放 PTS 并写 muxer impl EncState { // 抑制 clippy::too_many_arguments:构造函数需要 11 个参数(捕获尺寸/编码尺寸/码率/GOP/帧率/变换/共享 hw ctx), // 拆分为 builder 模式反而会增加 FFI 调用顺序出错的风险(FFmpeg 各步骤有严格依赖关系)。 #[allow(clippy::too_many_arguments)] // 构造编码器:类比 Go `func NewEncState(...) (*EncState, error)`,11 个参数对应 6 步流水线配置。 // 返回 `Result` 用 `?` 把任何 FFI/IO 错误传播给调用方(state.rs/state_portal.rs)。 pub fn new( drm_device: &Path, output_path: &Path, width: u32, height: u32, enc_width: u32, enc_height: u32, bitrate: u64, gop_size: u32, fps: u32, transform: Transform, existing_hw_ctx: Option, ) -> Result { tracing::info!( "EncState::new: {width}x{height} enc={enc_width}x{enc_height} transform={transform:?}" ); // 1. VAAPI device — reuse existing context if provided // `match` 是 Rust 的 exhaustive 模式匹配(类似 Go `switch` 但更强:编译器强制覆盖所有分支)。 // `Option` 等价于 Go `*AvHwDevCtx`(Some=value,None=nil),但类型系统强制处理 nil 情况。 let hw_device_ctx = match existing_hw_ctx { Some(ctx) => ctx, None => AvHwDevCtx::new_vaapi(drm_device)?, }; let frames_rgb = AvHwFrameCtx::for_capture(&hw_device_ctx, width, height, ff::format::Pixel::BGRA)?; // 3. Filter graph — must be built BEFORE encoder config so we can derive // hw_frames_ctx from the buffersink output (correct surface pool dimensions). let mut video_filter = build_filter_graph( &hw_device_ctx, &frames_rgb, width, height, enc_width, enc_height, fps, transform, )?; let mut sink_ctx = video_filter .get("out") // `ok_or_else(|| ...)` 把 `Option` 转为 `Result`:None 时执行闭包构造错误。 // 闭包 `|| ...` 延迟构造(类比 Go `if x == nil { return fmt.Errorf(...) }`); // 末尾 `?` 把 Err 传播给调用方,Ok(t) 则解包继续。 .ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?; // 中文概述:从 buffersink 取出硬件帧上下文并 `av_buffer_ref` 提升为拥有引用。 // SAFETY: sink_ctx is a live buffersink; the returned hw_frames_ctx is // borrowed, so av_buffer_ref creates an owned reference. let sink_hw_frames = unsafe { let raw = ffi::av_buffersink_get_hw_frames_ctx(sink_ctx.as_mut_ptr()); if raw.is_null() { bail!("buffersink has no hw_frames_ctx — filter graph may not be configured for hardware output"); } let hw_ref = ffi::av_buffer_ref(raw); if hw_ref.is_null() { bail!("av_buffer_ref failed for buffersink hw_frames_ctx — likely out of memory"); } hw_ref }; // 中文概述:解引用 sink_hw_frames 检查 filter graph 输出尺寸与编码器期望尺寸是否一致; // 不一致仅 warn(filter 可能做隐式 scale),不视为硬错误。 // SAFETY: sink_hw_frames is an owned AVBufferRef to an AVHWFramesContext // returned by the validated filter graph. unsafe { let fc = (*sink_hw_frames).data as *mut ffi::AVHWFramesContext; let actual_w = (*fc).width as u32; let actual_h = (*fc).height as u32; if actual_w != enc_width || actual_h != enc_height { tracing::warn!( "Filter output dimensions {actual_w}x{actual_h} differ from encoder dimensions {enc_width}x{enc_height}" ); } } // 4. Find h264_vaapi encoder // `ff::encoder::find_by_name("h264_vaapi")` 返回 `Option`:FFmpeg 未编译 VAAPI 时为 None。 // `.ok_or_else(...)?` 链式:Option → Result → 自动传播。 let codec = ff::encoder::find_by_name("h264_vaapi") .ok_or_else(|| anyhow::anyhow!("h264_vaapi encoder not found"))?; // 块表达式 `{ ... }` 求值为最后一个表达式(无分号)的值:这里 `ctx.encoder().video()?` // 返回 `Result>`,`?` 解开为 `Encoder