docs(avhw): [3/4] 中文注释软件编码路径 SwEnc*
This commit is contained in:
+194
@@ -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 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.
|
/// 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 风格)
|
||||||
|
// - Channel:crossbeam 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 喂给 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 {
|
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 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 {
|
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/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)?;
|
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 =
|
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/VAAPI,scale=enc_width×enc_height,format=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 }`)。
|
||||||
|
// ? 自动传播 Result;mut 因为 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/VAAPI,VASurfaceID 在 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 编码"半部:消费 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 {
|
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_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_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 SwEncEncode:6 个方法按数据流顺序
|
||||||
|
// (1) new_muxer:构造函数,输出到 MP4 文件(new_with_resolution_control 调用)
|
||||||
|
// (2) new_webrtc:构造函数,输出到 crossbeam Sender<EncodedH264Frame>
|
||||||
|
// (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 {
|
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_len:Y 平面所需最小字节数 = (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-frame(libx264 也可由 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 半高平面)转 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;
|
// 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_frame,av_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_base(muxer 写入时用)。
|
||||||
|
// 必须校验 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=0;Muxer 模式下 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 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 {
|
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) };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user