fix: video: apply the configured jitter buffer target to RTSP streams - #2949
Conversation
Automated PR Review — round 1Warning RTSP video in Cockpit is played by a second, separate connection manager that never applied the user's video-latency setting, so those streams ran on whatever buffering the browser decided on its own. This PR moves the one-line-per-receiver tuning into a shared function, hands it to the RTSP manager as well, and additionally makes the RTSP connection report the browser-side receive statistics (buffer delay, dropped frames) that the debug overlay and the data lake previously only had for the other stream type. What still needs attention
Change map — what was established before judgingClaims (from the PR body — the author's hypothesis, checked against the code):
Failure site: Entry points
Invariants
5. Performance — 1 finding5.1 — go2rtc stats connections are registered per reconnect and never removed —
Because the id is new on every connection, the guard never matches after a reconnect and (I could not check whether Second, smaller half of the same problem: both Fix: remove the stream's previous peer before adding the new one (keep the last registered id per stream next to Consequence: on a machine with an RTSP camera that reconnects, Cockpit slowly piles up measurement loops it never stops, so video and the whole interface get progressively slower the longer the session runs. 6. UI / UX — 1 finding6.1 — the added
The RTSP branch is fine: 11 rows after the two additions, last baseline 132. Fix options, cheapest first: bump the default Consequence: with a video player's stats overlay enabled, the bottom line of text is drawn over the little bitrate/framerate charts, so neither reads cleanly. 11. Nitpicks / Optional — 2 findings11.1 — needless lint suppression in the new test —
Consequence: none at runtime; one less piece of misleading scaffolding for the next reader of the test. 11.2 — test file does not mirror the source folder — The code lands in Consequence: none at runtime; tests stay findable from the file they cover. Sections with nothing to report (8)1. Correctness & Implementation Bugs — ✅ (moved body is identical to the deleted 2. Persistence & User Data — ✅ (the only persisted key involved, 3. AGENTS.md Adherence — ✅ (shared logic extracted once for its two call sites rather than duplicated, per "Reuse before reinventing"; no new dependency — 4. Security — ✅ (no new dependency, no new network host — the 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ (three commits from 9. Tests — ✅ (the PR only adds 10. Documentation — ✅ (RTSP stays Standalone-only and the Lite guard plus its explanatory dialog at Generated by Claude. This is advisory; a human reviewer must still approve. |
f744099 to
c5f3aa8
Compare
The receiver tuning lived inside the mavlink-camera-manager session, so the RTSP-via-go2rtc manager had no way to reach it. Move it to a free function that any peer connection can be handed to, with a test covering the video-only filter, the clamp, and that a zero target leaves the legacy hint unset.
The receiver tuning ran only on the mavlink-camera-manager path, so a nonzero target set in the video configuration reached WebRTC streams and was silently ignored by RTSP ones. At the default of zero both paths end up on the browser's own adaptive buffer, so this changes nothing until the setting is actually used.
c5f3aa8 to
66d33f9
Compare
Review follow-up — round 1Done
Done differently
Won't change (with reasoning)
|
|
/review |
Automated PR Review — round 2Warning Cockpit plays RTSP video through a second, separate connection manager that never applied the user's video-buffer setting, so that setting silently did nothing on those streams. This PR moves the per-receiver tuning into one shared function and hands it to the RTSP manager too, and additionally exposes the RTSP connection's browser-side receive statistics (buffer delay, dropped frames) to the debug overlay and the data lake, which previously only had them for the other stream type. What still needs attention
🙋 Decisions for a human5.1 — go2rtc stats connections registered per reconnect and never removed
The checkbox records the decision; the finding itself closes only on Since round 1 — 3 closed, 1 disputed, comparing f744099 → 66d33f9Range. The increment is not usable this round. All three commits in No 6.1 — Added The finding asked for the 12-row list to regain clearance over the plot band. 11.1 — Needless The suppression pair is gone from The finding's stated reason was wrong, and the author is right about why: 11.2 — Test file does not mirror the source folder — ✅ Addressed The test now lands at 5.1 — go2rtc stats connections registered per reconnect and never removed — 💬 Disputed (stays open) No code changed at the two registration sites or in Discussion since round 1. One substantive comment, from the PR author (#issuecomment-5343722105), plus a bare Nothing in Change map — what was established before judgingClaims (from the rewritten PR body — the author's hypothesis, checked against the code):
Failure site: Entry points
Invariants
5. Performance — 1 finding (disputed, carried from round 1)5.1 — go2rtc stats connections are registered per reconnect and never removed —
Because the id is new on every connection, the guard never matches after a reconnect and The author disputes both halves (comment), on the grounds that If the maintainers accept the argument, the right close for this is Consequence: if the third-party statistics library does not drop closed connections by itself, a topside computer with a flaky RTSP camera accumulates measurement targets it never releases and gets slower the longer the session runs — on exactly the low-powered machines this PR was written for. Sections with nothing to report (10)1. Correctness & Implementation Bugs — ✅ (moved body is identical to the deleted 2. Persistence & User Data — ✅ (collapsed because the PR's persistence footprint is empty: the only key involved, 3. AGENTS.md Adherence — ✅ (shared logic extracted once for its two call sites rather than duplicated, per "Reuse before reinventing"; no new dependency — 4. Security — ✅ (no new dependency, no new network host — the 6. UI / UX — ✅ (the overlay overflow from round 1 is fixed and re-measured against 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ (three commits in 9. Tests — ✅ (the PR adds 10. Documentation — ✅ (RTSP stays Standalone-only and the Lite guard plus its explanatory dialog at 11. Nitpicks / Optional — ✅ (both round-1 nits are closed this round — the lint suppression and the test's location, see the since-last-round block — and the reread of Generated by Claude. This is advisory; a human reviewer must still approve. |
66d33f9 to
e17a7e4
Compare
Review follow-up — round 2Done
Won't change (with reasoning)
|
|
/review |
|
/resolve 5.1 - Should be fixed. |
|
Recorded: rafaellehmkuhl resolved 5.1. Comment |
Automated PR Review — round 3Warning Cockpit plays RTSP video through a second, separate connection manager that never applied the user's video-buffer setting, so that setting silently did nothing on those streams. This PR moves the per-receiver tuning into one shared function and hands it to the RTSP manager too, and additionally exposes the RTSP connection's browser-side receive statistics (buffer delay, dropped frames) to the debug overlay and to the data lake, which previously had them only for the other stream type. What still needs attention
🙋 Decisions for a human5.1 — go2rtc stats connections registered per reconnect and never removed
The checkbox records the decision; the finding itself closes only on Since round 2 — 0 closed, 1 still disputed, comparing 66d33f9 → e17a7e4Range. The increment is again not usable as a delta. No 5.1 — go2rtc stats connections registered per reconnect and never removed — 💬 Disputed (stays open) One of the three things this finding asked for landed in code: Discussion since round 2. One substantive comment from the PR author (#issuecomment-5343900160), plus a bare Nothing in Change map — what was established before judgingClaims (from the PR body — the author's hypothesis, checked against the code):
Failure site: Entry points
Invariants
5. Performance — 1 finding (disputed, carried from round 1)5.1 — go2rtc stats connections are registered per reconnect and never removed —
Because the id is new on every connection, the guard never matches after a reconnect and What changed this round. The accessor now documents the assumption: The author disputes both remaining halves (comment), now quoting the built One in-tree detail corroborates the "the library does not watch our handlers" part without verifying it: If the maintainers accept the argument, the right close for this is Consequence: if the third-party statistics library does not drop closed connections by itself, a topside computer with a flaky RTSP camera accumulates measurement targets it never releases and gets slower the longer the session runs — on exactly the low-powered machines this PR was written for. Sections with nothing to report (10)1. Correctness & Implementation Bugs — ✅ (moved body is identical to the deleted 2. Persistence & User Data — ✅ (collapsed because the PR's persistence footprint is empty: the only key involved, 3. AGENTS.md Adherence — ✅ (shared logic extracted once for its two call sites rather than duplicated, per "Reuse before reinventing"; the new module under 4. Security — ✅ (no new dependency, no new network host — the 6. UI / UX — ✅ (the round-1 overlay overflow stays fixed and was re-measured this round against the code rather than the claim: 7. Code Quality & Style — ✅ ( 8. Commit Hygiene — ✅ (same three commits as round 2 — 9. Tests — ✅ (the PR adds 10. Documentation — ✅ (RTSP stays Standalone-only and the Lite guard plus its explanatory dialog at 11. Nitpicks / Optional — ✅ (both round-1 nits stay closed, and this round's reread of Generated by Claude. This is advisory; a human reviewer must still approve. |
e17a7e4 to
14e2c8d
Compare
Review follow-up — round 3Done
My reading of the library still says the removal was not strictly required, but the code no longer depends on that reading being right, which is the better place to be. |
|
/review |
Automated PR Review — round 4Note 📝 MINOR SUGGESTIONS — 1 open finding (1 minor, new this round); 4 closed: 5.1 resolved by a maintainer this round, 3 addressed in earlier rounds. Cockpit plays RTSP video through a second, separate connection manager that never applied the user's video-buffer setting, so that setting silently did nothing on those streams. This PR moves the per-receiver tuning into one shared function and hands it to the RTSP manager too, exposes the RTSP connection's browser-side receive statistics (buffer delay, dropped frames) to the debug overlay and to the data lake, and — new this round — makes both statistics registration sites go through one helper that replaces the previous connection's monitor instead of piling a new one on top, and moves the RTSP reconnect trigger off a property another holder of the connection could overwrite. What still needs attention
Since round 3 — 1 closed (resolved by a maintainer), 1 new finding, comparing e17a7e4 → 14e2c8dRange. The increment is again not usable as a delta. One 5.1 — go2rtc stats connections registered per reconnect and never removed — ☑️ Resolved by All three halves of the round-2/3 remedy are now in the diff, so this would have been ✅ Addressed even without the resolution:
The one-line hardening round 3 flagged as worth doing whichever way 5.1 was settled also landed: 6.1, 11.1, 11.2 — closed in earlier rounds, unchanged this round; carried in the ledger. Discussion since round 3. One substantive comment from the PR author (#issuecomment-5344247374) plus a bare Nothing in Change map — what was established before judgingClaims (from the PR body and the round-3 follow-up comment — the author's hypothesis, checked against the code):
Failure site: Entry points
Invariants
7. Code Quality & Style — 1 finding
7.1 — The new shared stats helper drops monitors it does not own, and would drop the one it just added —
Two preconditions are load-bearing here, and neither is stated in the JSDoc nor checked in the function:
Consequence: nothing misbehaves today, but the one function both statistics paths now go through will silently stop measuring a stream if it is ever handed an instance shared between streams or a peer id it already knows, and the only symptom is numbers that stop moving in the debug overlay and in the recorded stream variables. Sections with nothing to report (10)1. Correctness & Implementation Bugs — ✅ (the 2. Persistence & User Data — ✅ (collapsed because the PR's persistence footprint is empty: the only key involved, 3. AGENTS.md Adherence — ✅ (the round-3 addition went into 4. Security — ✅ (no new dependency and no new network host — the 5. Performance — ✅ (the accumulation raised as 5.1 is gone in code: 6. UI / UX — ✅ (re-measured the overlay geometry against the code rather than the claim: 8. Commit Hygiene — ✅ (four commits — 9. Tests — ✅ (the PR adds 10. Documentation — ✅ (RTSP stays Standalone-only and the Lite guard plus its explanatory dialog at 11. Nitpicks / Optional — ✅ (both round-1 nits stay closed, and this round's reread of Generated by Claude. This is advisory; a human reviewer must still approve. |
The stats overlay and the data-lake stream variables only ever saw the mavlink-camera-manager peer connection, so RTSP streams reported go2rtc's ingest rates and nothing about what the browser did with the frames after that. Expose their peer connection through the same store accessor, keyed by a fresh id per connection so a reconnect re-registers instead of leaving the stats pinned to a closed one, and show the jitter buffer delay on both protocols so their latency can be compared.
The reconnect logic lived on pc.onconnectionstatechange, a single-slot property any other holder of the peer connection can overwrite, which would leave an RTSP stream never reconnecting and give no sign of why.
14e2c8d to
c9a7c0f
Compare
Review follow-up — round 4Done
Both folded into |
|
/review |
Automated PR Review — round 5Note 📝 MINOR SUGGESTIONS — 1 open finding (1 minor, new this round); 5 closed: 7.1 addressed this round, 5.1 resolved by a maintainer in round 4, 3 addressed in earlier rounds. Cockpit plays RTSP video through a second, separate connection manager that never applied the user's video-buffer setting, so that setting silently did nothing on those streams. This PR moves the per-receiver tuning into one shared function and hands it to the RTSP manager too, exposes the RTSP connection's browser-side receive statistics (buffer delay, dropped frames) to the debug overlay and to the recorded stream variables, routes both statistics registration sites through one helper that replaces the previous connection's monitor instead of piling a new one on top, and moves the RTSP reconnect trigger off a property another holder of the connection could overwrite. What still needs attention
Since round 4 — 1 closed (7.1 addressed), 1 new finding, comparing 14e2c8d → c9a7c0fRange. The increment is again not usable as a delta. The one 7.1 — Shared stats helper drops monitors it does not own and would drop the peer it just added — ✅ Addressed The finding asked for two things, and both landed:
The second test case in 5.1, 6.1, 11.1, 11.2 — closed in earlier rounds, unchanged this round; carried in the ledger. Discussion since round 4. One substantive comment from the PR author (#issuecomment-5344489069) plus a bare Nothing in Change map — what was established before judgingClaims (from the PR body, checked against the code — the author's hypothesis, not adopted):
Failure site: Entry points
Invariants
1. Correctness & Implementation Bugs — 1 finding1.1 — The new go2rtc branch of
const go2rtcManager = data?.go2rtcManager
if (go2rtcManager?.peerConnection) {
const { peerConnection, connectionId } = go2rtcManager
return { peerConnection, peerId: connectionId, sessionId: connectionId }
}
The sibling WebRTC branch three lines above does it the other way, reading if (go2rtcManager?.peerConnection) {
const { connectionId } = go2rtcManager
return { peerConnection: go2rtcManager.peerConnection, peerId: connectionId, sessionId: connectionId }
}Two things bound how far this goes, and both belong in the finding:
Sections with nothing to report (10)2. Persistence & User Data — ✅ (collapsed because the PR's persistence footprint is empty: the only key involved, 3. AGENTS.md Adherence — ✅ (the shared helper lives in 4. Security — ✅ (no new dependency and no new network host — the 5. Performance — ✅ (re-walked the automatic paths: 6. UI / UX — ✅ (re-measured the overlay geometry from the code rather than the claim: 7. Code Quality & Style — ✅ (7.1 closed this round — see the since-round-4 block; 8. Commit Hygiene — ✅ (four commits — 9. Tests — ✅ ( 10. Documentation — ✅ (RTSP stays Standalone-only and the Lite guard plus its explanatory dialog at 11. Nitpicks / Optional — ✅ (both round-1 nits stay closed, and this round's reread of Generated by Claude. This is advisory; a human reviewer must still approve. |
Review follow-up — round 5Won't change (with reasoning)
No code changed this round, so I have not re-triggered a review. |
Summary
This is not a latency fix — it changes nothing at the default setting. It closes a configuration gap and makes RTSP streams observable. Please read the section below before assuming it helps with RTSP lag.
The "RTP Jitter Buffer (Target) duration" setting was only ever applied to mavlink-camera-manager streams:
Sessionpushes it onto its receivers on every track, whileGo2RTCManagerbuilds its ownRTCPeerConnectionand never did. So a user who raises the target gets it on WebRTC streams and is silently ignored on RTSP ones. Separately, RTSP streams report nothing about what the browser does with their frames, which makes any latency investigation on that path guesswork.src/libs/webrtc/jitter-buffer.ts), moved out ofSessionso both managers can reach the same code. No behavior change for WebRTC streams.The target is read when a stream is activated, so changing the setting requires restarting the stream. That matches the existing WebRTC behavior.
Why this cannot affect the default case
Worth writing down, because it is counter-intuitive and it took reading the Chromium and libwebrtc sources to establish:
0(cockpit-jitter-buffer-target), and clearing the field snaps it back to0rather than leaving it empty, so in practice every user is on0.0the code sendsplayoutDelayHint = null, which means "no hint, use the browser default". So the WebRTC path, at the default, explicitly asks for the same adaptive behavior that the RTSP path was getting by not asking for anything.RTCRtpReceiver.jitterBufferTargetonly shipped in Chrome 124. Electron 29.4.6 pins Chromium 122, so in this build that assignment is a dead property on a JS object and reaches no WebRTC code at all. Only the legacyplayoutDelayHintdoes anything here.SetJitterBufferMinimumDelay, which setsmin_playout_delay_in libwebrtc'sVCMTiming, and the target ismax(min_playout_delay_, jitter_delay_ + RequiredDecodeTime() + render_delay_). It is a floor, not a ceiling: it can only add delay on top of the adaptive estimate, and it cannot make Chromium drop frames. Zero means no floor, which is already the default. The genuine "render frames as they arrive" mode needsmax_playout_delay <= 500msas well, and that has no JavaScript API — only the sender-side RTP playout-delay extension, which go2rtc does not send.So the parity is real and worth having, but the RTSP latency that prompted this (on a 1.1GHz Celeron topside machine, where RTSP takes a permanent latency step on every resource spike and never recovers) is not explained by it. That investigation continues, and the stats added here are what it needs: if the new
Bufferline climbs during a spike and stays up, the delay is on the browser's receive side; if it stays at a few tens of milliseconds while the picture is seconds behind, the delay is accumulating upstream in go2rtc or its socket, which is where I would now look first.Test plan
BufferandFrame drops.Checks
src/tests/libs/webrtc/jitter-buffer.test.tscovers the video-only receiver filter, the 0–4000ms clamp, and that a zero target leaves the legacy hint unset.yarn lintclean with--max-warnings=0,yarn test:unitpassing.yarn typecheckexits early in this repo withlanguageId not found for src/App.vueand checks nothing. I ran plaintscinstead; the only errors in the touched files are two pre-existingURI | undefinedcomplaints on untouchednew WebRTCManager(...)lines.