feat: M2 P1.5 robustness — IDR-on-request, send pacing, min-parity floor

Graceful FEC behavior on a lossy link: at a realistic 2% packet loss the stream
is now steady 0% (was spiking 40-60%). Verified live.

- IDR/RFI handling: the control thread recognizes the client's recovery requests
  (0x0301 invalidate-reference-frames, 0x0302 request-IDR, 0x0305) and sets a
  shared force_idr flag; the video thread forces an NVENC keyframe on the next
  frame (Encoder::request_keyframe → input frame pict_type = I). Without this, a
  frame that exceeds the FEC budget broke the reference chain until the next GOP
  IDR (~2s), cascading to most of the stream being undecodable.
- Min-parity floor: honor the client's x-nv-vqos[0].fec.minRequiredFecPackets
  (it asks for 2). Small P-frames previously got m=ceil(k*20/100)=1 parity — a
  single loss broke them; flooring m>=2 (capped so k+m<=255, wire pct recomputed)
  protects them. This is what turned the 2% spikes into steady 0%.
- Send pacing: spread each frame's packets evenly across the frame interval
  instead of blasting them at line rate (a real link drops microbursts), matching
  Sunshine's rate-controlled sends; sub-500us sleeps skipped (unreliable).

Note: sustained ~8% uniform loss still degrades — that exceeds 20% FEC for
reference-frame video and real Sunshine degrades there too; real networks are
<1% or bursty, which this now handles cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 12:14:59 +00:00
parent 72f8c05aa3
commit af4360c930
7 changed files with 96 additions and 24 deletions
+14
View File
@@ -42,6 +42,8 @@ pub struct NvencEncoder {
fps: u32,
/// Monotonic presentation index, in `1/fps` time-base units.
frame_idx: i64,
/// Force the next submitted frame to be an IDR (set by [`request_keyframe`]).
force_kf: bool,
}
impl NvencEncoder {
@@ -95,6 +97,7 @@ impl NvencEncoder {
height,
fps,
frame_idx: 0,
force_kf: false,
})
}
}
@@ -147,10 +150,21 @@ impl Encoder for NvencEncoder {
}
self.frame.set_pts(Some(self.frame_idx));
self.frame_idx += 1;
// Force an IDR when requested (client RFI); otherwise let NVENC pick (GOP/P-frame).
if self.force_kf {
self.frame.set_kind(ffmpeg::picture::Type::I);
self.force_kf = false;
} else {
self.frame.set_kind(ffmpeg::picture::Type::None);
}
self.enc.send_frame(&self.frame).context("send_frame")?;
Ok(())
}
fn request_keyframe(&mut self) {
self.force_kf = true;
}
fn poll(&mut self) -> Result<Option<EncodedFrame>> {
let mut pkt = Packet::empty();
match self.enc.receive_packet(&mut pkt) {