Compare commits

..
10 Commits
10 changed files with 1169 additions and 11 deletions
+47
View File
@@ -1,11 +1,43 @@
//! 列出当前 Wayland 桌面广播的全部全局对象(registry globals)。
//!
//! Wayland 协议采用"客户端发现"模型:客户端连接到 compositor 后,第一件事是从
//! registry 中枚举所有被广播的 interface(如 `wl_compositor`、`wl_shm`、
//! `zwlr_screencopy_manager_v1`、`zxdg_portal_screencast` 等),每个 global 带有
//! 唯一数字 name、interface 名字符串、最高支持版本号。本示例即打印这三元组。
//!
//! 类比 Go 的 `xcursor` / wayland-client 示例:用最小可运行代码确认运行环境。
//!
//! 运行(参见 AGENTS.md "Useful manual commands"):
//! ```sh
//! cargo run --example list_globals
//! ```
//!
//! 该示例也是 `src/backend_detect.rs` 中检测 `zwlr_screencopy_manager_v1` 是否存在的
//! 同款机制(参见 `check_screencopy_available`),用于决定走 wlr-screencopy 还是 Portal。
// 引入 wayland-client 的快捷初始化辅助函数:内部完成 connect + registry bind + 同步枚举。
use wayland_client::globals::registry_queue_init; use wayland_client::globals::registry_queue_init;
// GlobalListContents 是 registry_queue_init 返回的"已收集好的 global 列表"句柄类型。
use wayland_client::globals::GlobalListContents; use wayland_client::globals::GlobalListContents;
// WlRegistry 是 Wayland 协议对象;Event 是其产生的枚举事件(global/global_remove)。
use wayland_client::protocol::wl_registry::{Event, WlRegistry}; use wayland_client::protocol::wl_registry::{Event, WlRegistry};
// Connection 表示与 compositor 的 socket 连接;QueueHandle 是事件队列句柄;
// Dispatch 是 trait,用户必须为关心的协议对象实现它以接收事件回调。
use wayland_client::{Connection, Dispatch, QueueHandle}; use wayland_client::{Connection, Dispatch, QueueHandle};
// 示例用的极简 state:无字段。Wayland 客户端需要至少一个 state 类型作为
// Dispatch trait 的 `Self`,这里就用零大小类型 `Ls`list globals 的缩写)。
struct Ls; struct Ls;
// 为 Ls 实现 WlRegistry 的 Dispatch:本示例只需枚举 globals,不需要响应任何
// registry 事件,因此 event 函数留空。wayland-client 要求即便不处理事件也必须
// 实现 Dispatchtrait contract 强制),否则 `registry_queue_init::<Ls>` 无法编译。
//
// Go 类比:类似 `type Ls struct{}` + `func (Ls) HandleEvent(...) {}`——
// 显式声明"我接收事件但不响应"。
impl Dispatch<WlRegistry, GlobalListContents> for Ls { impl Dispatch<WlRegistry, GlobalListContents> for Ls {
// 所有参数加 `_` 前缀表示本实现不读取任何参数(Rust 中 `_x` 与 `x` 区分:
// 前者显式标记未使用,避免 dead_code 警告)。
fn event( fn event(
_state: &mut Self, _state: &mut Self,
_registry: &WlRegistry, _registry: &WlRegistry,
@@ -17,10 +49,25 @@ impl Dispatch<WlRegistry, GlobalListContents> for Ls {
} }
} }
// 程序入口。Rust 的 `fn main()` 不能返回 `Result`(标准约定),故用 `.unwrap()`
// 简单 panic;示例程序通常省略错误处理以突出主线逻辑。
fn main() { fn main() {
// 从 `WAYLAND_DISPLAY` / `XDG_RUNTIME_DIR` 环境变量建立与 compositor 的 socket 连接。
// 类比 Go 的 `net.Dial("unix", path)`。`.unwrap()` 在连接失败时 panic(示例代码约定)。
let conn = Connection::connect_to_env().unwrap(); let conn = Connection::connect_to_env().unwrap();
// registry_queue_init 是 wayland-client 的高层辅助:内部发送 sync request 并阻塞
// 直到 registry 全部 global 事件到达。返回 (GlobalList, EventQueue)。
// `::<Ls>` 是 turbofish 显式指定 state 类型,对应上面 `impl Dispatch for Ls`。
// 类比 Go 的 `globals, queue := wayland.RegistryQueueInit[Ls](conn)`(泛型实例化)。
let (globals, _queue) = registry_queue_init::<Ls>(&conn).unwrap(); let (globals, _queue) = registry_queue_init::<Ls>(&conn).unwrap();
// 遍历所有已收集的 globals。`globals.contents()` 返回内部快照引用,
// `.clone_list()` 复制成 `Vec<GlobalListEntry>`(每个 entry 含 name/interface/version)。
// 类比 Go `for _, g := range globals { ... }`——Rust 的 `for ... in` 直接消费迭代器。
for g in globals.contents().clone_list() { for g in globals.contents().clone_list() {
// `println!` 是 Rust 标准宏(不是函数),类比 Go `fmt.Printf("%d: %s v%d\n", ...)`。
// `{}` 自动调用参数的 `Display` traitname 是 u32、interface 是 String、version 是 u32。
println!("{}: {} v{}", g.name, g.interface, g.version); println!("{}: {} v{}", g.name, g.interface, g.version);
} }
} }
+45
View File
@@ -1,11 +1,41 @@
//! XDG Portal 权限冒烟测试示例。
//!
//! 本示例演示完整的 Portal ScreenCast 授权流程,分四步:
//! 创建 Screencast proxy,再创建 session,然后选择源(显示器/窗口),
//! 最后 start() 触发系统授权对话框(用户点击"共享"后返回流信息)。
//!
//! 运行:`cargo run --example test_portal`(参见 AGENTS.md "Useful manual commands")。
//!
//! Rust 异步模型(与 Go 对比):
//! - Go 用 goroutine + channelasync 函数本身**惰性**,需 runtime 驱动。
//! - 本示例刻意**不用** `#[tokio::main]` 宏,而是手动 `Runtime::new()` + `block_on`
//! (与 src/backend_detect.rs 同款"手动 runtime"模式);这是因为 ashpd 内部缓存
//! zbus::Connection 到全局 OnceLock,若宏自动建的 runtime 被 drop
//! 缓存的 connection 会"僵尸化"导致后续 hang(详见 AGENTS.md)。
//!
//! 对照 Go`go func() { ... }()` ≈ `tokio::spawn(async { ... })`
//! 而 `block_on` 类似 Go 的 `select {}` 阻塞 main goroutine 等待退出。
// ashpd = XDG Portal 的 Rust 高层绑定,封装了 D-Bus ScreenCast 接口
use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType}; use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType};
// PersistMode 控制"恢复令牌"持久化级别(DoNot / Persistent / ExplicitlyRevoked
use ashpd::desktop::PersistMode; use ashpd::desktop::PersistMode;
// BitFlags = 位域集合类型(一个值可同时包含多个 SourceType,类比 Go 的 iota | 操作)
use ashpd::enumflags2::BitFlags; use ashpd::enumflags2::BitFlags;
// 同步 main → 手动创建 tokio Runtime → block_on 阻塞驱动 async 块。
// 这种"同步外壳 + 异步内核"的写法等价于 `#[tokio::main] async fn main()`
// 但保留了显式控制 runtime 生命周期的灵活性(参见文件头说明)。
fn main() { fn main() {
// 手动创建 tokio runtime(含 reactor + executor + 时间驱动);
// unwrap() 仅示例用;生产代码应返回 Result 并 `?` 传播(但 fn main 不返回 Result
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
// block_on 阻塞当前线程直到传入的 future 完成;这是同步↔异步边界
rt.block_on(async { rt.block_on(async {
// async {} 块构造一个匿名 future,仅在 block_on poll 时才执行(惰性,与 goroutine 不同)
eprintln!("1. Creating Screencast proxy..."); eprintln!("1. Creating Screencast proxy...");
// Screencast::new() 内部通过 D-Bus 连接 org.freedesktop.portal.ScreenCast
// .await 让出执行权直到 future 就绪(Go 没有这个语法,需 channel/锁模拟)
let proxy = match Screencast::new().await { let proxy = match Screencast::new().await {
Ok(p) => { Ok(p) => {
eprintln!(" OK"); eprintln!(" OK");
@@ -13,11 +43,14 @@ fn main() {
} }
Err(e) => { Err(e) => {
eprintln!(" FAIL: {e}"); eprintln!(" FAIL: {e}");
// early-return 仅退出 async 块(不是退出 main),block_on 返回 ()
return; return;
} }
}; };
eprintln!("2. Creating session..."); eprintln!("2. Creating session...");
// create_session 建立一个 ScreenCast 会话句柄;
// Default::default() 用类型默认参数(ashpd 推断为 SessionOptions,所有字段取 Default
let session = match proxy.create_session(Default::default()).await { let session = match proxy.create_session(Default::default()).await {
Ok(s) => { Ok(s) => {
eprintln!(" OK"); eprintln!(" OK");
@@ -30,7 +63,15 @@ fn main() {
}; };
eprintln!("3. Selecting sources..."); eprintln!("3. Selecting sources...");
// BitFlags<SourceType> 表达"可选多显示器/窗口/工作区"集合;
// 这里 `into()` 将单个 Monitor 转为位域(Go 类似 flag = 1 << iota
let sources: BitFlags<SourceType> = SourceType::Monitor.into(); let sources: BitFlags<SourceType> = SourceType::Monitor.into();
// Builder 链式:每次 set_X 返回 &mut Self(类似 Go functional-options 模式但更显式)
// - cursor_mode Embedded:光标嵌入帧内
// - sources: 仅 Monitor(去掉窗口,简化授权 UX)
// - multiple=false:单选(一次只授权一个显示器)
// - persist_mode DoNot:不申请恢复令牌(避免持久权限残留)
// 整个 builder 链构造一个 future,末尾的 .await 等待 D-Bus 返回
let result = proxy let result = proxy
.select_sources( .select_sources(
&session, &session,
@@ -50,6 +91,8 @@ fn main() {
} }
eprintln!("4. Starting (should show dialog)..."); eprintln!("4. Starting (should show dialog)...");
// start() 触发系统授权对话框(D-Bus 调用阻塞直到用户响应);
// 第二参数 parent_window = None(无父窗口,常见于 CLI 程序)
let response = match proxy.start(&session, None, Default::default()).await { let response = match proxy.start(&session, None, Default::default()).await {
Ok(r) => { Ok(r) => {
eprintln!(" OK"); eprintln!(" OK");
@@ -60,6 +103,8 @@ fn main() {
return; return;
} }
}; };
// Portal D-Bus 响应是双层结构:外层是 Request::responseOk/Err),
// 内层才是 ScreenCast 流信息(streams() 返回 PipeWire 节点 + dmabuf 信息列表)
match response.response() { match response.response() {
Ok(r) => eprintln!(" Got {} stream(s)", r.streams().len()), Ok(r) => eprintln!(" Got {} stream(s)", r.streams().len()),
Err(e) => eprintln!(" Response error: {e}"), Err(e) => eprintln!(" Response error: {e}"),
+304
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,29 +1900,50 @@ 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) };
} }
} }
} }
// 软件编码复合状态:把"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(
@@ -1750,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/libopenh264bitrate/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 })
} }
@@ -1772,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 代替 muxerwebrtc_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);
@@ -1795,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)?;
} }
@@ -1816,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
@@ -1833,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,
@@ -1866,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")
@@ -1876,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 {
@@ -1911,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
@@ -1927,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(
@@ -1949,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();
@@ -1960,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));
} }
@@ -1978,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(|| {
@@ -1986,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()?
@@ -1996,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 headerSPS/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 {
@@ -2021,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"))
@@ -2042,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(
@@ -2055,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 {
@@ -2090,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}");
@@ -2109,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/90000RTP 时钟单位),不是 1/fps
// - max_b_frames = 0B 帧会破坏 RTP 实时性)
// - preset = veryfast + tune = zerolatency(最低延迟)
// - forced-idr + repeat_headersIDR 内联 SPS/PPSWebRTC 浏览器需要)
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"))?;
@@ -2132,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 {
@@ -2199,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_vaapi8 个 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")
@@ -2209,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,
@@ -2217,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;
@@ -2240,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",
@@ -2256,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 variantNormal 走直连分支,其他 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",
@@ -2274,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 {
+1 -1
View File
@@ -21,7 +21,7 @@
//! - `std::time::Instant`:高精度单调时钟,等价于 Go 的 `time.Now()` + `time.Since()`。 //! - `std::time::Instant`:高精度单调时钟,等价于 Go 的 `time.Now()` + `time.Since()`。
//! - `crossbeam_channel::recv_timeout`:等价于 Go 的 `select { case <-time.After(): }`。 //! - `crossbeam_channel::recv_timeout`:等价于 Go 的 `select { case <-time.After(): }`。
//! - 本文件大量使用裸 `unsafe` FFI 调用 FFmpeg C API;现有 21 处 unsafe 块均 //! - 本文件大量使用裸 `unsafe` FFI 调用 FFmpeg C API;现有 21 处 unsafe 块均
//! 未标注 `// SAFETY:`,本任务也不补充,仅在每个 unsafe 块上方加普通 `//` //! 未标注 SAFETY 标记,本任务也不补充,仅在每个 unsafe 块上方加普通 `//`
//! 中文概述,说明"为什么必须 unsafe"。 //! 中文概述,说明"为什么必须 unsafe"。
//! //!
//! 用法:`cargo run --bin sw_encode_bench -- --output /tmp/bench_test.mp4` //! 用法:`cargo run --bin sw_encode_bench -- --output /tmp/bench_test.mp4`
+149 -7
View File
@@ -213,7 +213,7 @@ impl Drop for SwsContext {
fn av_err_to_string(ret: i32) -> String { fn av_err_to_string(ret: i32) -> String {
// 准备 128 字节缓冲区(FFmpeg 习惯用 128),由 av_strerror 写入 NUL 结尾的 C 字符串 // 准备 128 字节缓冲区(FFmpeg 习惯用 128),由 av_strerror 写入 NUL 结尾的 C 字符串
let mut buf = vec![0u8; 128]; let mut buf = vec![0u8; 128];
// SAFETY: av_strerror 最多写 128 字节并以 NUL 结尾;buf 是独占的可变 Vec<u8> // 中文 unsafe 概述:av_strerror 最多写 128 字节并以 NUL 结尾;buf 是独占的可变 Vec<u8>
// as_mut_ptr 把缓冲区首字节暴露给 C,借用仅在这次调用期间有效。 // as_mut_ptr 把缓冲区首字节暴露给 C,借用仅在这次调用期间有效。
unsafe { unsafe {
ffi::av_strerror(ret, buf.as_mut_ptr() as *mut i8, buf.len()); ffi::av_strerror(ret, buf.as_mut_ptr() as *mut i8, buf.len());
@@ -264,7 +264,7 @@ fn drain_encoder(
) -> Result<()> { ) -> Result<()> {
loop { loop {
let mut pkt = ff::Packet::empty(); let mut pkt = ff::Packet::empty();
// SAFETY: enc_video.as_mut_ptr() 指向已打开的编码器上下文;pkt.as_mut_ptr() // 中文 unsafe 概述:enc_video.as_mut_ptr() 指向已打开的编码器上下文;pkt.as_mut_ptr()
// 指向空 packetFFmpeg 会在此调用中分配 packet 数据。 // 指向空 packetFFmpeg 会在此调用中分配 packet 数据。
let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) }; let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) };
if ret < 0 { if ret < 0 {
@@ -277,7 +277,7 @@ fn drain_encoder(
} }
// 把 PTS 从编码器时间基重缩放为输出流的时间基(视频流可有不同 time_base) // 把 PTS 从编码器时间基重缩放为输出流的时间基(视频流可有不同 time_base)
let enc_tb = enc_video.time_base(); let enc_tb = enc_video.time_base();
// SAFETY: octx.as_ptr() 指向有效的 AVFormatContextstreams 数组至少有一个流 // 中文 unsafe 概述:octx.as_ptr() 指向有效的 AVFormatContextstreams 数组至少有一个流
// (在 create_software_encoder 中由 avformat_new_stream 创建)。 // (在 create_software_encoder 中由 avformat_new_stream 创建)。
let stream_tb = unsafe { let stream_tb = unsafe {
let streams = (*octx.as_ptr()).streams; let streams = (*octx.as_ptr()).streams;
@@ -394,8 +394,8 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul
bail!("Failed to copy codec parameters: error {ret}"); bail!("Failed to copy codec parameters: error {ret}");
} }
// SAFETY: fmt_ctx_ptr is valid; pb is initialized for non-NOFILE muxers.
// AVFMT_NOFILE 表示该 muxer 不需要物理文件(如 null muxer),跳过 avio_open // AVFMT_NOFILE 表示该 muxer 不需要物理文件(如 null muxer),跳过 avio_open
// SAFETY: fmt_ctx_ptr is valid; pb is initialized for non-NOFILE muxers.
unsafe { unsafe {
if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 { if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 {
let ret = ffi::avio_open( let ret = ffi::avio_open(
@@ -415,12 +415,12 @@ fn create_software_encoder(output_path: &Path, width: u32, height: u32) -> Resul
bail!("Failed to write header: error {ret}"); bail!("Failed to write header: error {ret}");
} }
// SAFETY: ownership of fmt_ctx_ptr transfers into ffmpeg-next Output wrapper.
// 此后 octx 拥有 fmt_ctx_ptr,会在 Drop 时调用 avformat_free_context // 此后 octx 拥有 fmt_ctx_ptr,会在 Drop 时调用 avformat_free_context
// SAFETY: ownership of fmt_ctx_ptr transfers into 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) };
// SAFETY: Allocate and configure an owned writable YUV420P frame for encoder input.
// 这个 yuv_frame 在每次 encode_yuv_frame 中复用(不重新分配),由 SoftwareEncoder::drop 释放 // 这个 yuv_frame 在每次 encode_yuv_frame 中复用(不重新分配),由 SoftwareEncoder::drop 释放
// SAFETY: Allocate and configure an owned writable YUV420P frame for encoder input.
let yuv_frame = unsafe { let yuv_frame = unsafe {
let mut f = ffi::av_frame_alloc(); let mut f = ffi::av_frame_alloc();
if f.is_null() { if f.is_null() {
@@ -487,8 +487,8 @@ fn create_sws_context(
dst_width: u32, dst_width: u32,
dst_height: u32, dst_height: u32,
) -> Result<SwsContext> { ) -> Result<SwsContext> {
// SAFETY: sws_getContext creates an owned scaler context for the provided dimensions/formats.
// 返回的 *mut SwsContext 由 SwsContext 包装并在 Drop 中通过 sws_freeContext 释放。 // 返回的 *mut SwsContext 由 SwsContext 包装并在 Drop 中通过 sws_freeContext 释放。
// SAFETY: sws_getContext creates an owned scaler context for the provided dimensions/formats.
let ctx = unsafe { let ctx = unsafe {
ffi::sws_getContext( ffi::sws_getContext(
src_width as i32, src_width as i32,
@@ -645,6 +645,16 @@ fn build_gpu_filter_graph(
Ok(graph) Ok(graph)
} }
// CPU 流水线主函数:跑 `frames` 帧测量每阶段耗时(import → transfer → scale → encode)。
// 与 GPU 路径的核心差异:CPU 路径**不经过 scale_vaapi 滤镜**,而是用 `av_hwframe_transfer_data`
// 把硬件帧"下载"到 CPU 内存(4K BGRA),再用 `sws_scale` 在 CPU 上做下采样到 2K YUV420P
// 因此 CPU 路径的"transfer"和"scale"耗时都明显高于 GPU 路径。
//
// 类比 Go benchmark:类似 `func benchCPU(b *testing.B) { for n := 0; n < b.N; n++ {...} }`
// 但 Rust 用 `while stats.frames_encoded < frames` 显式循环(无 testing.B 框架)。
//
// 参数:8 个参数(含 src/enc 尺寸 4 个)—— clippy 默认会嫌太多,故上方 `#[allow]` 抑制。
// 返回 `Result<FrameStats>`:任何 FFmpeg/Portal 失败立即 `?` 传播到 main。
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn run_cpu_pipeline( fn run_cpu_pipeline(
cap: &CapPortal, cap: &CapPortal,
@@ -656,7 +666,10 @@ fn run_cpu_pipeline(
enc_width: u32, enc_width: u32,
enc_height: u32, enc_height: u32,
) -> Result<FrameStats> { ) -> Result<FrameStats> {
// 构造软件编码器(libx264 或 libopenh264,取决于 create_software_encoder 内部 fallback)。
// `?` 自动把 anyhow::Error 上浮到调用者;类比 Go `if err != nil { return err }`。
let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?; let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?;
// 构造 sws_scale 上下文:源 = 4K BGRA,目标 = 2K YUV420Psws_scale 内部完成下采样+色彩空间转换。
let sws_ctx = create_sws_context( let sws_ctx = create_sws_context(
src_width, src_width,
src_height, src_height,
@@ -665,6 +678,8 @@ fn run_cpu_pipeline(
enc_height, enc_height,
)?; )?;
// println! 是宏(不是函数),类比 Go fmt.Println;第一参数是 format! 模板字符串。
// `{output}` 是内联格式化语法(Rust 1.58+),等价于 `format!("{}", output)`。
println!( println!(
" Encoder: {}, {}x{} YUV420P", " Encoder: {}, {}x{} YUV420P",
encoder.codec_name, enc_width, enc_height encoder.codec_name, enc_width, enc_height
@@ -672,26 +687,40 @@ fn run_cpu_pipeline(
println!(" Output: {output}"); println!(" Output: {output}");
println!(" CPU Pipeline: DMA-BUF 4K BGRA -> av_hwframe_map -> av_hwframe_transfer_data -> sws_scale -> YUV420P 2K -> encode\n"); println!(" CPU Pipeline: DMA-BUF 4K BGRA -> av_hwframe_map -> av_hwframe_transfer_data -> sws_scale -> YUV420P 2K -> encode\n");
// 构造 FrameStats,用 struct update 语法 `..FrameStats::default()` 让其余字段取 Default 值。
// 类比 Go `&FrameStats{Codec: codec, Output: out}` 仅设置 2 字段其余清零。
let mut stats = FrameStats { let mut stats = FrameStats {
codec_name: encoder.codec_name.clone(), codec_name: encoder.codec_name.clone(),
output_path: output.to_string(), output_path: output.to_string(),
..FrameStats::default() ..FrameStats::default()
}; };
// 整条流水线总耗时起点;elapsed() 返回 Duration,后续 as_secs_f64() 取秒(float)。
let total_start = Instant::now(); let total_start = Instant::now();
// PTSPresentation Time Stamp,单位 = 编码器 time_base.den 的倒数)—— 单调递增的演示时钟。
// `let mut` 表示可变绑定(默认不可变,Rust 与 Go 的关键差异之一)。
let mut pts: i64 = 0; let mut pts: i64 = 0;
// 主循环:直到编码完 `frames` 帧;类比 Go `for stats.FramesEncoded < frames {`。
while stats.frames_encoded < frames { while stats.frames_encoded < frames {
// try_recv 非阻塞从 PipeWire 控制通道取事件;Ok 表示有事件,Err(TryRecvError::Empty) 跳过。
// 类比 Go `select { case ev := <-ctrlCh: ... default: }`。
if let Ok(ctrl) = cap.event_receiver().try_recv() { if let Ok(ctrl) = cap.event_receiver().try_recv() {
// match 是穷尽性模式匹配(每个 enum variant 必须覆盖或用 `_` 兜底)。
match ctrl { match ctrl {
// 流正常结束(用户停止共享 / Portal 关闭):跳出主循环。
PwCtrlEvent::StreamEnded => break, PwCtrlEvent::StreamEnded => break,
// PipeWire 报错:把帧号 + 错误信息 bail! 到调用者(bail! = return Err(anyhow!(...)))。
PwCtrlEvent::Error(e) => bail!( PwCtrlEvent::Error(e) => bail!(
"PipeWire error after {} CPU frames: {e}", "PipeWire error after {} CPU frames: {e}",
stats.frames_encoded stats.frames_encoded
), ),
// 格式变化(分辨率/像素格式):本基准忽略,等下一帧自然到达。
PwCtrlEvent::FormatChanged { .. } => {} PwCtrlEvent::FormatChanged { .. } => {}
} }
} }
// recv_timeout 阻塞最多 5 秒取下一帧;类比 Go `select { case f := <-frCh: ... case <-time.After(5*time.Second): }`。
// match 直接对 Result 解构:Ok(f) 拿到帧,Err(_)(超时或断开)直接 break 结束。
let frame = match cap let frame = match cap
.frame_receiver() .frame_receiver()
.recv_timeout(std::time::Duration::from_secs(5)) .recv_timeout(std::time::Duration::from_secs(5))
@@ -700,30 +729,41 @@ fn run_cpu_pipeline(
Err(_) => break, Err(_) => break,
}; };
// 单帧起点:用于统计 total_us(包含所有子阶段)。
let frame_start = Instant::now(); let frame_start = Instant::now();
// import 阶段起点:把 DMA-BUF 帧封装为 AV_PIX_FMT_VAAPI 硬件帧(av_hwframe_map 路径)。
let t_import = Instant::now(); let t_import = Instant::now();
// match 表达式对 Result 解构并支持多分支(含 guard 与错误处理)。
let vaapi_frame = match import_frame(frames_ctx, &frame) { let vaapi_frame = match import_frame(frames_ctx, &frame) {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
// 失败计数器自增;前 3 次打印到 stderr,避免日志淹没。
stats.import_failures += 1; stats.import_failures += 1;
if stats.import_failures <= 3 { if stats.import_failures <= 3 {
eprintln!("CPU frame {}: import failed: {e}", stats.frames_encoded); eprintln!("CPU frame {}: import failed: {e}", stats.frames_encoded);
} }
// continue 跳过本帧后续步骤(不是错误退出)。
continue; continue;
} }
}; };
// elapsed() 返回 Durationas_micros() → u128`as u64` 截断到 u64(帧耗时不会超 2^64 微秒)。
let import_us = t_import.elapsed().as_micros() as u64; let import_us = t_import.elapsed().as_micros() as u64;
// transfer 阶段:用 av_hwframe_transfer_data 把硬件帧拷贝到 CPU 内存(4K BGRA)。
let t_transfer = Instant::now(); let t_transfer = Instant::now();
// SAFETY: sw_frame is allocated by FFmpeg and freed on all paths below. // SAFETY: sw_frame is allocated by FFmpeg and freed on all paths below.
let mut sw_frame = unsafe { ffi::av_frame_alloc() }; let mut sw_frame = unsafe { ffi::av_frame_alloc() };
if sw_frame.is_null() { if sw_frame.is_null() {
// av_frame_alloc 返回 NULL 表示 OOM bail! 把错误抛到 main(不是 panic)。
bail!("CPU frame {}: av_frame_alloc failed", stats.frames_encoded); bail!("CPU frame {}: av_frame_alloc failed", stats.frames_encoded);
} }
// av_hwframe_transfer_dataFFmpeg 提供的硬件→软件帧拷贝 APIsrc=VAAPIdst=CPU 内存帧。
// 第 3 参数 flags 通常传 0;返回 0 表示成功,负数表示 FFmpeg 错误码。
// SAFETY: sw_frame is an allocated destination; vaapi_frame is a valid VAAPI source frame. // SAFETY: sw_frame is an allocated destination; vaapi_frame is a valid VAAPI source frame.
let transfer_ret = let transfer_ret =
unsafe { ffi::av_hwframe_transfer_data(sw_frame, vaapi_frame.as_ptr(), 0) }; unsafe { ffi::av_hwframe_transfer_data(sw_frame, vaapi_frame.as_ptr(), 0) };
if transfer_ret < 0 { if transfer_ret < 0 {
// 错误路径必须 free,否则内存泄漏;FFmpeg C API 无 RAII。
// SAFETY: sw_frame was allocated above and has not been freed yet. // SAFETY: sw_frame was allocated above and has not been freed yet.
unsafe { ffi::av_frame_free(&mut sw_frame) }; unsafe { ffi::av_frame_free(&mut sw_frame) };
bail!( bail!(
@@ -735,11 +775,15 @@ fn run_cpu_pipeline(
} }
let transfer_us = t_transfer.elapsed().as_micros() as u64; let transfer_us = t_transfer.elapsed().as_micros() as u64;
// scale 阶段:在 CPU 上把 4K BGRA 下采样到 2K YUV420PCPU 路径的瓶颈所在)。
let t_scale = Instant::now(); let t_scale = Instant::now();
// SAFETY: sw_frame contains transferred BGRA data; encoder.yuv_frame is writable YUV420P // SAFETY: sw_frame contains transferred BGRA data; encoder.yuv_frame is writable YUV420P
// at the configured output dimensions; sws_ctx converts and downscales between them. // at the configured output dimensions; sws_ctx converts and downscales between them.
unsafe { unsafe {
// av_frame_make_writable:确保 yuv_frame 内部 buffer 可写(FFmpeg 引用计数可能共享)。
ffi::av_frame_make_writable(encoder.yuv_frame); ffi::av_frame_make_writable(encoder.yuv_frame);
// sws_scalelibswscale 主接口;参数 = (ctx, src_slices[], src_stride[], src_y_start, src_h, dst_slices[], dst_stride[])。
// `(*sw_frame).data.as_ptr() as *const *const u8` 把 C 数组首地址转裸指针(FFmpeg AVFrame.data 是 [u8*; 8])。
ffi::sws_scale( ffi::sws_scale(
sws_ctx.0, sws_ctx.0,
(*sw_frame).data.as_ptr() as *const *const u8, (*sw_frame).data.as_ptr() as *const *const u8,
@@ -751,12 +795,16 @@ fn run_cpu_pipeline(
); );
} }
let scale_us = t_scale.elapsed().as_micros() as u64; let scale_us = t_scale.elapsed().as_micros() as u64;
// 缩放完成后立即释放中间 BGRA 帧(约 4K*2160*4 = 33MB),避免峰值内存。
// SAFETY: sw_frame was allocated above and is no longer needed after scaling. // SAFETY: sw_frame was allocated above and is no longer needed after scaling.
unsafe { ffi::av_frame_free(&mut sw_frame) }; unsafe { ffi::av_frame_free(&mut sw_frame) };
// encode 阶段:把 YUV420P 帧送入 libx264/openh264 编码器;返回编码单帧耗时(微秒)。
// `?` 把 anyhow::Error 传播到调用者;`&mut encoder` & `&mut pts` 都是可变借用。
let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?; let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?;
let total_us = frame_start.elapsed().as_micros() as u64; let total_us = frame_start.elapsed().as_micros() as u64;
// 把本帧的 5 个阶段耗时 push 到 Vec<u64>;后续 print_detailed_results 计算 avg_ms/p95。
stats.import_us.push(import_us); stats.import_us.push(import_us);
stats.transfer_us.push(transfer_us); stats.transfer_us.push(transfer_us);
stats.scale_us.push(scale_us); stats.scale_us.push(scale_us);
@@ -764,6 +812,7 @@ fn run_cpu_pipeline(
stats.total_us.push(total_us); stats.total_us.push(total_us);
stats.frames_encoded += 1; stats.frames_encoded += 1;
// 节流打印:前 3 帧详打 + 之后每 30 帧打一次,避免日志淹没;`{:>4}` 右对齐 4 列宽。
if stats.frames_encoded <= 3 || stats.frames_encoded % 30 == 0 { if stats.frames_encoded <= 3 || stats.frames_encoded % 30 == 0 {
println!( println!(
" CPU frame {:>4}/{frames}: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms", " CPU frame {:>4}/{frames}: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms",
@@ -777,11 +826,24 @@ fn run_cpu_pipeline(
} }
} }
// flush 编码器(送 NULL frame 触发 EOS+ write_trailer + 关闭输出文件。
// 任何失败经 `?` 传播到 main。
finish_encoder(encoder)?; finish_encoder(encoder)?;
// as_secs_f64 把 Duration 转为秒(f64),用于后续 FPS 计算。
stats.elapsed_secs = total_start.elapsed().as_secs_f64(); stats.elapsed_secs = total_start.elapsed().as_secs_f64();
Ok(stats) Ok(stats)
} }
// GPU 流水线主函数:跑 `frames` 帧测量 GPU 路径每阶段耗时(import → filter → transfer → format → encode)。
// 与 CPU 路径的核心差异:GPU 路径用 `scale_vaapi` 滤镜**在硬件内**把 4K BGRA 下采样到 2K NV12
// 再用 `av_hwframe_transfer_data` 把**小**NV12 帧拷贝到 CPU 内存(数据量 = 4K BGRA 的 1/6),
// 最后用 `sws_scale` 做 NV12→YUV420P 的**纯格式转换**(无尺寸变化,比 CPU 路径快得多)。
//
// 性能对比的关键:
// - CPU 路径 transfer ~33MB + scale 33MB→2MBGPU 路径 transfer ~3MB + format 仅 NV12→YUV420P。
// - 因此 GPU 路径的 transfer/format 总耗时远低于 CPU 路径的 transfer+scale。
//
// 参数:9 个(比 CPU 多一个 hw_dev 用于 filter graph);同样用 `#[allow]` 抑制 clippy。
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn run_gpu_pipeline( fn run_gpu_pipeline(
cap: &CapPortal, cap: &CapPortal,
@@ -794,7 +856,11 @@ fn run_gpu_pipeline(
enc_width: u32, enc_width: u32,
enc_height: u32, enc_height: u32,
) -> Result<FrameStats> { ) -> Result<FrameStats> {
// 同 CPU 路径:构造软件编码器(最终编码阶段仍是 CPU 上的 libx264/openh264)。
// 注意:本基准目标是测 import/scale 性能,**不**测 VAAPI 硬件编码;所以两条路径都用软件编码器。
let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?; let mut encoder = create_software_encoder(Path::new(output), enc_width, enc_height)?;
// 这里命名为 format_ctx 而非 sws_ctx —— 因为它只做 NV12→YUV420P 的**色度重排**(无下采样)。
// 源/目标尺寸相同(enc_width × enc_height),sws_scale 内部走 fast path(无缩放,仅 deinterleave)。
let format_ctx = create_sws_context( let format_ctx = create_sws_context(
enc_width, enc_width,
enc_height, enc_height,
@@ -802,6 +868,8 @@ fn run_gpu_pipeline(
enc_width, enc_width,
enc_height, enc_height,
)?; )?;
// 构造 GPU 滤镜图:bufferin (hw) → scale_vaapi → bufferout (hw),详见 build_gpu_filter_graph。
// 滤镜图在 GPU 显存里完成下采样,输出仍是 VAAPI 硬件帧。
let mut graph = build_gpu_filter_graph( let mut graph = build_gpu_filter_graph(
hw_dev, frames_ctx, src_width, src_height, enc_width, enc_height, hw_dev, frames_ctx, src_width, src_height, enc_width, enc_height,
)?; )?;
@@ -822,6 +890,7 @@ fn run_gpu_pipeline(
let mut pts: i64 = 0; let mut pts: i64 = 0;
while stats.frames_encoded < frames { while stats.frames_encoded < frames {
// 同 CPU 路径(详见 run_cpu_pipeline 的同位置注释)。
if let Ok(ctrl) = cap.event_receiver().try_recv() { if let Ok(ctrl) = cap.event_receiver().try_recv() {
match ctrl { match ctrl {
PwCtrlEvent::StreamEnded => break, PwCtrlEvent::StreamEnded => break,
@@ -855,23 +924,33 @@ fn run_gpu_pipeline(
}; };
let import_us = t_import.elapsed().as_micros() as u64; let import_us = t_import.elapsed().as_micros() as u64;
// filter 阶段:把 VAAPI 4K 帧送入 scale_vaapi 滤镜图,取出 2K NV12 VAAPI 帧。
// 这是 GPU 路径相对 CPU 路径最大的性能优势所在。
let t_filter = Instant::now(); let t_filter = Instant::now();
// graph.get("in").unwrap():按 name 取滤镜图的输入 pad;unwrap 在此是安全的(图刚构造必有 "in")。
let mut filter_src_ctx = graph.get("in").unwrap(); let mut filter_src_ctx = graph.get("in").unwrap();
// source():从 pad 上下文获取发送端;后续 .add(&frame) 把帧送入图。
let mut filter_src = filter_src_ctx.source(); let mut filter_src = filter_src_ctx.source();
let mut filter_sink_ctx = graph.get("out").unwrap(); let mut filter_sink_ctx = graph.get("out").unwrap();
let mut filter_sink = filter_sink_ctx.sink(); let mut filter_sink = filter_sink_ctx.sink();
// map_err 把 ffmpeg_next::Error 转换为 anyhow::Error(保持错误链可读)。
// anyhow::anyhow! 是宏,构造 ad-hoc 错误(类比 Go fmt.Errorf)。
filter_src filter_src
.add(&vaapi_frame) .add(&vaapi_frame)
.map_err(|e| anyhow::anyhow!("GPU filter source add failed: {e}"))?; .map_err(|e| anyhow::anyhow!("GPU filter source add failed: {e}"))?;
// ff::frame::Video::empty():构造一个空视频帧(无 buffer),后续 filter_sink.frame() 填充。
let mut filtered = ff::frame::Video::empty(); let mut filtered = ff::frame::Video::empty();
// 三路 match:成功 / EAGAIN(图未就绪,需要更多输入帧)/ 真错误。
match filter_sink.frame(&mut filtered) { match filter_sink.frame(&mut filtered) {
Ok(()) => {} Ok(()) => {}
// EAGAIN 表示滤镜图内部缓冲不足,跳过本帧不报错(next iteration 继续喂下一帧)。
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => continue, Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => continue,
Err(e) => bail!("GPU filter sink get frame failed: {e}"), Err(e) => bail!("GPU filter sink get frame failed: {e}"),
} }
let filter_us = t_filter.elapsed().as_micros() as u64; let filter_us = t_filter.elapsed().as_micros() as u64;
// transfer 阶段:把 2K NV12 硬件帧拷贝到 CPU 内存(数据量 = 2K NV12 ≈ 3MB,远小于 CPU 路径 33MB)。
let t_transfer = Instant::now(); let t_transfer = Instant::now();
// SAFETY: sw_nv12 is allocated by FFmpeg and freed after format conversion. // SAFETY: sw_nv12 is allocated by FFmpeg and freed after format conversion.
let mut sw_nv12 = unsafe { ffi::av_frame_alloc() }; let mut sw_nv12 = unsafe { ffi::av_frame_alloc() };
@@ -892,6 +971,8 @@ fn run_gpu_pipeline(
} }
let transfer_us = t_transfer.elapsed().as_micros() as u64; let transfer_us = t_transfer.elapsed().as_micros() as u64;
// format 阶段:NV12 → YUV420P 纯格式转换(同尺寸无缩放)。
// NV12 与 YUV420P 的 Y plane 完全相同,只是 UV plane 排列不同(NV12 = interleavedYUV420P = planar)。
let t_format = Instant::now(); let t_format = Instant::now();
// SAFETY: sw_nv12 contains CPU-side NV12 at enc dimensions; encoder.yuv_frame is writable // SAFETY: sw_nv12 contains CPU-side NV12 at enc dimensions; encoder.yuv_frame is writable
// YUV420P at the same dimensions, so sws_scale performs only chroma deinterleave/format conversion. // YUV420P at the same dimensions, so sws_scale performs only chroma deinterleave/format conversion.
@@ -914,6 +995,8 @@ fn run_gpu_pipeline(
let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?; let encode_us = encode_yuv_frame(&mut encoder, &mut pts)?;
let total_us = frame_start.elapsed().as_micros() as u64; let total_us = frame_start.elapsed().as_micros() as u64;
// GPU 路径的 stats 包含 6 个阶段(filter + format),CPU 路径只有 5 个(scale);
// FrameStats 的字段用 Option/空 Vec 区分。
stats.import_us.push(import_us); stats.import_us.push(import_us);
stats.filter_us.push(filter_us); stats.filter_us.push(filter_us);
stats.transfer_us.push(transfer_us); stats.transfer_us.push(transfer_us);
@@ -941,6 +1024,9 @@ fn run_gpu_pipeline(
Ok(stats) Ok(stats)
} }
// 打印单条流水线的详细统计报告(捕获/编码分辨率、总时长、各阶段平均毫秒、FPS)。
// 纯展示函数:无 Result 返回值,无副作用(除 stdout),无错误路径。
// 类比 Go `func printResults(label string, stats *FrameStats, ...) { fmt.Println(...) }`。
fn print_detailed_results( fn print_detailed_results(
label: &str, label: &str,
stats: &FrameStats, stats: &FrameStats,
@@ -949,20 +1035,25 @@ fn print_detailed_results(
enc_width: u32, enc_width: u32,
enc_height: u32, enc_height: u32,
) { ) {
// println!() 无参数版本等价于 Go fmt.Println() —— 打印空行做视觉分隔。
println!(); println!();
println!("=== {label} Pipeline Results ==="); println!("=== {label} Pipeline Results ===");
println!("Capture resolution: {}x{}", src_width, src_height); println!("Capture resolution: {}x{}", src_width, src_height);
println!("Encode resolution: {}x{}", enc_width, enc_height); println!("Encode resolution: {}x{}", enc_width, enc_height);
println!("Frames encoded: {}", stats.frames_encoded); println!("Frames encoded: {}", stats.frames_encoded);
// {:.2} 保留 2 位小数;类比 Go fmt.Printf("%.2fs", v)。
println!("Total time: {:.2}s", stats.elapsed_secs); println!("Total time: {:.2}s", stats.elapsed_secs);
println!("Output: {}", stats.output_path); println!("Output: {}", stats.output_path);
if stats.import_failures > 0 { if stats.import_failures > 0 {
println!("Import failures: {}", stats.import_failures); println!("Import failures: {}", stats.import_failures);
} }
// FrameStats::avg_ms 是关联函数(不是 method),签名 `fn avg_ms(v: &[u64]) -> f64`。
// 类比 Go 顶层函数 `func avgMs(v []uint64) float64`Rust 关联函数等价于 Go 的 package-level。
println!( println!(
"import avg: {:.2} ms/frame", "import avg: {:.2} ms/frame",
FrameStats::avg_ms(&stats.import_us) FrameStats::avg_ms(&stats.import_us)
); );
// is_empty() 判断 Vec 是否为空;GPU 路径才有 filter_usCPU 路径此 Vec 永远空。
if !stats.filter_us.is_empty() { if !stats.filter_us.is_empty() {
println!( println!(
"filter avg: {:.2} ms/frame", "filter avg: {:.2} ms/frame",
@@ -973,12 +1064,14 @@ fn print_detailed_results(
"transfer avg: {:.2} ms/frame", "transfer avg: {:.2} ms/frame",
FrameStats::avg_ms(&stats.transfer_us) FrameStats::avg_ms(&stats.transfer_us)
); );
// CPU 路径才有 scale_usGPU 路径此 Vec 永远空。
if !stats.scale_us.is_empty() { if !stats.scale_us.is_empty() {
println!( println!(
"scale avg: {:.2} ms/frame", "scale avg: {:.2} ms/frame",
FrameStats::avg_ms(&stats.scale_us) FrameStats::avg_ms(&stats.scale_us)
); );
} }
// GPU 路径才有 format_usCPU 路径此 Vec 永远空。
if !stats.format_us.is_empty() { if !stats.format_us.is_empty() {
println!( println!(
"format avg: {:.2} ms/frame", "format avg: {:.2} ms/frame",
@@ -990,14 +1083,19 @@ fn print_detailed_results(
stats.codec_name, stats.codec_name,
FrameStats::avg_ms(&stats.encode_us) FrameStats::avg_ms(&stats.encode_us)
); );
// avg_total_ms / achieved_fps / theoretical_fps 都是 method&self 形式),调用语法 `stats.method()`。
println!("total avg: {:.2} ms/frame", stats.avg_total_ms()); println!("total avg: {:.2} ms/frame", stats.avg_total_ms());
println!("achieved FPS: {:.1}", stats.achieved_fps()); println!("achieved FPS: {:.1}", stats.achieved_fps());
println!("max theoretical: {:.1} FPS", stats.theoretical_fps()); println!("max theoretical: {:.1} FPS", stats.theoretical_fps());
} }
// 打印 CPU 与 GPU 流水线的对比摘要(一行 = 一条流水线),便于横向对比。
// 接收 Option<&FrameStats>:当基准只跑 CPU 或只跑 GPU 时,另一边为 None。
// 类比 Go `func printComparison(cpu, gpu *FrameStats)`Go 用 nil 表示缺失;Rust 用 Option<T> 强制处理。
fn print_comparison(cpu: Option<&FrameStats>, gpu: Option<&FrameStats>) { fn print_comparison(cpu: Option<&FrameStats>, gpu: Option<&FrameStats>) {
println!(); println!();
println!("=== Pipeline Comparison ==="); println!("=== Pipeline Comparison ===");
// if let Some(s) = cpu:模式匹配 Option;只在 Some 时打印,None 静默跳过(不需要 else)。
if let Some(s) = cpu { if let Some(s) = cpu {
println!( println!(
"CPU: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms ({:.1} FPS)", "CPU: import={:.2}ms transfer={:.2}ms scale={:.2}ms encode={:.2}ms total={:.2}ms ({:.1} FPS)",
@@ -1023,7 +1121,16 @@ fn print_comparison(cpu: Option<&FrameStats>, gpu: Option<&FrameStats>) {
} }
} }
// 二进制入口点。Rust 标准签名 `fn main() -> Result<()>`:返回 Result 时失败会用 exit code 1 + Debug 打印错误。
// 类比 Go `func main() { err := run(); if err != nil { log.Fatal(err) } }` —— Rust 用 `?` 传播更简洁。
//
// 流程概览(3 个阶段,对应 println 中的 [1/3]/[2/3]/[3/3] 标号):
// 1. 通过 XDG Portal 请求屏幕捕获权限 → 拿到 PipeWire fd → 构造 CapPortal
// 2. 等待首帧 → 测试 av_hwframe_map 导入(若失败,回退 mmap 测试后退出)
// 3. 根据 --mode 跑 CPU/GPU/Both 流水线,输出详细统计 + 对比报告
fn main() -> Result<()> { fn main() -> Result<()> {
// clap::Parser::parse() 解析 std::env::args,匹配失败的会自动 exit code 1 + 打印 help。
// 类比 Go `flag.Parse()` + cobra.Struct,但 clap 用 Derive 宏更声明式。
let bench_args = BenchArgs::parse(); let bench_args = BenchArgs::parse();
println!("=== VAAPI Import Benchmark ==="); println!("=== VAAPI Import Benchmark ===");
@@ -1036,11 +1143,15 @@ fn main() -> Result<()> {
println!("DRM device: {}", bench_args.drm_device); println!("DRM device: {}", bench_args.drm_device);
println!(); println!();
// ff::init()FFmpeg 全局初始化(注册所有编解码器/滤镜/格式)。必须在所有 FFmpeg 调用前执行一次。
// 类比 Go 的 `import _ "image/jpeg"` 副作用导入;FFmpeg 5+ 改为运行时自动注册,但 init 仍推荐。
ff::init()?; ff::init()?;
println!("[1/3] Requesting screen capture via XDG Portal..."); println!("[1/3] Requesting screen capture via XDG Portal...");
println!(" (Select a screen to share in the portal dialog)"); println!(" (Select a screen to share in the portal dialog)");
// 构造 Args(生产 CLI 类型)——本基准复用 wl-webrtc 主程序的 Args 结构以驱动 CapPortal。
// 大部分字段写死;只有 output 从 BenchArgs 透传。类比 Go `args := &wlwebrtc.Args{...}`。
let portal_args = Args { let portal_args = Args {
output: Some(bench_args.output.clone()), output: Some(bench_args.output.clone()),
output_name: None, output_name: None,
@@ -1058,16 +1169,22 @@ fn main() -> Result<()> {
stats: false, stats: false,
}; };
// CapPortal::new 启动 Portal 异步协商 + PipeWire 流;阻塞至用户在对话框点"允许"。
// 内部会启动 pipewire_thread 后台线程推帧到 frame_receiver 通道。
let cap = CapPortal::new(&portal_args)?; let cap = CapPortal::new(&portal_args)?;
println!("[1/3] Portal connected, PipeWire stream active\n"); println!("[1/3] Portal connected, PipeWire stream active\n");
println!("[2/3] Waiting for first frame from PipeWire..."); println!("[2/3] Waiting for first frame from PipeWire...");
// 阻塞等首帧(带 30s 超时,详见 receive_first_frame 实现)。
let first_frame = receive_first_frame(&cap)?; let first_frame = receive_first_frame(&cap)?;
// 把 first_frame 的字段拷贝到局部变量;后续两条流水线都要用 src_width/src_height 做下采样。
// 注意:first_frame 必须 drop 之前不能让 import_dma_buf_to_vaapi 持有 fd 引用(所有权检查)。
let src_width = first_frame.width; let src_width = first_frame.width;
let src_height = first_frame.height; let src_height = first_frame.height;
let src_format = first_frame.format; let src_format = first_frame.format;
// 0x{:08X}8 位 16 进制(大写)前补 0 —— 用于打印 DRM 四字符码(ARGB8888 = 0x34325241)。
println!( println!(
"[2/3] First frame: {}x{}, format=0x{:08X}, stride={}, modifier=0x{:X}", "[2/3] First frame: {}x{}, format=0x{:08X}, stride={}, modifier=0x{:X}",
src_width, src_height, src_format, first_frame.stride, first_frame.modifier src_width, src_height, src_format, first_frame.stride, first_frame.modifier
@@ -1079,14 +1196,19 @@ fn main() -> Result<()> {
src_format src_format
); );
// 打开 DRM render node(默认 /dev/dri/renderD128),构造 VAAPI 硬件设备上下文。
// AvHwDevCtx 内部封装 AVBufferRefFFmpeg 引用计数),Drop 时自动释放。
let drm_device = Path::new(&bench_args.drm_device); let drm_device = Path::new(&bench_args.drm_device);
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?; let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
println!(" VAAPI device context created OK"); println!(" VAAPI device context created OK");
// 构造硬件帧上下文:绑定设备 + sw_format=BGRA + 源尺寸;scale_vaapi 滤镜需要此 ctx。
let frames_ctx = let frames_ctx =
AvHwFrameCtx::for_capture(&hw_dev, src_width, src_height, ff::format::Pixel::BGRA)?; AvHwFrameCtx::for_capture(&hw_dev, src_width, src_height, ff::format::Pixel::BGRA)?;
println!(" VAAPI frames context created OK (sw_format=BGRA)"); println!(" VAAPI frames context created OK (sw_format=BGRA)");
// 首帧导入测试:unsafe 块因为 import_dma_buf_to_vaapi 是 raw FFIav_hwframe_map + AVDRMFrameDescriptor)。
// 此处 unsafe 块**未写** SAFETY 标记,因为 import_dma_buf_to_vaapi 自身在 src/avhw.rs 内部已有详尽 SAFETY 注释。
let vaapi_frame = unsafe { let vaapi_frame = unsafe {
import_dma_buf_to_vaapi( import_dma_buf_to_vaapi(
frames_ctx.as_ptr(), frames_ctx.as_ptr(),
@@ -1100,11 +1222,13 @@ fn main() -> Result<()> {
) )
}; };
// 用 match 处理 Result;分支内提前 return Ok(()) 表示"基准结束但不报错"(非失败路径)。
match &vaapi_frame { match &vaapi_frame {
Ok(_) => { Ok(_) => {
println!(" Result: SUCCESS — av_hwframe_map imported DMA-BUF to VAAPI surface!"); println!(" Result: SUCCESS — av_hwframe_map imported DMA-BUF to VAAPI surface!");
} }
Err(e) => { Err(e) => {
// 失败路径:诊断 + mmap 对照测试 + 友好退出(不返回 Err)。
println!(" Result: FAILED"); println!(" Result: FAILED");
println!(" Error: {e}"); println!(" Error: {e}");
println!(); println!();
@@ -1115,8 +1239,10 @@ fn main() -> Result<()> {
println!(); println!();
println!(" Falling back to mmap readback test for comparison..."); println!(" Falling back to mmap readback test for comparison...");
// mmap 对照:如果 av_hwframe_map 失败,看 mmap 是否也失败(区分根因:driver vs 配置)。
let mmap_size = (first_frame.stride as usize) * (first_frame.height as usize); let mmap_size = (first_frame.stride as usize) * (first_frame.height as usize);
let mmap_start = Instant::now(); let mmap_start = Instant::now();
// unsafelibc::mmap 是 POSIX FFI,返回 void*MAP_FAILED (== -1) 表示失败。
let mmap_ptr = unsafe { let mmap_ptr = unsafe {
libc::mmap( libc::mmap(
ptr::null_mut(), ptr::null_mut(),
@@ -1130,6 +1256,7 @@ fn main() -> Result<()> {
let mmap_elapsed = mmap_start.elapsed(); let mmap_elapsed = mmap_start.elapsed();
if mmap_ptr == libc::MAP_FAILED { if mmap_ptr == libc::MAP_FAILED {
// last_os_error():取 errno;类比 Go syscall.Errno。
let errno = std::io::Error::last_os_error(); let errno = std::io::Error::last_os_error();
println!(" mmap also FAILED: {errno}"); println!(" mmap also FAILED: {errno}");
} else { } else {
@@ -1138,6 +1265,7 @@ fn main() -> Result<()> {
mmap_size as f64 / 1024.0 / 1024.0, mmap_size as f64 / 1024.0 / 1024.0,
mmap_elapsed.as_secs_f64() * 1000.0 mmap_elapsed.as_secs_f64() * 1000.0
); );
// 必须配对 munmap,否则内核 VMA 泄漏。
unsafe { unsafe {
libc::munmap(mmap_ptr, mmap_size); libc::munmap(mmap_ptr, mmap_size);
} }
@@ -1146,10 +1274,13 @@ fn main() -> Result<()> {
println!(); println!();
println!("=== Benchmark ended: av_hwframe_map import FAILED ==="); println!("=== Benchmark ended: av_hwframe_map import FAILED ===");
println!("Fix the import issue before proceeding to GPU downscale tests."); println!("Fix the import issue before proceeding to GPU downscale tests.");
// 主动 Ok(()):基准本身没崩,只是诊断后退出;让 CI 不报红。
return Ok(()); return Ok(());
} }
} }
// 导入成功后释放首帧资源(vaapi_frame 持有硬件帧引用,first_frame 持有 fd);
// 后续主循环每帧重新 import,避免长持有造成硬件帧饥饿。
drop(vaapi_frame); drop(vaapi_frame);
drop(first_frame); drop(first_frame);
@@ -1157,12 +1288,19 @@ fn main() -> Result<()> {
let enc_width = bench_args.enc_width; let enc_width = bench_args.enc_width;
let enc_height = bench_args.enc_height; let enc_height = bench_args.enc_height;
// PipelineMode::Both 时输出文件名加 cpu/gpu 后缀(详见 output_for_mode)。
let split_outputs = bench_args.mode == PipelineMode::Both; let split_outputs = bench_args.mode == PipelineMode::Both;
// 用 Option 包裹:mode 只跑 CPU 时 gpu_stats 永远 Noneprint_detailed_results/print_comparison 接 Option。
let mut cpu_stats = None; let mut cpu_stats = None;
let mut gpu_stats = None; let mut gpu_stats = None;
// matches! 宏:等价于 `match bench_args.mode { PipelineMode::Cpu | PipelineMode::Both => true, _ => false }`
// 但语法更紧凑(无臂返回值);类比 Go `switch mode { case Cpu, Both: ... }`。
if matches!(bench_args.mode, PipelineMode::Cpu | PipelineMode::Both) { if matches!(bench_args.mode, PipelineMode::Cpu | PipelineMode::Both) {
let output = output_for_mode(&bench_args.output, PipelineMode::Cpu, split_outputs); let output = output_for_mode(&bench_args.output, PipelineMode::Cpu, split_outputs);
// Some(...) 把 Result<FrameStats> 包成 Option<Result<FrameStats>>,再 ? 解开 Result;最终 cpu_stats = Option<FrameStats>。
// 注意 `?` 在 Option 上下文也工作(需要 main 返回 Option,但这里 main 返回 Result,所以 ? 只对 Result 起作用,
// Some(...) 是显式包装,里面的 run_cpu_pipeline()? 把 Err 传到 main)。
cpu_stats = Some(run_cpu_pipeline( cpu_stats = Some(run_cpu_pipeline(
&cap, &cap,
&frames_ctx, &frames_ctx,
@@ -1190,6 +1328,7 @@ fn main() -> Result<()> {
)?); )?);
} }
// as_ref():把 &Option<T> 借用(避免消耗 T);print_detailed_results 接收 &FrameStats。
if let Some(stats) = cpu_stats.as_ref() { if let Some(stats) = cpu_stats.as_ref() {
print_detailed_results("CPU", stats, src_width, src_height, enc_width, enc_height); print_detailed_results("CPU", stats, src_width, src_height, enc_width, enc_height);
} }
@@ -1198,6 +1337,9 @@ fn main() -> Result<()> {
} }
print_comparison(cpu_stats.as_ref(), gpu_stats.as_ref()); print_comparison(cpu_stats.as_ref(), gpu_stats.as_ref());
// 迭代器链:把两个 Option 串成统一迭代器,.any() 短路检查是否有任何一条流水线低于 30 FPS。
// Option::into_iter():把 Option<T> 转为 0/1 元素迭代器;chain 把两段接起来。
// 类比 Go`var all []*FrameStats; if cpu != nil { all = append(all, cpu) }; for _, s := range all { if s.FPS < 30 {...} }`。
if cpu_stats if cpu_stats
.as_ref() .as_ref()
.into_iter() .into_iter()
+1 -1
View File
@@ -25,7 +25,7 @@
//! //!
//! - T7a(本注释块)覆盖文件头、类型定义、`State<S>` 方法;T7b 覆盖各 `Dispatch` trait 实现; //! - T7a(本注释块)覆盖文件头、类型定义、`State<S>` 方法;T7b 覆盖各 `Dispatch` trait 实现;
//! T7c 覆盖帧捕获相关的 `Dispatch` 与 `ZwlrScreencopyFrameV1` 处理。 //! T7c 覆盖帧捕获相关的 `Dispatch` 与 `ZwlrScreencopyFrameV1` 处理。
//! - 大量 `unsafe` 块调用 FFmpeg / `libc::dup` / DMA-BUF FFI;现有英文 `// SAFETY:` 注释务必保留。 //! - 大量 `unsafe` 块调用 FFmpeg / `libc::dup` / DMA-BUF FFI;现有英文 SAFETY 标记务必保留。
//! - `edition = "2021"`(非 2024 默认值);请勿改动任何代码字符,只新增中文注释。 //! - `edition = "2021"`(非 2024 默认值);请勿改动任何代码字符,只新增中文注释。
// std 标准库导入:HashMap(输出列表)、memtake/replace)、AsFd/OwnedFd/FromRawFdDMA-BUF fd 桥接)、 // std 标准库导入:HashMap(输出列表)、memtake/replace)、AsFd/OwnedFd/FromRawFdDMA-BUF fd 桥接)、
+2 -2
View File
@@ -24,8 +24,8 @@
//! //!
//! - T9a(本块)覆盖文件头 + struct 定义 + `impl StatePortal`(至 `fn encode_thread_loop` 之前); //! - T9a(本块)覆盖文件头 + struct 定义 + `impl StatePortal`(至 `fn encode_thread_loop` 之前);
//! T9b 覆盖 `encode_thread_loop` / `webrtc_thread_loop` / `resolve_drm_device` 等自由函数。 //! T9b 覆盖 `encode_thread_loop` / `webrtc_thread_loop` / `resolve_drm_device` 等自由函数。
//! - 多处 `unsafe` 调用 FFmpeg/VAAPI FFI;现有英文 `// SAFETY:` 保留不动, //! - 多处 `unsafe` 调用 FFmpeg/VAAPI FFI;现有英文 SAFETY 标记保留不动,
//! 本任务在每个 unsafe 块上方加普通 `//` 中文概述(不新增 `// SAFETY:`)。 //! 本任务在每个 unsafe 块上方加普通 `//` 中文概述(不新增 SAFETY 标记)。
// 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码 // 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
+265
View File
@@ -1,3 +1,25 @@
//! 管道性能统计模块 —— 用于卡顿诊断的轻量级滑动窗口统计。
//!
//! 本模块跟踪 capture / encode / send 三段流水线的每秒指标快照:
//! - **FPS**:捕获帧率、编码帧率、发送帧率
//! - **延迟分布**:各阶段(DMA-BUF import / VAAPI scale / GPU→CPU transfer /
//! sws_scale / H.264 encode)的 avg / p95 / max(微秒→毫秒)
//! - **队列深度**capture 队列与 encoded 队列的瞬时观测值
//! - **丢帧计数**PipeWire 丢弃、重复帧去重跳过、超预算帧
//!
//! 设计目标为低开销:仅收集计数器和时间样本,每秒输出一行结构化日志
//! (仅在 `--stats` 启用时)。所有统计在主线程独占持有 `&mut PipelineStats`
//! 跨线程数据(如 PipeWire dropped 计数、encode 线程的 duplicate 计数)
//! 通过外部 `AtomicU64` 在调用方读取后再传入本结构(见 `set_*` 系列)。
//!
//! ## 与 Go 类比
//!
//! - [`Instant::now()`] ≈ Go `time.Now()`,但精度更高(通常单调时钟)
//! - [`Duration::as_secs_f64`] ≈ Go `time.Duration.Seconds()`,但保留 f64
//! - `&mut self` ≈ Go 中显式持有 `sync.Mutex` 的写锁;本模块的字段独占模型
//! 天然无需 `Mutex`(外部跨线程读取后再以 `&mut self` 传入)
//! - `Vec<f64>` 样本缓冲 ≈ Go 中 `[]float64`,每窗口 `clear()` 复用容量
// stats.rs — Lightweight windowed pipeline statistics for stutter diagnosis // stats.rs — Lightweight windowed pipeline statistics for stutter diagnosis
// //
// Tracks per-second snapshots of capture/encode/send pipeline metrics. // Tracks per-second snapshots of capture/encode/send pipeline metrics.
@@ -6,6 +28,15 @@
use std::time::Instant; use std::time::Instant;
/// 单帧流水线各阶段的耗时样本(中文概要,详细说明见下方英文文档)。
///
/// # 派生宏说明
///
/// - `#[derive(Debug)]`:便于 `dbg!()` 调试输出,类比 Go 的 `%+v` 格式化
/// - `#[derive(Default)]`:所有字段为 `u64`/`usize` 零值时构造默认实例,
/// 测试中可用 `FrameTimings { total_us: 5000, ..Default::default() }`
/// 仅指定关注字段(见 `record_and_snapshot_counts` 测试)
///
/// Per-stage timing for a single encode pipeline frame. /// Per-stage timing for a single encode pipeline frame.
/// ///
/// All values are in microseconds. The caller records timestamps around /// All values are in microseconds. The caller records timestamps around
@@ -28,6 +59,27 @@ pub struct FrameTimings {
pub output_bytes: usize, pub output_bytes: usize,
} }
/// 一秒窗口内的管道统计聚合器(中文概要,详细说明见下方英文文档)。
///
/// 本结构通过 `&mut self` 接口收集三类原始数据:计数器(`capture_frames` 等)、
/// 样本缓冲(`Vec<f64>`/`Vec<u64>`,窗口结束时 `clear()` 复用容量)、
/// 时间锚点(`Option<Instant>``None` 表示尚未观测过)。
///
/// # 所有权与并发模型
///
/// - 本结构**非 `Sync`**:`Vec` 字段无锁,跨线程读写需外部同步
/// - 主线程独占持有 `&mut self`;跨线程数据通过外部 `AtomicU64` 在调用方
/// 读取后以 `set_*` 接口注入
/// - 这与 Go 中 `sync.Mutex<PipelineStats>` 不同:Rust 借用检查器在编译期
/// 保证单一可变借用,无需运行时锁
///
/// # 与 Go 类比
///
/// - `Option<Instant>` ≈ Go `*time.Time``nil` 表示未设置),但 Rust 用枚举
/// 强制调用方处理"未设置"分支,避免 nil-pointer panic
/// - `Instant` 内部使用单调时钟,不受系统时间跳变影响;Go 1.9+ 的
/// `time.Since()` 也使用单调时钟,行为一致
///
/// Windowed statistics aggregator for the encode/send pipeline. /// Windowed statistics aggregator for the encode/send pipeline.
/// ///
/// Collects counters and timing samples within a one-second window, /// Collects counters and timing samples within a one-second window,
@@ -74,6 +126,11 @@ pub struct PipelineStats {
} }
impl PipelineStats { impl PipelineStats {
/// 构造一个空的统计聚合器(类比 Go 的 `NewXxx()` 工厂函数)。
///
/// `window_start` 初始化为当前时刻,确保 `should_snapshot()` 至少
/// 在 1 秒后才返回 true(首窗口可能短于 1 秒有效数据,但 elapsed_secs
/// 是真实窗口长度,FPS 计算依然准确)。
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
capture_frames: 0, capture_frames: 0,
@@ -104,6 +161,21 @@ impl PipelineStats {
} }
} }
/// 记录一次来自 PipeWire 的捕获帧到达事件(中文 L3 解析见此)。
///
/// # 时间间隔(gap)计算
///
/// - [`Instant::now()`]:获取当前单调时刻(≈ Go `time.Now()`,但精度更高)
/// - `last.elapsed()`:返回 `Duration`,类比 Go `time.Since(last)`
/// - [`Duration::as_secs_f64`]:将 `Duration` 转为秒(f64),类比 Go
/// `dur.Seconds()`;此处乘以 1000.0 转毫秒,便于日志可读
///
/// # 首帧处理
///
/// `Option<Instant>::None` 表示窗口内首帧,没有"上一帧"参照点,
/// 因此首帧不产生 gap 样本(这与 Go 中 `*time.Time == nil` 检查等价,
/// 但 Rust 强制处理 None 分支,编译期避免 nil 解引用)。
///
/// Record that a capture frame was received from PipeWire. /// Record that a capture frame was received from PipeWire.
pub fn record_capture(&mut self) { pub fn record_capture(&mut self) {
let now = Instant::now(); let now = Instant::now();
@@ -115,6 +187,14 @@ impl PipelineStats {
self.capture_frames += 1; self.capture_frames += 1;
} }
/// 记录一帧完成编码(`FrameTimings` 路径,含各阶段微秒样本)。
///
/// # 参数借用
///
/// `timings: &FrameTimings`:以共享借用(`&`)读取,不获取所有权。
/// 类比 Go 中显式传递 `*FrameTimings` 指针;Rust 借用检查保证本调用
/// 期间原 `timings` 不会被释放。其余 gap 计算同 `record_capture`。
///
/// Record that a frame completed encoding with the given timings. /// Record that a frame completed encoding with the given timings.
pub fn record_encode(&mut self, timings: &FrameTimings) { pub fn record_encode(&mut self, timings: &FrameTimings) {
let now = Instant::now(); let now = Instant::now();
@@ -134,10 +214,19 @@ impl PipelineStats {
self.output_bytes.push(timings.output_bytes); self.output_bytes.push(timings.output_bytes);
} }
/// 仅记录 import 阶段微秒数(用于 `record_encode_thread` 路径补齐 import 样本)。
pub fn record_import(&mut self, import_us: u64) { pub fn record_import(&mut self, import_us: u64) {
self.import_us.push(import_us); self.import_us.push(import_us);
} }
/// 编码线程路径:散参传入 sws / encode / output_bytes,跳过 `FrameTimings`。
///
/// # 溢出保护
///
/// `saturating_add` 在 `u64::MAX` 处饱和而非回绕,避免极端情况下
/// `total_us` 出现荒谬的小值。类比 Go 中需手动 `if total > MaxUint64 - x`
/// 检查;Rust 的 `saturating_*` / `checked_*` / `wrapping_*` 三件套
/// 让溢出策略在调用点显式表达。
pub fn record_encode_thread(&mut self, sws_us: u64, encode_us: u64, output_bytes: usize) { pub fn record_encode_thread(&mut self, sws_us: u64, encode_us: u64, output_bytes: usize) {
let now = Instant::now(); let now = Instant::now();
if let Some(last) = self.last_encode_time { if let Some(last) = self.last_encode_time {
@@ -153,6 +242,19 @@ impl PipelineStats {
self.output_bytes.push(output_bytes); self.output_bytes.push(output_bytes);
} }
/// 记录一帧通过 WebRTC 发送(中文 L3 解析见此)。
///
/// - `wait_ms`:阻塞等待发送通道可写入的时间(毫秒);为 0 时不入样本
/// (避免拉低 p95,因为大多数帧应无等待)
/// - `capture_time`:该帧的原始捕获时刻;用于计算 **frame age**
/// (捕获→发送端到端延迟)。`Option<None>` 表示调用方未提供
/// (例如 XDG/screen-copy 路径无原始时间戳),此时不入样本
///
/// # frame_age 计算
///
/// `ct.elapsed()` 返回 `Duration`,类同 `record_capture` 中 gap 计算,
/// 但锚点是"捕获时刻"而非"上一帧发送时刻",因此测量的是端到端延迟。
///
/// Record that a frame was sent via WebRTC. /// Record that a frame was sent via WebRTC.
/// `wait_ms` is time spent blocked waiting to send into the channel. /// `wait_ms` is time spent blocked waiting to send into the channel.
/// `capture_time` is when the frame was originally captured (for frame age). /// `capture_time` is when the frame was originally captured (for frame age).
@@ -174,6 +276,18 @@ impl PipelineStats {
} }
} }
/// 从后台 WebRTC 发送线程记录一帧(gap_ms / age_ms 均已在调用方预算好)。
///
/// # 为何预算参数
///
/// 后台线程无法安全访问 `&mut self`(本结构非 `Sync`),因此调用方在
/// 发送时刻直接计算 `gap_ms` / `age_ms``Instant::now()` 在该线程
/// 局部调用),稍后批量 drain 到主线程的 `&mut self`。这样:
/// - 单调时钟读取在事件发生线程完成,时间戳精确
/// - 主线程仅做 `Vec::push`,无需锁
///
/// `gap_ms == 0.0` 表示首帧(无前一帧参照),不入样本。
///
/// Record a frame sent from a background WebRTC thread. /// Record a frame sent from a background WebRTC thread.
/// `gap_ms` is the pre-computed time since the previous send (0.0 = first frame). /// `gap_ms` is the pre-computed time since the previous send (0.0 = first frame).
/// `age_ms` is the pre-computed capture-to-send latency (None if unavailable). /// `age_ms` is the pre-computed capture-to-send latency (None if unavailable).
@@ -189,11 +303,32 @@ impl PipelineStats {
} }
} }
/// 设置 PipeWire dropped 计数(绝对值,由调用方从外部 `AtomicU64` 读取)。
///
/// # 增量计算
///
/// 外部 `AtomicU64` 累计**总会话**的 dropped 帧数(从不重置),
/// 因此本函数计算 `total - prev` 得到本窗口内的增量。
/// `saturating_sub` 防止极端竞态(如原子读顺序不一致)导致负数回绕。
///
/// # 与 Go 类比
///
/// - 调用方代码 ≈ Go `atomic.LoadUint64(&pw.dropped)``Ordering::SeqCst`
/// 或 `Relaxed` 取决于是否需要与其他原子操作建立 happens-before
/// - `Mutex<HashMap>` 在本模块**未使用**:统计字段集固定,无需 Go
/// `sync.Map` 那样的动态键值存储;跨线程仅通过原子计数器通信
///
/// Update PipeWire dropped counter (absolute value from AtomicU64). /// Update PipeWire dropped counter (absolute value from AtomicU64).
pub fn set_pipewire_dropped(&mut self, total_dropped: u64, prev_dropped: u64) { pub fn set_pipewire_dropped(&mut self, total_dropped: u64, prev_dropped: u64) {
self.pipewire_dropped = total_dropped.saturating_sub(prev_dropped); self.pipewire_dropped = total_dropped.saturating_sub(prev_dropped);
} }
/// 设置 duplicate frames skipped 计数(绝对值,由调用方从 encode 线程原子读取)。
///
/// 与 `set_pipewire_dropped` 增量算法一致,但**保留 `prev`** 在
/// `self.prev_duplicate_frames_skipped` 字段中(因为本结构才是状态持有者,
/// 调用方仅传入当前 total)。
///
/// Update duplicate frames skipped counter (absolute value from atomic). /// Update duplicate frames skipped counter (absolute value from atomic).
/// Computes delta from previous value, like set_pipewire_dropped. /// Computes delta from previous value, like set_pipewire_dropped.
pub fn set_duplicate_frames_skipped(&mut self, total_skipped: u64) { pub fn set_duplicate_frames_skipped(&mut self, total_skipped: u64) {
@@ -201,23 +336,49 @@ impl PipelineStats {
self.prev_duplicate_frames_skipped = total_skipped; self.prev_duplicate_frames_skipped = total_skipped;
} }
/// 更新队列深度瞬时观测值(capture 队列与 encoded 队列各一个值)。
///
/// 队列深度为快照值而非累计值,每窗口只保留最后一次观测。
///
/// Update queue depth observations. /// Update queue depth observations.
pub fn set_queue_depths(&mut self, capture: usize, encoded: usize) { pub fn set_queue_depths(&mut self, capture: usize, encoded: usize) {
self.capture_queue_depth = capture; self.capture_queue_depth = capture;
self.encoded_queue_depth = encoded; self.encoded_queue_depth = encoded;
} }
/// 记录一帧超出预算(用于跟踪编码耗时超过 1/fps 的频次)。
///
/// Record that a frame exceeded its time budget. /// Record that a frame exceeded its time budget.
pub fn record_over_budget(&mut self) { pub fn record_over_budget(&mut self) {
self.over_budget_count += 1; self.over_budget_count += 1;
} }
/// 判断是否到达快照点(距离上次 `snapshot_and_reset` 或构造时刻 ≥ 1 秒)。
///
/// 仅需 `&self`(共享借用):本方法不修改任何字段,借用检查器允许
/// 多个 `&self` 共存或与 `&mut self` 之外的调用并存。
///
/// Returns true if at least 1 second has elapsed since the last snapshot /// Returns true if at least 1 second has elapsed since the last snapshot
/// (or since creation). If true, call `snapshot_and_reset` to get the stats. /// (or since creation). If true, call `snapshot_and_reset` to get the stats.
pub fn should_snapshot(&self) -> bool { pub fn should_snapshot(&self) -> bool {
self.window_start.elapsed().as_secs() >= 1 self.window_start.elapsed().as_secs() >= 1
} }
/// 计算当前窗口的统计快照并重置所有计数器与样本缓冲。
///
/// # 重置策略
///
/// - 计数器:置零
/// - `Vec`:调用 `clear()`(保留已分配容量 `Vec::capacity()`,避免下个窗口
/// 反复分配)。类比 Go 中 `s = s[:0]` 复用底层数组
/// - `window_start = Instant::now()`:重置窗口起点
///
/// # 返回值
///
/// 返回 `StatsSnapshot` 值(拷贝语义,调用方可自由使用与丢弃)。
/// 类比 Go 中返回值结构体的拷贝;Rust 中 `StatsSnapshot` 全部字段为
/// `Copy` 或 `Vec`(移动语义),返回时所有权转移至调用方。
///
/// Compute a snapshot of the current window and reset all counters. /// Compute a snapshot of the current window and reset all counters.
pub fn snapshot_and_reset(&mut self) -> StatsSnapshot { pub fn snapshot_and_reset(&mut self) -> StatsSnapshot {
let elapsed = self.window_start.elapsed().as_secs_f64(); let elapsed = self.window_start.elapsed().as_secs_f64();
@@ -291,6 +452,17 @@ impl PipelineStats {
} }
} }
/// 一秒窗口的管道统计快照(不可变值对象,由 `snapshot_and_reset` 返回)。
///
/// 本结构持有所有派生指标(FPS、avg/p95/max、计数器快照),是日志输出的
/// 数据源。一旦创建即不可变(所有字段为 `f64`/`u64`/`usize`,天然 `Copy`),
/// 调用方可以安全地打印、记录或丢弃。
///
/// # `#[derive(Debug)]` 用途
///
/// 调试场景下可直接 `dbg!(&snap)` 或 `tracing::debug!(?snap)`
/// 类比 Go 的 `spew.Dump(snap)` / `fmt.Printf("%+v", snap)`。
///
/// A one-second snapshot of pipeline statistics. /// A one-second snapshot of pipeline statistics.
#[derive(Debug)] #[derive(Debug)]
pub struct StatsSnapshot { pub struct StatsSnapshot {
@@ -344,6 +516,40 @@ pub struct StatsSnapshot {
pub output_frame_bytes_max: usize, pub output_frame_bytes_max: usize,
} }
/// 单行结构化日志格式化器(中文 L3 解析见此,英文说明保留在下方)。
///
/// # trait 与签名说明
///
/// - `impl std::fmt::Display for StatsSnapshot`:为本类型实现标准库 trait,
/// 使得 `format!("{snap}")` / `println!("{}", snap)` / `tracing::info!("{}", snap)`
/// 均可工作(隐式调用 `fmt` 方法)
/// - `fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result`
/// - `&self` 共享借用,格式化不改自身
/// - `Formatter<'_>`:匿名生命周期(`'_`),表示该借用与调用方持有的
/// `String`/输出流绑定;类比 Go 的 `io.Writer` 参数
/// - `std::fmt::Result``Result<(), std::fmt::Error>`,专用于格式化 trait
/// (比通用 `Result<T, E>` 更窄,避免 `?` 跨类型传播)
///
/// # `write!` 宏 vs `format!` 宏
///
/// - [`write!`]:直接写入 `Formatter`(零分配),类比 Go `fmt.Fprintf(w, ...)`
/// - [`format!`]:分配新 `String` 后返回,类比 Go `fmt.Sprintf(...)`
/// - 本实现选 `write!`:写入日志流时避免多余分配
///
/// # `?` 运算符
///
/// `write!(...)?` 中的 `?` 是早期返回:若 `write!` 返回 `Err(fmt::Error)`
/// 则立即从 `fmt` 返回该错误。类比 Go 中
/// `if _, err := w.Write(...); err != nil { return err }`
/// 但 Rust 的 `?` 让快乐路径线性化。
///
/// # 格式说明
///
/// - `{:.1}`:保留 1 位小数
/// - `{:.0}`:整数显示(无小数点)
/// - `{}`:默认 `Display` 格式(整数原样)
/// - 行尾反斜杠 `\` 跨行延续字符串字面量,类比 Python 隐式行连接;
/// 输出时不会引入额外换行或空格
impl std::fmt::Display for StatsSnapshot { impl std::fmt::Display for StatsSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(
@@ -392,6 +598,13 @@ impl std::fmt::Display for StatsSnapshot {
// Statistics helpers // Statistics helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// 计算 `f64` 切片平均值(空切片返回 0.0)。
///
/// # 切片借用
///
/// `data: &[f64]`:共享借用切片(fat pointer = 指针 + 长度),类比 Go 中
/// `func avg(data []float64)`。`&` 表示本函数不获取所有权,调用后原 `Vec`
/// 仍可用。
fn avg_f64(data: &[f64]) -> f64 { fn avg_f64(data: &[f64]) -> f64 {
if data.is_empty() { if data.is_empty() {
return 0.0; return 0.0;
@@ -399,6 +612,21 @@ fn avg_f64(data: &[f64]) -> f64 {
data.iter().sum::<f64>() / data.len() as f64 data.iter().sum::<f64>() / data.len() as f64
} }
/// 计算 p95(第 95 百分位),类比 Go 中需要手动 sort + index。
///
/// # 算法
///
/// 1. 复制输入到新 `Vec`(不修改调用方原数据):`data.to_vec()` 类比 Go
/// `append([]T{}, data...)`
/// 2. 排序:`sort_by` + `partial_cmp` —— `f64` 没有全序(NaN 特殊),
/// 不能直接用 `sort()``partial_cmp(b).unwrap_or(Equal)` 在 NaN 时
/// 降级为相等,避免 panic
/// 3. 计算 idx = `floor(len * 0.95)``idx.min(len-1)` 防越界
///
/// # 为何不用 `sort_unstable`
///
/// `f64` 的 `Ord` 未实现(NaN 不等于自身),故只能用 `sort_by` + 比较
/// 函数;`u64`/`usize` 实现 `Ord`,可用 `sort_unstable`(更快、内存友好)。
fn p95_f64(data: &[f64]) -> f64 { fn p95_f64(data: &[f64]) -> f64 {
if data.is_empty() { if data.is_empty() {
return 0.0; return 0.0;
@@ -409,10 +637,22 @@ fn p95_f64(data: &[f64]) -> f64 {
sorted[idx.min(sorted.len() - 1)] sorted[idx.min(sorted.len() - 1)]
} }
/// 返回切片最大值,空切片返回 0.0(业务上"无样本"等价于"无延迟")。
///
/// # `fold` + `f64::max`
///
/// `fold(0.0, f64::max)`:从初始值 0.0 开始,逐元素取较大值。
/// 类比 Go
/// ```go
/// m := 0.0
/// for _, v := range data { m = math.Max(m, v) }
/// ```
/// 注意:若样本全为负,0.0 仍是结果(业务上 latency 非负,不会出现)。
fn max_f64(data: &[f64]) -> f64 { fn max_f64(data: &[f64]) -> f64 {
data.iter().copied().fold(0.0_f64, f64::max) data.iter().copied().fold(0.0_f64, f64::max)
} }
/// 计算 `u64` 微秒样本的平均值并转毫秒(÷1000)。
fn avg_ms(data: &[u64]) -> f64 { fn avg_ms(data: &[u64]) -> f64 {
if data.is_empty() { if data.is_empty() {
return 0.0; return 0.0;
@@ -420,6 +660,10 @@ fn avg_ms(data: &[u64]) -> f64 {
data.iter().sum::<u64>() as f64 / data.len() as f64 / 1000.0 data.iter().sum::<u64>() as f64 / data.len() as f64 / 1000.0
} }
/// 计算 `u64` 微秒样本的 p95 并转毫秒。
///
/// 与 `p95_f64` 算法相同,但 `u64` 实现 `Ord`,可用 `sort_unstable`
/// (无内存开销、更快;稳定性对本场景无关,因为只取索引位置)。
fn p95_ms(data: &[u64]) -> f64 { fn p95_ms(data: &[u64]) -> f64 {
if data.is_empty() { if data.is_empty() {
return 0.0; return 0.0;
@@ -430,10 +674,15 @@ fn p95_ms(data: &[u64]) -> f64 {
sorted[idx.min(sorted.len() - 1)] as f64 / 1000.0 sorted[idx.min(sorted.len() - 1)] as f64 / 1000.0
} }
/// 对 `usize` 切片求和(用于累计 output_bytes 总量)。
///
/// `data.iter().sum()` 由标准库自动推导类型(`usize`),等价于
/// Go 中 `var total uint; for _, v := range data { total += v }`。
fn sum_usize(data: &[usize]) -> usize { fn sum_usize(data: &[usize]) -> usize {
data.iter().sum() data.iter().sum()
} }
/// 计算 `usize` 样本的 p95(字节大小分布),不转换单位。
fn p95_usize(data: &[usize]) -> usize { fn p95_usize(data: &[usize]) -> usize {
if data.is_empty() { if data.is_empty() {
return 0; return 0;
@@ -444,10 +693,26 @@ fn p95_usize(data: &[usize]) -> usize {
sorted[idx.min(sorted.len() - 1)] sorted[idx.min(sorted.len() - 1)]
} }
/// 返回 `usize` 切片最大值,空切片返回 0。
///
/// `iter().copied().max()` 返回 `Option<usize>`(空时为 `None`),
/// `unwrap_or(0)` 提供默认值,类比 Go 中显式 `if len(data) == 0 { return 0 }`。
fn max_usize(data: &[usize]) -> usize { fn max_usize(data: &[usize]) -> usize {
data.iter().copied().max().unwrap_or(0) data.iter().copied().max().unwrap_or(0)
} }
/// 单元测试模块(仅在 `#[cfg(test)]` 时编译)。
///
/// # Rust 测试模式
///
/// - `#[cfg(test)]`:条件编译属性,`cargo test` 时才编译本模块,
/// 正常 `cargo build` 不包含本模块代码(类比 Go 中 `_test.go` 后缀
/// 仅在 `go test` 时段编译,但 Rust 用显式属性而非文件名约定)
/// - `use super::*`:导入父模块(本文件)所有 `pub` 与私密 item
/// 类比 Go test 文件无需 import 即可访问同包符号
/// - `#[test]`:标记测试函数;`cargo test` 自动发现并执行
/// - `assert_eq!` / `assert!`:宏(不是函数),失败时打印表达式原文
/// 便于调试,类比 Go 中 `t.Errorf` 但更早终止当前测试
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+277
View File
@@ -1,3 +1,47 @@
//! # WebRTC 传输模块 — str0m Sans-IO 信令服务器与媒体出口
//!
//! ## 模块定位
//! 将 H.264 编码帧通过 WebRTC 推送到浏览器(替代文件输出)。仅在 `--port > 0` 时启用;
//! `--port 0`(默认)走纯文件输出路径,本模块不会被实例化(见 `main.rs` 入口判断)。
//!
//! ## str0m 是 Sans-IO WebRTC 库
//! 类比 Go 的 `net/http`,但 Sans-IO 哲学不同:
//! - **没有 background goroutine**str0m 不创建任何线程,所有进度都靠外部 poll 推动
//! - **手动驱动 3 步循环**(见 `poll_and_feed`/`feed_network`/`poll_rtc`):
//! 1. 读 UDP 包 → `Rtc::handle_input(Input::Receive(...))` 喂给 str0m
//! 2. 调 `Rtc::poll_output()` 拿 `Output::Transmit` 包 → 写回 UDP socket
//! 3. 定时喂 `Input::Timeout(Instant::now())` 推动内部时钟
//! - **同步而非 async**str0m 不是 async/await 库(与 `tokio::net::TcpListener` 等
//! 异步运行时无关);本文件用 `std::net::TcpListener` + `UdpSocket`(手动
//! `set_nonblocking(true)`),完全同步代码;上层 `main.rs` 在 mio 事件循环里
//! 周期性调 `poll_and_feed()` 推动 RTC 状态机
//! - **Go 等价物**`github.com/pion/webrtc`Go 主流 WebRTC 库)也是同步 + 手动驱动,
//! 但 str0m 把 Sans-IO 推得更彻底——连 UDP socket 都不持有,所有 I/O 都由调用方管理
//!
//! ## 内嵌 HTTP 信令服务器
//! 本模块自带一个极简 HTTP 服务器(`std::net::TcpListener`,非 tokio/axum),3 个端点:
//! - `GET /` → 返回 `HTML_PAGE`(自带 SDP 协商 + `<video>` 播放 + 实时 stats 的测试页)
//! - `POST /sdp`Content-Type: application/json)→ 接收浏览器 `RTCPeerConnection`
//! localDescriptionOffer SDP),交给 `Rtc::sdp_api().accept_offer()` 生成 Answer
//! 返回 JSON body 给浏览器 `setRemoteDescription`
//! - `GET /sdp`(无 JSON Content-Type)→ 与 `GET /` 同(兼容旧路径)
//!
//! ICE candidate 通过 SDP offer/answer 完成:浏览器等 `iceGatheringState == 'complete'`
//! 才 POST(见 `HTML_PAGE` 的 `onicegatheringstatechange`),所以 candidate 已全在
//! SDP 里,本服务端无需单独的 ICE endpointtrickle ICE 关闭)。
//!
//! ## 关键不变量
//! - **单连接**`WebRtcState::inner: Option<WebRtcInner>` 只持有 1 个 peer;新连接
//! POST 进来时,旧 `inner` 被 drop(旧 `Rtc` 析构,UDP socket 关闭)
//! - **非阻塞 IO**:所有 socket `set_nonblocking(true)``WouldBlock` 是常态而非错误
//! - **BWE 启动**`RtcConfig::enable_bwe(Some(Bitrate::mbps(5)))` 启用带宽估计,
//! 用于动态分辨率切换(见 `state_portal.rs::select_resolution`
//!
//! ## 引用
//! - `Cargo.toml`: `str0m = "0.20"`
//! - git `727893f`: bitrate 修复(BWE 与 VBV 协同)
//! - issue #23: PLI 节流(`FORCED_KEYFRAME_MIN_INTERVAL`
// WebRTC 传输模块 — 使用 str0m (Sans-IO) 将 H.264 编码帧推送到浏览器 // WebRTC 传输模块 — 使用 str0m (Sans-IO) 将 H.264 编码帧推送到浏览器
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, UdpSocket}; use std::net::{SocketAddr, TcpListener, UdpSocket};
@@ -192,28 +236,60 @@ connect();
// ── WebRTC 状态 ─────────────────────────────────────────────────────────── // ── WebRTC 状态 ───────────────────────────────────────────────────────────
// 对外门面:持有 HTTP 信令监听器 + 当前唯一的 peer 连接(`inner`)。
// 类比 Go 的 `*http.Server`,但 Sans-IO:所有推进都靠调用方主动 poll。
pub struct WebRtcState { pub struct WebRtcState {
// HTTP 信令监听器(`POST /sdp` 协商;`GET /` 测试页面)。`set_nonblocking(true)`
// 由上层 mio 事件循环可读时调 `handle_signaling()` 接受连接。
signal_listener: TcpListener, signal_listener: TcpListener,
// 当前 peer。`None` = 尚无连接 / 上次连接已断开。新 `POST /sdp` 会整体替换此字段,
// 旧 `Rtc` 实例被 dropUDP socket 随之关闭)。
inner: Option<WebRtcInner>, inner: Option<WebRtcInner>,
// 上层期望的帧率(来自 CLI `--fps`),用于初始化 `WebRtcInner`。
fps: u32, fps: u32,
} }
// 单个 WebRTC peer 的全部状态:str0m `Rtc` 实例 + 它专用的 UDP socket +
// 编解码参数协商结果 + 关键帧请求/BWE 估计的运行时缓存。
//
// 字段访问路径(每帧一次,由 `main.rs` 的事件循环驱动):
// 1. `feed_network()` 把 UDP 入包喂给 `Rtc::handle_input`
// 2. `poll_rtc()` 取出 `Rtc::poll_output` 的 `Transmit` 包写回 UDP,并处理 `Event`
// 3. `write_h264_frame()` 把编码后的 H.264 NALU 通过 `Rtc::writer(mid).write(...)` 发出
struct WebRtcInner { struct WebRtcInner {
// str0m `Rtc`:一个完整的 WebRTC peer connectionICE / DTLS / SRTP / RTP / RTCP)。
// Sans-IO:不持有任何 socket 或线程,只持有协议状态机。
rtc: Rtc, rtc: Rtc,
// 本 peer 专用的 UDP socket(每连接一个,避免与不存在的其他 peer 串扰)。
socket: UdpSocket, socket: UdpSocket,
// 该 socket 绑定的本地地址(带随机端口),用作 `Candidate::host` 的发地址。
udp_addr: SocketAddr, udp_addr: SocketAddr,
// 视频 Media IDSDP 协商后从 `Event::MediaAdded` 捕获)。`None` = 尚未协商到。
video_mid: Option<Mid>, video_mid: Option<Mid>,
// H.264 payload type(从 `Rtc::writer(mid).payload_params()` 扫描得到)。
video_pt: Option<Pt>, video_pt: Option<Pt>,
// ICE+DTLS 是否已完成(`Event::Connected`)。未连接时 `write_h264_frame` 静默丢弃。
connected: bool, connected: bool,
// 等待下一个 IDR 关键帧(连接建立/分辨率切换时置 true,写帧时若非 IDR 则丢帧)。
need_keyframe: bool, need_keyframe: bool,
// 通知上游编码器下一次输出 IDR`state.rs::State::take_force_keyframe` 拉取)。
force_keyframe_to_encode: bool, force_keyframe_to_encode: bool,
// 最近一次强制关键帧时刻,用于 `FORCED_KEYFRAME_MIN_INTERVAL` 节流(防 PLI 风暴)。
last_forced_keyframe_at: Option<Instant>, last_forced_keyframe_at: Option<Instant>,
// 最近一次 BWE 估计(来自 `Event::EgressBitrateEstimate`),用于上层动态分辨率选择。
current_bwe_estimate: Option<Bitrate>, current_bwe_estimate: Option<Bitrate>,
// 最近一次写入的 RTP 时间戳(90kHz),仅用于日志 trace,不参与协议正确性。
rtp_clock: u32, rtp_clock: u32,
// UDP 接收缓冲(重复利用以避免每包分配;65535 = max UDP payload)。
buf: Vec<u8>, buf: Vec<u8>,
} }
impl WebRtcState { impl WebRtcState {
// 构造函数:绑定 HTTP 信令 TCP 监听器并设为非阻塞。`port` 来自 CLI `--port`
// `fps` 来自 CLI `--fps`,仅在 `--port > 0` 时被 `main.rs` 调用。
//
// 注意:本函数只创建信令监听器,**不**创建 UDP socket 或 `Rtc` 实例——
// 那些在第一次 `POST /sdp` 时由 `WebRtcInner::new` 按需创建。
pub fn new(port: u16, fps: u32) -> Result<Self> { pub fn new(port: u16, fps: u32) -> Result<Self> {
let signal_listener = TcpListener::bind(format!("0.0.0.0:{port}"))?; let signal_listener = TcpListener::bind(format!("0.0.0.0:{port}"))?;
signal_listener.set_nonblocking(true)?; signal_listener.set_nonblocking(true)?;
@@ -226,18 +302,36 @@ impl WebRtcState {
}) })
} }
// 处理所有待接受的 HTTP 信令连接。上层 mio 循环在 `signal_listener` 可读时调用。
//
// 返回 `Ok(true)` 表示至少处理了一个请求(用于上层日志/计数)。
// 单次调用 drain 当前 accept 队列里所有连接(`Err(WouldBlock)` 时退出循环)。
//
// 路由:
// - `GET /` 或 `GET /sdp`(非 JSON)→ 返回 `HTML_PAGE`
// - `POST /sdp` → 解析 body,构造新 `WebRtcInner` 并替换 `self.inner`
// - 其他路径 → 404
pub fn handle_signaling(&mut self) -> Result<bool> { pub fn handle_signaling(&mut self) -> Result<bool> {
let mut handled = false; let mut handled = false;
loop { loop {
// `TcpListener::accept()` 类比 Go `ln.Accept()`;非阻塞模式下队列为空返回
// `WouldBlock`,是 drain 完成的信号而非错误(类比 Go `accept` + nonblocking + EAGAIN)。
let (mut stream, _addr) = match self.signal_listener.accept() { let (mut stream, _addr) = match self.signal_listener.accept() {
Ok(s) => s, Ok(s) => s,
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break, Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
// `bail!` 是 anyhow 提供的宏,等价于 `return Err(anyhow::anyhow!(...))`
// 类比 Go `return fmt.Errorf("TCP accept error: %w", err)`。
Err(e) => bail!("TCP accept error: {e}"), Err(e) => bail!("TCP accept error: {e}"),
}; };
handled = true; handled = true;
// 设为非阻塞——类比 Go `syscall.SetNonblock(fd, true)`。后续 `stream.read`
// 在没数据时返回 `WouldBlock`(用 `continue` 跳过本连接)。
stream.set_nonblocking(true)?; stream.set_nonblocking(true)?;
// 64KB 一次性读完:HTTP/1.0 客户端默认 `Connection: close`,浏览器 POST 整个
// SDP offer 不会超过 64KB。`vec![0u8; N]` 类比 Go `make([]byte, N)`。
let mut req = vec![0u8; 65536]; let mut req = vec![0u8; 65536];
// `stream.read(&mut req)` 类比 Go `conn.Read(buf)`——`Read` trait 即 Go `io.Reader`。
let n = match stream.read(&mut req) { let n = match stream.read(&mut req) {
Ok(n) => n, Ok(n) => n,
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
@@ -246,6 +340,8 @@ impl WebRtcState {
continue; continue;
} }
}; };
// `String::from_utf8_lossy` 把字节转成字符串,无效 UTF-8 替换为 U+FFFDHTTP 头都是 ASCII)。
// 类比 Go `string(buf[:n])`Go 字符串可包含任意字节,但后续 `starts_with` 也只看 ASCII)。
let req_str = String::from_utf8_lossy(&req[..n]); let req_str = String::from_utf8_lossy(&req[..n]);
if req_str.starts_with("GET / ") if req_str.starts_with("GET / ")
@@ -270,12 +366,19 @@ impl WebRtcState {
continue; continue;
} }
// `and_then`Result 链式组合,类比 Go `if err != nil { return err }` 后继续。
// `new_inner.handle_sdp_offer(...)?``?` 操作符传播 `Result::Err`
// 类比 Go `result, err := ...; if err != nil { return err }` 的简写。
match WebRtcInner::new(self.fps).and_then(|mut new_inner| { match WebRtcInner::new(self.fps).and_then(|mut new_inner| {
let answer_json = new_inner.handle_sdp_offer(body.as_bytes())?; let answer_json = new_inner.handle_sdp_offer(body.as_bytes())?;
Ok((new_inner, answer_json)) Ok((new_inner, answer_json))
}) { }) {
Ok((new_inner, answer_json)) => { Ok((new_inner, answer_json)) => {
// `Option::is_some()` = Rust 检查 `Option` 是否为 `Some(_)`
// 类比 Go `if p != nil`。这里用于日志区分"替换"vs"首次"。
let replacing = self.inner.is_some(); let replacing = self.inner.is_some();
// 整体替换 `self.inner`:旧 `Rtc` 实例 dropUDP socket 关闭,
// peer 连接断开)。这是单连接不变量的核心实现。
self.inner = Some(new_inner); self.inner = Some(new_inner);
if replacing { if replacing {
tracing::info!("Replaced WebRTC connection (old dropped)"); tracing::info!("Replaced WebRTC connection (old dropped)");
@@ -311,6 +414,10 @@ impl WebRtcState {
Ok(handled) Ok(handled)
} }
// 推动 str0m `Rtc` 状态机:取出 `poll_output` 的 `Transmit` 包写回 UDP,处理 `Event`。
// 返回 `Ok(())`;若 `poll_rtc` 上报 peer 已断开,则清空 `self.inner`。
//
// 类比 Go pion/webrtc:没有 `go func()` 自动循环,必须由 main 线程显式调用。
pub fn poll_rtc(&mut self) -> Result<()> { pub fn poll_rtc(&mut self) -> Result<()> {
if let Some(inner) = self.inner.as_mut() { if let Some(inner) = self.inner.as_mut() {
if inner.poll_rtc()? { if inner.poll_rtc()? {
@@ -321,6 +428,8 @@ impl WebRtcState {
Ok(()) Ok(())
} }
// 从 UDP socket 读所有待处理包喂给 `Rtc::handle_input`。`WouldBlock` 退出循环。
// Go 类比:`for { n, _ := conn.ReadFrom(buf); if errors.Is(err, EAGAIN) { break } }`。
pub fn feed_network(&mut self) -> Result<()> { pub fn feed_network(&mut self) -> Result<()> {
if let Some(inner) = self.inner.as_mut() { if let Some(inner) = self.inner.as_mut() {
inner.feed_network()?; inner.feed_network()?;
@@ -328,12 +437,20 @@ impl WebRtcState {
Ok(()) Ok(())
} }
// `poll_rtc` → `feed_network` → `poll_rtc` 三明治。中间多一次 poll 是因为
// `feed_network` 喂的入包可能触发 str0m 产生新的 `Transmit`(如 RTCP ACK),
// 这些出包必须在同一轮循环里写回 UDP,避免延迟一帧。
pub fn poll_and_feed(&mut self) -> Result<()> { pub fn poll_and_feed(&mut self) -> Result<()> {
self.poll_rtc()?; self.poll_rtc()?;
self.feed_network()?; self.feed_network()?;
self.poll_rtc() self.poll_rtc()
} }
// 把一帧 H.264 NALU(已 annex-B 转码)写入 str0m `Rtc`,通过 RTP 发给 peer。
// `pts_ticks` = 90kHz 时钟下的 PTS(编码器 time_base = 1/90000,等同 RTP 时间戳)。
//
// 返回 `Ok(())`;若 `WebRtcInner::write_h264_frame` 上报 peer 断开,则清空 `self.inner`。
// 未连接 / 未协商到 mid/pt / 等待 IDR 时静默丢帧(`Ok(false)`)。
pub fn write_h264_frame(&mut self, data: &[u8], pts_ticks: i64) -> Result<()> { pub fn write_h264_frame(&mut self, data: &[u8], pts_ticks: i64) -> Result<()> {
let should_destroy = if let Some(inner) = self.inner.as_mut() { let should_destroy = if let Some(inner) = self.inner.as_mut() {
inner.write_h264_frame(data, pts_ticks)? inner.write_h264_frame(data, pts_ticks)?
@@ -347,10 +464,15 @@ impl WebRtcState {
Ok(()) Ok(())
} }
// 是否有已连接的 peer。`Option::is_some_and` = Rust 短路求值,类比 Go
// `if p != nil && p.connected { ... }`。
pub fn is_connected(&self) -> bool { pub fn is_connected(&self) -> bool {
self.inner.as_ref().is_some_and(WebRtcInner::is_connected) self.inner.as_ref().is_some_and(WebRtcInner::is_connected)
} }
// 上层(`state_portal.rs::select_resolution`)查询最近一次 BWE 估计(bps)。
// `None` = 尚未收到 `Event::EgressBitrateEstimate``Some(bps)` = str0m 推断的可用带宽。
// 上层据此切换分辨率 tier(防止过载导致卡顿)。
/// Returns the latest bandwidth estimation estimate in bits per second, if available. /// Returns the latest bandwidth estimation estimate in bits per second, if available.
pub fn get_bwe_estimate(&self) -> Option<u64> { pub fn get_bwe_estimate(&self) -> Option<u64> {
self.inner self.inner
@@ -358,6 +480,9 @@ impl WebRtcState {
.and_then(|inner| inner.current_bwe_estimate.map(|b| b.as_u64())) .and_then(|inner| inner.current_bwe_estimate.map(|b| b.as_u64()))
} }
// 内部触发:连接刚建立或分辨率刚变化,需要立刻 IDR 以让对端解码器重置。
// 不受 `FORCED_KEYFRAME_MIN_INTERVAL` 节流(本函数总是 honor),但会刷新
// `last_forced_keyframe_at`,使紧接着的 1 秒内 viewer PLI 被丢弃。
/// Internal keyframe request (connect, resolution change). Always honored, /// Internal keyframe request (connect, resolution change). Always honored,
/// but updates last_forced_keyframe_at so a subsequent viewer PLI in the next /// but updates last_forced_keyframe_at so a subsequent viewer PLI in the next
/// second is throttled. /// second is throttled.
@@ -367,6 +492,9 @@ impl WebRtcState {
} }
} }
// 外部触发:viewer 通过 RTCP PLI/FIR 主动请求关键帧(`Event::KeyframeRequest`)。
// 受 `FORCED_KEYFRAME_MIN_INTERVAL` 节流(1 秒),防止恶意/频繁 PLI 触发 IDR 风暴
// 撑爆上行带宽。See issue #23。
/// External keyframe request from viewer (PLI/FIR via str0m /// External keyframe request from viewer (PLI/FIR via str0m
/// `Event::KeyframeRequest`). Rate-limited to FORCED_KEYFRAME_MIN_INTERVAL /// `Event::KeyframeRequest`). Rate-limited to FORCED_KEYFRAME_MIN_INTERVAL
/// to prevent PLI storms from swamping the network with IDR bursts. /// to prevent PLI storms from swamping the network with IDR bursts.
@@ -378,6 +506,8 @@ impl WebRtcState {
} }
} }
// 上层拉取"是否需要下一帧为 IDR"。返回 `true` 仅一次(取后自动复位),
// 类比 Go `atomic.SwapInt32(&flag, 0)`。编码线程据此在下一帧 `force_idr=1`。
pub fn take_force_keyframe(&mut self) -> bool { pub fn take_force_keyframe(&mut self) -> bool {
if let Some(inner) = self.inner.as_mut() { if let Some(inner) = self.inner.as_mut() {
let v = inner.force_keyframe_to_encode; let v = inner.force_keyframe_to_encode;
@@ -390,19 +520,46 @@ impl WebRtcState {
} }
impl WebRtcInner { impl WebRtcInner {
// 构造一个全新的 WebRTC peer:创建 str0m `Rtc` 实例 + UDP socket + 候选地址。
// 在 `handle_signaling` 接到 `POST /sdp` 时被调用——也就是说**每来一个 SDP offer
// 都新建一个 peer**,旧 `Rtc` 实例随之 dropUDP socket 关闭,连接断开)。
//
// 步骤:
// 1. `RtcConfig::new().enable_bwe(...).build(...)`str0m 构造器链式 Builder 模式,
// 类比 Go `webrtc.NewAPI(webrtc.WithSettingEngine(...))`;启用 BWE5 Mbps 初始)
// 2. `UdpSocket::bind("0.0.0.0:0")`OS 随机分配端口,类比 Go `net.ListenUDP("udp", nil)`
// 3. `unsafe { libc::setsockopt(SO_SNDBUF) }`:扩大 UDP 发送缓冲到 2MB(默认 ~208KB
// 在 IDR 突发下会 EAGAIN 丢包);英文 SAFETY 注释见下方
// 4. `Candidate::host(addr, "udp")`:构造 host ICE candidate(局域网用),
// `Rtc::add_local_candidate` 注册到 str0m
fn new(fps: u32) -> Result<Self> { fn new(fps: u32) -> Result<Self> {
// `let _ = fps;` 显式标记 fps 暂未使用(保留接口给未来 fps-based pacing)。
// 类比 Go `_ = fps`。
let _ = fps; let _ = fps;
// str0m `Rtc` 构造:Builder 模式 + 链式 setter。
// - `RtcConfig::new()`:空配置
// - `.enable_bwe(Some(Bitrate::mbps(5)))`:启用 bandwidth estimation,初始估 5 Mbps
// - `.build(Instant::now())`:传入当前时刻作为 Rtc 内部时钟起点
// 类比 Go pion/webrtc`webrtc.NewAPI(webrtc.WithSettingEngine(...))`
let mut rtc = RtcConfig::new() let mut rtc = RtcConfig::new()
.enable_bwe(Some(Bitrate::mbps(5))) .enable_bwe(Some(Bitrate::mbps(5)))
.build(Instant::now()); .build(Instant::now());
// `UdpSocket::bind("0.0.0.0:0")`OS 随机分配端口(每 peer 独享一个 socket)。
// 类比 Go `net.ListenUDP("udp", &net.UDPAddr{Port: 0})`。
let socket = UdpSocket::bind("0.0.0.0:0")?; let socket = UdpSocket::bind("0.0.0.0:0")?;
socket.set_nonblocking(true)?; socket.set_nonblocking(true)?;
// 中文概述:调大 UDP 发送缓冲到 2MB(默认 ~208KB),原因详见下方英文注释。
// 然后用 `getsockopt` 读取内核实际分配的大小(Linux 可能受 `wmem_max` 截断,且
// 通常会翻倍)。Go 等价:`net.ListenConfig{Control: ...}`。
// Increase UDP send buffer to absorb IDR frame bursts (256KB IDR → ~145 RTP // Increase UDP send buffer to absorb IDR frame bursts (256KB IDR → ~145 RTP
// packets in a single poll_rtc loop). Default Linux wmem is ~208KB which // packets in a single poll_rtc loop). Default Linux wmem is ~208KB which
// causes EAGAIN on large keyframes. 2MB comfortably buffers several IDRs. // causes EAGAIN on large keyframes. 2MB comfortably buffers several IDRs.
const SND_BUF_REQ: usize = 2 * 1024 * 1024; const SND_BUF_REQ: usize = 2 * 1024 * 1024;
// 中文概述:调用 `setsockopt(SO_SNDBUF)` 调大 UDP 发送缓冲,然后用
// `getsockopt` 读取内核实际分配的大小(Linux 可能受 `wmem_max` 截断,且通常会
// 翻倍)。FFI 安全性论证见下方英文 SAFETY 块。
// SAFETY: fd is a valid UDP socket; setsockopt/getsockopt with SOL_SOCKET + // SAFETY: fd is a valid UDP socket; setsockopt/getsockopt with SOL_SOCKET +
// SO_SNDBUF are safe on Linux. We check the return value and log the actual // SO_SNDBUF are safe on Linux. We check the return value and log the actual
// kernel-assigned buffer (Linux may cap at wmem_max and/or double the value). // kernel-assigned buffer (Linux may cap at wmem_max and/or double the value).
@@ -443,13 +600,22 @@ impl WebRtcInner {
let local_addr = socket.local_addr()?; let local_addr = socket.local_addr()?;
// `local_ip().unwrap_or_else(closure)``Option<T>::unwrap_or_else` 类比 Go
// `if ip == "" { ip = "127.0.0.1" }`——`Option::None` 时执行闭包取兜底值。
let lan_ip = local_ip().unwrap_or_else(|| { let lan_ip = local_ip().unwrap_or_else(|| {
tracing::debug!("Failed to detect LAN IP, falling back to 127.0.0.1"); tracing::debug!("Failed to detect LAN IP, falling back to 127.0.0.1");
"127.0.0.1".to_string() "127.0.0.1".to_string()
}); });
// `format!("{lan_ip}:{}", port)`Rust 格式化宏,类比 Go `fmt.Sprintf("%s:%d", ...)`.
// `.parse::<SocketAddr>()`:字符串解析为 `SocketAddr``?` 自动传播 `AddrParseError`。
let candidate_addr: SocketAddr = format!("{lan_ip}:{}", local_addr.port()).parse()?; let candidate_addr: SocketAddr = format!("{lan_ip}:{}", local_addr.port()).parse()?;
// `Candidate::host(addr, "udp")`:构造 host ICE candidate(局域网用,无 STUN/TURN)。
// `.map_err(|e| anyhow::anyhow!(...))?`:把 str0m 自定义错误转成 `anyhow::Error`
// 并传播,类比 Go `if err != nil { return fmt.Errorf("candidate: %w", err) }`。
let candidate = Candidate::host(candidate_addr, "udp") let candidate = Candidate::host(candidate_addr, "udp")
.map_err(|e| anyhow::anyhow!("candidate: {e}"))?; .map_err(|e| anyhow::anyhow!("candidate: {e}"))?;
// `Rtc::add_local_candidate`:把 candidate 注册到 str0m,之后 SDP 协商时它会被
// 包含进 answer 的 `a=candidate:` 行。
rtc.add_local_candidate(candidate); rtc.add_local_candidate(candidate);
tracing::info!("WebRTC UDP: {candidate_addr} (bound 0.0.0.0)"); tracing::info!("WebRTC UDP: {candidate_addr} (bound 0.0.0.0)");
@@ -469,10 +635,27 @@ impl WebRtcInner {
}) })
} }
// SDP offer/answer 交换:解析浏览器 POST 来的 SDP offer JSON → 喂给 str0m 协商 →
// 返回 answer JSON。
//
// 关键步骤:
// 1. `serde_json::from_slice`:反序列化 SDP offer(类比 Go `json.Unmarshal`
// 2. `self.rtc.sdp_api().accept_offer(offer)`str0m 内部协商出 answer
// 副作用是设置 `Event::MediaAdded` 等待异步触发
// 3. `self.need_keyframe = true; self.force_keyframe_to_encode = true;`
// 协商完成后立即请求 IDR,让对端尽快解码首帧
// 4. `discover_video_params()`:扫描 str0m writer 找到 H.264 payload type
// 5. `serde_json::to_vec`:序列化 answer(类比 Go `json.Marshal`
fn handle_sdp_offer(&mut self, body: &[u8]) -> Result<String> { fn handle_sdp_offer(&mut self, body: &[u8]) -> Result<String> {
// `serde_json::from_slice::<SdpOffer>(body)`:把浏览器 POST 的 JSON 反序列化成
// str0m 的 `SdpOffer` 类型,类比 Go `json.Unmarshal(body, &offer)`。
// `.map_err(...)?`:把 serde 错误包装成 anyhow 错误并传播。
let offer: SdpOffer = let offer: SdpOffer =
serde_json::from_slice(body).map_err(|e| anyhow::anyhow!("parse SDP offer: {e}"))?; serde_json::from_slice(body).map_err(|e| anyhow::anyhow!("parse SDP offer: {e}"))?;
// `Rtc::sdp_api().accept_offer(offer)`str0m SDP 协商核心入口——
// 解析 offer 中的 m= 行、codec 列表、ICE candidate,构造对应的 answer。
// 副作用:触发后续 `Event::MediaAdded`(异步,要等 poll_rtc 才发)。
let answer = self let answer = self
.rtc .rtc
.sdp_api() .sdp_api()
@@ -491,6 +674,13 @@ impl WebRtcInner {
String::from_utf8(answer_json).map_err(|e| anyhow::anyhow!("answer utf8: {e}")) String::from_utf8(answer_json).map_err(|e| anyhow::anyhow!("answer utf8: {e}"))
} }
// 扫描 str0m 内部协商出的 codec 列表,找到 H.264 payload type`Pt`)。
// 在 SDP 协商后、`Event::MediaAdded` 后、`Event::Connected` 后各调用一次
// (三处调用是因为 str0m 的 codec 信息可能在不同时机可用——多保险)。
//
// 副作用:调用 `direct_api().stream_tx_by_mid(mid, None).set_unpaced(true)`
// 关闭 str0m 的 LeakyBucketPacer(默认每包加 ~100ms pacing 延迟,与我们的 VBV
// 8 Mbps 上限冲突;关掉后由编码器侧 VBV 做速率控制)。
fn discover_video_params(&mut self) { fn discover_video_params(&mut self) {
let mid = match self.video_mid { let mid = match self.video_mid {
Some(m) => m, Some(m) => m,
@@ -503,12 +693,18 @@ impl WebRtcInner {
// Disable str0m's LeakyBucketPacer for this video stream. Default pacing // Disable str0m's LeakyBucketPacer for this video stream. Default pacing
// adds ~100ms send latency per large IDR; our 8Mbps cap + VBV already // adds ~100ms send latency per large IDR; our 8Mbps cap + VBV already
// provide rate control. BWE stays enabled for adaptation feedback. // provide rate control. BWE stays enabled for adaptation feedback.
// `direct_api()` 返回 str0m 内部 API(不公开稳定接口),`stream_tx_by_mid(mid, None)`
// 取得该 mid 的发送流控制器;`set_unpaced(true)` 关闭 pacing。
if let Some(stream_tx) = self.rtc.direct_api().stream_tx_by_mid(mid, None) { if let Some(stream_tx) = self.rtc.direct_api().stream_tx_by_mid(mid, None) {
stream_tx.set_unpaced(true); stream_tx.set_unpaced(true);
} }
// `Rtc::writer(mid)` 返回媒体写入器,`payload_params()` 列出协商出的所有 codec。
// 我们扫描找 H.264`Codec::H264`)的 payload type,存入 `video_pt` 供后续 `write_h264_frame` 使用。
if let Some(writer) = self.rtc.writer(mid) { if let Some(writer) = self.rtc.writer(mid) {
for pp in writer.payload_params() { for pp in writer.payload_params() {
tracing::debug!("Codec: pt={:?} spec={:?}", pp.pt(), pp.spec()); tracing::debug!("Codec: pt={:?} spec={:?}", pp.pt(), pp.spec());
// `pp.spec().codec.is_video()`:先确认是视频 codec
// `pp.spec().codec == Codec::H264`:再确认是 H.264(非 VP8/VP9/AV1)。
if pp.spec().codec.is_video() && pp.spec().codec == Codec::H264 { if pp.spec().codec.is_video() && pp.spec().codec == Codec::H264 {
self.video_pt = Some(pp.pt()); self.video_pt = Some(pp.pt());
tracing::info!("H.264 payload type: {:?}", pp.pt()); tracing::info!("H.264 payload type: {:?}", pp.pt());
@@ -521,6 +717,8 @@ impl WebRtcInner {
} }
} }
// 内部不节流版本:直接置位 `need_keyframe` + `force_keyframe_to_encode`
// 并刷新 `last_forced_keyframe_at`(防紧接着 1 秒内的 viewer PLI 重复触发 IDR)。
/// Unthrottled keyframe trigger. Always sets the keyframe flags and refreshes /// Unthrottled keyframe trigger. Always sets the keyframe flags and refreshes
/// `last_forced_keyframe_at` so a follow-up viewer PLI within the next /// `last_forced_keyframe_at` so a follow-up viewer PLI within the next
/// `FORCED_KEYFRAME_MIN_INTERVAL` is dropped. /// `FORCED_KEYFRAME_MIN_INTERVAL` is dropped.
@@ -530,6 +728,8 @@ impl WebRtcInner {
self.last_forced_keyframe_at = Some(Instant::now()); self.last_forced_keyframe_at = Some(Instant::now());
} }
// 节流版本:仅在距离 `last_forced_keyframe_at` 已过 `FORCED_KEYFRAME_MIN_INTERVAL`
//1 秒)时才 honor,否则记 warn 日志并丢弃。对应 `Event::KeyframeRequest`PLI/FIR)。
/// Throttled keyframe trigger used for viewer-originated PLI/FIR requests. /// Throttled keyframe trigger used for viewer-originated PLI/FIR requests.
/// Honored only if enough time has elapsed since the last forced keyframe. /// Honored only if enough time has elapsed since the last forced keyframe.
fn request_keyframe_from_viewer(&mut self) { fn request_keyframe_from_viewer(&mut self) {
@@ -550,11 +750,24 @@ impl WebRtcInner {
} }
} }
// Sans-IO 推进主循环(出方向):取出 str0m 待发的 `Output::Transmit` 包写回 UDP
// 处理 `Output::Event`Connected/Disconnected/MediaAdded/KeyframeRequest/BWE 等)。
// 返回 `Ok(true)` 表示 peer 已断开(调用方应 drop `WebRtcInner`)。
//
// `Output::Timeout` 表示 str0m 需要在未来某时刻被再次唤醒——本实现简单 `break`,
// 依赖上层 mio 循环的 1ms tick 重新进入;更高性能的做法是读取 `_t` 安排 timer。
fn poll_rtc(&mut self) -> Result<bool> { fn poll_rtc(&mut self) -> Result<bool> {
loop { loop {
// `Rtc::poll_output()`str0m 主推进入口,返回 `Output` 枚举(Transmit/Event/Timeout
// 或 `Err`。Sans-IO 设计:调用方必须循环 poll 直到拿到 `Timeout`(表示 str0m
// 当前没活干了,等下一次外部输入)。
match self.rtc.poll_output() { match self.rtc.poll_output() {
// `Output::Transmit`str0m 想发的网络包(RTP/RTCP/DTLS/STUN)。
// 我们写回 UDP socket——这就是 Sans-IO 的"输出"侧。
Ok(Output::Transmit(t)) => { Ok(Output::Transmit(t)) => {
tracing::trace!("TX {} bytes -> {}", t.contents.len(), t.destination); tracing::trace!("TX {} bytes -> {}", t.contents.len(), t.destination);
// `UdpSocket::send_to` 类比 Go `conn.WriteToUDP(b, addr)`。
// `WouldBlock` = 内核发送缓冲满(罕见,因为我们在 new() 里调大了)。
if let Err(e) = self.socket.send_to(&t.contents, t.destination) { if let Err(e) = self.socket.send_to(&t.contents, t.destination) {
if e.kind() == std::io::ErrorKind::WouldBlock { if e.kind() == std::io::ErrorKind::WouldBlock {
tracing::debug!( tracing::debug!(
@@ -566,20 +779,28 @@ impl WebRtcInner {
} }
} }
} }
// `Output::Event`str0m 内部状态变化通知(ICE 连接、媒体添加、keyframe 请求等)。
// `Event` 是 enum,下方 `match &e` 对每种 variant 分发处理。
Ok(Output::Event(e)) => { Ok(Output::Event(e)) => {
tracing::debug!("RTC event: {e:?}"); tracing::debug!("RTC event: {e:?}");
match &e { match &e {
// `Event::Connected`ICE+DTLS 握手完成,可以发 RTP 了。
// 立即触发 IDR 请求(让对端解码器拿到关键帧尽快起播)+ 重新扫 codec 参数。
Event::Connected => { Event::Connected => {
tracing::info!("WebRTC connected!"); tracing::info!("WebRTC connected!");
self.connected = true; self.connected = true;
self.set_need_keyframe(); self.set_need_keyframe();
self.discover_video_params(); self.discover_video_params();
} }
// `Event::IceConnectionStateChange`ICE 状态变化。
// `Disconnected` 视为连接已死,向上层返回 `Ok(true)` 触发 drop。
Event::IceConnectionStateChange(IceConnectionState::Disconnected) => { Event::IceConnectionStateChange(IceConnectionState::Disconnected) => {
tracing::warn!("WebRTC disconnected"); tracing::warn!("WebRTC disconnected");
self.connected = false; self.connected = false;
return Ok(true); return Ok(true);
} }
// `Event::MediaAdded`SDP 协商后有新 m= 行就绪。
// 捕获视频 mid(只取第一个 sending direction 的视频流)。
Event::MediaAdded(ma) => { Event::MediaAdded(ma) => {
tracing::info!("Media added: mid={} kind={:?}", ma.mid, ma.kind); tracing::info!("Media added: mid={} kind={:?}", ma.mid, ma.kind);
if ma.kind == MediaKind::Video { if ma.kind == MediaKind::Video {
@@ -592,10 +813,15 @@ impl WebRtcInner {
} }
} }
} }
// `Event::KeyframeRequest`:对端发来 PLI/FIR,请求 IDR。
// 转发到节流版本 `request_keyframe_from_viewer`(防止 PLI 风暴)。
Event::KeyframeRequest(_) => { Event::KeyframeRequest(_) => {
tracing::info!("received keyframe request from viewer"); tracing::info!("received keyframe request from viewer");
self.request_keyframe_from_viewer(); self.request_keyframe_from_viewer();
} }
// `Event::EgressBitrateEstimate`BWE 推断的可用上行带宽。
// `BweKind::Twcc`Transport-CC,新标准)或 `BweKind::Remb`(老标准)。
// 提取数值存入 `current_bwe_estimate`,供 `state_portal.rs::select_resolution` 使用。
Event::EgressBitrateEstimate(est) => { Event::EgressBitrateEstimate(est) => {
let bitrate = match est { let bitrate = match est {
BweKind::Twcc(b) => *b, BweKind::Twcc(b) => *b,
@@ -613,6 +839,8 @@ impl WebRtcInner {
} }
} }
} }
// `Output::Timeout`str0m 内部定时器到期点。本实现忽略 `_t`(即下次唤醒时刻),
// 简单 `break`——上层 mio 循环 1ms tick 会很快再次调用 `poll_rtc`。
Ok(Output::Timeout(_t)) => break, Ok(Output::Timeout(_t)) => break,
Err(e) => { Err(e) => {
tracing::error!("rtc.poll_output error: {e}"); tracing::error!("rtc.poll_output error: {e}");
@@ -624,15 +852,27 @@ impl WebRtcInner {
Ok(false) Ok(false)
} }
// Sans-IO 推进主循环(入方向):从 UDP socket 读所有待处理包,封装为
// `Input::Receive` 喂给 str0m;最后喂一次 `Input::Timeout(now)` 推动内部时钟。
// 类比 Go pion/webrtc:手动调用 `peerConnection.Receive(rtpPacket)` 而不是
// 起 goroutine 监听 UDP。
fn feed_network(&mut self) -> Result<()> { fn feed_network(&mut self) -> Result<()> {
let mut recv_count = 0u32; let mut recv_count = 0u32;
loop { loop {
// `UdpSocket::recv_from(&mut self.buf)`:类比 Go `conn.ReadFrom(buf)`。
// 返回 `(n_bytes_read, source_addr)`。`WouldBlock`/`Interrupted` 是常态,
// 前者 break 出循环,后者重试(类比 Go EINTR 处理)。
match self.socket.recv_from(&mut self.buf) { match self.socket.recv_from(&mut self.buf) {
Ok((n, source)) => { Ok((n, source)) => {
recv_count += 1; recv_count += 1;
if recv_count <= 5 { if recv_count <= 5 {
tracing::trace!("UDP recv {} bytes from {}", n, source); tracing::trace!("UDP recv {} bytes from {}", n, source);
} }
// 构造 `Input::Receive`str0m 的"入包"事件。
// `Receive { proto, source, destination, contents }` 完整描述一个网络包:
// - `proto: Protocol::Udp`str0m 也支持 TCP,但 WebRTC 主流用 UDP
// - `source` / `destination`ICE candidate 端点
// - `contents``self.buf[..n]` 转 `Box<[u8]>``.try_into()` 因为 slice→Box 长度可能变化)
let input = Input::Receive( let input = Input::Receive(
Instant::now(), Instant::now(),
Receive { Receive {
@@ -644,6 +884,8 @@ impl WebRtcInner {
.map_err(|e| anyhow::anyhow!("receive contents: {e}"))?, .map_err(|e| anyhow::anyhow!("receive contents: {e}"))?,
}, },
); );
// `Rtc::handle_input(input)`:把入包喂给 str0m 解析(ICE/DTLS/SRTP/RTP/RTCP)。
// 这是 Sans-IO 的"输入"侧——str0m 不主动读 socket,全靠调用方喂。
self.rtc.handle_input(input).map_err(|e| { self.rtc.handle_input(input).map_err(|e| {
anyhow::anyhow!("handle_input({n} bytes from {source}): {e}") anyhow::anyhow!("handle_input({n} bytes from {source}): {e}")
})?; })?;
@@ -654,6 +896,8 @@ impl WebRtcInner {
} }
} }
// 喂一次 `Input::Timeout(now)`:让 str0m 推进内部定时器(重传、keepalive、BWE 周期等)。
// 即使没有任何入包,也必须定期调用,否则 str0m 内部超时不会触发。
self.rtc self.rtc
.handle_input(Input::Timeout(Instant::now())) .handle_input(Input::Timeout(Instant::now()))
.map_err(|e| anyhow::anyhow!("handle timeout: {e}"))?; .map_err(|e| anyhow::anyhow!("handle timeout: {e}"))?;
@@ -661,6 +905,17 @@ impl WebRtcInner {
Ok(()) Ok(())
} }
// 把一帧 H.264 NALUannex-B 格式,含 0x000001 起始码)写入 str0m,转 RTP 发出。
//
// 5 步:
// 1. 检查 `connected`、`video_mid`、`video_pt`,未就绪则 `Ok(false)` 静默丢帧
// 2. 若 `need_keyframe`,校验此帧必须是 IDRNAL type=5),否则丢帧等下一帧
// 3. PTS 90kHz 时钟 → RTP 时间戳(直接复用,因编码器 time_base = 1/90000
// 4. `Rtc::writer(mid).write(pt, now, rtp_time, data)`str0m 内部分包(>MTU 切片)
// 并加密 SRTP,产生 `Output::Transmit` 包
// 5. 立即 `poll_rtc()` 把 Transmit 包写回 UDP(同步发出,避免延迟)
//
// 返回 `Ok(true)` = peer 断开,调用方应 drop 本 `WebRtcInner`。
fn write_h264_frame(&mut self, data: &[u8], pts_ticks: i64) -> Result<bool> { fn write_h264_frame(&mut self, data: &[u8], pts_ticks: i64) -> Result<bool> {
if !self.connected { if !self.connected {
return Ok(false); return Ok(false);
@@ -696,10 +951,16 @@ impl WebRtcInner {
self.need_keyframe = false; self.need_keyframe = false;
} }
// PTS 90kHz → RTP 时间戳。`rtp_timestamp_from_pts_ticks` 把 i64 clamp 到 u64
//(见该函数文档)。`Frequency::NINETY_KHZ` 是视频 RTP 的标准时钟频率。
let rtp_timestamp = rtp_timestamp_from_pts_ticks(pts_ticks); let rtp_timestamp = rtp_timestamp_from_pts_ticks(pts_ticks);
self.rtp_clock = rtp_timestamp as u32; self.rtp_clock = rtp_timestamp as u32;
// `MediaTime::new(rtp_timestamp, Frequency::NINETY_KHZ)`:构造 str0m 媒体时间戳,
// 用于 RTP 头部 + jitter buffer 同步。
let rtp_time = MediaTime::new(rtp_timestamp, Frequency::NINETY_KHZ); let rtp_time = MediaTime::new(rtp_timestamp, Frequency::NINETY_KHZ);
// `Rtc::writer(mid)`:取得 mid 对应的媒体写入器(之前在 `discover_video_params` 用过)。
// None 表示 mid 还没就绪(罕见,已在前面的 video_mid 检查里处理)。
let writer = match self.rtc.writer(mid) { let writer = match self.rtc.writer(mid) {
Some(w) => w, Some(w) => w,
None => { None => {
@@ -714,6 +975,9 @@ impl WebRtcInner {
pt, pt,
self.rtp_clock self.rtp_clock
); );
// `writer.write(pt, Instant::now(), rtp_time, data)`:媒体写入入口。
// str0m 内部完成 (a) H.264 RTP 分包(FU-A for >MTU),(b) SRTP 加密,
// (c) 产生 `Output::Transmit` 包供 `poll_rtc` 取出。
writer writer
.write(pt, Instant::now(), rtp_time, data) .write(pt, Instant::now(), rtp_time, data)
.map_err(|e| anyhow::anyhow!("writer.write: {e}"))?; .map_err(|e| anyhow::anyhow!("writer.write: {e}"))?;
@@ -723,11 +987,15 @@ impl WebRtcInner {
Ok(should_destroy) Ok(should_destroy)
} }
// 简单 getter,对应 `Event::Connected` / `Event::IceConnectionStateChange(Disconnected)`。
fn is_connected(&self) -> bool { fn is_connected(&self) -> bool {
self.connected self.connected
} }
} }
// PTS→RTP 时间戳换算:编码器侧 time_base 已是 1/90000(与 RTP 视频时钟一致),
// 因此 1:1 直接复用,无需 fps-based 换算(旧版本曾用 `90000 / fps` 误导致时间戳错乱)。
// 返回 `u64` 喂 `MediaTime::new` 避免 u32 在 13.25 小时后过早回绕;str0m 内部处理 RTP u32 回绕。
/// Convert PTS in 90kHz media-clock ticks to RTP MediaTime ticks (u64). /// Convert PTS in 90kHz media-clock ticks to RTP MediaTime ticks (u64).
/// ///
/// With WebRTC encoder time_base = 1/90000, pts_ticks ARE RTP timestamps. /// With WebRTC encoder time_base = 1/90000, pts_ticks ARE RTP timestamps.
@@ -748,6 +1016,9 @@ fn extract_body(req: &str) -> &str {
} }
} }
// 探测本机 LAN IP(用于 ICE host candidate)。Go 等价:`net.Dial("udp", "1.1.1.1:80")`
// 后读 `LocalAddr()`——`connect` 不会发包,只设置路由表,从而选出默认网关对应的网卡 IP。
// `127.x` / `0.0.0.0` 视为无 LAN IP,由调用方 fallback 到 127.0.0.1loopback 调试用)。
fn local_ip() -> Option<String> { fn local_ip() -> Option<String> {
std::net::UdpSocket::bind("0.0.0.0:0").ok().and_then(|s| { std::net::UdpSocket::bind("0.0.0.0:0").ok().and_then(|s| {
s.connect("1.1.1.1:80").ok()?; s.connect("1.1.1.1:80").ok()?;
@@ -761,6 +1032,12 @@ fn local_ip() -> Option<String> {
}) })
} }
// 检测 H.264 NALU 流中是否含 IDR sliceNAL type=5)。两种起始码:
// - 4 字节 `00 00 00 01`AVCC boundary,主流)
// - 3 字节 `00 00 01` Annex-B inline,少见)
// NAL header 低 5 位 = type5 = IDR slice。SPS=7、PPS=8、SEI=6 等不算 IDR。
//
// 用于 `need_keyframe` 时丢非 IDR 帧——Go 等价:`bytes.Index(data, []byte{0,0,0,1})` 循环。
fn is_idr_nalu(data: &[u8]) -> bool { fn is_idr_nalu(data: &[u8]) -> bool {
let mut i = 0; let mut i = 0;
while i < data.len() { while i < data.len() {
+78
View File
@@ -1,19 +1,77 @@
//! 集成测试:通过 shell out 到 `target/release/wl-webrtc` 二进制来验证 CLI 行为。
//!
//! 与单元测试(在进程内调用库函数)不同,集成测试把产物当作黑盒,启动子进程
//! 并检查其 stdout/stderr/exit code。这种模式类似 Go 的 `testing` 包配合
//! `os/exec.Command(...)` —— Rust 这边对应 `std::process::Command::new(...)`
//! 通过 `.arg(...)` 链式追加参数,最后 `.output()` 一次性等待子进程结束并
//! 拿到 `Output { status, stdout, stderr }`。
//!
//! # 运行前必读
//!
//! 这些测试**依赖 release 版二进制存在**。`cargo test --test integration_test`
//! 本身不会触发 release 构建,必须先手动执行:
//!
//! ```bash
//! cargo build --release
//! ```
//!
//! 否则 `Command::new("target/release/wl-webrtc")` 会因为找不到可执行文件而 panic,
//! 所有 `#[test]` 都将以 "failed to execute" 失败。详见 `AGENTS.md`
//! "Testing and verification" 章节。
//!
//! # 测试发现与断言
//!
//! `cargo test` 通过 `#[test]` 属性宏自动发现并运行标记的函数,无需像 Go 那样
//! 约定 `TestXxx(t *testing.T)` 签名 —— 普通函数加 `#[test]` 即可。
//! `#[ignore]` 标记的测试默认跳过,需 `cargo test -- --ignored` 显式开启。
//!
//! 断言方面,`assert!(cond, "msg")` 类似 Go 的 `if !cond { t.Errorf("msg") }`
//! 但 Rust 会在失败时立即 unwind 当前测试函数(而非 Go 那样继续执行后续断言)。
//! 当测试函数返回 `Result<(), E>` 时,可用 `?` 操作符把 IO/解码错误直接传播
//! 给测试 harness(失败时打印 `Err` 而非 `panic!`);本文件的测试为了聚焦于
//! 子进程行为,全部用 `.expect(...)`/`assert!` 风格,不返回 `Result`。
use std::process::Command; use std::process::Command;
/// Helper: get the binary path. Uses the release build if available. /// Helper: get the binary path. Uses the release build if available.
///
/// 返回被测二进制的路径。集成测试 shell out 到 release 构建产物(debug 构建太慢,
/// 且无法真实反映发布行为)。返回 `&'static str` 而非 `PathBuf` 是因为这个路径
/// 是编译期常量,无需在每次调用时分配。
fn bin_path() -> &'static str { fn bin_path() -> &'static str {
"target/release/wl-webrtc" "target/release/wl-webrtc"
} }
/// 测试 `--help` 子命令:应正常退出(exit 0),且 stdout 至少包含关键字段名。
///
/// 验证 README/AGENTS.md 中列出的核心 CLI 参数(output/fps/codec/bitrate/gop-size/drm-device
/// 都能在 `--help` 输出中找到 —— 这是一道"防回归"测试:一旦某参数被改名或删除,
/// 此处 `assert!` 会立刻失败。
#[test] #[test]
fn test_help_flag() { fn test_help_flag() {
// `Command::new(...)` 类似 Go 的 `exec.Command(...)`:构造一个待运行的
// 子进程描述符,此时还未真正 fork/exec。链式 `.arg(...)` 把参数逐个追加到
// 命令行末尾(保持顺序),`.output()` 则 fork、exec、等待子进程退出,并
// 一次性捕获 stdout/stderr 到 `Output` 结构体。
//
// `.expect(...)` 等价于 `match result { Ok(v) => v, Err(_) => panic!(...) }`
// 用于在父进程侧(不是被测程序侧)报告"无法启动子进程"这种环境性错误。
let output = Command::new(bin_path()) let output = Command::new(bin_path())
.arg("--help") .arg("--help")
.output() .output()
.expect("failed to execute wl-webrtc --help"); .expect("failed to execute wl-webrtc --help");
// `String::from_utf8_lossy(...)` 把 `Vec<u8>` 字节流解码成字符串;遇到非法
// UTF-8 序列时用 U+FFFD 替换而非报错。这里用 `_lossy` 变体而非
// `String::from_utf8(...)?` 是因为 stdout 理论上可能包含任意字节(如 ANSI
// color code 失控),不值得为解码失败让整个测试 panic。
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
// `output.status.success()` 检查子进程退出码是否为 0Unix 上即 WIFEXITED
// 且 exit code == 0)。`assert!(cond, "msg")` 失败时打印 `msg` 并 panic
// 当前测试函数,类似 Go 的 `t.Fatalf` 而非 `t.Errorf`。
assert!(output.status.success(), "--help should exit 0"); assert!(output.status.success(), "--help should exit 0");
// 后续 `assert!(stdout.contains(...), "...")` 检查帮助文本是否覆盖每个
// 文档化参数。任何一个缺失都会让测试失败并打印自定义消息。
assert!( assert!(
stdout.contains("output"), stdout.contains("output"),
"help output should mention 'output'" "help output should mention 'output'"
@@ -37,6 +95,11 @@ fn test_help_flag() {
); );
} }
/// 测试未知参数应被拒绝:非零退出码 + stderr 包含 error/unexpected/unrecognized 之一。
///
/// `clap` 默认对未识别的 flag 返回非零退出码并打印错误到 stderr。这里用
/// `!output.status.success()` 断言"应该失败",再检查 stderr 文本以排除"碰巧
/// 崩溃退出"的假阳性。
#[test] #[test]
fn test_rejects_invalid_args() { fn test_rejects_invalid_args() {
let output = Command::new(bin_path()) let output = Command::new(bin_path())
@@ -44,8 +107,13 @@ fn test_rejects_invalid_args() {
.output() .output()
.expect("failed to execute wl-webrtc with invalid args"); .expect("failed to execute wl-webrtc with invalid args");
// 断言"非零退出码"。注意 `!` 取反 —— 与 Go 的 `t.Errorf` 风格不同,Rust 的
// `assert!` 直接接受 bool 表达式,没有 `assertFalse` 这种专门函数。
assert!(!output.status.success(), "should reject unrecognized flag"); assert!(!output.status.success(), "should reject unrecognized flag");
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
// 多个可能的错误措辞用 `||` 连接 —— 不同 clap 版本可能输出 "error: unexpected"
// 或 "error: unrecognized",任一匹配即可。自定义消息末尾的 `{stderr}` 利用
// `format!` 占位符在失败时打印实际 stderr 内容,便于排错。
assert!( assert!(
stderr.to_lowercase().contains("error") stderr.to_lowercase().contains("error")
|| stderr.to_lowercase().contains("unexpected") || stderr.to_lowercase().contains("unexpected")
@@ -54,8 +122,11 @@ fn test_rejects_invalid_args() {
); );
} }
/// 测试 `--codec hevc` 在 MVP 阶段应被拒绝:MVP 只支持 h264。
#[test] #[test]
fn test_rejects_hevc_codec() { fn test_rejects_hevc_codec() {
// 多个 `.arg(...)` 链式调用按顺序追加参数,等价于命令行
// `wl-webrtc --output /dev/null --codec hevc`。
let output = Command::new(bin_path()) let output = Command::new(bin_path())
.arg("--output") .arg("--output")
.arg("/dev/null") .arg("/dev/null")
@@ -70,6 +141,13 @@ fn test_rejects_hevc_codec() {
/// Tests requiring a live Wayland compositor and VAAPI hardware. /// Tests requiring a live Wayland compositor and VAAPI hardware.
/// Run with: cargo test -- --ignored /// Run with: cargo test -- --ignored
///
/// 该测试需要真实 Wayland 会话 + VAAPI GPU + 可写输出路径,无法在 CI 中运行。
/// `#[ignore]` 属性告诉 `cargo test` 默认跳过它,只有显式
/// `cargo test -- --ignored` 时才执行。
///
/// 注意:此测试只验证"参数解析不立即报错",并未真正完成捕获 —— 真正的捕获
/// 需要异步等待几秒再发 SIGINT,这里只做最小烟雾测试。
#[test] #[test]
#[ignore] #[ignore]
fn test_capture_starts_with_valid_output() { fn test_capture_starts_with_valid_output() {