mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-01-16 00:09:14 +01:00
add retention settings for snapshots
This commit is contained in:
parent
25e3fe8eab
commit
63e14a98f9
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user