mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Improve recording retention logic (#20506)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Change default event retention * Update docs * Handle both record and event record * Catch edge case * Undo motion change and improve motion behavior * fix typo * Remove record retention banner * Remove unused * Fix tests
This commit is contained in:
parent
3c8ef0c71c
commit
e592c7044b
@ -25,16 +25,16 @@ record:
|
|||||||
alerts:
|
alerts:
|
||||||
retain:
|
retain:
|
||||||
days: 30
|
days: 30
|
||||||
mode: motion
|
mode: all
|
||||||
detections:
|
detections:
|
||||||
retain:
|
retain:
|
||||||
days: 30
|
days: 30
|
||||||
mode: motion
|
mode: all
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reduced storage: Only saving video when motion is detected
|
### 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
|
```yaml
|
||||||
record:
|
record:
|
||||||
@ -53,7 +53,7 @@ record:
|
|||||||
|
|
||||||
### Minimum: Alerts only
|
### 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
|
```yaml
|
||||||
record:
|
record:
|
||||||
|
|||||||
@ -537,7 +537,7 @@ record:
|
|||||||
# Optional: Retention settings for recordings of alerts
|
# Optional: Retention settings for recordings of alerts
|
||||||
retain:
|
retain:
|
||||||
# Required: Retention days (default: shown below)
|
# Required: Retention days (default: shown below)
|
||||||
days: 14
|
days: 10
|
||||||
# Optional: Mode for retention. (default: shown below)
|
# Optional: Mode for retention. (default: shown below)
|
||||||
# all - save all recording segments for alerts regardless of activity
|
# all - save all recording segments for alerts regardless of activity
|
||||||
# motion - save all recordings segments for alerts with any detected motion
|
# motion - save all recordings segments for alerts with any detected motion
|
||||||
@ -557,7 +557,7 @@ record:
|
|||||||
# Optional: Retention settings for recordings of detections
|
# Optional: Retention settings for recordings of detections
|
||||||
retain:
|
retain:
|
||||||
# Required: Retention days (default: shown below)
|
# Required: Retention days (default: shown below)
|
||||||
days: 14
|
days: 10
|
||||||
# Optional: Mode for retention. (default: shown below)
|
# Optional: Mode for retention. (default: shown below)
|
||||||
# all - save all recording segments for detections regardless of activity
|
# all - save all recording segments for detections regardless of activity
|
||||||
# motion - save all recordings segments for detections with any detected motion
|
# motion - save all recordings segments for detections with any detected motion
|
||||||
|
|||||||
@ -180,7 +180,11 @@ class RecordingCleanup(threading.Thread):
|
|||||||
# Delete recordings outside of the retention window or based on the retention mode
|
# Delete recordings outside of the retention window or based on the retention mode
|
||||||
if (
|
if (
|
||||||
not keep
|
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)
|
or (mode == RetainModeEnum.active_objects and recording.objects == 0)
|
||||||
):
|
):
|
||||||
Path(recording.path).unlink(missing_ok=True)
|
Path(recording.path).unlink(missing_ok=True)
|
||||||
|
|||||||
@ -57,14 +57,25 @@ class SegmentInfo:
|
|||||||
self.average_dBFS = average_dBFS
|
self.average_dBFS = average_dBFS
|
||||||
|
|
||||||
def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool:
|
def should_discard_segment(self, retain_mode: RetainModeEnum) -> bool:
|
||||||
return (
|
keep = False
|
||||||
retain_mode == RetainModeEnum.motion
|
|
||||||
and self.motion_count == 0
|
# all mode should never discard
|
||||||
and self.average_dBFS == 0
|
if retain_mode == RetainModeEnum.all:
|
||||||
) or (
|
keep = True
|
||||||
retain_mode == RetainModeEnum.active_objects
|
|
||||||
and self.active_object_count == 0
|
# 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):
|
class RecordingMaintainer(threading.Thread):
|
||||||
@ -348,8 +359,33 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
elif record_config.motion.days > 0:
|
elif record_config.motion.days > 0:
|
||||||
highest = "motion"
|
highest = "motion"
|
||||||
|
|
||||||
# continuous / motion recording is not enabled
|
# if we have continuous or motion recording enabled
|
||||||
if highest is None:
|
# 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 = (
|
||||||
|
camera_info[-1][0] if len(camera_info) > 0 else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure delayed segment info does not lead to lost segments
|
||||||
|
if (
|
||||||
|
datetime.datetime.fromtimestamp(
|
||||||
|
most_recently_processed_frame_time
|
||||||
|
).astimezone(datetime.timezone.utc)
|
||||||
|
>= end_time
|
||||||
|
):
|
||||||
|
record_mode = (
|
||||||
|
RetainModeEnum.all
|
||||||
|
if highest == "continuous"
|
||||||
|
else RetainModeEnum.motion
|
||||||
|
)
|
||||||
|
return await self.move_segment(
|
||||||
|
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:
|
# if the cached segment overlaps with the review items:
|
||||||
overlaps = False
|
overlaps = False
|
||||||
for review in reviews:
|
for review in reviews:
|
||||||
@ -367,10 +403,7 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
# and stop looking at review items
|
# and stop looking at review items
|
||||||
if (
|
if (
|
||||||
review.end_time is None
|
review.end_time is None
|
||||||
or (
|
or (review.end_time + record_config.get_review_post_capture(severity))
|
||||||
review.end_time
|
|
||||||
+ record_config.get_review_post_capture(severity)
|
|
||||||
)
|
|
||||||
>= start_time.timestamp()
|
>= start_time.timestamp()
|
||||||
):
|
):
|
||||||
overlaps = True
|
overlaps = True
|
||||||
@ -393,7 +426,8 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
)
|
)
|
||||||
# if it doesn't overlap with an review item, go ahead and drop the segment
|
# 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
|
# if it ends more than the configured pre_capture for the camera
|
||||||
else:
|
# BUT only if continuous/motion is NOT enabled (otherwise wait for processing)
|
||||||
|
elif highest is None:
|
||||||
camera_info = self.object_recordings_info[camera]
|
camera_info = self.object_recordings_info[camera]
|
||||||
most_recently_processed_frame_time = (
|
most_recently_processed_frame_time = (
|
||||||
camera_info[-1][0] if len(camera_info) > 0 else 0
|
camera_info[-1][0] if len(camera_info) > 0 else 0
|
||||||
@ -403,29 +437,6 @@ class RecordingMaintainer(threading.Thread):
|
|||||||
).astimezone(datetime.timezone.utc)
|
).astimezone(datetime.timezone.utc)
|
||||||
if end_time < retain_cutoff:
|
if end_time < retain_cutoff:
|
||||||
self.drop_segment(cache_path)
|
self.drop_segment(cache_path)
|
||||||
# continuous / motion is enabled
|
|
||||||
else:
|
|
||||||
# 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 = (
|
|
||||||
camera_info[-1][0] if len(camera_info) > 0 else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# ensure delayed segment info does not lead to lost segments
|
|
||||||
if (
|
|
||||||
datetime.datetime.fromtimestamp(
|
|
||||||
most_recently_processed_frame_time
|
|
||||||
).astimezone(datetime.timezone.utc)
|
|
||||||
>= end_time
|
|
||||||
):
|
|
||||||
record_mode = (
|
|
||||||
RetainModeEnum.all
|
|
||||||
if highest == "continuous"
|
|
||||||
else RetainModeEnum.motion
|
|
||||||
)
|
|
||||||
return await self.move_segment(
|
|
||||||
camera, start_time, end_time, duration, cache_path, record_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
def segment_stats(
|
def segment_stats(
|
||||||
self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime
|
self, camera: str, start_time: datetime.datetime, end_time: datetime.datetime
|
||||||
|
|||||||
@ -12,11 +12,11 @@ class TestRecordRetention(unittest.TestCase):
|
|||||||
assert not segment_info.should_discard_segment(RetainModeEnum.motion)
|
assert not segment_info.should_discard_segment(RetainModeEnum.motion)
|
||||||
assert segment_info.should_discard_segment(RetainModeEnum.active_objects)
|
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(
|
segment_info = SegmentInfo(
|
||||||
motion_count=0, active_object_count=1, region_count=0, average_dBFS=0
|
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)
|
assert not segment_info.should_discard_segment(RetainModeEnum.active_objects)
|
||||||
|
|
||||||
def test_all_should_keep_all(self):
|
def test_all_should_keep_all(self):
|
||||||
|
|||||||
@ -165,8 +165,7 @@
|
|||||||
"all": "All",
|
"all": "All",
|
||||||
"motion": "Motion",
|
"motion": "Motion",
|
||||||
"active_objects": "Active Objects"
|
"active_objects": "Active Objects"
|
||||||
},
|
}
|
||||||
"notAllTips": "Your {{source}} recording retention configuration is set to <code>mode: {{effectiveRetainMode}}</code>, so this on-demand recording will only keep segments with {{effectiveRetainModeName}}."
|
|
||||||
},
|
},
|
||||||
"editLayout": {
|
"editLayout": {
|
||||||
"label": "Edit Layout",
|
"label": "Edit Layout",
|
||||||
|
|||||||
@ -108,7 +108,7 @@ import axios from "axios";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
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 { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
import PtzControlPanel from "@/components/overlay/PtzControlPanel";
|
import PtzControlPanel from "@/components/overlay/PtzControlPanel";
|
||||||
import ObjectSettingsView from "../settings/ObjectSettingsView";
|
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" ? (
|
|
||||||
<div>
|
|
||||||
<Trans
|
|
||||||
ns="views/live"
|
|
||||||
values={{
|
|
||||||
source,
|
|
||||||
effectiveRetainMode,
|
|
||||||
effectiveRetainModeName: t(
|
|
||||||
"effectiveRetainMode.modes." + effectiveRetainMode,
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
effectiveRetainMode.notAllTips
|
|
||||||
</Trans>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FrigateCameraFeaturesProps = {
|
type FrigateCameraFeaturesProps = {
|
||||||
camera: CameraConfig;
|
camera: CameraConfig;
|
||||||
recordingEnabled: boolean;
|
recordingEnabled: boolean;
|
||||||
@ -841,11 +805,10 @@ function FrigateCameraFeatures({
|
|||||||
const toastId = toast.success(
|
const toastId = toast.success(
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<div className="font-semibold">{t("manualRecording.started")}</div>
|
<div className="font-semibold">{t("manualRecording.started")}</div>
|
||||||
{!camera.record.enabled || camera.record.alerts.retain.days == 0 ? (
|
{!camera.record.enabled ||
|
||||||
|
(camera.record.alerts.retain.days == 0 && (
|
||||||
<div>{t("manualRecording.recordDisabledTips")}</div>
|
<div>{t("manualRecording.recordDisabledTips")}</div>
|
||||||
) : (
|
))}
|
||||||
<OnDemandRetentionMessage camera={camera} />
|
|
||||||
)}
|
|
||||||
</div>,
|
</div>,
|
||||||
{
|
{
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user