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