From 26241b08779f29529848f30b9de5d94edf397a2b Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 5 Dec 2021 11:05:03 -0600 Subject: [PATCH 01/18] no need to expire recordings every minute --- frigate/record.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frigate/record.py b/frigate/record.py index 91024317b..3a9ac6520 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -443,16 +443,15 @@ class RecordingCleanup(threading.Thread): logger.debug("End sync recordings.") def run(self): - # Expire recordings every minute, clean directories every hour. + # Expire tmp clips every minute, recordings and clean directories every hour. for counter in itertools.cycle(range(60)): if self.stop_event.wait(60): logger.info(f"Exiting recording cleanup...") break - - self.expire_recordings() self.clean_tmp_clips() if counter == 0: + self.expire_recordings() self.expire_files() remove_empty_directories(RECORD_DIR) self.sync_recordings() From 92e08b92f580b03cf1f0eb6bdbe60b3e397dbcf2 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 5 Dec 2021 11:06:11 -0600 Subject: [PATCH 02/18] sync recordings with disk once on startup --- frigate/record.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frigate/record.py b/frigate/record.py index 3a9ac6520..f20dfb5dc 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -443,6 +443,9 @@ class RecordingCleanup(threading.Thread): logger.debug("End sync recordings.") def run(self): + # on startup sync recordings with disk + self.sync_recordings() + # Expire tmp clips every minute, recordings and clean directories every hour. for counter in itertools.cycle(range(60)): if self.stop_event.wait(60): @@ -454,4 +457,3 @@ class RecordingCleanup(threading.Thread): self.expire_recordings() self.expire_files() remove_empty_directories(RECORD_DIR) - self.sync_recordings() From af001321a8437550fad5bc388f511f5e8e9e2c2f Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 5 Dec 2021 11:06:39 -0600 Subject: [PATCH 03/18] fix process_clip --- process_clip.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/process_clip.py b/process_clip.py index 3220f6a47..569998526 100644 --- a/process_clip.py +++ b/process_clip.py @@ -162,9 +162,12 @@ class ProcessClip: ) total_regions += len(regions) total_motion_boxes += len(motion_boxes) + top_score = 0 for id, obj in self.camera_state.tracked_objects.items(): if not obj.false_positive: object_ids.add(id) + if obj.top_score > top_score: + top_score = obj.top_score total_frames += 1 @@ -175,6 +178,7 @@ class ProcessClip: "total_motion_boxes": total_motion_boxes, "true_positive_objects": len(object_ids), "total_frames": total_frames, + "top_score": top_score, } def save_debug_frame(self, debug_path, frame_time, tracked_objects): @@ -277,6 +281,7 @@ def process(path, label, output, debug_path): frigate_config = FrigateConfig(**json_config) runtime_config = frigate_config.runtime_config + runtime_config.cameras["camera"].create_ffmpeg_cmds() process_clip = ProcessClip(c, frame_shape, runtime_config) process_clip.load_frames() From f3efc0667f5fe038e01f7b78b93ee3e72ab17fae Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Fri, 10 Dec 2021 22:56:29 -0600 Subject: [PATCH 04/18] retain frame data for recording maintenance --- frigate/app.py | 8 ++++- frigate/object_processing.py | 13 +++++++ frigate/record.py | 68 ++++++++++++++++++++++++++++-------- 3 files changed, 74 insertions(+), 15 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 5833d5fe9..9be814b91 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -108,6 +108,9 @@ class FrigateApp: maxsize=len(self.config.cameras.keys()) * 2 ) + # Queue for recordings info + self.recordings_info_queue = mp.Queue() + def init_database(self): # Migrate DB location old_db_path = os.path.join(CLIPS_DIR, "frigate.db") @@ -206,6 +209,7 @@ class FrigateApp: self.event_queue, self.event_processed_queue, self.video_output_queue, + self.recordings_info_queue, self.stop_event, ) self.detected_frames_processor.start() @@ -273,7 +277,9 @@ class FrigateApp: self.event_cleanup.start() def start_recording_maintainer(self): - self.recording_maintainer = RecordingMaintainer(self.config, self.stop_event) + self.recording_maintainer = RecordingMaintainer( + self.config, self.recordings_info_queue, self.stop_event + ) self.recording_maintainer.start() def start_recording_cleanup(self): diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 9a8ad8cc6..f618f9af8 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -584,6 +584,7 @@ class TrackedObjectProcessor(threading.Thread): event_queue, event_processed_queue, video_output_queue, + recordings_info_queue, stop_event, ): threading.Thread.__init__(self) @@ -595,6 +596,7 @@ class TrackedObjectProcessor(threading.Thread): self.event_queue = event_queue self.event_processed_queue = event_processed_queue self.video_output_queue = video_output_queue + self.recordings_info_queue = recordings_info_queue self.stop_event = stop_event self.camera_states: Dict[str, CameraState] = {} self.frame_manager = SharedMemoryFrameManager() @@ -823,6 +825,17 @@ class TrackedObjectProcessor(threading.Thread): ) ) + # send info on this frame to the recordings maintainer + self.recordings_info_queue.put( + ( + camera, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) + ) + # update zone counts for each label # for each zone in the current camera for zone in self.config.cameras[camera].zones.keys(): diff --git a/frigate/record.py b/frigate/record.py index f20dfb5dc..41ed0cc7e 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -1,13 +1,15 @@ import datetime -import time import itertools import logging +import multiprocessing as mp import os +import queue import random import shutil import string import subprocess as sp import threading +import time from collections import defaultdict from pathlib import Path @@ -40,22 +42,28 @@ def remove_empty_directories(directory): class RecordingMaintainer(threading.Thread): - def __init__(self, config: FrigateConfig, stop_event): + def __init__( + self, config: FrigateConfig, recordings_info_queue: mp.Queue, stop_event + ): threading.Thread.__init__(self) self.name = "recording_maint" self.config = config + self.recordings_info_queue = recordings_info_queue self.stop_event = stop_event self.first_pass = True + self.recordings_info = defaultdict(list) self.end_time_cache = {} def move_files(self): - cache_files = [ - d - for d in os.listdir(CACHE_DIR) - if os.path.isfile(os.path.join(CACHE_DIR, d)) - and d.endswith(".mp4") - and not d.startswith("clip_") - ] + cache_files = sorted( + [ + d + for d in os.listdir(CACHE_DIR) + if os.path.isfile(os.path.join(CACHE_DIR, d)) + and d.endswith(".mp4") + and not d.startswith("clip_") + ] + ) files_in_use = [] for process in psutil.process_iter(): @@ -93,16 +101,22 @@ class RecordingMaintainer(threading.Thread): keep_count = 5 for camera in grouped_recordings.keys(): if len(grouped_recordings[camera]) > keep_count: - sorted_recordings = sorted( - grouped_recordings[camera], key=lambda i: i["start_time"] - ) - to_remove = sorted_recordings[:-keep_count] + to_remove = grouped_recordings[camera][:-keep_count] for f in to_remove: Path(f["cache_path"]).unlink(missing_ok=True) self.end_time_cache.pop(f["cache_path"], None) - grouped_recordings[camera] = sorted_recordings[-keep_count:] + grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] for camera, recordings in grouped_recordings.items(): + + # clear out all the recording info for old frames + while ( + len(self.recordings_info[camera]) > 0 + and self.recordings_info[camera][0][0] + < recordings[0]["start_time"].timestamp() + ): + self.recordings_info[camera].pop(0) + # get all events with the end time after the start of the oldest cache file # or with end_time None events: Event = ( @@ -167,6 +181,8 @@ class RecordingMaintainer(threading.Thread): # and remove this segment if event.start_time > end_time.timestamp(): overlaps = False + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) break # if the event is in progress or ends after the recording starts, keep it @@ -235,6 +251,30 @@ class RecordingMaintainer(threading.Thread): wait_time = 5 while not self.stop_event.wait(wait_time): run_start = datetime.datetime.now().timestamp() + + # empty the recordings info queue + while True: + try: + ( + camera, + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) = self.recordings_info_queue.get(False) + + if self.config.cameras[camera].record.enabled: + self.recordings_info[camera].append( + ( + frame_time, + current_tracked_objects, + motion_boxes, + regions, + ) + ) + except queue.Empty: + break + try: self.move_files() except Exception as e: From 63f8034e463cfd6a770a141715a1248d4bc5127e Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 08:59:54 -0600 Subject: [PATCH 05/18] pass processed tracked objects --- frigate/object_processing.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index f618f9af8..30f0f1c6f 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -176,6 +176,7 @@ class TrackedObject: "box": self.obj_data["box"], "area": self.obj_data["area"], "region": self.obj_data["region"], + "motionless_count": self.obj_data["motionless_count"], "current_zones": self.current_zones.copy(), "entered_zones": list(self.entered_zones).copy(), "has_clip": self.has_clip, @@ -815,11 +816,15 @@ class TrackedObjectProcessor(threading.Thread): frame_time, current_tracked_objects, motion_boxes, regions ) + tracked_objects = [ + o.to_dict() for o in camera_state.tracked_objects.values() + ] + self.video_output_queue.put( ( camera, frame_time, - current_tracked_objects, + tracked_objects, motion_boxes, regions, ) @@ -830,7 +835,7 @@ class TrackedObjectProcessor(threading.Thread): ( camera, frame_time, - current_tracked_objects, + tracked_objects, motion_boxes, regions, ) From 9f18629df3af5251dfe3be09695946665af332f1 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 09:22:44 -0600 Subject: [PATCH 06/18] switch to retain config instead of retain_days --- frigate/config.py | 28 +++++++++++++++++++++++++++- frigate/record.py | 14 +++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frigate/config.py b/frigate/config.py index 0de01a711..2e5f1d97b 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -65,8 +65,17 @@ class MqttConfig(FrigateBaseModel): return v +class RetainModeEnum(str, Enum): + all = "all" + motion = "motion" + active_objects = "active_objects" + + class RetainConfig(FrigateBaseModel): default: float = Field(default=10, title="Default retention period.") + mode: RetainModeEnum = Field( + default=RetainModeEnum.active_objects, title="Retain mode." + ) objects: Dict[str, float] = Field( default_factory=dict, title="Object retention period." ) @@ -88,9 +97,18 @@ class EventsConfig(FrigateBaseModel): ) +class RecordRetainConfig(FrigateBaseModel): + days: float = Field(default=0, title="Default retention period.") + mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.") + + class RecordConfig(FrigateBaseModel): enabled: bool = Field(default=False, title="Enable record on all cameras.") - retain_days: float = Field(default=0, title="Recording retention period in days.") + # deprecated - to be removed in a future version + retain_days: Optional[float] = Field(title="Recording retention period in days.") + retain: RecordRetainConfig = Field( + default_factory=RecordRetainConfig, title="Record retention settings." + ) events: EventsConfig = Field( default_factory=EventsConfig, title="Event specific settings." ) @@ -810,6 +828,14 @@ class FrigateConfig(FrigateBaseModel): f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input." ) + # backwards compatibility for retain_days + if not camera_config.record.retain_days is None: + logger.warning( + "The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'" + ) + if camera_config.record.retain.days == 0: + camera_config.record.retain.days = camera_config.record.retain_days + config.cameras[name] = camera_config return config diff --git a/frigate/record.py b/frigate/record.py index 41ed0cc7e..b5181dcf5 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -165,12 +165,12 @@ class RecordingMaintainer(threading.Thread): Path(cache_path).unlink(missing_ok=True) continue - # if cached file's start_time is earlier than the retain_days for the camera + # if cached file's start_time is earlier than the retain days for the camera if start_time <= ( ( datetime.datetime.now() - datetime.timedelta( - days=self.config.cameras[camera].record.retain_days + days=self.config.cameras[camera].record.retain.days ) ) ): @@ -203,7 +203,7 @@ class RecordingMaintainer(threading.Thread): duration, cache_path, ) - # else retain_days includes this segment + # else retain days includes this segment else: self.store_segment( camera, start_time, end_time, duration, cache_path @@ -314,7 +314,7 @@ class RecordingCleanup(threading.Thread): logger.debug("Start deleted cameras.") # Handle deleted cameras - expire_days = self.config.record.retain_days + expire_days = self.config.record.retain.days expire_before = ( datetime.datetime.now() - datetime.timedelta(days=expire_days) ).timestamp() @@ -340,7 +340,7 @@ class RecordingCleanup(threading.Thread): datetime.datetime.now() - datetime.timedelta(seconds=config.record.events.max_seconds) ).timestamp() - expire_days = config.record.retain_days + expire_days = config.record.retain.days expire_before = ( datetime.datetime.now() - datetime.timedelta(days=expire_days) ).timestamp() @@ -416,14 +416,14 @@ class RecordingCleanup(threading.Thread): default_expire = ( datetime.datetime.now().timestamp() - - SECONDS_IN_DAY * self.config.record.retain_days + - SECONDS_IN_DAY * self.config.record.retain.days ) delete_before = {} for name, camera in self.config.cameras.items(): delete_before[name] = ( datetime.datetime.now().timestamp() - - SECONDS_IN_DAY * camera.record.retain_days + - SECONDS_IN_DAY * camera.record.retain.days ) # find all the recordings older than the oldest recording in the db From cbb28821234211a033d3c06f734b2a75e7cfd457 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 13:11:39 -0600 Subject: [PATCH 07/18] refactor segment stats logic --- frigate/record.py | 54 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/frigate/record.py b/frigate/record.py index b5181dcf5..2af5e916a 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -16,9 +16,10 @@ from pathlib import Path import psutil from peewee import JOIN, DoesNotExist -from frigate.config import FrigateConfig +from frigate.config import RetainModeEnum, FrigateConfig from frigate.const import CACHE_DIR, RECORD_DIR from frigate.models import Event, Recordings +from frigate.util import area logger = logging.getLogger(__name__) @@ -195,6 +196,9 @@ class RecordingMaintainer(threading.Thread): break if overlaps: + record_mode = self.config.cameras[ + camera + ].record.events.retain.mode # move from cache to recordings immediately self.store_segment( camera, @@ -202,14 +206,57 @@ class RecordingMaintainer(threading.Thread): end_time, duration, cache_path, + record_mode, ) # else retain days includes this segment else: + record_mode = self.config.cameras[camera].record.retain.mode self.store_segment( - camera, start_time, end_time, duration, cache_path + camera, start_time, end_time, duration, cache_path, record_mode ) - def store_segment(self, camera, start_time, end_time, duration, cache_path): + def segment_stats(self, camera, start_time, end_time): + active_count = 0 + motion_count = 0 + for frame in self.recordings_info[camera]: + # frame is after end time of segment + if frame[0] > end_time.timestamp(): + break + # frame is before start time of segment + if frame[0] < start_time.timestamp(): + continue + + active_count += len( + [ + o + for o in frame[1] + if not o["false_positive"] and o["motionless_count"] > 0 + ] + ) + + motion_count += sum([area(box) for box in frame[2]]) + + return (motion_count, active_count) + + def store_segment( + self, + camera, + start_time, + end_time, + duration, + cache_path, + store_mode: RetainModeEnum, + ): + motion_count, active_count = self.segment_stats(camera, start_time, end_time) + + # check if the segment shouldn't be stored + if (store_mode == RetainModeEnum.motion and motion_count == 0) or ( + store_mode == RetainModeEnum.active_objects and active_count == 0 + ): + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + return + directory = os.path.join(RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera) if not os.path.exists(directory): @@ -371,6 +418,7 @@ class RecordingCleanup(threading.Thread): ) # loop over recordings and see if they overlap with any non-expired events + # TODO: expire segments based on segment stats according to config event_start = 0 deleted_recordings = set() for recording in recordings.objects().iterator(): From df0246aed8a98c241bd26c817902514e0b41c687 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 13:33:52 -0600 Subject: [PATCH 08/18] warn when retention mismatch --- frigate/config.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frigate/config.py b/frigate/config.py index 2e5f1d97b..9b235bf6f 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -836,6 +836,17 @@ class FrigateConfig(FrigateBaseModel): if camera_config.record.retain.days == 0: camera_config.record.retain.days = camera_config.record.retain_days + # warning if the higher level record mode is potentially more restrictive than the events + if ( + camera_config.record.retain.days != 0 + and camera_config.record.retain.mode != RetainModeEnum.all + and camera_config.record.events.retain.mode + != camera_config.record.retain.mode + ): + logger.warning( + f"Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied." + ) + config.cameras[name] = camera_config return config From 18fd50dfce6106b31f708a67bfa53d7c45746a1f Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 13:47:59 -0600 Subject: [PATCH 09/18] store objects and motion counts in the db --- frigate/models.py | 2 + frigate/record.py | 2 + migrations/006_add_motion_active_objects.py | 47 +++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 migrations/006_add_motion_active_objects.py diff --git a/frigate/models.py b/frigate/models.py index e933abe90..35a397f5c 100644 --- a/frigate/models.py +++ b/frigate/models.py @@ -27,3 +27,5 @@ class Recordings(Model): start_time = DateTimeField() end_time = DateTimeField() duration = FloatField() + motion = IntegerField(null=True) + objects = IntegerField(null=True) diff --git a/frigate/record.py b/frigate/record.py index 2af5e916a..0d4070d63 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -284,6 +284,8 @@ class RecordingMaintainer(threading.Thread): start_time=start_time.timestamp(), end_time=end_time.timestamp(), duration=duration, + motion=motion_count, + objects=active_count, ) except Exception as e: logger.error(f"Unable to store recording segment {cache_path}") diff --git a/migrations/006_add_motion_active_objects.py b/migrations/006_add_motion_active_objects.py new file mode 100644 index 000000000..6bd564b8c --- /dev/null +++ b/migrations/006_add_motion_active_objects.py @@ -0,0 +1,47 @@ +"""Peewee migrations -- 004_add_bbox_region_area.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['model_name'] # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.python(func, *args, **kwargs) # Run python code + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.drop_index(model, *col_names) + > migrator.add_not_null(model, *field_names) + > migrator.drop_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + +""" + +import datetime as dt +import peewee as pw +from playhouse.sqlite_ext import * +from decimal import ROUND_HALF_EVEN +from frigate.models import Recordings + +try: + import playhouse.postgres_ext as pw_pext +except ImportError: + pass + +SQL = pw.SQL + + +def migrate(migrator, database, fake=False, **kwargs): + migrator.add_fields( + Recordings, + objects=pw.IntegerField(null=True), + motion=pw.IntegerField(null=True), + ) + + +def rollback(migrator, database, fake=False, **kwargs): + migrator.remove_fields(Recordings, ["objects", "motion"]) From b19a02888a4e8669a394225577d137be0f8ebd94 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 14:09:30 -0600 Subject: [PATCH 10/18] expire overlapping segments based on mode --- frigate/record.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frigate/record.py b/frigate/record.py index 0d4070d63..43076b70a 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -448,8 +448,19 @@ class RecordingCleanup(threading.Thread): if event.end_time < recording.start_time: event_start = idx - # Delete recordings outside of the retention window - if not keep: + # Delete recordings outside of the retention window or based on the retention mode + if ( + not keep + or ( + config.record.events.retain.mode == RetainModeEnum.motion + and recording.motion == 0 + ) + or ( + config.record.events.retain.mode + == RetainModeEnum.active_objects + and recording.objects == 0 + ) + ): Path(recording.path).unlink(missing_ok=True) deleted_recordings.add(recording.id) From 589432bc89e9af5af4b2342d3021224d06dd114b Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sat, 11 Dec 2021 14:25:35 -0600 Subject: [PATCH 11/18] update docs --- docs/docs/configuration/index.md | 28 +++++++++++++++++++++++----- docs/docs/configuration/record.md | 5 ++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index f807a8038..5ce361198 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -224,15 +224,23 @@ motion: record: # Optional: Enable recording (default: shown below) enabled: False - # Optional: Number of days to retain recordings regardless of events (default: shown below) - # NOTE: This should be set to 0 and retention should be defined in events section below - # if you only want to retain recordings of events. - retain_days: 0 + # Optional: Retention settings for recording + retain: + # Optional: Number of days to retain recordings regardless of events (default: shown below) + # NOTE: This should be set to 0 and retention should be defined in events section below + # if you only want to retain recordings of events. + days: 0 + # Optional: Mode for retention. Available options are: all, motion, and active_objects + # all - save all recording segments regardless of activity + # motion - save all recordings segments with any detected motion + # active_objects - save all recording segments with active/moving objects + # NOTE: this mode only applies when the days setting above is greater than 0 + mode: all # Optional: Event recording settings events: # Optional: Maximum length of time to retain video during long events. (default: shown below) # NOTE: If an object is being tracked for longer than this amount of time, the retained recordings - # will be the last x seconds of the event unless retain_days under record is > 0. + # will be the last x seconds of the event unless retain->days under record is > 0. max_seconds: 300 # Optional: Number of seconds before the event to include (default: shown below) pre_capture: 5 @@ -247,6 +255,16 @@ record: retain: # Required: Default retention days (default: shown below) default: 10 + # Optional: Mode for retention. (default: shown below) + # all - save all recording segments for events regardless of activity + # motion - save all recordings segments for events with any detected motion + # active_objects - save all recording segments for event with active/moving objects + # + # NOTE: If the retain mode for the camera is more restrictive than the mode configured + # here, the segments will already be gone by the time this mode is applied. + # For example, if the camera retain mode is "motion", the segments without motion are + # never stored, so setting the mode to "all" here won't bring them back. + mode: active_objects # Optional: Per object retention days objects: person: 15 diff --git a/docs/docs/configuration/record.md b/docs/docs/configuration/record.md index d59530e07..927daecfe 100644 --- a/docs/docs/configuration/record.md +++ b/docs/docs/configuration/record.md @@ -14,12 +14,11 @@ If you only used clips in previous versions with recordings disabled, you can us ```yaml record: enabled: True - retain_days: 0 events: retain: default: 10 ``` -This configuration will retain recording segments that overlap with events for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs. +This configuration will retain recording segments that overlap with events and have active tracked objects for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs. -When `retain_days` is set to `0`, segments will be deleted from the cache if no events are in progress +When `retain_days` is set to `0`, segments will be deleted from the cache if no events are in progress. From fcb4aaef0d232dfec898df6773c51feebf7abb1f Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 12 Dec 2021 08:03:38 -0600 Subject: [PATCH 12/18] limit vod response cache --- docker/rootfs/usr/local/nginx/conf/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/rootfs/usr/local/nginx/conf/nginx.conf b/docker/rootfs/usr/local/nginx/conf/nginx.conf index bd8826282..a392b5372 100644 --- a/docker/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/rootfs/usr/local/nginx/conf/nginx.conf @@ -58,7 +58,7 @@ http { # vod caches vod_metadata_cache metadata_cache 512m; - vod_mapping_cache mapping_cache 5m; + vod_mapping_cache mapping_cache 5m 10m; # gzip manifests gzip on; From a5c13e7455e37cce9a2df2e1b3b09420ad3bfb5c Mon Sep 17 00:00:00 2001 From: Matt Clayton Date: Mon, 29 Nov 2021 21:52:58 +0000 Subject: [PATCH 13/18] Add temperature of coral tpu to telemetry mqtt message --- frigate/stats.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/frigate/stats.py b/frigate/stats.py index 7370d7a53..f83b2ae46 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -4,6 +4,7 @@ import threading import time import psutil import shutil +import os from frigate.config import FrigateConfig from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR @@ -30,6 +31,24 @@ def get_fs_type(path): bestMatch = part.mountpoint return fsType +def read_temperature(path): + if os.path.isfile(path): + with open(path) as f: + line=f.readline().strip() + return int(line)/1000 + return None + +def get_temperatures(): + temps={} + + # Get temperatures for all attached Corals + base="/sys/class/apex/" + for apex in os.listdir(base): + temp=read_temperature(os.path.join(base,apex,"temp")) + if temp is not None: + temps[apex]=temp + + return temps def stats_snapshot(stats_tracking): camera_metrics = stats_tracking["camera_metrics"] @@ -61,6 +80,7 @@ def stats_snapshot(stats_tracking): "uptime": (int(time.time()) - stats_tracking["started"]), "version": VERSION, "storage": {}, + "temperatures" : get_temperatures() } for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]: From 156e1a4dc2ea06e7917b307ba6b1d4e6a7333aa3 Mon Sep 17 00:00:00 2001 From: Justin Goette <53531335+jcgoette@users.noreply.github.com> Date: Sun, 12 Dec 2021 10:27:05 -0500 Subject: [PATCH 14/18] Allow for ".yaml" (#2244) * allow for ".yaml" * remove unused import --- frigate/app.py | 6 ++++++ frigate/config.py | 4 ++-- frigate/const.py | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 9be814b91..792b1c712 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -67,6 +67,12 @@ class FrigateApp: def init_config(self): config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") + + # Check if we can use .yaml instead of .yml + config_file_yaml = config_file.replace(".yml", ".yaml") + if os.path.isfile(config_file_yaml): + config_file = config_file_yaml + user_config = FrigateConfig.parse_file(config_file) self.config = user_config.runtime_config diff --git a/frigate/config.py b/frigate/config.py index 9b235bf6f..0f9c1fcd0 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -12,7 +12,7 @@ import yaml from pydantic import BaseModel, Extra, Field, validator from pydantic.fields import PrivateAttr -from frigate.const import BASE_DIR, CACHE_DIR +from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT from frigate.edgetpu import load_labels from frigate.util import create_mask, deep_merge @@ -864,7 +864,7 @@ class FrigateConfig(FrigateBaseModel): with open(config_file) as f: raw_config = f.read() - if config_file.endswith(".yml"): + if config_file.endswith(YAML_EXT): config = yaml.safe_load(raw_config) elif config_file.endswith(".json"): config = json.loads(raw_config) diff --git a/frigate/const.py b/frigate/const.py index c2b0f8e9d..afb6075a7 100644 --- a/frigate/const.py +++ b/frigate/const.py @@ -2,3 +2,4 @@ BASE_DIR = "/media/frigate" CLIPS_DIR = f"{BASE_DIR}/clips" RECORD_DIR = f"{BASE_DIR}/recordings" CACHE_DIR = "/tmp/cache" +YAML_EXT = (".yaml", ".yml") From 251d29aa38ef74e4c500489c50871cf229999893 Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Sun, 12 Dec 2021 15:29:57 +0000 Subject: [PATCH 15/18] #2117 change entered_zones from set to list so that they are not automatically alphabetically ordered (#2212) --- frigate/object_processing.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 30f0f1c6f..ebc442054 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -71,7 +71,7 @@ class TrackedObject: self.camera_config = camera_config self.frame_cache = frame_cache self.current_zones = [] - self.entered_zones = set() + self.entered_zones = [] self.false_positive = True self.has_clip = False self.has_snapshot = False @@ -147,7 +147,8 @@ class TrackedObject: # if the object passed the filters once, dont apply again if name in self.current_zones or not zone_filtered(self, zone.filters): current_zones.append(name) - self.entered_zones.add(name) + if name not in self.entered_zones: + self.entered_zones.append(name) # if the zones changed, signal an update if not self.false_positive and set(self.current_zones) != set(current_zones): @@ -178,7 +179,7 @@ class TrackedObject: "region": self.obj_data["region"], "motionless_count": self.obj_data["motionless_count"], "current_zones": self.current_zones.copy(), - "entered_zones": list(self.entered_zones).copy(), + "entered_zones": self.entered_zones.copy(), "has_clip": self.has_clip, "has_snapshot": self.has_snapshot, } @@ -732,7 +733,7 @@ class TrackedObjectProcessor(threading.Thread): # if there are required zones and there is no overlap required_zones = snapshot_config.required_zones - if len(required_zones) > 0 and not obj.entered_zones & set(required_zones): + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): logger.debug( f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones" ) @@ -773,7 +774,7 @@ class TrackedObjectProcessor(threading.Thread): def should_mqtt_snapshot(self, camera, obj: TrackedObject): # if there are required zones and there is no overlap required_zones = self.config.cameras[camera].mqtt.required_zones - if len(required_zones) > 0 and not obj.entered_zones & set(required_zones): + if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones): logger.debug( f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones" ) From 95bdf9fe347ba39a5e823bcd14d1ee359a655340 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Sun, 12 Dec 2021 10:27:01 -0600 Subject: [PATCH 16/18] check for apex dir --- frigate/stats.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/frigate/stats.py b/frigate/stats.py index f83b2ae46..bc92907cd 100644 --- a/frigate/stats.py +++ b/frigate/stats.py @@ -31,25 +31,29 @@ def get_fs_type(path): bestMatch = part.mountpoint return fsType + def read_temperature(path): if os.path.isfile(path): with open(path) as f: - line=f.readline().strip() - return int(line)/1000 + line = f.readline().strip() + return int(line) / 1000 return None + def get_temperatures(): - temps={} + temps = {} # Get temperatures for all attached Corals - base="/sys/class/apex/" - for apex in os.listdir(base): - temp=read_temperature(os.path.join(base,apex,"temp")) - if temp is not None: - temps[apex]=temp + base = "/sys/class/apex/" + if os.path.isdir(base): + for apex in os.listdir(base): + temp = read_temperature(os.path.join(base, apex, "temp")) + if temp is not None: + temps[apex] = temp return temps + def stats_snapshot(stats_tracking): camera_metrics = stats_tracking["camera_metrics"] stats = {} @@ -80,7 +84,7 @@ def stats_snapshot(stats_tracking): "uptime": (int(time.time()) - stats_tracking["started"]), "version": VERSION, "storage": {}, - "temperatures" : get_temperatures() + "temperatures": get_temperatures(), } for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]: From 609b436ed8dac2d8b1c60f4984b59bf607fc4824 Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Mon, 13 Dec 2021 06:50:06 -0600 Subject: [PATCH 17/18] fix migrations --- migrations/003_create_recordings_table.py | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/migrations/003_create_recordings_table.py b/migrations/003_create_recordings_table.py index d61c9c06f..e13ae657e 100644 --- a/migrations/003_create_recordings_table.py +++ b/migrations/003_create_recordings_table.py @@ -28,17 +28,19 @@ SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): - migrator.create_model(Recordings) - - def add_index(): - # First add the index here, because there is a bug in peewee_migrate - # when trying to create an multi-column index in the same migration - # as the table: https://github.com/klen/peewee_migrate/issues/19 - Recordings.add_index("start_time", "end_time") - Recordings.create_table() - - migrator.python(add_index) + migrator.sql( + 'CREATE TABLE IF NOT EXISTS "recordings" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "recordings_camera" ON "recordings" ("camera")' + ) + migrator.sql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "recordings_path" ON "recordings" ("path")' + ) + migrator.sql( + 'CREATE INDEX IF NOT EXISTS "recordings_start_time_end_time" ON "recordings" (start_time, end_time)' + ) def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_model(Recordings) + pass From db1255aa7fe242491d3a5f1a148a2f255b78b42b Mon Sep 17 00:00:00 2001 From: Blake Blackshear Date: Mon, 13 Dec 2021 06:51:03 -0600 Subject: [PATCH 18/18] disable disk sync on startup --- frigate/record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frigate/record.py b/frigate/record.py index 43076b70a..3d6c0bd01 100644 --- a/frigate/record.py +++ b/frigate/record.py @@ -544,8 +544,8 @@ class RecordingCleanup(threading.Thread): logger.debug("End sync recordings.") def run(self): - # on startup sync recordings with disk - self.sync_recordings() + # on startup sync recordings with disk (disabled due to too much CPU usage) + # self.sync_recordings() # Expire tmp clips every minute, recordings and clean directories every hour. for counter in itertools.cycle(range(60)):