perf(agent): 关键帧周期从 4s/10s 统一到 1s,缩短丢帧后的兜底恢复窗口

背景
协议 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 配置一致。
This commit is contained in:
郑博文 2026-10-28 16:35:44 +08:00
parent 8410a8de2a
commit 6e83bda8b8
2 changed files with 29 additions and 6 deletions

View File

@ -79,11 +79,14 @@ pub struct EncoderSettings {
impl Default for EncoderSettings { impl Default for EncoderSettings {
fn default() -> Self { fn default() -> Self {
// 对标 UU:屏幕内容实时档;关键帧周期 300 帧 ≈ 10s // 关键帧周期:以**时间**为准而非固定帧数,避免帧率变化后恢复窗口漂移。
// 1 秒 @30fps = 30 帧。主恢复路径是 KeyFrameReq(协议 v2 后已确认生效),
// 自动 IDR 只是兜底;此前 300 帧 @30fps 意味着兜底窗口长达 10 秒。
let max_fps = 30.0f32;
Self { Self {
bitrate_bps: 5_000_000, bitrate_bps: 5_000_000,
max_fps: 30.0, max_fps,
intra_period: 300, intra_period: (max_fps as u32).max(1),
} }
} }
} }

View File

@ -39,6 +39,17 @@ use windows::Win32::Graphics::Dxgi::{
use std::time::{Duration, Instant}; 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 struct Duplication {
pub device: ID3D11Device, pub device: ID3D11Device,
context: ID3D11DeviceContext, context: ID3D11DeviceContext,
@ -201,8 +212,15 @@ impl NvencEncoder {
// CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps) // CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps)
config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR; config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR;
config.preset_cfg.rc_params.average_bit_rate = bitrate_bps; config.preset_cfg.rc_params.average_bit_rate = bitrate_bps;
config.preset_cfg.gop_len = 240; // 周期 IDR(60fps 下 4s):网络异常后的自愈恢复点, // 周期 IDR 是**兜底恢复点**,不是主恢复路径——主路径是 KeyFrameReq
// 配合 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 帧 config.preset_cfg.frame_interval_p = 1; // 无 B 帧
let init = InitParams { let init = InitParams {
@ -312,7 +330,9 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
.max_frame_rate(FrameRate::from_hz(60.0)) .max_frame_rate(FrameRate::from_hz(60.0))
.rate_control_mode(RateControlMode::Bitrate) .rate_control_mode(RateControlMode::Bitrate)
.usage_type(UsageType::ScreenContentRealTime) .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), .num_threads(4),
) { ) {
Ok(e) => { Ok(e) => {