docs(avhw): [2/4] 中文注释 EncState 编码主循环
This commit is contained in:
+105
@@ -476,6 +476,20 @@ pub(crate) fn ff_err(ret: i32) -> String {
|
|||||||
// EncState
|
// 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 {
|
pub struct EncState {
|
||||||
enc_video: ff::codec::encoder::video::Video,
|
enc_video: ff::codec::encoder::video::Video,
|
||||||
frames_rgb: AvHwFrameCtx,
|
frames_rgb: AvHwFrameCtx,
|
||||||
@@ -486,10 +500,26 @@ pub struct EncState {
|
|||||||
frames_written: bool,
|
frames_written: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 安全说明:FFmpeg 的 AVCodecContext/AVFormatContext 等 C 对象不是自动 Sync 的,
|
||||||
|
// 但本工程的并发模型保证 EncState 只在单一编码线程内被 `&mut` 访问,因此标记
|
||||||
|
// Send(跨线程移动)是安全的;若未来引入并发编码必须重新审视(参见 AGENTS.md
|
||||||
|
// 关于 avhw.rs 显式 `unsafe impl Send` 与独占性假设的说明)。
|
||||||
unsafe impl Send for EncState {}
|
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 {
|
impl EncState {
|
||||||
|
// 抑制 clippy::too_many_arguments:构造函数需要 11 个参数(捕获尺寸/编码尺寸/码率/GOP/帧率/变换/共享 hw ctx),
|
||||||
|
// 拆分为 builder 模式反而会增加 FFI 调用顺序出错的风险(FFmpeg 各步骤有严格依赖关系)。
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
// 构造编码器:类比 Go `func NewEncState(...) (*EncState, error)`,11 个参数对应 6 步流水线配置。
|
||||||
|
// 返回 `Result<Self>` 用 `?` 把任何 FFI/IO 错误传播给调用方(state.rs/state_portal.rs)。
|
||||||
pub fn new(
|
pub fn new(
|
||||||
drm_device: &Path,
|
drm_device: &Path,
|
||||||
output_path: &Path,
|
output_path: &Path,
|
||||||
@@ -507,6 +537,8 @@ impl EncState {
|
|||||||
"EncState::new: {width}x{height} enc={enc_width}x{enc_height} transform={transform:?}"
|
"EncState::new: {width}x{height} enc={enc_width}x{enc_height} transform={transform:?}"
|
||||||
);
|
);
|
||||||
// 1. VAAPI device — reuse existing context if provided
|
// 1. VAAPI device — reuse existing context if provided
|
||||||
|
// `match` 是 Rust 的 exhaustive 模式匹配(类似 Go `switch` 但更强:编译器强制覆盖所有分支)。
|
||||||
|
// `Option<AvHwDevCtx>` 等价于 Go `*AvHwDevCtx`(Some=value,None=nil),但类型系统强制处理 nil 情况。
|
||||||
let hw_device_ctx = match existing_hw_ctx {
|
let hw_device_ctx = match existing_hw_ctx {
|
||||||
Some(ctx) => ctx,
|
Some(ctx) => ctx,
|
||||||
None => AvHwDevCtx::new_vaapi(drm_device)?,
|
None => AvHwDevCtx::new_vaapi(drm_device)?,
|
||||||
@@ -530,7 +562,11 @@ impl EncState {
|
|||||||
|
|
||||||
let mut sink_ctx = video_filter
|
let mut sink_ctx = video_filter
|
||||||
.get("out")
|
.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"))?;
|
.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
|
// SAFETY: sink_ctx is a live buffersink; the returned hw_frames_ctx is
|
||||||
// borrowed, so av_buffer_ref creates an owned reference.
|
// borrowed, so av_buffer_ref creates an owned reference.
|
||||||
let sink_hw_frames = unsafe {
|
let sink_hw_frames = unsafe {
|
||||||
@@ -545,6 +581,8 @@ impl EncState {
|
|||||||
hw_ref
|
hw_ref
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 中文概述:解引用 sink_hw_frames 检查 filter graph 输出尺寸与编码器期望尺寸是否一致;
|
||||||
|
// 不一致仅 warn(filter 可能做隐式 scale),不视为硬错误。
|
||||||
// SAFETY: sink_hw_frames is an owned AVBufferRef to an AVHWFramesContext
|
// SAFETY: sink_hw_frames is an owned AVBufferRef to an AVHWFramesContext
|
||||||
// returned by the validated filter graph.
|
// returned by the validated filter graph.
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -559,9 +597,14 @@ impl EncState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Find h264_vaapi encoder
|
// 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")
|
let codec = ff::encoder::find_by_name("h264_vaapi")
|
||||||
.ok_or_else(|| anyhow::anyhow!("h264_vaapi encoder not found"))?;
|
.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 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()?
|
||||||
@@ -578,18 +621,21 @@ impl EncState {
|
|||||||
// VBV rate limiting: caps IDR burst size for WebRTC. Without this a 4K
|
// 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
|
// scene change can produce a 256KB keyframe that overflows the UDP send
|
||||||
// buffer. bufsize=bitrate/4 ≈ 250ms of video at the target bitrate.
|
// buffer. bufsize=bitrate/4 ≈ 250ms of video at the target bitrate.
|
||||||
|
// 中文概述:直接写裸指针字段 rc_max_rate/rc_buffer_size 设置 VBV 上限,避免 4K IDR 帧击爆 UDP 缓冲。
|
||||||
unsafe {
|
unsafe {
|
||||||
let ctx_ptr = enc.as_mut_ptr();
|
let ctx_ptr = enc.as_mut_ptr();
|
||||||
(*ctx_ptr).rc_max_rate = bitrate as i64;
|
(*ctx_ptr).rc_max_rate = bitrate as i64;
|
||||||
(*ctx_ptr).rc_buffer_size = (bitrate / 4) as i32;
|
(*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.
|
// SAFETY: AV_CODEC_FLAG_GLOBAL_HEADER must be set BEFORE opening the encoder.
|
||||||
// It triggers SPS/PPS extradata generation needed by the muxer for
|
// It triggers SPS/PPS extradata generation needed by the muxer for
|
||||||
// Annex B to AVCC conversion.
|
// Annex B to AVCC conversion.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*enc.as_mut_ptr()).flags |= ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32;
|
(*enc.as_mut_ptr()).flags |= ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32;
|
||||||
}
|
}
|
||||||
|
// 中文概述:把 hw_device_ctx 与 sink_hw_frames(filter 输出帧池)引用赋给编码器,编码器从该池分配 VAAPI surface。
|
||||||
// SAFETY: Assign hw device and frames ctx to the encoder.
|
// SAFETY: Assign hw device and frames ctx to the encoder.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*enc.as_mut_ptr()).hw_device_ctx = hw_device_ctx.ref_clone();
|
(*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.
|
// 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
|
let opened = enc
|
||||||
.open()
|
.open()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to open h264_vaapi encoder: {e}"))?;
|
||||||
let enc_video = opened.0;
|
let enc_video = opened.0;
|
||||||
|
|
||||||
// 6. Muxer setup (strict order)
|
// 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())?;
|
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();
|
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
|
||||||
|
|
||||||
|
// 中文概述:根据文件扩展名(.mp4/.mkv/...)分配输出 format context,但不打开文件。
|
||||||
// SAFETY: avformat_alloc_output_context2 creates format context from
|
// SAFETY: avformat_alloc_output_context2 creates format context from
|
||||||
// the file extension. Does NOT open the file.
|
// the file extension. Does NOT open the file.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
@@ -636,6 +690,7 @@ impl EncState {
|
|||||||
bail!("Failed to allocate output format context: {}", ff_err(ret));
|
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.
|
// SAFETY: avformat_query_codec checks codec+format compatibility.
|
||||||
let codec_id = unsafe { (*enc_video.as_ptr()).codec_id };
|
let codec_id = unsafe { (*enc_video.as_ptr()).codec_id };
|
||||||
let oformat = unsafe { (*fmt_ctx_ptr).oformat };
|
let oformat = unsafe { (*fmt_ctx_ptr).oformat };
|
||||||
@@ -646,12 +701,14 @@ impl EncState {
|
|||||||
bail!("H.264 codec not supported by output container format");
|
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.
|
// 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()) };
|
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 in output context");
|
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.
|
// SAFETY: avcodec_parameters_from_context copies encoder params + extradata.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr())
|
ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr())
|
||||||
@@ -663,11 +720,13 @@ impl EncState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述:把 encoder 的 time_base(1/fps)拷贝到 stream,muxer 写头部时按此时基打时间戳。
|
||||||
// SAFETY: Copy encoder time_base to stream.
|
// SAFETY: Copy encoder time_base to stream.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述:打开输出文件 IO 上下文(AVIO),把 pb(AVIOContext*)挂到 fmt_ctx。
|
||||||
// SAFETY: avio_open opens the output file for writing.
|
// SAFETY: avio_open opens the output file for writing.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
ffi::avio_open(
|
ffi::avio_open(
|
||||||
@@ -684,15 +743,19 @@ impl EncState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述:写入容器头部(MP4 ftyp box 等),失败需关闭 IO 释放资源(此处简化为直接 bail)。
|
||||||
// SAFETY: avformat_write_header writes the container header.
|
// SAFETY: avformat_write_header writes the container header.
|
||||||
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 output header: {}", ff_err(ret));
|
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.
|
// SAFETY: We created fmt_ctx_ptr above and it's valid.
|
||||||
let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
|
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 {
|
Ok(Self {
|
||||||
enc_video,
|
enc_video,
|
||||||
frames_rgb,
|
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 {
|
pub fn frames_rgb(&self) -> &AvHwFrameCtx {
|
||||||
&self.frames_rgb
|
&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<()> {
|
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
|
||||||
let mut filter_src_ctx = self
|
let mut filter_src_ctx = self
|
||||||
.video_filter
|
.video_filter
|
||||||
@@ -725,10 +793,17 @@ impl EncState {
|
|||||||
.add(hw_frame)
|
.add(hw_frame)
|
||||||
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Filter source add failed: {e}"))?;
|
||||||
|
|
||||||
|
// 持续从 sink 拉过滤后帧,直到 EAGAIN(filter 缓冲空)。
|
||||||
|
// `loop {}` 是 Rust 无限循环(类比 Go `for {}`),靠 `break` 退出。
|
||||||
loop {
|
loop {
|
||||||
|
// `ff::frame::Video::empty()` 分配零值帧(无数据),filter_sink.frame() 填充它。
|
||||||
|
// `&mut filtered` 把可变借用传给 ffmpeg-next,类比 Go `&filtered`。
|
||||||
let mut filtered = ff::frame::Video::empty();
|
let mut filtered = ff::frame::Video::empty();
|
||||||
|
// 多路 match:Ok 表示成功;带 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) {
|
match filter_sink.frame(&mut filtered) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
// 若 filter 没设 PTS(pts()==None),用输入 hw_frame 的 PTS 兜底。
|
||||||
if filtered.pts().is_none() {
|
if filtered.pts().is_none() {
|
||||||
filtered.set_pts(hw_frame.pts());
|
filtered.set_pts(hw_frame.pts());
|
||||||
}
|
}
|
||||||
@@ -743,18 +818,23 @@ impl EncState {
|
|||||||
}
|
}
|
||||||
let start_ts = self.starting_timestamp.unwrap();
|
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.
|
// SAFETY: avcodec_send_frame sends a valid NV12 VAAPI surface to the encoder.
|
||||||
let ret =
|
let ret =
|
||||||
unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) };
|
unsafe { ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), filtered.as_ptr()) };
|
||||||
if ret < 0 {
|
if ret < 0 {
|
||||||
bail!("avcodec_send_frame failed: {}", ff_err(ret));
|
bail!("avcodec_send_frame failed: {}", ff_err(ret));
|
||||||
}
|
}
|
||||||
|
// 立即 drain 编码器输出队列:FFmpeg 编码可能延迟一帧或多帧才产出 packet,
|
||||||
|
// send_frame 后必须 receive_packet 直到 EAGAIN,否则编码器内部缓冲溢出。
|
||||||
self.drain_encoder(start_ts)?;
|
self.drain_encoder(start_ts)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EOF 处理:在调用方(state.rs/state_portal.rs)完成所有 `encode_frame` 后调用,
|
||||||
|
// 负责把 filter 图残余帧排空 + 触发编码器 EOS + 写 muxer trailer(moov box 等)。
|
||||||
pub fn flush(&mut self) -> Result<()> {
|
pub fn flush(&mut self) -> Result<()> {
|
||||||
// Flush filter graph
|
// Flush filter graph
|
||||||
let mut filter_src_ctx = self
|
let mut filter_src_ctx = self
|
||||||
@@ -762,6 +842,7 @@ impl EncState {
|
|||||||
.get("in")
|
.get("in")
|
||||||
.ok_or_else(|| anyhow::anyhow!("filter 'in' not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("filter 'in' not found"))?;
|
||||||
let mut filter_src = filter_src_ctx.source();
|
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() {
|
if let Err(e) = filter_src.flush() {
|
||||||
tracing::debug!("filter source flush error: {e}");
|
tracing::debug!("filter source flush error: {e}");
|
||||||
}
|
}
|
||||||
@@ -771,12 +852,18 @@ impl EncState {
|
|||||||
.video_filter
|
.video_filter
|
||||||
.get("out")
|
.get("out")
|
||||||
.ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?;
|
.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();
|
let mut filter_sink = filter_sink_ctx.sink();
|
||||||
loop {
|
loop {
|
||||||
let mut filtered = ff::frame::Video::empty();
|
let mut filtered = ff::frame::Video::empty();
|
||||||
match filter_sink.frame(&mut filtered) {
|
match filter_sink.frame(&mut filtered) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
// `unwrap_or(0)` 是 Option 的兜底方法:None 返回 0,Some(v) 返回 v。
|
||||||
|
// flush 时若从未编码过帧(starting_timestamp=None),用 0 作为 PTS 基线。
|
||||||
let start_ts = self.starting_timestamp.unwrap_or(0);
|
let start_ts = self.starting_timestamp.unwrap_or(0);
|
||||||
|
// 中文概述:把 drain 出的帧送编码器;flush 路径失败也 bail(停止 flush)。
|
||||||
// SAFETY: filtered is a valid VAAPI frame drained from the
|
// SAFETY: filtered is a valid VAAPI frame drained from the
|
||||||
// filter graph; enc_video is an opened encoder.
|
// filter graph; enc_video is an opened encoder.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
@@ -791,15 +878,18 @@ impl EncState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述:发送 NULL 帧给编码器,触发 EOS(End Of Stream),编码器内部缓冲的帧会被强制输出。
|
||||||
// SAFETY: Sending null frame signals end of stream to encoder.
|
// SAFETY: Sending null frame signals end of stream to encoder.
|
||||||
unsafe {
|
unsafe {
|
||||||
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
|
ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_ts = self.starting_timestamp.unwrap_or(0);
|
let start_ts = self.starting_timestamp.unwrap_or(0);
|
||||||
|
// 最后一次 drain:取出 EOS 触发后所有剩余 packet。
|
||||||
self.drain_encoder(start_ts)?;
|
self.drain_encoder(start_ts)?;
|
||||||
|
|
||||||
// Write trailer only if at least one frame was encoded.
|
// Write trailer only if at least one frame was encoded.
|
||||||
|
// 写 trailer 条件:若全程 0 帧编码,写 trailer 会让 muxer 生成空 moov(损坏文件),所以用 frames_written 守卫。
|
||||||
if self.frames_written {
|
if self.frames_written {
|
||||||
self.octx
|
self.octx
|
||||||
.write_trailer()
|
.write_trailer()
|
||||||
@@ -809,14 +899,19 @@ impl EncState {
|
|||||||
Ok(())
|
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<()> {
|
fn drain_encoder(&mut self, start_ts: i64) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
let mut pkt = ff::Packet::empty();
|
let mut pkt = ff::Packet::empty();
|
||||||
|
// 中文概述:从编码器拉一个已编码 packet;返回 <0 时区分 EAGAIN(暂时无)和 EOF(流结束)。
|
||||||
// SAFETY: avcodec_receive_packet retrieves an encoded packet.
|
// SAFETY: avcodec_receive_packet retrieves an encoded packet.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
ffi::avcodec_receive_packet(self.enc_video.as_mut_ptr(), pkt.as_mut_ptr())
|
ffi::avcodec_receive_packet(self.enc_video.as_mut_ptr(), pkt.as_mut_ptr())
|
||||||
};
|
};
|
||||||
if ret < 0 {
|
if ret < 0 {
|
||||||
|
// 嵌套 if:先判 ret<0(FFI 错误),再判具体码(EAGAIN/EOF)。
|
||||||
|
// `ffi::AVERROR(ffi::EAGAIN)` 是 FFmpeg 的 errno 包装宏(负数)。
|
||||||
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
|
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -824,7 +919,10 @@ impl EncState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rescale timestamps from encoder time_base to stream time_base
|
// Rescale timestamps from encoder time_base to stream time_base
|
||||||
|
// 时间基重缩放:encoder 用 1/fps,stream 可能用 1/1000 等不同基。
|
||||||
|
// rescale_ts 用有理数运算,避免浮点误差累积。
|
||||||
let enc_tb = self.enc_video.time_base();
|
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
|
// SAFETY: octx was created with stream 0 during muxer setup; streams
|
||||||
// is non-null and stream 0 remains owned by the format context.
|
// is non-null and stream 0 remains owned by the format context.
|
||||||
let stream_tb = unsafe {
|
let stream_tb = unsafe {
|
||||||
@@ -832,12 +930,16 @@ impl EncState {
|
|||||||
if fmt.nb_streams == 0 || fmt.streams.is_null() {
|
if fmt.nb_streams == 0 || fmt.streams.is_null() {
|
||||||
bail!("no streams in output context");
|
bail!("no streams in output context");
|
||||||
}
|
}
|
||||||
|
// `fmt.streams.add(0)` 是裸指针算术: streams 是 `*mut *mut AVStream`,
|
||||||
|
// `.add(0)` 取首元素地址,外层 `*` 解引用得到 `*mut AVStream`。
|
||||||
let st = *fmt.streams.add(0);
|
let st = *fmt.streams.add(0);
|
||||||
ff::Rational::from((*st).time_base)
|
ff::Rational::from((*st).time_base)
|
||||||
};
|
};
|
||||||
pkt.rescale_ts(enc_tb, stream_tb);
|
pkt.rescale_ts(enc_tb, stream_tb);
|
||||||
|
|
||||||
// Offset timestamps so first frame starts at 0
|
// 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() {
|
if let Some(pts) = pkt.pts() {
|
||||||
pkt.set_pts(Some(pts - start_ts));
|
pkt.set_pts(Some(pts - start_ts));
|
||||||
}
|
}
|
||||||
@@ -845,10 +947,13 @@ impl EncState {
|
|||||||
pkt.set_dts(Some(dts - start_ts));
|
pkt.set_dts(Some(dts - start_ts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 标记 packet 属于 stream 0(视频流)。
|
||||||
pkt.set_stream(0);
|
pkt.set_stream(0);
|
||||||
|
// 交错写入:muxer 内部会按 DTS 排序后写出,类比 Go `mp4Writer.WriteInterleaved(pkt)`。
|
||||||
pkt.write_interleaved(&mut self.octx)
|
pkt.write_interleaved(&mut self.octx)
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
|
||||||
|
|
||||||
|
// 标记:至少一帧已写入 muxer(flush 时用此守卫决定是否写 trailer)。
|
||||||
self.frames_written = true;
|
self.frames_written = true;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user