diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md
index 2745ef27d..4dfd8b77c 100644
--- a/docs/docs/configuration/record.md
+++ b/docs/docs/configuration/record.md
@@ -25,16 +25,16 @@ record:
alerts:
retain:
days: 30
- mode: motion
+ mode: all
detections:
retain:
days: 30
- mode: motion
+ mode: all
```
### Reduced storage: Only saving video when motion is detected
-In order to reduce storage requirements, you can adjust your config to only retain video where motion was detected.
+In order to reduce storage requirements, you can adjust your config to only retain video where motion / activity was detected.
```yaml
record:
@@ -53,7 +53,7 @@ record:
### Minimum: Alerts only
-If you only want to retain video that occurs during a tracked object, this config will discard video unless an alert is ongoing.
+If you only want to retain video that occurs during activity caused by tracked object(s), this config will discard video unless an alert is ongoing.
```yaml
record:
diff --git a/docs/docs/configuration/reference.md b/docs/docs/configuration/reference.md
index 2d123be46..c12984d18 100644
--- a/docs/docs/configuration/reference.md
+++ b/docs/docs/configuration/reference.md
@@ -537,7 +537,7 @@ record:
# Optional: Retention settings for recordings of alerts
retain:
# Required: Retention days (default: shown below)
- days: 14
+ days: 10
# Optional: Mode for retention. (default: shown below)
# all - save all recording segments for alerts regardless of activity
# motion - save all recordings segments for alerts with any detected motion
@@ -557,7 +557,7 @@ record:
# Optional: Retention settings for recordings of detections
retain:
# Required: Retention days (default: shown below)
- days: 14
+ days: 10
# Optional: Mode for retention. (default: shown below)
# all - save all recording segments for detections regardless of activity
# motion - save all recordings segments for detections with any detected motion
diff --git a/frigate/record/cleanup.py b/frigate/record/cleanup.py
index b0359e71d..4ee6f48b1 100644
--- a/frigate/record/cleanup.py
+++ b/frigate/record/cleanup.py
@@ -180,7 +180,11 @@ class RecordingCleanup(threading.Thread):
# Delete recordings outside of the retention window or based on the retention mode
if (
not keep
- or (mode == RetainModeEnum.motion and recording.motion == 0)
+ or (
+ mode == RetainModeEnum.motion
+ and recording.motion == 0
+ and recording.objects == 0
+ )
or (mode == RetainModeEnum.active_objects and recording.objects == 0)
):
Path(recording.path).unlink(missing_ok=True)
diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py
index 7938df31f..8bfa726de 100644
--- a/frigate/record/maintainer.py
+++ b/frigate/record/maintainer.py
@@ -57,14 +57,25 @@ class SegmentInfo:
self.average_dBFS = average_dBFS
def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool:
- return (
- retain_mode == RetainModeEnum.motion
- and self.motion_count == 0
- and self.average_dBFS == 0
- ) or (
- retain_mode == RetainModeEnum.active_objects
- and self.active_object_count == 0
- )
+ keep = False
+
+ # all mode should never discard
+ if retain_mode == RetainModeEnum.all:
+ keep = True
+
+ # motion mode should keep if motion or audio is detected
+ if (
+ not keep
+ and retain_mode == RetainModeEnum.motion
+ and (self.motion_count > 0 or self.average_dBFS > 0)
+ ):
+ keep = True
+
+ # active objects mode should keep if any active objects are detected
+ if not keep and self.active_object_count > 0:
+ keep = True
+
+ return not keep
class RecordingMaintainer(threading.Thread):
@@ -348,63 +359,10 @@ class RecordingMaintainer(threading.Thread):
elif record_config.motion.days > 0:
highest = "motion"
- # continuous / motion recording is not enabled
- if highest is None:
- # if the cached segment overlaps with the review items:
- overlaps = False
- for review in reviews:
- severity = SeverityEnum[review.severity]
-
- # if the review item starts in the future, stop checking review items
- # and remove this segment
- if (
- review.start_time - record_config.get_review_pre_capture(severity)
- ) > end_time.timestamp():
- overlaps = False
- break
-
- # if the review item is in progress or ends after the recording starts, keep it
- # and stop looking at review items
- if (
- review.end_time is None
- or (
- review.end_time
- + record_config.get_review_post_capture(severity)
- )
- >= start_time.timestamp()
- ):
- overlaps = True
- break
-
- if overlaps:
- record_mode = (
- record_config.alerts.retain.mode
- if review.severity == "alert"
- else record_config.detections.retain.mode
- )
- # move from cache to recordings immediately
- return await self.move_segment(
- camera,
- start_time,
- end_time,
- duration,
- cache_path,
- record_mode,
- )
- # if it doesn't overlap with an review item, go ahead and drop the segment
- # if it ends more than the configured pre_capture for the camera
- else:
- camera_info = self.object_recordings_info[camera]
- most_recently_processed_frame_time = (
- camera_info[-1][0] if len(camera_info) > 0 else 0
- )
- retain_cutoff = datetime.datetime.fromtimestamp(
- most_recently_processed_frame_time - record_config.event_pre_capture
- ).astimezone(datetime.timezone.utc)
- if end_time < retain_cutoff:
- self.drop_segment(cache_path)
- # continuous / motion is enabled
- else:
+ # if we have continuous or motion recording enabled
+ # we should first just check if this segment matches that
+ # and avoid any DB calls
+ if highest is not None:
# assume that empty means the relevant recording info has not been received yet
camera_info = self.object_recordings_info[camera]
most_recently_processed_frame_time = (
@@ -427,6 +385,59 @@ class RecordingMaintainer(threading.Thread):
camera, start_time, end_time, duration, cache_path, record_mode
)
+ # we fell through the continuous / motion check, so we need to check the review items
+ # if the cached segment overlaps with the review items:
+ overlaps = False
+ for review in reviews:
+ severity = SeverityEnum[review.severity]
+
+ # if the review item starts in the future, stop checking review items
+ # and remove this segment
+ if (
+ review.start_time - record_config.get_review_pre_capture(severity)
+ ) > end_time.timestamp():
+ overlaps = False
+ break
+
+ # if the review item is in progress or ends after the recording starts, keep it
+ # and stop looking at review items
+ if (
+ review.end_time is None
+ or (review.end_time + record_config.get_review_post_capture(severity))
+ >= start_time.timestamp()
+ ):
+ overlaps = True
+ break
+
+ if overlaps:
+ record_mode = (
+ record_config.alerts.retain.mode
+ if review.severity == "alert"
+ else record_config.detections.retain.mode
+ )
+ # move from cache to recordings immediately
+ return await self.move_segment(
+ camera,
+ start_time,
+ end_time,
+ duration,
+ cache_path,
+ record_mode,
+ )
+ # if it doesn't overlap with an review item, go ahead and drop the segment
+ # if it ends more than the configured pre_capture for the camera
+ # BUT only if continuous/motion is NOT enabled (otherwise wait for processing)
+ elif highest is None:
+ camera_info = self.object_recordings_info[camera]
+ most_recently_processed_frame_time = (
+ camera_info[-1][0] if len(camera_info) > 0 else 0
+ )
+ retain_cutoff = datetime.datetime.fromtimestamp(
+ most_recently_processed_frame_time - record_config.event_pre_capture
+ ).astimezone(datetime.timezone.utc)
+ if end_time < retain_cutoff:
+ self.drop_segment(cache_path)
+
def segment_stats(
self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime
) -> SegmentInfo:
diff --git a/frigate/test/test_record_retention.py b/frigate/test/test_record_retention.py
index b9aead9da..b826c3afb 100644
--- a/frigate/test/test_record_retention.py
+++ b/frigate/test/test_record_retention.py
@@ -12,11 +12,11 @@ class TestRecordRetention(unittest.TestCase):
assert not segment_info.should_discard_segment(RetainModeEnum.motion)
assert segment_info.should_discard_segment(RetainModeEnum.active_objects)
- def test_object_should_keep_object_not_motion(self):
+ def test_object_should_keep_object_when_motion(self):
segment_info = SegmentInfo(
motion_count=0, active_object_count=1, region_count=0, average_dBFS=0
)
- assert segment_info.should_discard_segment(RetainModeEnum.motion)
+ assert not segment_info.should_discard_segment(RetainModeEnum.motion)
assert not segment_info.should_discard_segment(RetainModeEnum.active_objects)
def test_all_should_keep_all(self):
diff --git a/web/public/locales/en/views/live.json b/web/public/locales/en/views/live.json
index 3b47ed731..2534c3957 100644
--- a/web/public/locales/en/views/live.json
+++ b/web/public/locales/en/views/live.json
@@ -165,8 +165,7 @@
"all": "All",
"motion": "Motion",
"active_objects": "Active Objects"
- },
- "notAllTips": "Your {{source}} recording retention configuration is set to mode: {{effectiveRetainMode}}, so this on-demand recording will only keep segments with {{effectiveRetainModeName}}."
+ }
},
"editLayout": {
"label": "Edit Layout",
diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx
index 5e038ef9e..2253385ac 100644
--- a/web/src/views/live/LiveCameraView.tsx
+++ b/web/src/views/live/LiveCameraView.tsx
@@ -108,7 +108,7 @@ import axios from "axios";
import { toast } from "sonner";
import { Toaster } from "@/components/ui/sonner";
import { useIsAdmin } from "@/hooks/use-is-admin";
-import { Trans, useTranslation } from "react-i18next";
+import { useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import PtzControlPanel from "@/components/overlay/PtzControlPanel";
import ObjectSettingsView from "../settings/ObjectSettingsView";
@@ -712,42 +712,6 @@ export default function LiveCameraView({
);
}
-function OnDemandRetentionMessage({ camera }: { camera: CameraConfig }) {
- const { t } = useTranslation(["views/live", "views/events"]);
- const rankMap = { all: 0, motion: 1, active_objects: 2 };
- const getValidMode = (retain?: { mode?: string }): keyof typeof rankMap => {
- const mode = retain?.mode;
- return mode && mode in rankMap ? (mode as keyof typeof rankMap) : "all";
- };
-
- const recordRetainMode = getValidMode(camera.record.retain);
- const alertsRetainMode = getValidMode(camera.review.alerts.retain);
-
- const effectiveRetainMode =
- rankMap[alertsRetainMode] < rankMap[recordRetainMode]
- ? recordRetainMode
- : alertsRetainMode;
-
- const source = effectiveRetainMode === recordRetainMode ? "camera" : "alerts";
-
- return effectiveRetainMode !== "all" ? (
-