This is the AI Generated finding....
Summary
When a stream has both record=1 and detection_based_recording=1, the documented behaviour is annotation mode: detections are linked to the continuous recording and no separate MP4s are created. In practice separate detection_*.mp4 files are still written for the full duration of every motion event, duplicating footage that the continuous recorder has already captured.
The cause appears to be that annotation_only is plumbed all the way into the detection context and then never read by any non-test code.
Version
LightNVR v0.37.2
Build date: 2026-08-17
Image: ghcr.io/opensensor/lightnvr:latest
Also present in current main at the time of writing.
Documented / expected behaviour
docs/internal/PRE_DETECTION_BUFFER_IMPLEMENTATION.md:
Annotation mode: When both record=true and detection_based_recording=true, detections are stored with recording_id linking to continuous recordings (no separate MP4s created)
and its recording-mode table:
record |
detection_based_recording |
Detection Thread Behavior |
false |
true |
Creates MP4s on detection (annotation_only=false) |
true |
true |
Links to continuous recording (annotation_only=true) |
The struct field comment says the same (include/video/unified_detection_thread.h:119-121):
// Annotation-only mode: when true, detection runs but does NOT create separate MP4 files
// Detections are stored in the database and linked to the continuous recording
bool annotation_only;
Actual behaviour
Both files are produced concurrently for the same stream and the same wall-clock period:
$ find recordings -name '*.mp4' -mmin -60 -printf '%f %s\n' \
| awk '{split($1,a,"_"); s[a[1]]+=$2; n[a[1]]++} END {for(k in s) printf "%-10s %4d files %6.2f GB/hr\n", k, n[k], s[k]/1073741824}'
detection 19 files 4.03 GB/hr
recording 156 files 10.72 GB/hr
Per stream (Correspondence, camera encodes 720p H.264 CBR @ 2048 kbps):
recording_*.mp4 2.05 Mbps <- matches the camera bitrate exactly
detection_*.mp4 2.76 Mbps <- same footage, written a second time
So ~27% of total write volume is duplicate footage.
Database state agrees that annotation mode is not active — detections are almost never linked to a recording:
sqlite> select case when recording_id is null then 'NULL' else 'linked' end k, count(*)
from detections group by k;
NULL|28398
linked|28
sqlite> select trigger_type, count(*) from recordings group by trigger_type;
continuous|240
detection|27
Under annotation mode I would expect recording_id to be populated and trigger_type='detection' rows not to be created while continuous recording is active.
Suspected cause
annotation_only is computed correctly in every caller and stored on the context:
// src/video/stream_manager.c:210
bool annotation_only = db_streams[i].record;
...
// src/video/unified_detection_thread.c:1015
ctx->annotation_only = annotation_only;
but grep -rn annotation_only src/ include/ shows the struct member is only ever written. The single read of the value in unified_detection_thread.c:1027 is of the local parameter, and only to emit a log line:
if (annotation_only) {
log_info("[%s] Detection will annotate continuous recordings and create "
"detection clips while continuous recording is inactive",
stream_name);
}
The only code that reads ctx->annotation_only is tests/unit/test_external_motion_trigger.c, where the intended semantics are asserted:
if (!ctx->annotation_only) { ... } // :83
if (!ctx->annotation_only && current == UDT_STATE_RECORDING) { ... } // :98
void test_annotation_only_ignores_active_trigger(void) // :231
So the unit tests encode the correct behaviour against a local mock, while the production state machine in unified_detection_thread.c never consults the flag and transitions into UDT_STATE_RECORDING regardless.
Reproduction
- Configure a stream with
record=1, detection_based_recording=1, detection_model=onvif, is_onvif=1.
- Let it run against a camera that emits ONVIF motion events.
- Observe both
recording_*.mp4 and detection_*.mp4 being written for the same period.
- Observe
detections.recording_id is NULL rather than linked.
Impact
On a 12-camera 720p deployment this is ~97 GB/day of duplicate footage, cutting retention on a 3.6 TB volume from ~15 days to ~11.
Reducing post_detection_buffer mitigates it (shorter clips per event) but cannot eliminate it, since the duplicate-recording path runs regardless of the flag.
Note also that detection_based_recording=0 is not a viable workaround for users who want continuous recording with motion markers on the timeline: the ONVIF pull-point subscription lives inside the unified detection thread, which is only started when detection_based_recording is true (src/core/main.c:1062, src/video/stream_manager.c:203). Disabling it stops motion events being recorded at all.
Suggested fix
Gate the recording-state transition and MP4 writer in unified_detection_thread.c on ctx->annotation_only, matching the logic already asserted in tests/unit/test_external_motion_trigger.c — when true, store the detection and link recording_id to the active continuous recording instead of opening a new MP4.
This is the AI Generated finding....
Summary
When a stream has both
record=1anddetection_based_recording=1, the documented behaviour is annotation mode: detections are linked to the continuous recording and no separate MP4s are created. In practice separatedetection_*.mp4files are still written for the full duration of every motion event, duplicating footage that the continuous recorder has already captured.The cause appears to be that
annotation_onlyis plumbed all the way into the detection context and then never read by any non-test code.Version
Also present in current
mainat the time of writing.Documented / expected behaviour
docs/internal/PRE_DETECTION_BUFFER_IMPLEMENTATION.md:and its recording-mode table:
recorddetection_based_recordingfalsetrueannotation_only=false)truetrueannotation_only=true)The struct field comment says the same (
include/video/unified_detection_thread.h:119-121):Actual behaviour
Both files are produced concurrently for the same stream and the same wall-clock period:
Per stream (
Correspondence, camera encodes 720p H.264 CBR @ 2048 kbps):So ~27% of total write volume is duplicate footage.
Database state agrees that annotation mode is not active — detections are almost never linked to a recording:
Under annotation mode I would expect
recording_idto be populated andtrigger_type='detection'rows not to be created while continuous recording is active.Suspected cause
annotation_onlyis computed correctly in every caller and stored on the context:but
grep -rn annotation_only src/ include/shows the struct member is only ever written. The single read of the value inunified_detection_thread.c:1027is of the local parameter, and only to emit a log line:The only code that reads
ctx->annotation_onlyistests/unit/test_external_motion_trigger.c, where the intended semantics are asserted:So the unit tests encode the correct behaviour against a local mock, while the production state machine in
unified_detection_thread.cnever consults the flag and transitions intoUDT_STATE_RECORDINGregardless.Reproduction
record=1,detection_based_recording=1,detection_model=onvif,is_onvif=1.recording_*.mp4anddetection_*.mp4being written for the same period.detections.recording_idis NULL rather than linked.Impact
On a 12-camera 720p deployment this is ~97 GB/day of duplicate footage, cutting retention on a 3.6 TB volume from ~15 days to ~11.
Reducing
post_detection_buffermitigates it (shorter clips per event) but cannot eliminate it, since the duplicate-recording path runs regardless of the flag.Note also that
detection_based_recording=0is not a viable workaround for users who want continuous recording with motion markers on the timeline: the ONVIF pull-point subscription lives inside the unified detection thread, which is only started whendetection_based_recordingis true (src/core/main.c:1062,src/video/stream_manager.c:203). Disabling it stops motion events being recorded at all.Suggested fix
Gate the recording-state transition and MP4 writer in
unified_detection_thread.conctx->annotation_only, matching the logic already asserted intests/unit/test_external_motion_trigger.c— when true, store the detection and linkrecording_idto the active continuous recording instead of opening a new MP4.