add retention settings for snapshots

This commit is contained in:
Blake Blackshear 2021-01-13 06:49:05 -06:00
parent 25e3fe8eab
commit 63e14a98f9
3 changed files with 109 additions and 78 deletions

View File

@ -43,7 +43,7 @@ MQTT_SCHEMA = vol.Schema(
} }
) )
CLIPS_RETAIN_SCHEMA = vol.Schema( RETAIN_SCHEMA = vol.Schema(
{ {
vol.Required('default',default=10): int, vol.Required('default',default=10): int,
'objects': { 'objects': {
@ -56,7 +56,7 @@ CLIPS_SCHEMA = vol.Schema(
{ {
vol.Optional('max_seconds', default=300): int, vol.Optional('max_seconds', default=300): int,
'tmpfs_cache_size': str, 'tmpfs_cache_size': str,
vol.Optional('retain', default={}): CLIPS_RETAIN_SCHEMA vol.Optional('retain', default={}): RETAIN_SCHEMA
} }
) )
@ -183,7 +183,7 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('pre_capture', default=5): int, vol.Optional('pre_capture', default=5): int,
vol.Optional('post_capture', default=5): int, vol.Optional('post_capture', default=5): int,
'objects': [str], 'objects': [str],
vol.Optional('retain', default={}): CLIPS_RETAIN_SCHEMA, vol.Optional('retain', default={}): RETAIN_SCHEMA,
}, },
vol.Optional('record', default={}): { vol.Optional('record', default={}): {
'enabled': bool, 'enabled': bool,
@ -197,7 +197,8 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('timestamp', default=False): bool, vol.Optional('timestamp', default=False): bool,
vol.Optional('bounding_box', default=False): bool, vol.Optional('bounding_box', default=False): bool,
vol.Optional('crop', default=False): bool, vol.Optional('crop', default=False): bool,
'height': int 'height': int,
vol.Optional('retain', default={}): RETAIN_SCHEMA,
}, },
vol.Optional('mqtt', default={}): { vol.Optional('mqtt', default={}): {
vol.Optional('enabled', default=True): bool, vol.Optional('enabled', default=True): bool,
@ -228,6 +229,9 @@ FRIGATE_CONFIG_SCHEMA = vol.Schema(
vol.Optional('default', default='info'): vol.In(['info', 'debug', 'warning', 'error', 'critical']), vol.Optional('default', default='info'): vol.In(['info', 'debug', 'warning', 'error', 'critical']),
vol.Optional('logs', default={}): {str: vol.In(['info', 'debug', 'warning', 'error', 'critical']) } vol.Optional('logs', default={}): {str: vol.In(['info', 'debug', 'warning', 'error', 'critical']) }
}, },
vol.Optional('snapshots', default={}): {
vol.Optional('retain', default={}): RETAIN_SCHEMA
},
vol.Optional('clips', default={}): CLIPS_SCHEMA, vol.Optional('clips', default={}): CLIPS_SCHEMA,
vol.Optional('record', default={}): { vol.Optional('record', default={}): {
vol.Optional('enabled', default=False): bool, vol.Optional('enabled', default=False): bool,
@ -406,7 +410,7 @@ class CameraFfmpegConfig():
def output_args(self): def output_args(self):
return {k: v if isinstance(v, list) else v.split(' ') for k, v in self._output_args.items()} return {k: v if isinstance(v, list) else v.split(' ') for k, v in self._output_args.items()}
class ClipsRetainConfig(): class RetainConfig():
def __init__(self, global_config, config): def __init__(self, global_config, config):
self._default = config.get('default', global_config.get('default')) self._default = config.get('default', global_config.get('default'))
self._objects = config.get('objects', global_config.get('objects', {})) self._objects = config.get('objects', global_config.get('objects', {}))
@ -429,7 +433,7 @@ class ClipsConfig():
def __init__(self, config): def __init__(self, config):
self._max_seconds = config['max_seconds'] self._max_seconds = config['max_seconds']
self._tmpfs_cache_size = config.get('tmpfs_cache_size', '').strip() self._tmpfs_cache_size = config.get('tmpfs_cache_size', '').strip()
self._retain = ClipsRetainConfig(config['retain'], config['retain']) self._retain = RetainConfig(config['retain'], config['retain'])
@property @property
def max_seconds(self): def max_seconds(self):
@ -523,12 +527,13 @@ class ObjectConfig():
} }
class CameraSnapshotsConfig(): class CameraSnapshotsConfig():
def __init__(self, config): def __init__(self, global_config, config):
self._enabled = config['enabled'] self._enabled = config['enabled']
self._timestamp = config['timestamp'] self._timestamp = config['timestamp']
self._bounding_box = config['bounding_box'] self._bounding_box = config['bounding_box']
self._crop = config['crop'] self._crop = config['crop']
self._height = config.get('height') self._height = config.get('height')
self._retain = RetainConfig(global_config['snapshots']['retain'], config['retain'])
@property @property
def enabled(self): def enabled(self):
@ -550,13 +555,18 @@ class CameraSnapshotsConfig():
def height(self): def height(self):
return self._height return self._height
@property
def retain(self):
return self._retain
def to_dict(self): def to_dict(self):
return { return {
'enabled': self.enabled, 'enabled': self.enabled,
'timestamp': self.timestamp, 'timestamp': self.timestamp,
'bounding_box': self.bounding_box, 'bounding_box': self.bounding_box,
'crop': self.crop, 'crop': self.crop,
'height': self.height 'height': self.height,
'retain': self.retain.to_dict()
} }
class CameraMqttConfig(): class CameraMqttConfig():
@ -602,7 +612,7 @@ class CameraClipsConfig():
self._pre_capture = config['pre_capture'] self._pre_capture = config['pre_capture']
self._post_capture = config['post_capture'] self._post_capture = config['post_capture']
self._objects = config.get('objects', global_config['objects']['track']) self._objects = config.get('objects', global_config['objects']['track'])
self._retain = ClipsRetainConfig(global_config['clips']['retain'], config['retain']) self._retain = RetainConfig(global_config['clips']['retain'], config['retain'])
@property @property
def enabled(self): def enabled(self):
@ -758,7 +768,7 @@ class CameraConfig():
self._clips = CameraClipsConfig(global_config, config['clips']) self._clips = CameraClipsConfig(global_config, config['clips'])
self._record = RecordConfig(global_config['record'], config['record']) self._record = RecordConfig(global_config['record'], config['record'])
self._rtmp = CameraRtmpConfig(global_config, config['rtmp']) self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
self._snapshots = CameraSnapshotsConfig(config['snapshots']) self._snapshots = CameraSnapshotsConfig(global_config, config['snapshots'])
self._mqtt = CameraMqttConfig(config['mqtt']) self._mqtt = CameraMqttConfig(config['mqtt'])
self._objects = ObjectConfig(global_config['objects'], config.get('objects', {})) self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}))
self._motion = MotionConfig(global_config['motion'], config['motion'], self._height) self._motion = MotionConfig(global_config['motion'], config['motion'], self._height)

View File

@ -200,28 +200,21 @@ class EventCleanup(threading.Thread):
self.name = 'event_cleanup' self.name = 'event_cleanup'
self.config = config self.config = config
self.stop_event = stop_event self.stop_event = stop_event
self.camera_keys = list(self.config.cameras.keys())
def run(self): def expire(self, media):
counter = 0 ## Expire events from unlisted cameras based on the global config
while(True): if media == 'clips':
if self.stop_event.is_set():
logger.info(f"Exiting event cleanup...")
break
# only expire events every 10 minutes, but check for stop events every 10 seconds
time.sleep(10)
counter = counter + 1
if counter < 60:
continue
counter = 0
camera_keys = list(self.config.cameras.keys())
# Expire events from unlisted cameras based on the global config
retain_config = self.config.clips.retain retain_config = self.config.clips.retain
file_extension = 'mp4'
update_params = {'has_clip': False}
else:
retain_config = self.config.snapshots.retain
file_extension = 'jpg'
update_params = {'has_snapshot': False}
distinct_labels = (Event.select(Event.label) distinct_labels = (Event.select(Event.label)
.where(Event.camera.not_in(camera_keys)) .where(Event.camera.not_in(self.camera_keys))
.distinct()) .distinct())
# loop over object types in db # loop over object types in db
@ -232,27 +225,30 @@ class EventCleanup(threading.Thread):
# grab all events after specific time # grab all events after specific time
expired_events = ( expired_events = (
Event.select() Event.select()
.where(Event.camera.not_in(camera_keys), .where(Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label) Event.label == l.label)
) )
# delete the grabbed clips from disk # delete the media from disk
for event in expired_events: for event in expired_events:
clip_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
clip = Path(f"{os.path.join(CLIPS_DIR, clip_name)}.mp4") media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}")
clip.unlink(missing_ok=True) media.unlink(missing_ok=True)
# delete the event for this type from the db # update the clips attribute for the db entry
delete_query = ( update_query = (
Event.delete() Event.update(update_params)
.where(Event.camera.not_in(camera_keys), .where(Event.camera.not_in(self.camera_keys),
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label) Event.label == l.label)
) )
delete_query.execute() update_query.execute()
# Expire events from cameras based on the camera config ## Expire events from cameras based on the camera config
for name, camera in self.config.cameras.items(): for name, camera in self.config.cameras.items():
if media == 'clips':
retain_config = camera.clips.retain retain_config = camera.clips.retain
else:
retain_config = camera.snapshots.retain
# get distinct objects in database for this camera # get distinct objects in database for this camera
distinct_labels = (Event.select(Event.label) distinct_labels = (Event.select(Event.label)
.where(Event.camera == name) .where(Event.camera == name)
@ -272,14 +268,39 @@ class EventCleanup(threading.Thread):
) )
# delete the grabbed clips from disk # delete the grabbed clips from disk
for event in expired_events: for event in expired_events:
clip_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
clip = Path(f"{os.path.join(CLIPS_DIR, clip_name)}.mp4") media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}")
clip.unlink(missing_ok=True) media.unlink(missing_ok=True)
# delete the event for this type from the db # update the clips attribute for the db entry
delete_query = ( update_query = (
Event.delete() Event.update(update_params)
.where( Event.camera == name, .where( Event.camera == name,
Event.start_time < expire_after, Event.start_time < expire_after,
Event.label == l.label) Event.label == l.label)
) )
update_query.execute()
def run(self):
counter = 0
while(True):
if self.stop_event.is_set():
logger.info(f"Exiting event cleanup...")
break
# only expire events every 10 minutes, but check for stop events every 10 seconds
time.sleep(10)
counter = counter + 1
if counter < 60:
continue
counter = 0
self.expire('clips')
self.expire('snapshots')
# drop events from db where has_clip and has_snapshot are false
delete_query = (
Event.delete()
.where( Event.has_clip == False,
Event.has_snapshot == False)
)
delete_query.execute() delete_query.execute()

View File

@ -325,7 +325,7 @@ class TestConfig(TestCase):
'ffmpeg': { 'ffmpeg': {
'inputs': [ 'inputs': [
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect', 'rtmp'] }, { 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect', 'rtmp'] },
{ 'path': 'rtsp://10.0.0.1:554/clips', 'roles': ['clips'] } { 'path': 'rtsp://10.0.0.1:554/record', 'roles': ['record'] }
] ]
}, },
'height': 1080, 'height': 1080,