Request an IDR frame when the FEC queue reports a loss without RFI - #147
Open
anderizquierdo wants to merge 1 commit into
Open
Conversation
anderizquierdo
force-pushed
the
fix/idr-request-after-fec-queue-loss-without-rfi
branch
from
July 30, 2026 11:19
95eac7d to
8997c9d
Compare
notifyFrameLost() is called by the video RTP FEC queue to report a frame loss early, so recovery can begin without waiting for the next received frame. When reference frame invalidation is unavailable (strictIdrFrameWait), dropFrameState() sets waitingForIdrFrame and notifyFrameLost() then returns without doing anything else: it neither notifies the host nor sets waitingForNextSuccessfulFrame. waitingForNextSuccessfulFrame is only ever set by the frame gap detection in processRtpPayload(), so a loss reported through this path leaves the client waiting for an IDR frame that it never requests. Every subsequent frame is dropped with "Waiting for IDR frame" until consecutiveFrameDrops reaches CONSECUTIVE_DROP_LIMIT, which finally requests one - 120 frames later. That is 2 seconds of frozen video at 60 FPS and 4 seconds at 30 FPS for a loss that the existing mechanism would have recovered from after the next fully received frame. Set waitingForNextSuccessfulFrame in that branch so the existing request path in processRtpPayload() applies, keeping its deliberate delay until the first fully received frame to avoid requesting IDR frames while the network is still unstable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
anderizquierdo
force-pushed
the
fix/idr-request-after-fec-queue-loss-without-rfi
branch
from
July 30, 2026 11:19
8997c9d to
1b3ef51
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On a client where reference frame invalidation is not available, a frame loss reported by the video RTP FEC queue leaves the client waiting for an IDR frame that it never requests. Recovery is delayed until
CONSECUTIVE_DROP_LIMITis reached — 120 frames, which is 2 seconds of frozen video at 60 FPS and 4 seconds at 30 FPS.notifyFrameLost()exists specifically to speed up recovery ("This lets us avoid having to wait until the next received frame to determine that we lost a frame"). In the non-RFI case it currently makes recovery an order of magnitude slower than the frame gap path it was meant to improve on.What happens
On the setup below this is reproducible every few minutes, on a link that is not losing a single packet:
reconstructFrame()predicts the frame is unrecoverable. With only that one shard present,missingPackets(46) exceedstotalPackets - neededPackets(8 parity shards), so it reports a speculative loss for a frame that completes microseconds later. The code already anticipates being wrong here:notifyFrameLost()drops the frame and returns without asking for anything, because with RFI unavailable every other action in that function is behindif (!waitingForIdrFrame).waitingForNextSuccessfulFrame— the flag that triggers the request — is only set by a different code path that did not run.CONSECUTIVE_DROP_LIMITsafety net finally requests one. That is the freeze.This PR addresses step 4, which is where the 3 µs fault becomes seconds of frozen video.
Why recovery stalls
notifyFrameLost()callsdropFrameState(), which decides between RFI and IDR recovery:with
strictIdrFrameWait = !isReferenceFrameInvalidationEnabled();.Back in
notifyFrameLost(), everything else is insideif (!waitingForIdrFrame), so with RFI unavailable the function returns having done nothing but drop the frame:The IDR request would normally come from
processRtpPayload():But
waitingForNextSuccessfulFrame = trueis set in exactly one place — theNetwork dropped N framesgap detection inprocessRtpPayload(). A loss reported throughnotifyFrameLost()never sets it, so the guard is always false andLiRequestIdrFrame()is never called. The client logsWaiting for IDR framefor every subsequent frame untildropFrameState()hits the drop limit and requests one as a last resort.Note that the sibling loss paths in
processRtpPayload()already handle this correctly:The fix
Set
waitingForNextSuccessfulFramein the non-RFI branch, so the existing request path applies. This deliberately keeps the "wait for the first fully received frame" behaviour rather than requesting an IDR frame immediately, to preserve the congestion-collapse protection that comment describes.Evidence
Client: Raspberry Pi 4 / DietPi,
moonlight-qt6.1.0, EGLFS, HEVC via the kernel V4L2 request decoder, wired gigabit. Host: Sunshine 2026.516.143833, NVENC. Stream 2560x1440.Symptom: video freezes for 2-4 s while audio and the game continue, roughly every 5-12 minutes.
Every episode has an identical signature, and the frame count is always exactly 120:
Across a 45-minute session: 5 episodes,
Waiting for IDR framelogged 600 times (= 5 x 120 exactly),Sending speculative RFI request0 times (confirming RFI is off on this client),Network dropped0 times (confirming the gap path never ran).A packet capture on the client NIC (
tcpdump -s 80, one RX queue, RPS off, GRO off, zero kernel drops) shows nothing was actually lost. Over ~100,000 video packets there were 0 missing and exactly one reordering event per episode:Impact
Any client without RFI, on any path that occasionally reorders a packet, gets a 120-frame freeze instead of a ~1-frame recovery. It also affects genuinely lost frames reported by the FEC queue, not just this false positive.
Testing
Validated end-to-end on the affected device. The patched
moonlight-common-cwas built into moonlight-qt v6.1.0 using the officialrpi64/trixiepackaging container, so the only difference from the released package is this patch.Two sessions of comparable length, same host, same game, same stream settings:
Leaving speculative RFI mode after OOS video dataWaiting for IDR frameReached consecutive drop limitConnection status update: 1(connection poor)The trigger still fires with the patch, so this is a like-for-like comparison rather than an absence of events. The capture shows the same thing both times: the last FEC parity shard of a frame arrives ~2 µs before that frame's first data packet (46 sequence positions early), with zero missing and zero duplicate packets.
What changes is the cost. The IDR frame is now requested in the same second the out-of-sequence data is seen, instead of 120 frames later, and
Reached consecutive drop limitnever appears. Incoming, decoding and rendering frame rates were identical (56.58 / 56.58 / 56.57 FPS), so nothing else regressed.Build sanity:
moonlight-common-calso configures and builds standalone with the change (CMake + GCC 15, Release);VideoDepacketizer.ccompiles with no new warnings.