diff --git a/src/avhw.rs b/src/avhw.rs index 47da914..4ae445a 100644 --- a/src/avhw.rs +++ b/src/avhw.rs @@ -964,6 +964,20 @@ impl EncState { // SwEncState - VAAPI GPU downscale + software H.264 encode // --------------------------------------------------------------------------- +// 编码完成的 H.264 帧载体,是软件编码路径(SwEncEncode → WebRTC 通道)的输出单元。 +// 与硬件路径 EncState 不同:硬件路径直接对接 avformat muxer,PTS 由 muxer 内部维护; +// 软件路径通过 crossbeam channel 把帧送到 webrtc 线程,必须显式携带 PTS 与捕获时间戳。 +// +// 字段语义: +// - data:H.264 NAL 字节流(Annex B 或 AVCC,取决于 encoder 配置;本工程用 libx264 默认 Annex B) +// - pts_ticks:编码器 time_base 单位下的 PTS(WebRTC 模式下 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. /// /// 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, } +// 编码器输出目的地二选一枚举: +// - Muxer:直接对接 FFmpeg avformat 写 MP4 文件(自包含 PTS,硬编码路径 EncState 风格) +// - Channel:crossbeam Sender,把帧送 WebRTC 信令线程(必须显式带 PTS) +// 类比 Go:`type FrameOutput interface { Write(Frame) error }` 的两种实现。 +// 该枚举让 SwEncEncode 在 new_muxer / new_webrtc 两个构造函数间共享同一 drain_encoder 主循环。 pub enum FrameOutput { Muxer(ff::format::context::Output), Channel(crossbeam_channel::Sender), } +// 主线程持有:完成 VAAPI GPU 下采样 + av_hwframe_transfer_data 回到 CPU 的 NV12 帧缓冲。 +// 数据流:PipeWire/wlr 帧通过 import_and_scale 进入 → VAAPI scale_vaapi 转 NV12 → +// transfer_filtered_to_cpu 拷贝到 Vec → 通过 channel 跨线程送 SwEncEncode 编码线程。 +// NV12 格式:Y 平面满分辨率,UV 交错平面半高(chroma 高度减半);stride 可能 > width +// (对齐到 GPU 要求)。字段不带 GPU 资源(纯 CPU 内存),自动 Send。 /// Owned CPU NV12 frame data for cross-thread transfer. /// Produced by main thread (VAAPI import + GPU scale + transfer), consumed by encode thread. pub struct CpuNv12Frame { @@ -1000,6 +1024,18 @@ pub struct CpuNv12Frame { pub capture_time: std::time::Instant, } +// 软件编码路径的"导入 + GPU 缩放"半部:负责把外部 DMA-BUF 帧(PipeWire/wlr)经 +// VAAPI 硬件零拷贝导入 → scale_vaapi GPU 下采样 → av_hwframe_transfer_data 回传 CPU NV12, +// 最后封装为 CpuNv12Frame 喂给 SwEncEncode(libx264 软编)。 +// +// 与 EncState(硬编直连)的区别:本结构仅做 GPU 辅助的导入/缩放,H.264 编码由 CPU 完成; +// 因此自始至终只在导入线程访问,跨线程边界是 channel(CpuNv12Frame)。 +// +// 字段: +// - hw_dev / frames_rgb:VAAPI 设备 + BGRA 硬件帧上下文(DMA-BUF 导入目的地) +// - filter_graph:FFmpeg 滤镜图(scale_vaapi=M,w=enc:h=enc,format=nv12) +// - width/height:源帧尺寸;enc_width/enc_height:目标缩放后尺寸 +// - resolution_rx / encoder_resolution_tx:可选的动态分辨率控制通道对 pub struct SwEncImport { hw_dev: AvHwDevCtx, frames_rgb: AvHwFrameCtx, @@ -1013,6 +1049,12 @@ pub struct SwEncImport { encoder_resolution_tx: Option>, } +// impl SwEncImport:5 个方法按数据流顺序 +// (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_import:EOS 时排空 filter graph 缓冲 +// (5) poll_resolution_commands + transfer_filtered_to_cpu:私有辅助 impl SwEncImport { #[allow(clippy::too_many_arguments)] pub fn new( @@ -1023,9 +1065,18 @@ impl SwEncImport { enc_height: u32, fps: u32, ) -> Result { + // drm_device 形如 /dev/dri/renderD128;AvHwDevCtx::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)?; + // 硬件帧上下文:调用 vaCreateSurfaces 申请 BGRA surface 池供 DMA-BUF 导入使用; + // AV_PIX_FMT_BGRA 对应 PipeWire/wlr 帧的常见 RGB 格式。返回的 AvHwFrameCtx 持有 + // AVHWFramesContext,surface 池大小由 FFmpeg 默认(通常 4-8 帧)。 let frames_rgb = AvHwFrameCtx::for_capture(&hw_dev, width, height, ff::format::Pixel::BGRA)?; + // 建立 scale_vaapi 滤镜图:源=BGRA/VAAPI,scale=enc_width×enc_height,format=nv12。 + // 滤镜图绑定的 hw_dev 与 frames_rgb 必须同属一个 VAAPI 设备,否则 vaCreateSurfaces + // 在不同 display 上会失败。 let filter_graph = build_swenc_filter_graph( &hw_dev, &frames_rgb, @@ -1036,6 +1087,9 @@ impl SwEncImport { fps, )?; + // Self 是返回类型的别名(Rust 构造器惯用法,类比 Go `return &Foo{...}` 中的 Foo)。 + // 字段简写:当局部变量名与字段名相同时可省略 `field: value`,等价于 Go 结构体字面量简写。 + // resolution_rx / encoder_resolution_tx 留 None,由 new_with_resolution_control 后续填充。 Ok(Self { hw_dev, frames_rgb, @@ -1073,8 +1127,12 @@ impl SwEncImport { } pub fn import_and_scale(&mut self, hw_frame: &ff::frame::Video) -> Result { + // 先消费 ResolutionChange 命令:若分辨率改变,本函数后续的 filter graph 已是新图。 self.poll_resolution_commands()?; + // 拿到滤镜图的输入/输出端点("in"/"out" 是 build_swenc_filter_graph 中注册的名称)。 + // ok_or_else 把 Option 转 Result(延迟构造错误,类比 Go `if x == nil { return err }`)。 + // ? 自动传播 Result;mut 因为 source()/sink() 返回 &mut 借用。 let mut filter_src_ctx = self .filter_graph .get("in") @@ -1086,16 +1144,22 @@ impl SwEncImport { .ok_or_else(|| anyhow::anyhow!("filter 'out' not found"))?; let mut filter_sink = filter_sink_ctx.sink(); + // 把外部导入的硬件帧(hw_frame 是 AV_PIX_FMT_DRM_PRIME/VAAPI,VASurfaceID 在 data[3]) + // 送入滤镜图入口。滤镜图内部 scale_vaapi 执行 GPU 下采样 + 格式转换。 + // map_err 把 ffmpeg::Error 包装成 anyhow::Error 加上下文。 filter_src .add(hw_frame) .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 extra_count = 0usize; loop { let mut filtered = ff::frame::Video::empty(); match filter_sink.frame(&mut filtered) { Ok(()) => { + // 若 filter 没有显式设置 PTS(罕见),用源帧 PTS 兜底。 if filtered.pts().is_none() { filtered.set_pts(hw_frame.pts()); } @@ -1106,6 +1170,8 @@ impl SwEncImport { extra_count += 1; } } + // ffi::EAGAIN:滤镜图当前没有输出可取;这是正常的"等下一帧"信号,break 主循环。 + // 类比 Go 中 io.Read 返回 EAGAIN。 Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break, 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")) } @@ -1197,6 +1265,8 @@ impl SwEncImport { } fn transfer_filtered_to_cpu(&self, filtered: &ff::frame::Video) -> Result { + // 中文概述:分配一个 CPU 侧的 AVFrame 作为 av_hwframe_transfer_data 的目标。 + // 英文 SAFETY 详细论证见下方。 // SAFETY: av_frame_alloc returns a newly allocated AVFrame or null, // which is checked below. let mut sw_nv12 = unsafe { ffi::av_frame_alloc() }; @@ -1204,10 +1274,14 @@ impl SwEncImport { 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 // surface produced by scale_vaapi at encoder dimensions. let transfer_ret = unsafe { ffi::av_hwframe_transfer_data(sw_nv12, filtered.as_ptr(), 0) }; if transfer_ret < 0 { + // 中文概述:transfer 失败时必须释放 sw_nv12 防止泄漏;FFmpeg C API 无 RAII。 // SAFETY: sw_nv12 was allocated above and has not been freed yet. unsafe { ffi::av_frame_free(&mut sw_nv12) }; bail!( @@ -1216,6 +1290,9 @@ impl SwEncImport { ); } + // 中文概述:从 AVFrame 字段读 Y/UV 平面指针、linesize、尺寸,校验后拷贝到 Vec。 + // 错误路径同样调用 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 // initialized for enc_width x enc_height; linesize values define each row's byte span. let frame = unsafe { @@ -1235,6 +1312,8 @@ impl SwEncImport { } let y_len = y_stride * self.enc_height as usize; 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 uv_data = slice::from_raw_parts(uv_ptr, uv_len).to_vec(); let pts = filtered.pts().unwrap_or(0); @@ -1253,6 +1332,17 @@ impl SwEncImport { } } +// 软件编码路径的"CPU 编码"半部:消费 CpuNv12Frame(NV12),用 sws_scale 转 YUV420P, +// 喂 libx264 软件 H.264 编码器,输出 EncodedH264Frame。与 SwEncImport 配对使用: +// SwEncImport 负责 GPU 辅助导入/缩放,SwEncEncode 负责 CPU 编码;两者通过 channel 通信。 +// +// 持有的 C 资源(裸指针,非 Send/Sync 自动,需手写 unsafe impl Send 见下): +// - sws_ctx:FFmpeg SwsContext(NV12 → YUV420P 颜色空间转换器,无 resize) +// - yuv_frame:可复用的 AVFrame(YUV420P,尺寸 = enc_width × enc_height) +// - enc_video:打开的 libx264 AVCodecContext +// +// 其他字段是控制状态(动态码率/分辨率/关键帧请求、去重哈希、计时统计、WebRTC 暂停/断开)。 +// 详见每个字段上方英文 ///(保留)。 pub struct SwEncEncode { sws_ctx: *mut ffi::SwsContext, enc_video: ff::codec::encoder::video::Video, @@ -1285,16 +1375,40 @@ pub struct SwEncEncode { last_capture_time: Option, } +// FNV-1a 64 位哈希常量,用于帧去重(见 encode_cpu_frame 调用 hash_sampled_y_plane): +// - FNV1A_OFFSET_BASIS:初始哈希值(FNV 论文推荐的 64 位 offset basis) +// - FNV1A_PRIME:每字节乘法因子(64 位素数) +// - Y_PLANE_HASH_ROW_STEP:Y 平面采样步长(仅哈希每 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_PRIME: u64 = 0x100000001b3; 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). /// Used as encoder time_base denominator for WebRTC mode (1/90000) so that /// PTS values directly become RTP timestamps with microsecond precision. /// MP4 mode keeps 1/fps time_base for file output simplicity. 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 { 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 } +// 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. // It is moved to a single encode thread and only accessed through &mut self there. unsafe impl Send for SwEncEncode {} +// impl SwEncEncode:6 个方法按数据流顺序 +// (1) new_muxer:构造函数,输出到 MP4 文件(new_with_resolution_control 调用) +// (2) new_webrtc:构造函数,输出到 crossbeam Sender +// (3) flush:EOS 时排空编码器(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 { #[allow(clippy::too_many_arguments)] fn new_muxer( @@ -1324,10 +1450,17 @@ impl SwEncEncode { bitrate: u64, gop_size: u32, ) -> Result { + // 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 (enc_video, octx) = create_software_h264_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?; 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 更优雅:避免每个调用点都 match Option。 let (dummy_tx, bitrate_rx) = crossbeam_channel::bounded(1); drop(dummy_tx); let (dummy_resolution_tx, resolution_rx) = crossbeam_channel::bounded(1); @@ -1399,6 +1532,10 @@ impl SwEncEncode { } 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; // no frame data is dereferenced. enc_video is exclusively borrowed via &mut self. unsafe { @@ -1418,15 +1555,19 @@ impl SwEncEncode { } pub fn encode_cpu_frame(&mut self, frame: &CpuNv12Frame) -> Result { + // 每帧开始:把计时统计置零,避免上一帧的 stale 值泄漏(last_timing 字段注释见 struct)。 self.last_timing = SwEncodeTiming::default(); // Save capture_time so drain_encoder can propagate it into the // EncodedH264Frame emitted via the WebRTC channel (issue #20). self.last_capture_time = Some(frame.capture_time); + // WebRTC 通道已断开(之前 drain_encoder 检测到 Disconnected)→ 后续帧直接跳过。 if self.webrtc_disconnected { return Ok(EncodeOutcome::SkippedDisconnected); } + // 必须先 drain bitrate 命令再做 stride 检查:导入线程在 emit ResolutionChange 后 + // 才送来新的(更小 stride 的)帧;若先检查 stride 会因旧 stride 与新帧不匹配误报。 // Must drain before the stride check: the import thread emits // ResolutionChange before the new (smaller-stride) frame arrives. 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); tracing::info!(target_bps, "updating encoder bitrate from BWE feedback"); self.bitrate = target_bps; + // 中文概述:直接写 libx264 AVCodecContext 的 bit_rate 字段(i64), + // 实时调整编码器输出码率(BWE 反馈驱动)。英文 SAFETY 见下方。 // SAFETY: enc_video is an opened AVCodecContext exclusively owned by &mut self. unsafe { 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; + // drain ResolutionChange 通道:若分辨率变化,调用 recreate_encoder 重建 libx264 + + // sws_ctx + yuv_frame(避免在帧处理中途重建,确保后续 unsafe 块看到的字段一致)。 while let Ok(change) = self.resolution_rx.try_recv() { 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 { bail!("CPU NV12 frame stride is smaller than encoder width"); } + // 暂停检查:webrtc_paused 是 Arc(跨线程共享),Relaxed 内存序足够 + // (单字段读,不参与 happens-before 协调;类比 Go atomic.LoadInt32)。 if let Some(ref paused) = self.webrtc_paused { if paused.load(Ordering::Relaxed) { return Ok(EncodeOutcome::SkippedPaused); @@ -1470,6 +1618,8 @@ impl SwEncEncode { let width = self.enc_width as usize; let height = self.enc_height as usize; + // required_y_len:Y 平面所需最小字节数 = (height-1) 行完整 stride + 末行 width 字节。 + // 用于校验源帧尺寸合法(防御式编程,避免 from_raw_parts 越界)。 let required_y_len = frame.y_stride * height.saturating_sub(1) + width; if frame.y_data.len() < required_y_len { bail!("CPU NV12 frame Y plane is smaller than encoder dimensions"); @@ -1477,7 +1627,10 @@ impl SwEncEncode { let frame_index = self.frame_count; 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); + // GOP 强制关键帧:每 gop_size 帧一个 I-frame(libx264 也可由 keyint 控制,但这里 + // 用 force_gop_frame 显式触发)。 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 { tracing::debug!(frame_index, "skipping duplicate frame"); @@ -1487,6 +1640,10 @@ impl SwEncEncode { self.last_frame_hash = current_hash; let sws_start = Instant::now(); + // 中文概述:sws_scale 把 NV12(含交错的 UV 半高平面)转 YUV420P(Y + U + V 三平面)。 + // yuv_frame 是预分配的可复用 AVFrame,av_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; // sws_ctx was created for NV12 -> YUV420P with no resize, so sws_scale only converts format. unsafe { @@ -1523,6 +1680,10 @@ impl SwEncEncode { let start_ts = self.starting_timestamp.unwrap_or(0); 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. // 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 @@ -1547,6 +1708,7 @@ impl SwEncEncode { self.force_keyframe_pending = false; } + // drain_encoder 内部循环 avcodec_receive_packet,把所有编码好的 packet 写 muxer 或 channel。 let output_bytes = self.drain_encoder(start_ts)?; let encode_us = enc_start.elapsed().as_micros() as u64; @@ -1571,11 +1733,14 @@ impl SwEncEncode { ); 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. unsafe { ffi::sws_freeContext(self.sws_ctx) }; self.sws_ctx = ptr::null_mut(); } if !self.yuv_frame.is_null() { + // 中文概述:同步释放旧 yuv_frame,av_frame_free 内部清空 data/extended_data。 // SAFETY: yuv_frame is owned exclusively by self and will be replaced below. unsafe { ffi::av_frame_free(&mut self.yuv_frame) }; } @@ -1605,8 +1770,13 @@ impl SwEncEncode { fn drain_encoder(&mut self, start_ts: i64) -> Result { 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 { let mut pkt = ff::Packet::empty(); + // 中文概述:从打开的 libx264 编码器取出一个已编码 packet。 + // 英文 SAFETY 见下方。 // SAFETY: enc_video is an open encoder; pkt is writable packet storage. let ret = unsafe { 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 // 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; // the size field is valid and initialized. let pkt_size = unsafe { (*pkt.as_mut_ptr()).size }; @@ -1627,9 +1799,13 @@ impl SwEncEncode { total_bytes += pkt_size as usize; } + // 输出目的地二选一:Muxer 写文件,Channel 送 WebRTC 线程。 match self.output { Some(FrameOutput::Muxer(ref mut octx)) => { let enc_tb = self.enc_video.time_base(); + // 中文概述:从 AVFormatContext 读 stream 0 的 time_base(muxer 写入时用)。 + // 必须校验 nb_streams > 0 && streams 非空,防止空 muxer 触发 UB。 + // 英文 SAFETY 见下方。 // SAFETY: muxer output was created with stream 0 during setup; // streams is non-null and stream 0 remains owned by the format context. let stream_tb = unsafe { @@ -1640,8 +1816,10 @@ impl SwEncEncode { let st = *fmt.streams.add(0); ff::Rational::from((*st).time_base) }; + // rescale_ts:编码器 time_base → 容器 time_base 的有理数换算。 pkt.rescale_ts(enc_tb, stream_tb); + // 减去起始 PTS 让首帧 PTS=0;Muxer 模式下 AVFormatContext 自己管 DTS。 if let Some(pts) = pkt.pts() { pkt.set_pts(Some(pts - start_ts)); } @@ -1655,11 +1833,15 @@ impl SwEncEncode { self.frames_written = true; } Some(FrameOutput::Channel(ref tx)) => { + // 中文概述:把 AVPacket 字段读出到栈上 raw 结构体,用于检查 size/data。 + // 英文 SAFETY 见下方。 // SAFETY: pkt is a valid AVPacket just filled by // avcodec_receive_packet; this copies fields for // read-only inspection before pkt is dropped. let raw = unsafe { *pkt.as_mut_ptr() }; 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 // `avcodec_receive_packet` call. We checked `size > 0` and // `data` is non-null, so `data` points to `size` initialized @@ -1682,6 +1864,8 @@ impl SwEncEncode { continue; } }; + // try_send 非阻塞:通道满或断开都立即返回(不阻塞 encode 线程)。 + // 类比 Go 的 `select { case ch <- frame: default: drop }`。 match tx.try_send(EncodedH264Frame { data: data.to_vec(), pts_ticks, @@ -1701,6 +1885,8 @@ impl SwEncEncode { "WebRTC channel disconnected: {} bytes lost", frame.data.len() ); + // 设置断开标志,后续 encode_cpu_frame 调用立即 SkippedDisconnected, + // 避免每帧都重复走 sws_scale + encode + drain 的浪费路径。 self.webrtc_disconnected = true; break; } @@ -1714,14 +1900,22 @@ impl SwEncEncode { } } +// impl Drop for SwEncEncode:C 资源释放。enc_video 由 ff::codec::encoder::video::Video +// 包装,Drop 会自动调用 avcodec_close;sws_ctx 和 yuv_frame 是裸指针,必须在此手动释放。 +// 类比 Go 的 `defer ffi.sws_freeContext(sws_ctx)`——Rust 没有原生 defer,但 Drop trait 等价。 impl Drop for SwEncEncode { fn drop(&mut self) { 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. unsafe { ffi::sws_freeContext(self.sws_ctx) }; self.sws_ctx = ptr::null_mut(); } 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. unsafe { ffi::av_frame_free(&mut self.yuv_frame) }; }