docs(avhw): [2/4] 中文注释 EncState 编码主循环

This commit is contained in:
dailz
2026-06-22 17:44:08 +08:00
parent 1518da4f30
commit 8460c56bd5
+105
View File
@@ -476,6 +476,20 @@ pub(crate) fn ff_err(ret: i32) -> String {
// 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,
@@ -486,10 +500,26 @@ pub struct EncState {
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<Self>` 用 `?` 把任何 FFI/IO 错误传播给调用方(state.rs/state_portal.rs)。
pub fn new(
drm_device: &Path,
output_path: &Path,
@@ -507,6 +537,8 @@ impl EncState {
"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<AvHwDevCtx>` 等价于 Go `*AvHwDevCtx`Some=valueNone=nil),但类型系统强制处理 nil 情况。
let hw_device_ctx = match existing_hw_ctx {
Some(ctx) => ctx,
None => AvHwDevCtx::new_vaapi(drm_device)?,
@@ -530,7 +562,11 @@ impl EncState {
let mut sink_ctx = video_filter
.get("out")
// `ok_or_else(|| ...)` 把 `Option<T>` 转为 `Result<T, E>`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 {
@@ -545,6 +581,8 @@ impl EncState {
hw_ref
};
// 中文概述:解引用 sink_hw_frames 检查 filter graph 输出尺寸与编码器期望尺寸是否一致;
// 不一致仅 warnfilter 可能做隐式 scale),不视为硬错误。
// SAFETY: sink_hw_frames is an owned AVBufferRef to an AVHWFramesContext
// returned by the validated filter graph.
unsafe {
@@ -559,9 +597,14 @@ impl EncState {
}
// 4. Find h264_vaapi encoder
// `ff::encoder::find_by_name("h264_vaapi")` 返回 `Option<Codec>`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<Video>>``?` 解开为 `Encoder<Video>`,作为整个块的值赋给 `enc`。
// 类比 Go 的 `enc := func() *Encoder { ... return x }()` 但 Rust 的块是表达式级。
let mut enc = {
let ctx = ff::codec::Context::new_with_codec(codec);
ctx.encoder().video()?
@@ -578,18 +621,21 @@ impl EncState {
// VBV rate limiting: caps IDR burst size for WebRTC. Without this a 4K
// scene change can produce a 256KB keyframe that overflows the UDP send
// buffer. bufsize=bitrate/4 ≈ 250ms of video at the target bitrate.
// 中文概述:直接写裸指针字段 rc_max_rate/rc_buffer_size 设置 VBV 上限,避免 4K IDR 帧击爆 UDP 缓冲。
unsafe {
let ctx_ptr = enc.as_mut_ptr();
(*ctx_ptr).rc_max_rate = bitrate as i64;
(*ctx_ptr).rc_buffer_size = (bitrate / 4) as i32;
}
// 中文概述:AV_CODEC_FLAG_GLOBAL_HEADER 必须在 open 之前设置,触发 SPS/PPS extradata 生成(muxer 做 Annex B → AVCC 转换时需要)。
// SAFETY: AV_CODEC_FLAG_GLOBAL_HEADER must be set BEFORE opening the encoder.
// It triggers SPS/PPS extradata generation needed by the muxer for
// Annex B to AVCC conversion.
unsafe {
(*enc.as_mut_ptr()).flags |= ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32;
}
// 中文概述:把 hw_device_ctx 与 sink_hw_framesfilter 输出帧池)引用赋给编码器,编码器从该池分配 VAAPI surface。
// SAFETY: Assign hw device and frames ctx to the encoder.
unsafe {
(*enc.as_mut_ptr()).hw_device_ctx = hw_device_ctx.ref_clone();
@@ -613,15 +659,23 @@ impl EncState {
}
// 5. Open encoder. Video::open() returns Encoder(Video); .0 extracts the Video.
// `.map_err(|e| ...)?` 用闭包包装错误上下文(保留原始 ffmpeg::Error,附加上下文消息),
// `?` 自动传播。元组解构 `opened.0``Encoder(Video)` 是 newtype 包装,`.0` 取内部 Video。
let opened = enc
.open()
.map_err(|e| anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}"))?;
let enc_video = opened.0;
// 6. Muxer setup (strict order)
// `CString` 是 Rust ↔ C FFI 的桥梁:FFmpeg C API 需要以 `\0` 结尾的字符串,
// Rust `String`/`&str` 不带终止符,必须用 `CString::new` 包装。
// `output_path.to_str().unwrap()` 把 `Path` 转 UTF-8 字符串切片(非 UTF-8 路径会 panic)。
let output_cstr = CString::new(output_path.to_str().unwrap())?;
// 裸指针声明:`*mut ffi::AVFormatContext` 是 C 指针(FFI 类型),Rust 中默认不可解引用。
// `ptr::null_mut()` 等价于 C 的 `NULL`FFmpeg C API 接收 `**` 时会重新分配并写回。
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
// 中文概述:根据文件扩展名(.mp4/.mkv/...)分配输出 format context,但不打开文件。
// SAFETY: avformat_alloc_output_context2 creates format context from
// the file extension. Does NOT open the file.
let ret = unsafe {
@@ -636,6 +690,7 @@ impl EncState {
bail!("Failed to allocate output format context: {}", ff_err(ret));
}
// 中文概述:检查编码器 codec_id 与 muxer oformat 是否兼容(H.264 + MP4 应返回 ≥0)。
// SAFETY: avformat_query_codec checks codec+format compatibility.
let codec_id = unsafe { (*enc_video.as_ptr()).codec_id };
let oformat = unsafe { (*fmt_ctx_ptr).oformat };
@@ -646,12 +701,14 @@ impl EncState {
bail!("H.264 codec not supported by output container format");
}
// 中文概述:在 format context 中新建一条 stream(视频流),返回的指针由 fmt_ctx 拥有,无需手动释放。
// SAFETY: avformat_new_stream creates a new stream in the format context.
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
if stream_ptr.is_null() {
bail!("Failed to create new stream in output context");
}
// 中文概述:从 encoder 拷贝参数(含 SPS/PPS extradata)到 stream->codecpar,供 demuxer 解析。
// SAFETY: avcodec_parameters_from_context copies encoder params + extradata.
let ret = unsafe {
ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr())
@@ -663,11 +720,13 @@ impl EncState {
);
}
// 中文概述:把 encoder 的 time_base1/fps)拷贝到 stream,muxer 写头部时按此时基打时间戳。
// SAFETY: Copy encoder time_base to stream.
unsafe {
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
}
// 中文概述:打开输出文件 IO 上下文(AVIO),把 pbAVIOContext*)挂到 fmt_ctx。
// SAFETY: avio_open opens the output file for writing.
let ret = unsafe {
ffi::avio_open(
@@ -684,15 +743,19 @@ impl EncState {
);
}
// 中文概述:写入容器头部(MP4 ftyp box 等),失败需关闭 IO 释放资源(此处简化为直接 bail)。
// SAFETY: avformat_write_header writes the container header.
let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) };
if ret < 0 {
bail!("Failed to write output header: {}", ff_err(ret));
}
// 中文概述:把裸 fmt_ctx_ptr 包装回 safe Rust 的 Output 类型,由 octx 的 Drop 负责最终释放。
// SAFETY: We created fmt_ctx_ptr above and it's valid.
let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
// `Ok(Self { ... })` 是 Rust 构造语法:struct literal 字段简写(`enc_video` 等价于 `enc_video: enc_video`)。
// 类比 Go `return &EncState{enc_video: ..., ...}, nil`,但 Rust 的 `Self` 是类型别名。
Ok(Self {
enc_video,
frames_rgb,
@@ -704,10 +767,15 @@ impl EncState {
})
}
// 共享访问器:返回内部 hw frames 池的引用,捕获侧(state.rs/state_portal.rs)用它
// 给 `import_dma_buf_to_vaapi` 分配目标 VAAPI surface。借用检查器保证调用方不能
// 在持有 `&AvHwFrameCtx` 期间 `&mut self`(例如调用 `encode_frame`)。
pub fn frames_rgb(&self) -> &AvHwFrameCtx {
&self.frames_rgb
}
// 单帧驱动主循环:`&mut self` 表示独占借用(类比 Go `*receiver` + 单线程保证)。
// 接收一个 VAAPI 硬件帧,通过 filter 图(BGRA→NV12 + scale/crop)后送给编码器。
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
let mut filter_src_ctx = self
.video_filter
@@ -725,10 +793,17 @@ impl EncState {
.add(hw_frame)
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
// 持续从 sink 拉过滤后帧,直到 EAGAINfilter 缓冲空)。
// `loop {}` 是 Rust 无限循环(类比 Go `for {}`),靠 `break` 退出。
loop {
// `ff::frame::Video::empty()` 分配零值帧(无数据),filter_sink.frame() 填充它。
// `&mut filtered` 把可变借用传给 ffmpeg-next,类比 Go `&filtered`。
let mut filtered = ff::frame::Video::empty();
// 多路 matchOk 表示成功;带 guard 的 `Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN`
// 是 pattern guard(类比 Go `switch v := err.(type); ...`);通配 `Err(e)` 兜底。
match filter_sink.frame(&mut filtered) {
Ok(()) => {
// 若 filter 没设 PTSpts()==None),用输入 hw_frame 的 PTS 兜底。
if filtered.pts().is_none() {
filtered.set_pts(hw_frame.pts());
}
@@ -743,18 +818,23 @@ impl EncState {
}
let start_ts = self.starting_timestamp.unwrap();
// 中文概述:把过滤后的 NV12 VAAPI surface 发送给编码器输入队列;<0 表示 FFI 失败。
// SAFETY: avcodec_send_frame sends a valid NV12 VAAPI surface to the encoder.
let ret =
unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) };
if ret < 0 {
bail!("avcodec_send_frame failed: {}", ff_err(ret));
}
// 立即 drain 编码器输出队列:FFmpeg 编码可能延迟一帧或多帧才产出 packet,
// send_frame 后必须 receive_packet 直到 EAGAIN,否则编码器内部缓冲溢出。
self.drain_encoder(start_ts)?;
}
Ok(())
}
// EOF 处理:在调用方(state.rs/state_portal.rs)完成所有 `encode_frame` 后调用,
// 负责把 filter 图残余帧排空 + 触发编码器 EOS + 写 muxer trailermoov box 等)。
pub fn flush(&mut self) -> Result<()> {
// Flush filter graph
let mut filter_src_ctx = self
@@ -762,6 +842,7 @@ impl EncState {
.get("in")
.ok_or_else(|| anyhow::anyhow!("filter 'in' not found"))?;
let mut filter_src = filter_src_ctx.source();
// `if let Err(e) = ...` 模式匹配:只关心 Err 分支,Ok 丢弃。类比 Go `if err := ...; err != nil {}`。
if let Err(e) = filter_src.flush() {
tracing::debug!("filter source flush error: {e}");
}
@@ -771,12 +852,18 @@ impl EncState {
.video_filter
.get("out")
.ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?;
// Drain filter:循环拉 sink,把 filter 图中残余的帧全部送入编码器。
// 与 encode_frame 不同:这里 Err 通配 `Err(_) => break`(任何错误都视为 drain 完毕),
// 因为 flush 是收尾阶段,不再传播错误。
let mut filter_sink = filter_sink_ctx.sink();
loop {
let mut filtered = ff::frame::Video::empty();
match filter_sink.frame(&mut filtered) {
Ok(()) => {
// `unwrap_or(0)` 是 Option 的兜底方法:None 返回 0Some(v) 返回 v。
// flush 时若从未编码过帧(starting_timestamp=None),用 0 作为 PTS 基线。
let start_ts = self.starting_timestamp.unwrap_or(0);
// 中文概述:把 drain 出的帧送编码器;flush 路径失败也 bail(停止 flush)。
// SAFETY: filtered is a valid VAAPI frame drained from the
// filter graph; enc_video is an opened encoder.
let ret = unsafe {
@@ -791,15 +878,18 @@ impl EncState {
}
}
// 中文概述:发送 NULL 帧给编码器,触发 EOSEnd Of Stream),编码器内部缓冲的帧会被强制输出。
// SAFETY: Sending null frame signals end of stream to encoder.
unsafe {
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
}
let start_ts = self.starting_timestamp.unwrap_or(0);
// 最后一次 drain:取出 EOS 触发后所有剩余 packet。
self.drain_encoder(start_ts)?;
// Write trailer only if at least one frame was encoded.
// 写 trailer 条件:若全程 0 帧编码,写 trailer 会让 muxer 生成空 moov(损坏文件),所以用 frames_written 守卫。
if self.frames_written {
self.octx
.write_trailer()
@@ -809,14 +899,19 @@ impl EncState {
Ok(())
}
// 内部辅助方法(无 pub):每次 send_frame 后调用,循环 receive_packet 直到 EAGAIN/EOF。
// `start_ts: i64` 是首帧 PTS,用于把所有 packet 的 PTS/DTS 减去首帧 PTS(归零起点)。
fn drain_encoder(&mut self, start_ts: i64) -> Result<()> {
loop {
let mut pkt = ff::Packet::empty();
// 中文概述:从编码器拉一个已编码 packet;返回 <0 时区分 EAGAIN(暂时无)和 EOF(流结束)。
// SAFETY: avcodec_receive_packet retrieves an encoded packet.
let ret = unsafe {
ffi::avcodec_receive_packet(self.enc_video.as_mut_ptr(), pkt.as_mut_ptr())
};
if ret < 0 {
// 嵌套 if:先判 ret<0FFI 错误),再判具体码(EAGAIN/EOF)。
// `ffi::AVERROR(ffi::EAGAIN)` 是 FFmpeg 的 errno 包装宏(负数)。
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
break;
}
@@ -824,7 +919,10 @@ impl EncState {
}
// Rescale timestamps from encoder time_base to stream time_base
// 时间基重缩放:encoder 用 1/fpsstream 可能用 1/1000 等不同基。
// rescale_ts 用有理数运算,避免浮点误差累积。
let enc_tb = self.enc_video.time_base();
// 中文概述:通过裸指针读 octx.streams[0].time_base,得到 stream 的时间基(由 muxer 决定)。
// SAFETY: octx was created with stream 0 during muxer setup; streams
// is non-null and stream 0 remains owned by the format context.
let stream_tb = unsafe {
@@ -832,12 +930,16 @@ impl EncState {
if fmt.nb_streams == 0 || fmt.streams.is_null() {
bail!("no streams in output context");
}
// `fmt.streams.add(0)` 是裸指针算术: streams 是 `*mut *mut AVStream`
// `.add(0)` 取首元素地址,外层 `*` 解引用得到 `*mut AVStream`。
let st = *fmt.streams.add(0);
ff::Rational::from((*st).time_base)
};
pkt.rescale_ts(enc_tb, stream_tb);
// Offset timestamps so first frame starts at 0
// PTS/DTS 归零:把首帧 PTS 减去自身得到 0,后续帧相对首帧的偏移。`if let Some(pts) = ...`
// 模式匹配:只有当 PTS 存在时才改写(部分 packet 可能没有 PTS)。
if let Some(pts) = pkt.pts() {
pkt.set_pts(Some(pts - start_ts));
}
@@ -845,10 +947,13 @@ impl EncState {
pkt.set_dts(Some(dts - start_ts));
}
// 标记 packet 属于 stream 0(视频流)。
pkt.set_stream(0);
// 交错写入:muxer 内部会按 DTS 排序后写出,类比 Go `mp4Writer.WriteInterleaved(pkt)`。
pkt.write_interleaved(&mut self.octx)
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
// 标记:至少一帧已写入 muxer(flush 时用此守卫决定是否写 trailer)。
self.frames_written = true;
}
Ok(())