docs(avhw): [3/4] 中文注释软件编码路径 SwEnc*

This commit is contained in:
dailz
2026-06-22 18:19:31 +08:00
parent 98eb72e2a2
commit 052729529e
+194
View File
@@ -964,6 +964,20 @@ impl EncState {
// SwEncState - VAAPI GPU downscale + software H.264 encode // SwEncState - VAAPI GPU downscale + software H.264 encode
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 编码完成的 H.264 帧载体,是软件编码路径(SwEncEncode → WebRTC 通道)的输出单元。
// 与硬件路径 EncState 不同:硬件路径直接对接 avformat muxerPTS 由 muxer 内部维护;
// 软件路径通过 crossbeam channel 把帧送到 webrtc 线程,必须显式携带 PTS 与捕获时间戳。
//
// 字段语义:
// - dataH.264 NAL 字节流(Annex B 或 AVCC,取决于 encoder 配置;本工程用 libx264 默认 Annex B
// - pts_ticks:编码器 time_base 单位下的 PTSWebRTC 模式下 time_base=1/90000
// 直接对应 RTP 时间戳,90kHz 精度)
// - capture_time:墙上时钟捕获时刻,从 CpuNv12Frame 透传,用于 stats 的 frame_age 指标
//
// issue #24 背景:Wayland damage-driven 帧率波动大,若 PTS 用帧计数 * (1/fps) 估算,
// 浏览器 jitter buffer 会膨胀到秒级;必须用真实捕获时刻换算 PTS。
//
// 与 Go 对照:类似 Go 视频包结构体 `type Frame struct { Data []byte; PTSTicks int64; CaptureTime time.Time }`。
/// Encoded H.264 frame with timing metadata for WebRTC output. /// Encoded H.264 frame with timing metadata for WebRTC output.
/// ///
/// MP4 file output (FrameOutput::Muxer) does NOT use this - it writes via /// MP4 file output (FrameOutput::Muxer) does NOT use this - it writes via
@@ -982,11 +996,21 @@ pub struct EncodedH264Frame {
pub capture_time: std::time::Instant, pub capture_time: std::time::Instant,
} }
// 编码器输出目的地二选一枚举:
// - Muxer:直接对接 FFmpeg avformat 写 MP4 文件(自包含 PTS,硬编码路径 EncState 风格)
// - Channelcrossbeam Sender<EncodedH264Frame>,把帧送 WebRTC 信令线程(必须显式带 PTS)
// 类比 Go`type FrameOutput interface { Write(Frame) error }` 的两种实现。
// 该枚举让 SwEncEncode 在 new_muxer / new_webrtc 两个构造函数间共享同一 drain_encoder 主循环。
pub enum FrameOutput { pub enum FrameOutput {
Muxer(ff::format::context::Output), Muxer(ff::format::context::Output),
Channel(crossbeam_channel::Sender<EncodedH264Frame>), Channel(crossbeam_channel::Sender<EncodedH264Frame>),
} }
// 主线程持有:完成 VAAPI GPU 下采样 + av_hwframe_transfer_data 回到 CPU 的 NV12 帧缓冲。
// 数据流:PipeWire/wlr 帧通过 import_and_scale 进入 → VAAPI scale_vaapi 转 NV12 →
// transfer_filtered_to_cpu 拷贝到 Vec<u8> → 通过 channel 跨线程送 SwEncEncode 编码线程。
// NV12 格式:Y 平面满分辨率,UV 交错平面半高(chroma 高度减半);stride 可能 > width
// (对齐到 GPU 要求)。字段不带 GPU 资源(纯 CPU 内存),自动 Send。
/// Owned CPU NV12 frame data for cross-thread transfer. /// Owned CPU NV12 frame data for cross-thread transfer.
/// Produced by main thread (VAAPI import + GPU scale + transfer), consumed by encode thread. /// Produced by main thread (VAAPI import + GPU scale + transfer), consumed by encode thread.
pub struct CpuNv12Frame { pub struct CpuNv12Frame {
@@ -1000,6 +1024,18 @@ pub struct CpuNv12Frame {
pub capture_time: std::time::Instant, pub capture_time: std::time::Instant,
} }
// 软件编码路径的"导入 + GPU 缩放"半部:负责把外部 DMA-BUF 帧(PipeWire/wlr)经
// VAAPI 硬件零拷贝导入 → scale_vaapi GPU 下采样 → av_hwframe_transfer_data 回传 CPU NV12
// 最后封装为 CpuNv12Frame 喂给 SwEncEncodelibx264 软编)。
//
// 与 EncState(硬编直连)的区别:本结构仅做 GPU 辅助的导入/缩放,H.264 编码由 CPU 完成;
// 因此自始至终只在导入线程访问,跨线程边界是 channelCpuNv12Frame)。
//
// 字段:
// - hw_dev / frames_rgbVAAPI 设备 + BGRA 硬件帧上下文(DMA-BUF 导入目的地)
// - filter_graphFFmpeg 滤镜图(scale_vaapi=M,w=enc:h=enc,format=nv12
// - width/height:源帧尺寸;enc_width/enc_height:目标缩放后尺寸
// - resolution_rx / encoder_resolution_tx:可选的动态分辨率控制通道对
pub struct SwEncImport { pub struct SwEncImport {
hw_dev: AvHwDevCtx, hw_dev: AvHwDevCtx,
frames_rgb: AvHwFrameCtx, frames_rgb: AvHwFrameCtx,
@@ -1013,6 +1049,12 @@ pub struct SwEncImport {
encoder_resolution_tx: Option<crossbeam_channel::Sender<ResolutionChange>>, encoder_resolution_tx: Option<crossbeam_channel::Sender<ResolutionChange>>,
} }
// impl SwEncImport5 个方法按数据流顺序
// (1) new / new_with_resolution_control:构造函数(建立 VAAPI + filter graph
// (2) frames_rgb:访问器(暴露硬件帧上下文给外部 DMA-BUF 导入)
// (3) import_and_scale:单帧主流程(filter_src.add → filter_sink.frame 循环 → transfer_to_cpu
// (4) flush_importEOS 时排空 filter graph 缓冲
// (5) poll_resolution_commands + transfer_filtered_to_cpu:私有辅助
impl SwEncImport { impl SwEncImport {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
@@ -1023,9 +1065,18 @@ impl SwEncImport {
enc_height: u32, enc_height: u32,
fps: u32, fps: u32,
) -> Result<Self> { ) -> Result<Self> {
// drm_device 形如 /dev/dri/renderD128AvHwDevCtx::new_vaapi 内部打开 DRM fd 并
// 调 vaGetDisplayDRM / vaInitialize 建立 VAAPI 连接(FFmpeg hwcontext_vaapi)。
// ? 自动传播 Result(类比 Go `if err != nil { return err }`)。
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?; let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
// 硬件帧上下文:调用 vaCreateSurfaces 申请 BGRA surface 池供 DMA-BUF 导入使用;
// AV_PIX_FMT_BGRA 对应 PipeWire/wlr 帧的常见 RGB 格式。返回的 AvHwFrameCtx 持有
// AVHWFramesContextsurface 池大小由 FFmpeg 默认(通常 4-8 帧)。
let frames_rgb = let frames_rgb =
AvHwFrameCtx::for_capture(&hw_dev, width, height, ff::format::Pixel::BGRA)?; AvHwFrameCtx::for_capture(&hw_dev, width, height, ff::format::Pixel::BGRA)?;
// 建立 scale_vaapi 滤镜图:源=BGRA/VAAPIscale=enc_width×enc_heightformat=nv12。
// 滤镜图绑定的 hw_dev 与 frames_rgb 必须同属一个 VAAPI 设备,否则 vaCreateSurfaces
// 在不同 display 上会失败。
let filter_graph = build_swenc_filter_graph( let filter_graph = build_swenc_filter_graph(
&hw_dev, &hw_dev,
&frames_rgb, &frames_rgb,
@@ -1036,6 +1087,9 @@ impl SwEncImport {
fps, fps,
)?; )?;
// Self 是返回类型的别名(Rust 构造器惯用法,类比 Go `return &Foo{...}` 中的 Foo)。
// 字段简写:当局部变量名与字段名相同时可省略 `field: value`,等价于 Go 结构体字面量简写。
// resolution_rx / encoder_resolution_tx 留 None,由 new_with_resolution_control 后续填充。
Ok(Self { Ok(Self {
hw_dev, hw_dev,
frames_rgb, frames_rgb,
@@ -1073,8 +1127,12 @@ impl SwEncImport {
} }
pub fn import_and_scale(&mut self, hw_frame: &ff::frame::Video) -> Result<CpuNv12Frame> { pub fn import_and_scale(&mut self, hw_frame: &ff::frame::Video) -> Result<CpuNv12Frame> {
// 先消费 ResolutionChange 命令:若分辨率改变,本函数后续的 filter graph 已是新图。
self.poll_resolution_commands()?; self.poll_resolution_commands()?;
// 拿到滤镜图的输入/输出端点("in"/"out" 是 build_swenc_filter_graph 中注册的名称)。
// ok_or_else 把 Option<Result> 转 Result(延迟构造错误,类比 Go `if x == nil { return err }`)。
// ? 自动传播 Resultmut 因为 source()/sink() 返回 &mut 借用。
let mut filter_src_ctx = self let mut filter_src_ctx = self
.filter_graph .filter_graph
.get("in") .get("in")
@@ -1086,16 +1144,22 @@ impl SwEncImport {
.ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?; .ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?;
let mut filter_sink = filter_sink_ctx.sink(); let mut filter_sink = filter_sink_ctx.sink();
// 把外部导入的硬件帧(hw_frame 是 AV_PIX_FMT_DRM_PRIME/VAAPIVASurfaceID 在 data[3]
// 送入滤镜图入口。滤镜图内部 scale_vaapi 执行 GPU 下采样 + 格式转换。
// map_err 把 ffmpeg::Error 包装成 anyhow::Error 加上下文。
filter_src filter_src
.add(hw_frame) .add(hw_frame)
.map_err(|e| anyhow::anyhow!("software pipeline filter source add failed: {e}"))?; .map_err(|e| anyhow::anyhow!("software pipeline filter source add failed: {e}"))?;
// filter graph 可能产生多帧(理论上 scale_vaapi 1:1,但 deinterlace/ fps 滤镜可能 1:N)。
// 这里只取第一帧;额外帧计数后丢并 warn。EAGAIN 表示滤镜图已 drain 完毕。
let mut first = None; let mut first = None;
let mut extra_count = 0usize; let mut extra_count = 0usize;
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(()) => {
// 若 filter 没有显式设置 PTS(罕见),用源帧 PTS 兜底。
if filtered.pts().is_none() { if filtered.pts().is_none() {
filtered.set_pts(hw_frame.pts()); filtered.set_pts(hw_frame.pts());
} }
@@ -1106,6 +1170,8 @@ impl SwEncImport {
extra_count += 1; extra_count += 1;
} }
} }
// ffi::EAGAIN:滤镜图当前没有输出可取;这是正常的"等下一帧"信号,break 主循环。
// 类比 Go 中 io.Read 返回 EAGAIN。
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break, Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break,
Err(e) => bail!("software pipeline filter sink get frame failed: {e}"), Err(e) => bail!("software pipeline filter sink get frame failed: {e}"),
} }
@@ -1117,6 +1183,8 @@ impl SwEncImport {
); );
} }
// first 一定存在:filter_src.add 成功后 scale_vaapi 至少产出一帧;
// 若不产出说明上游 filter graph 配置错误或 hw_frame 无效,返回错误。
first.ok_or_else(|| anyhow::anyhow!("software pipeline produced no scaled frame")) first.ok_or_else(|| anyhow::anyhow!("software pipeline produced no scaled frame"))
} }
@@ -1197,6 +1265,8 @@ impl SwEncImport {
} }
fn transfer_filtered_to_cpu(&self, filtered: &ff::frame::Video) -> Result<CpuNv12Frame> { fn transfer_filtered_to_cpu(&self, filtered: &ff::frame::Video) -> Result<CpuNv12Frame> {
// 中文概述:分配一个 CPU 侧的 AVFrame 作为 av_hwframe_transfer_data 的目标。
// 英文 SAFETY 详细论证见下方。
// SAFETY: av_frame_alloc returns a newly allocated AVFrame or null, // SAFETY: av_frame_alloc returns a newly allocated AVFrame or null,
// which is checked below. // which is checked below.
let mut sw_nv12 = unsafe { ffi::av_frame_alloc() }; let mut sw_nv12 = unsafe { ffi::av_frame_alloc() };
@@ -1204,10 +1274,14 @@ impl SwEncImport {
bail!("av_frame_alloc failed for NV12 transfer frame"); bail!("av_frame_alloc failed for NV12 transfer frame");
} }
// 中文概述:GPU→CPU 数据回传。filtered 指向 VAAPI NV12 surface
// av_hwframe_transfer_data 内部调用 vaGetImage 等同步原语把 GPU 显存拷贝到 sw_nv12。
// 英文 SAFETY 论证下方。
// SAFETY: sw_nv12 is an allocated destination frame; filtered is a valid VAAPI NV12 // SAFETY: sw_nv12 is an allocated destination frame; filtered is a valid VAAPI NV12
// surface produced by scale_vaapi at encoder dimensions. // surface produced by scale_vaapi at encoder dimensions.
let transfer_ret = unsafe { ffi::av_hwframe_transfer_data(sw_nv12, filtered.as_ptr(), 0) }; let transfer_ret = unsafe { ffi::av_hwframe_transfer_data(sw_nv12, filtered.as_ptr(), 0) };
if transfer_ret < 0 { if transfer_ret < 0 {
// 中文概述:transfer 失败时必须释放 sw_nv12 防止泄漏;FFmpeg C API 无 RAII。
// SAFETY: sw_nv12 was allocated above and has not been freed yet. // SAFETY: sw_nv12 was allocated above and has not been freed yet.
unsafe { ffi::av_frame_free(&mut sw_nv12) }; unsafe { ffi::av_frame_free(&mut sw_nv12) };
bail!( bail!(
@@ -1216,6 +1290,9 @@ impl SwEncImport {
); );
} }
// 中文概述:从 AVFrame 字段读 Y/UV 平面指针、linesize、尺寸,校验后拷贝到 Vec<u8>。
// 错误路径同样调用 av_frame_free 释放。最后再次 av_frame_free 释放 sw_nv12 自身。
// 英文 SAFETY 详细论证见下方。
// SAFETY: sw_nv12 was filled by av_hwframe_transfer_data. NV12 planes 0 and 1 are // SAFETY: sw_nv12 was filled by av_hwframe_transfer_data. NV12 planes 0 and 1 are
// initialized for enc_width x enc_height; linesize values define each row's byte span. // initialized for enc_width x enc_height; linesize values define each row's byte span.
let frame = unsafe { let frame = unsafe {
@@ -1235,6 +1312,8 @@ impl SwEncImport {
} }
let y_len = y_stride * self.enc_height as usize; let y_len = y_stride * self.enc_height as usize;
let uv_len = uv_stride * (self.enc_height as usize / 2); let uv_len = uv_stride * (self.enc_height as usize / 2);
// slice::from_raw_parts:把 C 的裸指针 + 长度变成 Rust 切片(零拷贝)。
// 紧接着 .to_vec() 立即深拷贝,避免 sw_nv12 释放后切片悬空。
let y_data = slice::from_raw_parts(y_ptr, y_len).to_vec(); let y_data = slice::from_raw_parts(y_ptr, y_len).to_vec();
let uv_data = slice::from_raw_parts(uv_ptr, uv_len).to_vec(); let uv_data = slice::from_raw_parts(uv_ptr, uv_len).to_vec();
let pts = filtered.pts().unwrap_or(0); let pts = filtered.pts().unwrap_or(0);
@@ -1253,6 +1332,17 @@ impl SwEncImport {
} }
} }
// 软件编码路径的"CPU 编码"半部:消费 CpuNv12FrameNV12),用 sws_scale 转 YUV420P
// 喂 libx264 软件 H.264 编码器,输出 EncodedH264Frame。与 SwEncImport 配对使用:
// SwEncImport 负责 GPU 辅助导入/缩放,SwEncEncode 负责 CPU 编码;两者通过 channel 通信。
//
// 持有的 C 资源(裸指针,非 Send/Sync 自动,需手写 unsafe impl Send 见下):
// - sws_ctxFFmpeg SwsContextNV12 → YUV420P 颜色空间转换器,无 resize)
// - yuv_frame:可复用的 AVFrameYUV420P,尺寸 = enc_width × enc_height
// - enc_video:打开的 libx264 AVCodecContext
//
// 其他字段是控制状态(动态码率/分辨率/关键帧请求、去重哈希、计时统计、WebRTC 暂停/断开)。
// 详见每个字段上方英文 ///(保留)。
pub struct SwEncEncode { pub struct SwEncEncode {
sws_ctx: *mut ffi::SwsContext, sws_ctx: *mut ffi::SwsContext,
enc_video: ff::codec::encoder::video::Video, enc_video: ff::codec::encoder::video::Video,
@@ -1285,16 +1375,40 @@ pub struct SwEncEncode {
last_capture_time: Option<Instant>, last_capture_time: Option<Instant>,
} }
// FNV-1a 64 位哈希常量,用于帧去重(见 encode_cpu_frame 调用 hash_sampled_y_plane):
// - FNV1A_OFFSET_BASIS:初始哈希值(FNV 论文推荐的 64 位 offset basis
// - FNV1A_PRIME:每字节乘法因子(64 位素数)
// - Y_PLANE_HASH_ROW_STEPY 平面采样步长(仅哈希每 8 行,性能与碰撞率权衡)
// 算法:hash = (hash XOR byte) * PRIME,每字节迭代;XOR + 乘法双混淆,分布均匀。
// 选择 FNV-1a 而非 CRC32:性能相当但无查表依赖,纯整数运算适合帧级 hot path。
// 64 位宽:1080p60 一帧 1920*1080*0.125 字节 ≈ 260KB 采样数据,32 位易碰撞。
const FNV1A_OFFSET_BASIS: u64 = 0xcbf29ce484222325; const FNV1A_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV1A_PRIME: u64 = 0x100000001b3; const FNV1A_PRIME: u64 = 0x100000001b3;
const Y_PLANE_HASH_ROW_STEP: usize = 8; const Y_PLANE_HASH_ROW_STEP: usize = 8;
// WebRTC 模式下的编码器 time_base 分母(90kHz)。匹配 RTP 视频时钟频率(RFC 3551)。
// 这样编码器输出的 PTS 直接就是 RTP 时间戳,无需二次换算(microsecond 精度足够)。
// MP4 模式保留 1/fps time_base 以简化文件写入。
/// WebRTC media clock frequency in Hz. Matches RTP clock for video (RFC 3551). /// WebRTC media clock frequency in Hz. Matches RTP clock for video (RFC 3551).
/// Used as encoder time_base denominator for WebRTC mode (1/90000) so that /// Used as encoder time_base denominator for WebRTC mode (1/90000) so that
/// PTS values directly become RTP timestamps with microsecond precision. /// PTS values directly become RTP timestamps with microsecond precision.
/// MP4 mode keeps 1/fps time_base for file output simplicity. /// MP4 mode keeps 1/fps time_base for file output simplicity.
pub const WEBRTC_RTP_CLOCK_HZ: i128 = 90_000; pub const WEBRTC_RTP_CLOCK_HZ: i128 = 90_000;
// 计算 NV12 Y 平面的 FNV-1a 采样哈希,用于检测连续帧是否像素相同(静帧去重)。
// 步长 Y_PLANE_HASH_ROW_STEP=8:仅哈希每 8 行的全部像素,跳过中间 7 行。
// 这是性能优化:1080p60 全量哈希 ~10MB/帧 * 60fps = 600MB/s 内存带宽;
// 采样到 ~1.3MB/帧仍能可靠检测桌面静止(多数静止帧大面积像素相同)。
//
// 等价 Go
// hash := uint64(0xcbf29ce484222325)
// for row := 0; row < height; row += 8 {
// off := row * stride
// for _, b := range y_data[off:off+width] {
// hash ^= uint64(b); hash *= 0x100000001b3
// }
// }
// wrapping_mul:故意允许溢出回绕(u64 取模 2^64),等价于 Go 无符号溢出语义。
fn hash_sampled_y_plane(y_data: &[u8], width: usize, height: usize, stride: usize) -> u64 { fn hash_sampled_y_plane(y_data: &[u8], width: usize, height: usize, stride: usize) -> u64 {
let mut hash = FNV1A_OFFSET_BASIS; let mut hash = FNV1A_OFFSET_BASIS;
@@ -1310,10 +1424,22 @@ fn hash_sampled_y_plane(y_data: &[u8], width: usize, height: usize, stride: usiz
hash hash
} }
// unsafe impl Send 中文概述:SwEncEncode 含裸指针(sws_ctx / yuv_frame / enc_video),
// Rust 默认认为裸指针非 Send(防止跨线程共享 C 资源)。这里手写 unsafe impl Send 表示:
// 本类型在构造完成后只被 move 到一个编码线程,所有访问通过 &mut self 独占借用,
// 满足"单线程独占"模型。AGENTS.md 明确警告:跨线程移动这些 wrapper 前必须重审 exclusivity 假设。
// 英文 SAFETY 详细论证下方保留不动。
// SAFETY: SwEncEncode owns sws_ctx/yuv_frame/enc_video exclusively after construction. // SAFETY: SwEncEncode owns sws_ctx/yuv_frame/enc_video exclusively after construction.
// It is moved to a single encode thread and only accessed through &mut self there. // It is moved to a single encode thread and only accessed through &mut self there.
unsafe impl Send for SwEncEncode {} unsafe impl Send for SwEncEncode {}
// impl SwEncEncode6 个方法按数据流顺序
// (1) new_muxer:构造函数,输出到 MP4 文件(new_with_resolution_control 调用)
// (2) new_webrtc:构造函数,输出到 crossbeam Sender<EncodedH264Frame>
// (3) flushEOS 时排空编码器(send null frame + drain
// (4) take_timing:取出最近一帧的 sws/encode 耗时统计
// (5) encode_cpu_frame:单帧主循环(NV12 → YUV420P → libx264 → drain → channel/muxer
// (6) recreate_encoder / write_trailer_if_needed / drain_encoder:私有辅助
impl SwEncEncode { impl SwEncEncode {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn new_muxer( fn new_muxer(
@@ -1324,10 +1450,17 @@ impl SwEncEncode {
bitrate: u64, bitrate: u64,
gop_size: u32, gop_size: u32,
) -> Result<Self> { ) -> Result<Self> {
// MP4 muxer 模式构造函数:用 avformat 写文件,不接 WebRTC。
// 调用专用工厂函数(create_software_h264_muxer 内部完成 avformat_alloc_output +
// libx264 encoder + avio_open + write_header)。
let sws_ctx = create_nv12_to_yuv420p_sws(enc_width, enc_height)?; let sws_ctx = create_nv12_to_yuv420p_sws(enc_width, enc_height)?;
let (enc_video, octx) = let (enc_video, octx) =
create_software_h264_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?; create_software_h264_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?;
let yuv_frame = alloc_yuv420p_frame(enc_width, enc_height)?; let yuv_frame = alloc_yuv420p_frame(enc_width, enc_height)?;
// 创建后立即 drop 发送端的 dummy channel idiom:构造一个容量 1 的 channel
// 拿到接收端后立刻丢弃发送端(drop(dummy_tx)),使接收端永远 recv 到 Disconnected。
// encode_cpu_frame 中的 try_recv 循环因此立即结束,等效"无分辨率控制"。
// 比用 Option<Receiver> 更优雅:避免每个调用点都 match Option。
let (dummy_tx, bitrate_rx) = crossbeam_channel::bounded(1); let (dummy_tx, bitrate_rx) = crossbeam_channel::bounded(1);
drop(dummy_tx); drop(dummy_tx);
let (dummy_resolution_tx, resolution_rx) = crossbeam_channel::bounded(1); let (dummy_resolution_tx, resolution_rx) = crossbeam_channel::bounded(1);
@@ -1399,6 +1532,10 @@ impl SwEncEncode {
} }
pub fn flush(&mut self) -> Result<()> { pub fn flush(&mut self) -> Result<()> {
// 中文概述:flush 阶段,向打开的 libx264 编码器发送 NULL frame 触发 drain 模式。
// FFmpeg 契约:send_frame(NULL) 表示输入 EOS,后续 receive_packet 直到返回 AVERROR_EOF。
// AVERROR_EOF 在内部被忽略(已 drain 完毕),其他错误传播。
// 英文 SAFETY 见下方。
// SAFETY: Sending a null frame flushes the opened software encoder; // SAFETY: Sending a null frame flushes the opened software encoder;
// no frame data is dereferenced. enc_video is exclusively borrowed via &mut self. // no frame data is dereferenced. enc_video is exclusively borrowed via &mut self.
unsafe { unsafe {
@@ -1418,15 +1555,19 @@ impl SwEncEncode {
} }
pub fn encode_cpu_frame(&mut self, frame: &CpuNv12Frame) -> Result<EncodeOutcome> { pub fn encode_cpu_frame(&mut self, frame: &CpuNv12Frame) -> Result<EncodeOutcome> {
// 每帧开始:把计时统计置零,避免上一帧的 stale 值泄漏(last_timing 字段注释见 struct)。
self.last_timing = SwEncodeTiming::default(); self.last_timing = SwEncodeTiming::default();
// Save capture_time so drain_encoder can propagate it into the // Save capture_time so drain_encoder can propagate it into the
// EncodedH264Frame emitted via the WebRTC channel (issue #20). // EncodedH264Frame emitted via the WebRTC channel (issue #20).
self.last_capture_time = Some(frame.capture_time); self.last_capture_time = Some(frame.capture_time);
// WebRTC 通道已断开(之前 drain_encoder 检测到 Disconnected)→ 后续帧直接跳过。
if self.webrtc_disconnected { if self.webrtc_disconnected {
return Ok(EncodeOutcome::SkippedDisconnected); return Ok(EncodeOutcome::SkippedDisconnected);
} }
// 必须先 drain bitrate 命令再做 stride 检查:导入线程在 emit ResolutionChange 后
// 才送来新的(更小 stride 的)帧;若先检查 stride 会因旧 stride 与新帧不匹配误报。
// Must drain before the stride check: the import thread emits // Must drain before the stride check: the import thread emits
// ResolutionChange before the new (smaller-stride) frame arrives. // ResolutionChange before the new (smaller-stride) frame arrives.
while let Ok(cmd) = self.bitrate_rx.try_recv() { while let Ok(cmd) = self.bitrate_rx.try_recv() {
@@ -1439,6 +1580,8 @@ impl SwEncEncode {
let target_bps = target_bps.min(ENCODER_BITRATE_HARD_CAP); let target_bps = target_bps.min(ENCODER_BITRATE_HARD_CAP);
tracing::info!(target_bps, "updating encoder bitrate from BWE feedback"); tracing::info!(target_bps, "updating encoder bitrate from BWE feedback");
self.bitrate = target_bps; self.bitrate = target_bps;
// 中文概述:直接写 libx264 AVCodecContext 的 bit_rate 字段(i64),
// 实时调整编码器输出码率(BWE 反馈驱动)。英文 SAFETY 见下方。
// SAFETY: enc_video is an opened AVCodecContext exclusively owned by &mut self. // SAFETY: enc_video is an opened AVCodecContext exclusively owned by &mut self.
unsafe { unsafe {
let ctx = self.enc_video.as_mut_ptr(); let ctx = self.enc_video.as_mut_ptr();
@@ -1453,8 +1596,11 @@ impl SwEncEncode {
} }
} }
// 暂存 keyframe 请求状态:实际使用在本函数后段(pict_type 设置处),中间可能被覆盖。
let force_this_frame = self.force_keyframe_pending; let force_this_frame = self.force_keyframe_pending;
// drain ResolutionChange 通道:若分辨率变化,调用 recreate_encoder 重建 libx264 +
// sws_ctx + yuv_frame(避免在帧处理中途重建,确保后续 unsafe 块看到的字段一致)。
while let Ok(change) = self.resolution_rx.try_recv() { while let Ok(change) = self.resolution_rx.try_recv() {
self.recreate_encoder(change.width, change.height)?; self.recreate_encoder(change.width, change.height)?;
} }
@@ -1462,6 +1608,8 @@ impl SwEncEncode {
if frame.y_stride < self.enc_width as usize || frame.uv_stride < self.enc_width as usize { if frame.y_stride < self.enc_width as usize || frame.uv_stride < self.enc_width as usize {
bail!("CPU NV12 frame stride is smaller than encoder width"); bail!("CPU NV12 frame stride is smaller than encoder width");
} }
// 暂停检查:webrtc_paused 是 Arc<AtomicBool>(跨线程共享),Relaxed 内存序足够
// (单字段读,不参与 happens-before 协调;类比 Go atomic.LoadInt32)。
if let Some(ref paused) = self.webrtc_paused { if let Some(ref paused) = self.webrtc_paused {
if paused.load(Ordering::Relaxed) { if paused.load(Ordering::Relaxed) {
return Ok(EncodeOutcome::SkippedPaused); return Ok(EncodeOutcome::SkippedPaused);
@@ -1470,6 +1618,8 @@ impl SwEncEncode {
let width = self.enc_width as usize; let width = self.enc_width as usize;
let height = self.enc_height as usize; let height = self.enc_height as usize;
// required_y_lenY 平面所需最小字节数 = (height-1) 行完整 stride + 末行 width 字节。
// 用于校验源帧尺寸合法(防御式编程,避免 from_raw_parts 越界)。
let required_y_len = frame.y_stride * height.saturating_sub(1) + width; let required_y_len = frame.y_stride * height.saturating_sub(1) + width;
if frame.y_data.len() < required_y_len { if frame.y_data.len() < required_y_len {
bail!("CPU NV12 frame Y plane is smaller than encoder dimensions"); bail!("CPU NV12 frame Y plane is smaller than encoder dimensions");
@@ -1477,7 +1627,10 @@ impl SwEncEncode {
let frame_index = self.frame_count; let frame_index = self.frame_count;
self.frame_count = self.frame_count.saturating_add(1); self.frame_count = self.frame_count.saturating_add(1);
// 帧去重:FNV-1a 哈希 Y 平面采样点,与上一帧相同则丢帧(静帧优化)。
let current_hash = hash_sampled_y_plane(&frame.y_data, width, height, frame.y_stride); let current_hash = hash_sampled_y_plane(&frame.y_data, width, height, frame.y_stride);
// GOP 强制关键帧:每 gop_size 帧一个 I-framelibx264 也可由 keyint 控制,但这里
// 用 force_gop_frame 显式触发)。
let force_gop_frame = self.gop_size > 0 && frame_index % u64::from(self.gop_size) == 0; let force_gop_frame = self.gop_size > 0 && frame_index % u64::from(self.gop_size) == 0;
if frame_index > 0 && !force_gop_frame && !force_this_frame && current_hash == self.last_frame_hash { if frame_index > 0 && !force_gop_frame && !force_this_frame && current_hash == self.last_frame_hash {
tracing::debug!(frame_index, "skipping duplicate frame"); tracing::debug!(frame_index, "skipping duplicate frame");
@@ -1487,6 +1640,10 @@ impl SwEncEncode {
self.last_frame_hash = current_hash; self.last_frame_hash = current_hash;
let sws_start = Instant::now(); let sws_start = Instant::now();
// 中文概述:sws_scale 把 NV12(含交错的 UV 半高平面)转 YUV420PY + U + V 三平面)。
// yuv_frame 是预分配的可复用 AVFrameav_frame_make_writable 确保独占写权限。
// src_slices/src_strides 是 4 元素数组(FFmpeg 固定 API),后两个填 null/0。
// 英文 SAFETY 见下方。
// SAFETY: yuv_frame is an owned reusable YUV420P frame at the same dimensions as sw_nv12; // SAFETY: yuv_frame is an owned reusable YUV420P frame at the same dimensions as sw_nv12;
// sws_ctx was created for NV12 -> YUV420P with no resize, so sws_scale only converts format. // sws_ctx was created for NV12 -> YUV420P with no resize, so sws_scale only converts format.
unsafe { unsafe {
@@ -1523,6 +1680,10 @@ impl SwEncEncode {
let start_ts = self.starting_timestamp.unwrap_or(0); let start_ts = self.starting_timestamp.unwrap_or(0);
let enc_start = Instant::now(); let enc_start = Instant::now();
// 中文概述:把 YUV420P 帧送入 libx264。pict_type 重置为 NONE(除非 force_this_frame
// 强制 I-frame)—— yuv_frame 复用,若不重置上一帧的 I-type 会泄漏到当前 P-frame。
// forced-idr=1 编码器选项让 AV_PICTURE_TYPE_I 触发真正的 IDR NALU(不是普通 I-frame)。
// 英文 SAFETY 详细论证下方。
// SAFETY: yuv_frame is initialized, writable, and matches the opened encoder format. // SAFETY: yuv_frame is initialized, writable, and matches the opened encoder format.
// pict_type is reset every frame: the AVFrame is reused, so without resetting to NONE // pict_type is reset every frame: the AVFrame is reused, so without resetting to NONE
// a previously-forced I-type would leak into subsequent P-frames. With forced-idr=1 // a previously-forced I-type would leak into subsequent P-frames. With forced-idr=1
@@ -1547,6 +1708,7 @@ impl SwEncEncode {
self.force_keyframe_pending = false; self.force_keyframe_pending = false;
} }
// drain_encoder 内部循环 avcodec_receive_packet,把所有编码好的 packet 写 muxer 或 channel。
let output_bytes = self.drain_encoder(start_ts)?; let output_bytes = self.drain_encoder(start_ts)?;
let encode_us = enc_start.elapsed().as_micros() as u64; let encode_us = enc_start.elapsed().as_micros() as u64;
@@ -1571,11 +1733,14 @@ impl SwEncEncode {
); );
if !self.sws_ctx.is_null() { if !self.sws_ctx.is_null() {
// 中文概述:分辨率变化前先释放旧 sws_ctx(NV12→YUV420P 转换器),
// 紧接着把字段置 null 防止二次释放。英文 SAFETY 见下方。
// SAFETY: sws_ctx is owned exclusively by self and will be replaced below. // SAFETY: sws_ctx is owned exclusively by self and will be replaced below.
unsafe { ffi::sws_freeContext(self.sws_ctx) }; unsafe { ffi::sws_freeContext(self.sws_ctx) };
self.sws_ctx = ptr::null_mut(); self.sws_ctx = ptr::null_mut();
} }
if !self.yuv_frame.is_null() { if !self.yuv_frame.is_null() {
// 中文概述:同步释放旧 yuv_frameav_frame_free 内部清空 data/extended_data。
// SAFETY: yuv_frame is owned exclusively by self and will be replaced below. // SAFETY: yuv_frame is owned exclusively by self and will be replaced below.
unsafe { ffi::av_frame_free(&mut self.yuv_frame) }; unsafe { ffi::av_frame_free(&mut self.yuv_frame) };
} }
@@ -1605,8 +1770,13 @@ impl SwEncEncode {
fn drain_encoder(&mut self, start_ts: i64) -> Result<usize> { fn drain_encoder(&mut self, start_ts: i64) -> Result<usize> {
let mut total_bytes = 0usize; let mut total_bytes = 0usize;
// drain 循环:avcodec_send_frame 之后,必须反复调用 avcodec_receive_packet 直到 EAGAIN/EOF。
// FFmpeg 编码 API 是异步的:send_frame 立即返回,packet 通过 receive_packet 取出。
// 类比 Go 的 chan: send_frame=send, receive_packet=recv, EAGAIN=chan 空。
loop { loop {
let mut pkt = ff::Packet::empty(); let mut pkt = ff::Packet::empty();
// 中文概述:从打开的 libx264 编码器取出一个已编码 packet。
// 英文 SAFETY 见下方。
// SAFETY: enc_video is an open encoder; pkt is writable packet storage. // SAFETY: enc_video is an open encoder; pkt is writable packet storage.
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())
@@ -1620,6 +1790,8 @@ impl SwEncEncode {
// Count encoded bytes produced before the Muxer/Channel match to // Count encoded bytes produced before the Muxer/Channel match to
// avoid branch duplication and handle multi-packet drain correctly. // avoid branch duplication and handle multi-packet drain correctly.
// 中文概述:从 AVPacket.size 读字节数累加,避免在 Muxer/Channel 分支里重复统计。
// 英文 SAFETY 见下方。
// SAFETY: pkt was just filled by a successful avcodec_receive_packet; // SAFETY: pkt was just filled by a successful avcodec_receive_packet;
// the size field is valid and initialized. // the size field is valid and initialized.
let pkt_size = unsafe { (*pkt.as_mut_ptr()).size }; let pkt_size = unsafe { (*pkt.as_mut_ptr()).size };
@@ -1627,9 +1799,13 @@ impl SwEncEncode {
total_bytes += pkt_size as usize; total_bytes += pkt_size as usize;
} }
// 输出目的地二选一:Muxer 写文件,Channel 送 WebRTC 线程。
match self.output { match self.output {
Some(FrameOutput::Muxer(ref mut octx)) => { Some(FrameOutput::Muxer(ref mut octx)) => {
let enc_tb = self.enc_video.time_base(); let enc_tb = self.enc_video.time_base();
// 中文概述:从 AVFormatContext 读 stream 0 的 time_basemuxer 写入时用)。
// 必须校验 nb_streams > 0 && streams 非空,防止空 muxer 触发 UB。
// 英文 SAFETY 见下方。
// SAFETY: muxer output was created with stream 0 during setup; // SAFETY: muxer output was created with stream 0 during setup;
// streams is non-null and stream 0 remains owned by the format context. // streams is non-null and stream 0 remains owned by the format context.
let stream_tb = unsafe { let stream_tb = unsafe {
@@ -1640,8 +1816,10 @@ impl SwEncEncode {
let st = *fmt.streams.add(0); let st = *fmt.streams.add(0);
ff::Rational::from((*st).time_base) ff::Rational::from((*st).time_base)
}; };
// rescale_ts:编码器 time_base → 容器 time_base 的有理数换算。
pkt.rescale_ts(enc_tb, stream_tb); pkt.rescale_ts(enc_tb, stream_tb);
// 减去起始 PTS 让首帧 PTS=0Muxer 模式下 AVFormatContext 自己管 DTS。
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));
} }
@@ -1655,11 +1833,15 @@ impl SwEncEncode {
self.frames_written = true; self.frames_written = true;
} }
Some(FrameOutput::Channel(ref tx)) => { Some(FrameOutput::Channel(ref tx)) => {
// 中文概述:把 AVPacket 字段读出到栈上 raw 结构体,用于检查 size/data。
// 英文 SAFETY 见下方。
// SAFETY: pkt is a valid AVPacket just filled by // SAFETY: pkt is a valid AVPacket just filled by
// avcodec_receive_packet; this copies fields for // avcodec_receive_packet; this copies fields for
// read-only inspection before pkt is dropped. // read-only inspection before pkt is dropped.
let raw = unsafe { *pkt.as_mut_ptr() }; let raw = unsafe { *pkt.as_mut_ptr() };
if raw.size > 0 && !raw.data.is_null() { if raw.size > 0 && !raw.data.is_null() {
// 中文概述:从 raw.data 裸指针构造临时切片(零拷贝),随即 .to_vec() 深拷贝。
// 类比 Go 的 unsafe Slice((*byte)(data), size) → append(nil, slice...)。
// SAFETY: `pkt` is a valid AVPacket just filled by a successful // SAFETY: `pkt` is a valid AVPacket just filled by a successful
// `avcodec_receive_packet` call. We checked `size > 0` and // `avcodec_receive_packet` call. We checked `size > 0` and
// `data` is non-null, so `data` points to `size` initialized // `data` is non-null, so `data` points to `size` initialized
@@ -1682,6 +1864,8 @@ impl SwEncEncode {
continue; continue;
} }
}; };
// try_send 非阻塞:通道满或断开都立即返回(不阻塞 encode 线程)。
// 类比 Go 的 `select { case ch <- frame: default: drop }`。
match tx.try_send(EncodedH264Frame { match tx.try_send(EncodedH264Frame {
data: data.to_vec(), data: data.to_vec(),
pts_ticks, pts_ticks,
@@ -1701,6 +1885,8 @@ impl SwEncEncode {
"WebRTC channel disconnected: {} bytes lost", "WebRTC channel disconnected: {} bytes lost",
frame.data.len() frame.data.len()
); );
// 设置断开标志,后续 encode_cpu_frame 调用立即 SkippedDisconnected
// 避免每帧都重复走 sws_scale + encode + drain 的浪费路径。
self.webrtc_disconnected = true; self.webrtc_disconnected = true;
break; break;
} }
@@ -1714,14 +1900,22 @@ impl SwEncEncode {
} }
} }
// impl Drop for SwEncEncodeC 资源释放。enc_video 由 ff::codec::encoder::video::Video
// 包装,Drop 会自动调用 avcodec_closesws_ctx 和 yuv_frame 是裸指针,必须在此手动释放。
// 类比 Go 的 `defer ffi.sws_freeContext(sws_ctx)`——Rust 没有原生 defer,但 Drop trait 等价。
impl Drop for SwEncEncode { impl Drop for SwEncEncode {
fn drop(&mut self) { fn drop(&mut self) {
if !self.sws_ctx.is_null() { if !self.sws_ctx.is_null() {
// 中文概述:释放 sws_ctx。释放后置 null 防止二次释放(虽然 drop 后 self 立即销毁,
// 但这是防御式编程惯例,与 recreate_encoder 中的释放路径保持一致)。
// 英文 SAFETY 见下方。
// SAFETY: sws_ctx is owned by this state and was returned by sws_getContext. // SAFETY: sws_ctx is owned by this state and was returned by sws_getContext.
unsafe { ffi::sws_freeContext(self.sws_ctx) }; unsafe { ffi::sws_freeContext(self.sws_ctx) };
self.sws_ctx = ptr::null_mut(); self.sws_ctx = ptr::null_mut();
} }
if !self.yuv_frame.is_null() { if !self.yuv_frame.is_null() {
// 中文概述:释放 yuv_frame。av_frame_free 接收 **mut 并内部置 null,无需手动置空。
// 英文 SAFETY 见下方。
// SAFETY: yuv_frame is owned by this state and was allocated by av_frame_alloc. // SAFETY: yuv_frame is owned by this state and was allocated by av_frame_alloc.
unsafe { ffi::av_frame_free(&mut self.yuv_frame) }; unsafe { ffi::av_frame_free(&mut self.yuv_frame) };
} }