fix(go2rtc): cut snapshot cache from 30s to 1s so detection sees fresh frames - #479
Draft
johnchia wants to merge 2 commits into
Draft
fix(go2rtc): cut snapshot cache from 30s to 1s so detection sees fresh frames#479johnchia wants to merge 2 commits into
johnchia wants to merge 2 commits into
Conversation
The snapshot URL requested cache=30s, which makes go2rtc hand back byte-identical JPEG data for up to 30 seconds. go2rtc's cache is an unconditional TTL cache checked before the stream is touched at all (internal/mjpeg/mjpeg.go): on a hit it writes the stored payload and returns, so a healthy, actively producing stream is served a stale frame exactly as a stalled one is. The comment here claimed the cache only came into play when the producer was unavailable, which is not what go2rtc implements. API detection calls detect_objects_api_snapshot() ahead of any frame decode, so every detection ran against that cached frame. A stream at detection_interval=1 therefore re-ran detection once a second on one frozen image, and an object entering the scene stayed invisible until the cache expired - up to 30s later. Observed on a live 1 Hz stream as 28 consecutive person detections carrying a single distinct confidence value and a single distinct bounding box. Measured against a running instance: six requests with cache=30s interleaved with six without, over the same 12s window, while the stream was demonstrably healthy. The cached requests returned 1 distinct frame (~0.001s each), the uncached ones 6 distinct frames (~1s each). 1s preserves the reconnect tolerance the parameter was added for while matching the shortest interval lightNVR can express, since both detection gates compare whole seconds.
johnchia
marked this pull request as ready for review
August 3, 2026 22:41
Author
|
Confirmed on bench cam running light-object-detect with tiled detection (openvino backend igpu execution) |
johnchia
marked this pull request as draft
August 3, 2026 22:55
… cost The rewritten comment still opened with the original framing -- that cache= exists so an unavailable producer does not stall the caller. That is the behaviour the old comment claimed, not the behaviour go2rtc implements: the TTL is checked unconditionally, ahead of any stream access, so stream health never enters into it. Also document the cost this shifts onto the caller. On a cache miss go2rtc waits for the next keyframe, so the fetch takes up to one GOP (~1s measured on a 1s GOP) against ~1ms for a hit, and that wait happens inline on the UDT main loop between av_read_frame() calls.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the go2rtc snapshot request behavior to reduce stale-frame reuse so API-based detection can operate on fresher frames when snapshots are polled frequently.
Changes:
- Reduce go2rtc snapshot TTL cache from
cache=30stocache=1s. - Rewrite the in-code comment to describe go2rtc’s
cache=semantics and the keyframe-wait latency tradeoff.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // detection freshness for less stalling. 1s bounds staleness at roughly one detection | ||
| // interval, which is as fine as lightNVR can express since both detection gates | ||
| // compare whole seconds. | ||
| snprintf(url, sizeof(url), "http://localhost:1984" GO2RTC_BASE_PATH "/api/frame.jpeg?src=%s&cache=1s", encoded_name); |
Comment on lines
+143
to
+149
| // On a miss go2rtc waits for the stream's *next* keyframe, so the fetch costs up to | ||
| // one GOP (measured ~1s on a 1s GOP; a cache hit is ~1ms). That cost lands on this | ||
| // thread: the UDT main loop calls run_detection_on_frame() between av_read_frame() | ||
| // calls, so a slow fetch stalls packet reading for the stream. A larger value trades | ||
| // detection freshness for less stalling. 1s bounds staleness at roughly one detection | ||
| // interval, which is as fine as lightNVR can express since both detection gates | ||
| // compare whole seconds. |
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.
go2rtc_snapshot.crequested snapshots withcache=30s. That parameter does not do what the comment beside it claimed, and the consequence is that API detection runs on frames up to 30 seconds old.What
cache=actually doesThe comment asserted the cache only comes into play when the producer is unavailable:
go2rtc implements it as an unconditional TTL cache, checked before the stream is touched at all —
handlerKeyframeininternal/mjpeg/mjpeg.go:There is no stream-health condition anywhere on that path. A healthy, actively producing stream is served a stale frame exactly as a stalled one is.
Impact
run_detection_on_frame()callsdetect_objects_api_snapshot()ahead of any frame decode, so with go2rtc enabled every API detection uses this cached JPEG. A stream atdetection_interval=1therefore re-runs detection once a second against one frozen image, and an object entering the scene stays invisible until the cache expires — up to 30 s later.Observed on a live 1 Hz 2560×1440 stream: 28 consecutive
personrows carrying a single distinct confidence value and a single distinct bounding box.In effect the 30 s cache silently defeats any
detection_intervalshorter than 30 s.Measurement
Six requests with
cache=30sinterleaved with six without, over the same 12 s window, with the stream demonstrably healthy throughout — the uncached requests interleaved between the cached ones returned a fresh frame every time:cache=30scacheparamWith
cache=1s, polling at 1 Hz for 10 requests: 10 of 10 distinct frames. The staleness is gone.Side effects of
cache=1sThis is not free, and the cost is worth stating plainly.
Fetch latency rises. On a miss go2rtc waits for the stream's next keyframe, so a fetch costs up to one GOP. Measured at 1 Hz polling against a 1 s GOP, latency alternates cleanly between a hit and a miss:
cache=30scache=1sThat wait is paid on the UDT main loop. The loop is
av_read_frame()(line 1726) →process_packet()(line 1746) →run_detection_on_frame(), single-threaded, so a slow snapshot fetch stalls packet reading for that stream. This repo already treats that as a hazard — the ONVIF path was moved to its own thread for exactly this reason, and the comment introducing it notes that "SOD and API detection paths are completely unaffected", i.e. still inline.So on a 1 s GOP at
detection_interval=1, the reader goes from stalling ~170 ms per cycle (the detect API POST alone) to roughly 170 ms + up to 1 s on the cycles that miss. I have not measured the downstream effect on recording continuity or on the effective detection rate; both could plausibly degrade, and the rate is partly self-limiting since a longer cycle means fewer detections. Flagging it rather than claiming it is benign.What is not affected:
AddConsumer/RemoveConsumerdoes not reconnect to the camera: go2rtc holds one persistent RTSP producer shared across consumers (confirmed via/api/streams— a single producer with 374 MB received), so snapshots attach to the existing feed.Alternatives, if the stall is judged too expensive
Both are larger changes than this PR and I have not implemented either:
detect_objects_api_snapshot()runs before the decode path, but the caller already holds the keyframe packet. Using the decoded frame costs a decode plus a JPEG encode locally and involves no network round trip and no keyframe wait.The value could also be made configurable rather than hardcoded, so deployments can pick their own point on the freshness/stall trade.
The change
cache=30s→cache=1s, plus the comment rewritten to describe the actual TTL behaviour and to record the keyframe-wait cost it shifts onto the caller.1 s bounds staleness at roughly one detection interval, which is as fine as lightNVR can express — both detection gates compare whole seconds in
unified_detection_thread.c, sodetection_intervalcannot go below 1.Draft because the fix is verified against go2rtc's source and by direct measurement of the live endpoint, but I have not yet run a rebuilt image end to end, and the packet-reader stall above deserves a maintainer's judgement.