From 6e83bda8b8c56e5a5d3b69ca4326f685cce711a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=91=E5=8D=9A=E6=96=87?= <1323515022@qq.com> Date: Wed, 28 Oct 2026 16:35:44 +0800 Subject: [PATCH] =?UTF-8?q?perf(agent):=20=E5=85=B3=E9=94=AE=E5=B8=A7?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E4=BB=8E=204s/10s=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=88=B0=201s=EF=BC=8C=E7=BC=A9=E7=9F=AD=E4=B8=A2=E5=B8=A7?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E5=85=9C=E5=BA=95=E6=81=A2=E5=A4=8D=E7=AA=97?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景 协议 v2 与 vendor nvenc 补丁之后,KeyFrameReq 已成为**真正生效**的恢复主路径 (此前 NVENC 缺 FORCEIDR 标志,请求被静默忽略)。因此周期 IDR 的角色变了: 从「唯一恢复手段」降级为「兜底手段」,只用于防「请求连续丢失」这种极端情况。 既然是兜底,就没必要配得那么长。 问题:三处周期不一致,且都过长 - agent/src/capture.rs NVENC:gop_len = 240 帧 @60fps = 4 秒 - agent/src/capture.rs 软编: intra_period = 300 帧 @60fps = 5 秒 - agent-core/src/lib.rs Linux: intra_period = 300 帧 @30fps = **10 秒** 同一产品在三种编码后端上的恢复等待时间差 2.5 倍,且 Linux 端最差。 改动 - 新增 agent/src/capture.rs::INTRA_PERIOD_FRAMES = 60(Windows 两个编码器共用) - agent-core 的 EncoderSettings::default() 改为按 max_fps 推导(1 秒对应帧数), 避免以后调帧率时周期又漂移 代价 IDR 是整帧内编码,实测一次约 95-120KB,而 P 帧约 3-10KB。1 秒一次 IDR 约 增加 0.8Mbps 开销,在 5Mbps 码率下可接受。 验证(dump_stream 观察 150 帧) [ 0] seq=0 IDR ← 首帧 [ 60] seq=60 IDR ← 间隔精确 60 帧 [120] seq=120 IDR @60fps 即每 1 秒一个自动 IDR,与 gop_len=60 配置一致。 --- agent-core/src/lib.rs | 9 ++++++--- agent/src/capture.rs | 26 +++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/agent-core/src/lib.rs b/agent-core/src/lib.rs index c6c6f67..c623a91 100644 --- a/agent-core/src/lib.rs +++ b/agent-core/src/lib.rs @@ -79,11 +79,14 @@ pub struct EncoderSettings { impl Default for EncoderSettings { fn default() -> Self { - // 对标 UU:屏幕内容实时档;关键帧周期 300 帧 ≈ 10s + // 关键帧周期:以**时间**为准而非固定帧数,避免帧率变化后恢复窗口漂移。 + // 1 秒 @30fps = 30 帧。主恢复路径是 KeyFrameReq(协议 v2 后已确认生效), + // 自动 IDR 只是兜底;此前 300 帧 @30fps 意味着兜底窗口长达 10 秒。 + let max_fps = 30.0f32; Self { bitrate_bps: 5_000_000, - max_fps: 30.0, - intra_period: 300, + max_fps, + intra_period: (max_fps as u32).max(1), } } } diff --git a/agent/src/capture.rs b/agent/src/capture.rs index a01c6f7..b760b50 100644 --- a/agent/src/capture.rs +++ b/agent/src/capture.rs @@ -39,6 +39,17 @@ use windows::Win32::Graphics::Dxgi::{ use std::time::{Duration, Instant}; +/// 周期 IDR 的帧间隔(兜底恢复点)。 +/// +/// 两个编码器共用同一常量,避免 NVENC 与软编的恢复窗口不一致——此前分别是 +/// 240 帧@60fps=4s 与 300 帧@60fps=5s,而 Linux 端又是 300 帧@30fps=10s。 +/// +/// 取值理由:**主恢复路径是 KeyFrameReq,不是自动 IDR**(协议 v2 后已确认请求 +/// 真正生效,见 vendor/nvenc 的 FORCEIDR 补丁)。自动 IDR 只用于兜底 +/// 「请求连续丢失」这种极端情况。60 帧 @60fps = 1 秒,既足够兜底, +/// 又不会因过密的整帧内编码抬高码率。 +pub const INTRA_PERIOD_FRAMES: u32 = 60; + pub struct Duplication { pub device: ID3D11Device, context: ID3D11DeviceContext, @@ -201,8 +212,15 @@ impl NvencEncoder { // CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps) config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR; config.preset_cfg.rc_params.average_bit_rate = bitrate_bps; - config.preset_cfg.gop_len = 240; // 周期 IDR(60fps 下 4s):网络异常后的自愈恢复点, - // 配合 KeyFrameReq 即时恢复,避免长时间冻结 + // 周期 IDR 是**兜底恢复点**,不是主恢复路径——主路径是 KeyFrameReq + // (协议 v2 后已确认真正生效,见 vendor/nvenc 的 FORCEIDR 补丁)。 + // + // 取值权衡:GOP 越短,丢帧后自动恢复越快,但每次 IDR 都是整帧内编码, + // 码率开销显著(1080p 桌面 IDR 通常数倍于 P 帧)。 + // 60 帧 @60fps = 1s:既覆盖「KeyFrameReq 连续丢失」这种极端情况, + // 又把兜底窗口压到 1 秒以内。原先 240 帧 = 4s,在请求已可靠的前提下 + // 只会让受害时长无谓地长 4 倍。 + config.preset_cfg.gop_len = INTRA_PERIOD_FRAMES; config.preset_cfg.frame_interval_p = 1; // 无 B 帧 let init = InitParams { @@ -312,7 +330,9 @@ pub fn run(tx: Sender>>, keyreq: Arc, congestion: Arc .max_frame_rate(FrameRate::from_hz(60.0)) .rate_control_mode(RateControlMode::Bitrate) .usage_type(UsageType::ScreenContentRealTime) - .intra_frame_period(IntraFramePeriod::from_num_frames(300)) + .intra_frame_period(IntraFramePeriod::from_num_frames( + INTRA_PERIOD_FRAMES, + )) .num_threads(4), ) { Ok(e) => {