docs(avhw): [4/4] 中文注释 SwEncState 与 unsafe impl Send
This commit is contained in:
+110
@@ -1922,15 +1922,28 @@ impl Drop for SwEncEncode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 软件编码复合状态:把"GPU 辅助导入/缩放半部"(SwEncImport)与"CPU 软编码半部"
|
||||||
|
// (SwEncEncode)拼成一个对外暴露的单一类型。调用方只看 SwEncState,不直接接触
|
||||||
|
// 两个内部组件——类似 Go 中把两个 struct 组合成上层 API 对象。
|
||||||
|
// 字段语义:
|
||||||
|
// - import:管理 DRM/VAAPI 设备 + scale_vaapi 滤镜图,把 BGRA 硬件帧下采样为 NV12
|
||||||
|
// - encode:管理 libx264/libopenh264 编码器 + 可选 muxer,消费 NV12 输出 H.264
|
||||||
|
// 数据流:CaptureSource → import.import_and_scale(hw_frame) → encode.encode_cpu_frame(nv12)
|
||||||
pub struct SwEncState {
|
pub struct SwEncState {
|
||||||
import: SwEncImport,
|
import: SwEncImport,
|
||||||
encode: SwEncEncode,
|
encode: SwEncEncode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述(上方):SwEncState 内部含裸指针 C 资源(VAAPI surfaces、AVCodecContext、
|
||||||
|
// AVFormatContext 等),Rust 默认不为含裸指针的类型实现 Send。这里手工声明 Send 表明
|
||||||
|
// **本工程的并发模型是"单线程独占"**——所有 FFI 调用都通过 &mut self 串行化,跨线程
|
||||||
|
// 移动只发生在外部序列化点(main.rs 中的编码线程独占)。AGENTS.md exclusivity 警告适用。
|
||||||
// SAFETY: SwEncState owns import and encode state exclusively and existing sync callers move it
|
// SAFETY: SwEncState owns import and encode state exclusively and existing sync callers move it
|
||||||
// between threads only with external serialization; all FFI handles are accessed through &mut self.
|
// between threads only with external serialization; all FFI handles are accessed through &mut self.
|
||||||
unsafe impl Send for SwEncState {}
|
unsafe impl Send for SwEncState {}
|
||||||
|
|
||||||
|
// SwEncState 实现:4 个 pub 方法按数据流顺序——new/new_webrtc 构造、frames_rgb
|
||||||
|
// 暴露硬件帧池给上游采集器、encode_frame 走单帧流水线、flush 处理 EOF。
|
||||||
impl SwEncState {
|
impl SwEncState {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -1944,12 +1957,16 @@ impl SwEncState {
|
|||||||
bitrate: u64,
|
bitrate: u64,
|
||||||
gop_size: u32,
|
gop_size: u32,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
// 文件输出模式构造:上游采集源分辨率 width×height,缩放到 enc_width×enc_height
|
||||||
|
// 后送入 libx264/libopenh264;bitrate/gop_size 由调用方计算(参见 create_encoder)。
|
||||||
|
// `?` 自动传播 anyhow Error(同 Go `if err != nil { return err }`)。
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"SwEncState::new: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264"
|
"SwEncState::new: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264"
|
||||||
);
|
);
|
||||||
let import = SwEncImport::new(drm_device, width, height, enc_width, enc_height, fps)?;
|
let import = SwEncImport::new(drm_device, width, height, enc_width, enc_height, fps)?;
|
||||||
let encode =
|
let encode =
|
||||||
SwEncEncode::new_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?;
|
SwEncEncode::new_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?;
|
||||||
|
// Self 是 impl 块当前类型的别名;struct literal 字段简写(同字段名变量直接写名字)。
|
||||||
Ok(Self { import, encode })
|
Ok(Self { import, encode })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1966,10 +1983,16 @@ impl SwEncState {
|
|||||||
tx: crossbeam_channel::Sender<EncodedH264Frame>,
|
tx: crossbeam_channel::Sender<EncodedH264Frame>,
|
||||||
webrtc_paused: Arc<AtomicBool>,
|
webrtc_paused: Arc<AtomicBool>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
// WebRTC 模式构造:H.264 NALU 通过 crossbeam channel 推给 str0m 信令线程(见 webrtc.rs)。
|
||||||
|
// 区别于 new:用 channel 代替 muxer;webrtc_paused 是 str0m ICE/DTLS 暂停标志(Arc<AtomicBool>
|
||||||
|
// 跨线程共享,Ordering::Relaxed 语义详见 state_portal.rs/T9b 注释)。
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"SwEncState::new_webrtc: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264 -> WebRTC"
|
"SwEncState::new_webrtc: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264 -> WebRTC"
|
||||||
);
|
);
|
||||||
let import = SwEncImport::new(drm_device, width, height, enc_width, enc_height, fps)?;
|
let import = SwEncImport::new(drm_device, width, height, enc_width, enc_height, fps)?;
|
||||||
|
// dummy channel idiom:创建后立即 drop 发送端,使接收端永远返回 Disconnected——
|
||||||
|
// 等价于"该信号源永不触发",编码器内部的 bitrate/resolution 切换逻辑因此走默认路径。
|
||||||
|
// 类比 Go:`ch := make(chan T, 1); close(ch)` 让 `<-ch` 立即返回零值(但语义不同)。
|
||||||
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);
|
||||||
@@ -1989,15 +2012,21 @@ impl SwEncState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn frames_rgb(&self) -> &AvHwFrameCtx {
|
pub fn frames_rgb(&self) -> &AvHwFrameCtx {
|
||||||
|
// 借用访问器:返回 import 持有的硬件帧池引用。借用检查器保证调用方在持有
|
||||||
|
// 这个 &AvHwFrameCtx 期间无法调 encode_frame(&mut self),避免数据竞争。
|
||||||
self.import.frames_rgb()
|
self.import.frames_rgb()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
|
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
|
||||||
|
// 单帧主循环:把硬件 BGRA 帧导入 VAAPI → scale_vaapi 下采样到 NV12 → transfer
|
||||||
|
// 到 CPU → libx264 编码。&mut self 独占借用保证本调用期间不会并发访问 import/encode。
|
||||||
let cpu_frame = self.import.import_and_scale(hw_frame)?;
|
let cpu_frame = self.import.import_and_scale(hw_frame)?;
|
||||||
self.encode.encode_cpu_frame(&cpu_frame).map(|_| ())
|
self.encode.encode_cpu_frame(&cpu_frame).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flush(&mut self) -> Result<()> {
|
pub fn flush(&mut self) -> Result<()> {
|
||||||
|
// EOF 处理 3 步:(1) 排空 import 中残留的 scale_vaapi 滤镜帧;(2) encode.flush()
|
||||||
|
// 给编码器送 NULL frame 触发 EOS drain;(3) write_trailer_if_needed 仅 muxer 模式生效。
|
||||||
for frame in self.import.flush_import()? {
|
for frame in self.import.flush_import()? {
|
||||||
self.encode.encode_cpu_frame(&frame)?;
|
self.encode.encode_cpu_frame(&frame)?;
|
||||||
}
|
}
|
||||||
@@ -2010,6 +2039,9 @@ impl SwEncState {
|
|||||||
// Shared encoder creation (used by both wlr-screencopy and portal paths)
|
// Shared encoder creation (used by both wlr-screencopy and portal paths)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// 中文概述(上方):本函数是 wlr-screencopy / Portal 两条采集路径共享的 EncState
|
||||||
|
// 构造器,封装了 bitrate/GOP 默认值计算 + 旋转输出维度的转置(transform)。
|
||||||
|
// 英文 /// block 保留不动。
|
||||||
/// Create a fully configured encoder with VAAPI hardware acceleration.
|
/// Create a fully configured encoder with VAAPI hardware acceleration.
|
||||||
///
|
///
|
||||||
/// Convenience wrapper around [`EncState::new`] that computes default values
|
/// Convenience wrapper around [`EncState::new`] that computes default values
|
||||||
@@ -2027,9 +2059,14 @@ pub fn create_encoder(
|
|||||||
gop_size: Option<u32>,
|
gop_size: Option<u32>,
|
||||||
existing_hw_ctx: Option<AvHwDevCtx>,
|
existing_hw_ctx: Option<AvHwDevCtx>,
|
||||||
) -> Result<EncState> {
|
) -> Result<EncState> {
|
||||||
|
// transform 决定编码方向:90°/270° 旋转时宽高对调(transpose_if_transform_transposed
|
||||||
|
// 见 transform.rs)。Option<T> ↔ Go `*T`,必须显式处理 None 分支。
|
||||||
let (enc_w, enc_h) = transpose_if_transform_transposed(transform, width as i32, height as i32);
|
let (enc_w, enc_h) = transpose_if_transform_transposed(transform, width as i32, height as i32);
|
||||||
|
// bitrate 默认值 = 2*W*H*fps/100(约 0.02 bits/pixel/frame,对应 H.264 中等质量)。
|
||||||
|
// unwrap_or_else 是延迟构造:闭包仅在 None 时求值(类比 Go `if x == nil { x = ... }`)。
|
||||||
let actual_bitrate =
|
let actual_bitrate =
|
||||||
bitrate.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100);
|
bitrate.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100);
|
||||||
|
// GOP 默认 = 1 秒(fps 个帧),平衡 IDR 刷新频率与压缩率。
|
||||||
let actual_gop_size = gop_size.unwrap_or(fps);
|
let actual_gop_size = gop_size.unwrap_or(fps);
|
||||||
EncState::new(
|
EncState::new(
|
||||||
drm_device,
|
drm_device,
|
||||||
@@ -2060,7 +2097,12 @@ fn build_swenc_filter_graph(
|
|||||||
enc_height: u32,
|
enc_height: u32,
|
||||||
fps: u32,
|
fps: u32,
|
||||||
) -> Result<ff::filter::Graph> {
|
) -> Result<ff::filter::Graph> {
|
||||||
|
// 构造软件编码用的 GPU scale_vaapi 滤镜图:BGRA 硬件帧 → VAAPI 下采样 → NV12。
|
||||||
|
// 这是 SwEncImport 的核心:保留 GPU 做缩放/色彩转换(CPU 不能高效处理 4K BGRA)。
|
||||||
let mut graph = ff::filter::Graph::new();
|
let mut graph = ff::filter::Graph::new();
|
||||||
|
// 通过名称查找 FFmpeg 滤镜(buffer=源、buffersink=汇、scale_vaapi=VAAPI 缩放)。
|
||||||
|
// ok_or_else 把 Option 转为 Result(None 时执行闭包构造错误),类比 Go 中
|
||||||
|
// `if v, ok := m[k]; !ok { return err }`。
|
||||||
let buffersrc =
|
let buffersrc =
|
||||||
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
|
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
|
||||||
let buffersink = ff::filter::find("buffersink")
|
let buffersink = ff::filter::find("buffersink")
|
||||||
@@ -2070,18 +2112,25 @@ fn build_swenc_filter_graph(
|
|||||||
|
|
||||||
// FFmpeg 8.0+ rejects VAAPI pix_fmt in buffer args before hw_frames_ctx is attached.
|
// FFmpeg 8.0+ rejects VAAPI pix_fmt in buffer args before hw_frames_ctx is attached.
|
||||||
// Use a SW placeholder, then override format/hw_frames_ctx with av_buffersrc_parameters_set.
|
// Use a SW placeholder, then override format/hw_frames_ctx with av_buffersrc_parameters_set.
|
||||||
|
// 中文补充:FFmpeg 8 起对 buffer 滤镜参数加了严格校验——args 字符串里写 pix_fmt=vaapi
|
||||||
|
// 会在 attach hw_frames_ctx 之前就被拒绝。workaround 是先用 bgra 占位构造 src_ctx,
|
||||||
|
// 然后用 av_buffersrc_parameters_set 覆盖真实 format/hw_frames_ctx。
|
||||||
let args = format!(
|
let args = format!(
|
||||||
"video_size={}x{}:pix_fmt=bgra:time_base=1/{fps}:pixel_aspect=1/1",
|
"video_size={}x{}:pix_fmt=bgra:time_base=1/{fps}:pixel_aspect=1/1",
|
||||||
width, height,
|
width, height,
|
||||||
);
|
);
|
||||||
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
|
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
|
||||||
|
|
||||||
|
// 中文概述(unsafe):分配 AVBufferSrcParameters 结构体——FFmpeg C API 返回新分配的
|
||||||
|
// 内存指针(需配对 av_free)。`is_null()` 检查后才能解引用。
|
||||||
// SAFETY: av_buffersrc_parameters_alloc returns newly allocated parameters
|
// SAFETY: av_buffersrc_parameters_alloc returns newly allocated parameters
|
||||||
// or null, which is checked below.
|
// or null, which is checked below.
|
||||||
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
|
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
|
||||||
if par.is_null() {
|
if par.is_null() {
|
||||||
bail!("av_buffersrc_parameters_alloc returned null");
|
bail!("av_buffersrc_parameters_alloc returned null");
|
||||||
}
|
}
|
||||||
|
// 中文概述(unsafe):把 VAAPI format / 尺寸 / 时基 / hw_frames_ctx 写入 par,再 set 给 src_ctx。
|
||||||
|
// ref_clone 增加 AVBufferRef 引用计数(FFmpeg 共享硬件帧池的标准方式)。
|
||||||
// SAFETY: par and src_ctx are valid; frames_rgb.ref_clone returns an owned hw_frames_ctx ref
|
// SAFETY: par and src_ctx are valid; frames_rgb.ref_clone returns an owned hw_frames_ctx ref
|
||||||
// that buffersrc consumes on successful parameter set.
|
// that buffersrc consumes on successful parameter set.
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -2105,12 +2154,15 @@ fn build_swenc_filter_graph(
|
|||||||
"scale",
|
"scale",
|
||||||
&format!("{enc_width}:{enc_height}:format=nv12"),
|
&format!("{enc_width}:{enc_height}:format=nv12"),
|
||||||
)?;
|
)?;
|
||||||
|
// 中文概述(unsafe):scale_vaapi 滤镜需要 hw_device_ctx 才能访问 VAAPI 设备。
|
||||||
|
// ref_clone 共享 hw_dev 的 AVHWDeviceContext,滤镜图存活期间引用计数 > 0。
|
||||||
// SAFETY: scale_vaapi keeps a ref-counted device context while the graph is alive.
|
// SAFETY: scale_vaapi keeps a ref-counted device context while the graph is alive.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*scale_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
|
(*scale_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
|
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
|
||||||
|
// 链接滤镜图:src(0) → scale(0) → sink(0),端口号 0 表示第 0 个输出/输入 pad。
|
||||||
src_ctx.link(0, &mut scale_ctx, 0);
|
src_ctx.link(0, &mut scale_ctx, 0);
|
||||||
scale_ctx.link(0, &mut sink_ctx, 0);
|
scale_ctx.link(0, &mut sink_ctx, 0);
|
||||||
graph
|
graph
|
||||||
@@ -2121,6 +2173,9 @@ fn build_swenc_filter_graph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn create_nv12_to_yuv420p_sws(width: u32, height: u32) -> Result<*mut ffi::SwsContext> {
|
fn create_nv12_to_yuv420p_sws(width: u32, height: u32) -> Result<*mut ffi::SwsContext> {
|
||||||
|
// 创建 FFmpeg 软件色彩转换器:NV12 → YUV420P,同尺寸无缩放(仅把 NV12 的 interleaved
|
||||||
|
// UV 半平面拆为 YUV420P 的 planar UV 两个半平面)。返回裸指针(调用方持有所有权,
|
||||||
|
// 必须配对 sws_freeContext——见 SwEncEncode::recreate_encoder/Drop)。
|
||||||
// SAFETY: sws_getContext creates an owned scaler context for same-size NV12 -> YUV420P.
|
// SAFETY: sws_getContext creates an owned scaler context for same-size NV12 -> YUV420P.
|
||||||
let ctx = unsafe {
|
let ctx = unsafe {
|
||||||
ffi::sws_getContext(
|
ffi::sws_getContext(
|
||||||
@@ -2143,6 +2198,7 @@ fn create_nv12_to_yuv420p_sws(width: u32, height: u32) -> Result<*mut ffi::SwsCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn alloc_yuv420p_frame(width: u32, height: u32) -> Result<*mut ffi::AVFrame> {
|
fn alloc_yuv420p_frame(width: u32, height: u32) -> Result<*mut ffi::AVFrame> {
|
||||||
|
// 分配一个 YUV420P AVFrame 并分配其可写缓冲区。返回裸指针——调用方负责 av_frame_free。
|
||||||
// SAFETY: Allocate an AVFrame, configure format/dimensions, then allocate writable buffers.
|
// SAFETY: Allocate an AVFrame, configure format/dimensions, then allocate writable buffers.
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut frame = ffi::av_frame_alloc();
|
let mut frame = ffi::av_frame_alloc();
|
||||||
@@ -2154,6 +2210,7 @@ fn alloc_yuv420p_frame(width: u32, height: u32) -> Result<*mut ffi::AVFrame> {
|
|||||||
(*frame).format = ffi::AVPixelFormat::AV_PIX_FMT_YUV420P as i32;
|
(*frame).format = ffi::AVPixelFormat::AV_PIX_FMT_YUV420P as i32;
|
||||||
let ret = ffi::av_frame_get_buffer(frame, 0);
|
let ret = ffi::av_frame_get_buffer(frame, 0);
|
||||||
if ret < 0 {
|
if ret < 0 {
|
||||||
|
// 失败路径必须先释放已分配的 frame,避免内存泄漏。
|
||||||
ffi::av_frame_free(&mut frame);
|
ffi::av_frame_free(&mut frame);
|
||||||
bail!("av_frame_get_buffer failed: {}", ff_err(ret));
|
bail!("av_frame_get_buffer failed: {}", ff_err(ret));
|
||||||
}
|
}
|
||||||
@@ -2172,7 +2229,12 @@ fn create_software_h264_muxer(
|
|||||||
ff::codec::encoder::video::Video,
|
ff::codec::encoder::video::Video,
|
||||||
ff::format::context::Output,
|
ff::format::context::Output,
|
||||||
)> {
|
)> {
|
||||||
|
// 文件 muxer 模式的软件 H.264 编码器构造。返回 (enc_video, octx) 元组——
|
||||||
|
// Rust 元组解构返回,类比 Go 的 multiple return values。
|
||||||
|
// CString 是 FFI 桥梁:FFmpeg C API 需要 NUL 终止字符串;to_str().unwrap() 假设 UTF-8。
|
||||||
let output_cstr = CString::new(output_path.to_str().unwrap())?;
|
let output_cstr = CString::new(output_path.to_str().unwrap())?;
|
||||||
|
// 优先 libx264(更高压缩率),fallback libopenh264(纯 C++ 实现,无 GPL 限制)。
|
||||||
|
// or_else + ok_or_else 三层链式:先尝试 A → 失败尝试 B → 都失败构造错误。
|
||||||
let codec = ff::encoder::find_by_name("libx264")
|
let codec = ff::encoder::find_by_name("libx264")
|
||||||
.or_else(|| ff::encoder::find_by_name("libopenh264"))
|
.or_else(|| ff::encoder::find_by_name("libopenh264"))
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
@@ -2180,6 +2242,8 @@ fn create_software_h264_muxer(
|
|||||||
})?;
|
})?;
|
||||||
let codec_name = codec.name().to_string();
|
let codec_name = codec.name().to_string();
|
||||||
|
|
||||||
|
// 块表达式(block expression)求值为最后一个表达式(ctx.encoder().video()?),
|
||||||
|
// 类比 Go IIFE:用 {} 创建临时作用域隔离 ctx,只保留 enc 借用。
|
||||||
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()?
|
||||||
@@ -2190,14 +2254,20 @@ fn create_software_h264_muxer(
|
|||||||
enc.set_bit_rate(bitrate as usize);
|
enc.set_bit_rate(bitrate as usize);
|
||||||
enc.set_gop(gop_size);
|
enc.set_gop(gop_size);
|
||||||
enc.set_time_base(ff::Rational::new(1, fps as i32));
|
enc.set_time_base(ff::Rational::new(1, fps as i32));
|
||||||
|
// B-frame = 双向预测帧,提高压缩率但增加延迟(max_b_frames=3 适合离线 muxer,
|
||||||
|
// 不适合 WebRTC,见 create_software_h264_encoder)。
|
||||||
enc.set_max_b_frames(3);
|
enc.set_max_b_frames(3);
|
||||||
|
|
||||||
|
// 中文概述(unsafe):MP4/mkv 容器需要 codec global header(SPS/PPS 在 extradata
|
||||||
|
// 而不是每个 IDR),其他 muxer 无副作用。
|
||||||
// SAFETY: global headers are needed by MP4 and harmless for other common muxers.
|
// SAFETY: global headers are needed by MP4 and harmless for other common muxers.
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if codec_name == "libx264" {
|
if codec_name == "libx264" {
|
||||||
|
// 中文概述(unsafe):通过 av_opt_set 设置 libx264 私有选项(preset/threads)。
|
||||||
|
// 每个 CString 仅在对应 av_opt_set 调用内存活——FFmpeg 在调用内复制字符串。
|
||||||
// SAFETY: priv_data and codec context belong to the unopened encoder;
|
// SAFETY: priv_data and codec context belong to the unopened encoder;
|
||||||
// strings live for each av_opt_set call.
|
// strings live for each av_opt_set call.
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -2215,11 +2285,14 @@ fn create_software_h264_muxer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开编码器:open() 消耗 enc,返回 (Video, ...) 元组。
|
||||||
let opened = enc
|
let opened = enc
|
||||||
.open()
|
.open()
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to open {codec_name} encoder: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to open {codec_name} encoder: {e}"))?;
|
||||||
let enc_video = opened.0;
|
let enc_video = opened.0;
|
||||||
|
|
||||||
|
// 路径含 "null" 时用 null muxer(丢弃所有输出,用于基准/调试)。
|
||||||
|
// map + unwrap_or 链式处理 Option<&str>,类比 Go `if s, ok := p.to_str(); ok { ... }`。
|
||||||
let use_null = output_path
|
let use_null = output_path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(|s| s.contains("null"))
|
.map(|s| s.contains("null"))
|
||||||
@@ -2236,6 +2309,8 @@ fn create_software_h264_muxer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
|
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
|
||||||
|
// 中文概述(unsafe):分配输出 AVFormatContext。FFmpeg 根据 output_path 后缀
|
||||||
|
// 自动推断 muxer(.mp4 → mp4 muxer,.mkv → matroska),或用 fmt_name 强制。
|
||||||
// SAFETY: fmt_ctx_ptr is initialized by FFmpeg; C strings live across the call.
|
// SAFETY: fmt_ctx_ptr is initialized by FFmpeg; C strings live across the call.
|
||||||
let ret = unsafe {
|
let ret = unsafe {
|
||||||
ffi::avformat_alloc_output_context2(
|
ffi::avformat_alloc_output_context2(
|
||||||
@@ -2249,23 +2324,29 @@ fn create_software_h264_muxer(
|
|||||||
bail!("Failed to allocate output format context: {}", ff_err(ret));
|
bail!("Failed to allocate output format context: {}", ff_err(ret));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述(unsafe):在 fmt_ctx 中新建一个 stream 容器(默认空 codecpar)。
|
||||||
// SAFETY: fmt_ctx_ptr is valid; stream and codec parameters are owned by the format context.
|
// SAFETY: fmt_ctx_ptr is valid; stream and codec parameters are owned by 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 output stream");
|
bail!("Failed to create output stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述(unsafe):从 encoder context 复制 codec 参数(分辨率/profile/level/extradata)
|
||||||
|
// 到 stream->codecpar,让 muxer 写入容器头。
|
||||||
// SAFETY: stream_ptr and encoder context are valid; parameters are copied into stream.
|
// SAFETY: stream_ptr and encoder context are valid; parameters are copied into stream.
|
||||||
let ret =
|
let ret =
|
||||||
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
|
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
|
||||||
if ret < 0 {
|
if ret < 0 {
|
||||||
bail!("Failed to copy codec parameters to stream: {}", ff_err(ret));
|
bail!("Failed to copy codec parameters to stream: {}", ff_err(ret));
|
||||||
}
|
}
|
||||||
|
// 中文概述(unsafe):stream 的 time_base 取自 encoder,保持 PTS 单位一致。
|
||||||
// SAFETY: stream_ptr is valid and writable during muxer setup.
|
// SAFETY: stream_ptr is valid and writable during muxer setup.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述(unsafe):对需要文件的 muxer(非 null muxer)打开 AVIO 写句柄。
|
||||||
|
// null muxer 设置 AVFMT_NOFILE 标志,跳过 avio_open。
|
||||||
// SAFETY: open an AVIO only for muxers that require files; null muxer advertises NOFILE.
|
// SAFETY: open an AVIO only for muxers that require files; null muxer advertises NOFILE.
|
||||||
unsafe {
|
unsafe {
|
||||||
if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 {
|
if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 {
|
||||||
@@ -2284,12 +2365,15 @@ fn create_software_h264_muxer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述(unsafe):写容器头(MP4 ftyp/moov atom、mkv EBML header 等)。
|
||||||
// SAFETY: fmt_ctx_ptr is fully configured.
|
// SAFETY: fmt_ctx_ptr is fully configured.
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 中文概述(unsafe):把裸 fmt_ctx_ptr 包装回 ffmpeg-next 的 Output 类型,
|
||||||
|
// 之后由 Rust 端管理生命周期——Drop 时调用 av_write_trailer + avformat_free_context。
|
||||||
// SAFETY: ownership of fmt_ctx_ptr transfers to ffmpeg-next Output wrapper.
|
// SAFETY: ownership of fmt_ctx_ptr transfers to ffmpeg-next Output wrapper.
|
||||||
let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
|
let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
|
||||||
tracing::info!("Using software H.264 encoder: {codec_name}");
|
tracing::info!("Using software H.264 encoder: {codec_name}");
|
||||||
@@ -2303,6 +2387,11 @@ fn create_software_h264_encoder(
|
|||||||
bitrate: u64,
|
bitrate: u64,
|
||||||
gop_size: u32,
|
gop_size: u32,
|
||||||
) -> Result<ff::codec::encoder::video::Video> {
|
) -> Result<ff::codec::encoder::video::Video> {
|
||||||
|
// WebRTC 模式的软件 H.264 编码器构造(仅返回 encoder,不带 muxer)。关键差异:
|
||||||
|
// - time_base = 1/90000(RTP 时钟单位),不是 1/fps
|
||||||
|
// - max_b_frames = 0(B 帧会破坏 RTP 实时性)
|
||||||
|
// - preset = veryfast + tune = zerolatency(最低延迟)
|
||||||
|
// - forced-idr + repeat_headers(IDR 内联 SPS/PPS,WebRTC 浏览器需要)
|
||||||
let codec = ff::encoder::find_by_name("libx264")
|
let codec = ff::encoder::find_by_name("libx264")
|
||||||
.or_else(|| ff::encoder::find_by_name("libopenh264"))
|
.or_else(|| ff::encoder::find_by_name("libopenh264"))
|
||||||
.ok_or_else(|| anyhow::anyhow!("No H.264 software encoder found"))?;
|
.ok_or_else(|| anyhow::anyhow!("No H.264 software encoder found"))?;
|
||||||
@@ -2326,9 +2415,13 @@ fn create_software_h264_encoder(
|
|||||||
// libx264 infers wrong fps from the 90kHz time_base and VBV rate control
|
// libx264 infers wrong fps from the 90kHz time_base and VBV rate control
|
||||||
// breaks. Per Oracle review round for #25.
|
// breaks. Per Oracle review round for #25.
|
||||||
enc.set_frame_rate(Some(ff::Rational::new(fps as i32, 1)));
|
enc.set_frame_rate(Some(ff::Rational::new(fps as i32, 1)));
|
||||||
|
// 关键:WebRTC 不允许 B 帧——B 帧需要"未来帧"参考,但 RTP 是顺序发送。
|
||||||
enc.set_max_b_frames(0);
|
enc.set_max_b_frames(0);
|
||||||
|
|
||||||
if codec_name == "libx264" {
|
if codec_name == "libx264" {
|
||||||
|
// 中文概述(unsafe):通过 av_opt_set 设置 libx264 私有选项(preset/tune/threads/
|
||||||
|
// forced-idr/x264opts)。每个 CString 仅在对应 av_opt_set 调用内存活——FFmpeg 在调用
|
||||||
|
// 内部复制字符串。tune=zerolatency 关闭所有缓冲(sync-lookahead=0, rc-lookahead=0)。
|
||||||
// SAFETY: priv_data and codec context belong to the unopened encoder;
|
// SAFETY: priv_data and codec context belong to the unopened encoder;
|
||||||
// each CString lives for the duration of its av_opt_set call.
|
// each CString lives for the duration of its av_opt_set call.
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -2393,8 +2486,12 @@ fn build_filter_graph(
|
|||||||
fps: u32,
|
fps: u32,
|
||||||
transform: Transform,
|
transform: Transform,
|
||||||
) -> Result<ff::filter::Graph> {
|
) -> Result<ff::filter::Graph> {
|
||||||
|
// 硬件 VAAPI 路径的滤镜图(EncState 用,与 build_swenc_filter_graph 区别是不下采样):
|
||||||
|
// src(BGRA hw) → scale_vaapi(原尺寸 + NV12 转换) → [transpose_vaapi(如非 Normal)] → sink。
|
||||||
|
// transform 决定是否插入 transpose_vaapi,8 个 Transform variant 各对应一个 dir 值。
|
||||||
let mut graph = ff::filter::Graph::new();
|
let mut graph = ff::filter::Graph::new();
|
||||||
|
|
||||||
|
// 同 build_swenc_filter_graph:通过名称查找三个核心滤镜。
|
||||||
let buffersrc =
|
let buffersrc =
|
||||||
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
|
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
|
||||||
let buffersink = ff::filter::find("buffersink")
|
let buffersink = ff::filter::find("buffersink")
|
||||||
@@ -2403,6 +2500,8 @@ fn build_filter_graph(
|
|||||||
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
|
||||||
|
|
||||||
// buffersrc — use AVBufferSrcParameters to set hw_frames_ctx properly
|
// buffersrc — use AVBufferSrcParameters to set hw_frames_ctx properly
|
||||||
|
// 中文补充:这里不像 build_swenc_filter_graph 那样用 bgra 占位——直接在 args 里
|
||||||
|
// 写 pix_fmt=VAAPI。实际行为相同(都被后续 av_buffersrc_parameters_set 覆盖)。
|
||||||
let args = format!(
|
let args = format!(
|
||||||
"video_size={}x{}:pix_fmt={}:time_base=1/{fps}:pixel_aspect=1/1",
|
"video_size={}x{}:pix_fmt={}:time_base=1/{fps}:pixel_aspect=1/1",
|
||||||
width,
|
width,
|
||||||
@@ -2411,11 +2510,13 @@ fn build_filter_graph(
|
|||||||
);
|
);
|
||||||
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
|
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
|
||||||
|
|
||||||
|
// 中文概述(unsafe):同 build_swenc_filter_graph,分配 AVBufferSrcParameters。
|
||||||
// SAFETY: av_buffersrc_parameters_alloc allocates params for the buffersrc.
|
// SAFETY: av_buffersrc_parameters_alloc allocates params for the buffersrc.
|
||||||
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
|
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
|
||||||
if par.is_null() {
|
if par.is_null() {
|
||||||
bail!("av_buffersrc_parameters_alloc returned null");
|
bail!("av_buffersrc_parameters_alloc returned null");
|
||||||
}
|
}
|
||||||
|
// 中文概述(unsafe):把 VAAPI hw_frames_ctx 附加到 src,让 scale_vaapi 能访问硬件帧池。
|
||||||
// SAFETY: Set hw_frames_ctx on the buffersrc parameters, then apply.
|
// SAFETY: Set hw_frames_ctx on the buffersrc parameters, then apply.
|
||||||
unsafe {
|
unsafe {
|
||||||
(*par).format = Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32;
|
(*par).format = Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32;
|
||||||
@@ -2434,6 +2535,8 @@ fn build_filter_graph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// scale_vaapi: hardware scaling and colourspace conversion (keeps original dimensions)
|
// scale_vaapi: hardware scaling and colourspace conversion (keeps original dimensions)
|
||||||
|
// 中文补充:与 build_swenc_filter_graph 不同,这里 scale 输出 = 输入尺寸(width:height),
|
||||||
|
// 仅做 BGRA→NV12 颜色空间转换,不做下采样。
|
||||||
let mut scale_ctx = graph.add(
|
let mut scale_ctx = graph.add(
|
||||||
&scale_vaapi,
|
&scale_vaapi,
|
||||||
"scale",
|
"scale",
|
||||||
@@ -2450,13 +2553,19 @@ fn build_filter_graph(
|
|||||||
// Build filter chain: src -> scale -> [transpose] -> sink
|
// Build filter chain: src -> scale -> [transpose] -> sink
|
||||||
src_ctx.link(0, &mut scale_ctx, 0);
|
src_ctx.link(0, &mut scale_ctx, 0);
|
||||||
|
|
||||||
|
// match 穷尽性:8 个 Transform variant,Normal 走直连分支,其他 7 个走 transpose 分支。
|
||||||
|
// `other =>` 通配符 arm 与 `Transform::Normal` 显式 arm 共存——类比 Go type switch。
|
||||||
match transform {
|
match transform {
|
||||||
Transform::Normal => {
|
Transform::Normal => {
|
||||||
scale_ctx.link(0, &mut sink_ctx, 0);
|
scale_ctx.link(0, &mut sink_ctx, 0);
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
|
// 非 Normal:插入 transpose_vaapi 滤镜。dir 值映射 FFmpeg transpose doc 中的
|
||||||
|
// "clockflip" 表(0-6 对应 8 种旋转/翻转组合)。
|
||||||
let transpose = ff::filter::find("transpose_vaapi")
|
let transpose = ff::filter::find("transpose_vaapi")
|
||||||
.ok_or_else(|| anyhow::anyhow!("filter 'transpose_vaapi' not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("filter 'transpose_vaapi' not found"))?;
|
||||||
|
// 嵌套 match:此处 other 已知不是 Normal,但 Rust 仍要求穷尽所有 variant,
|
||||||
|
// Normal 分支用 unreachable!() 标记(运行时若触发说明 enum 扩展未更新)。
|
||||||
let dir_val = match other {
|
let dir_val = match other {
|
||||||
Transform::Normal90 => "1",
|
Transform::Normal90 => "1",
|
||||||
Transform::Normal180 => "4",
|
Transform::Normal180 => "4",
|
||||||
@@ -2468,6 +2577,7 @@ fn build_filter_graph(
|
|||||||
Transform::Normal => unreachable!(),
|
Transform::Normal => unreachable!(),
|
||||||
};
|
};
|
||||||
let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
|
let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
|
||||||
|
// 中文概述(unsafe):transpose_vaapi 同样需要 hw_device_ctx 访问 VAAPI 设备。
|
||||||
// SAFETY: trans_ctx is a live transpose_vaapi filter context;
|
// SAFETY: trans_ctx is a live transpose_vaapi filter context;
|
||||||
// scale_vaapi/transpose_vaapi keep a ref-counted device context.
|
// scale_vaapi/transpose_vaapi keep a ref-counted device context.
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
Reference in New Issue
Block a user