diff --git a/frigate/config/camera/record.py b/frigate/config/camera/record.py index dec629b6b8..52d11e2a58 100644 --- a/frigate/config/camera/record.py +++ b/frigate/config/camera/record.py @@ -4,6 +4,7 @@ from pydantic import Field from frigate.const import MAX_PRE_CAPTURE +from frigate.review.types import SeverityEnum from ..base import FrigateBaseModel @@ -101,3 +102,15 @@ def event_pre_capture(self) -> int: self.alerts.pre_capture, self.detections.pre_capture, ) + + def get_review_pre_capture(self, severity: SeverityEnum) -> int: + if severity == SeverityEnum.alert: + return self.alerts.pre_capture + else: + return self.detections.pre_capture + + def get_review_post_capture(self, severity: SeverityEnum) -> int: + if severity == SeverityEnum.alert: + return self.alerts.post_capture + else: + return self.detections.post_capture diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index fd3e3c396e..f8c7a6bcbc 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -558,22 +558,26 @@ def handle_command( if not self._init_onvif(camera_name): return - if command == OnvifCommandEnum.init: - # already init - return - elif command == OnvifCommandEnum.stop: - self._stop(camera_name) - elif command == OnvifCommandEnum.preset: - self._move_to_preset(camera_name, param) - elif command == OnvifCommandEnum.move_relative: - _, pan, tilt = param.split("_") - self._move_relative(camera_name, float(pan), float(tilt), 0, 1) - elif ( - command == OnvifCommandEnum.zoom_in or command == OnvifCommandEnum.zoom_out - ): - self._zoom(camera_name, command) - else: - self._move(camera_name, command) + try: + if command == OnvifCommandEnum.init: + # already init + return + elif command == OnvifCommandEnum.stop: + self._stop(camera_name) + elif command == OnvifCommandEnum.preset: + self._move_to_preset(camera_name, param) + elif command == OnvifCommandEnum.move_relative: + _, pan, tilt = param.split("_") + self._move_relative(camera_name, float(pan), float(tilt), 0, 1) + elif ( + command == OnvifCommandEnum.zoom_in + or command == OnvifCommandEnum.zoom_out + ): + self._zoom(camera_name, command) + else: + self._move(camera_name, command) + except ONVIFError as e: + logger.error(f"Unable to handle onvif command: {e}") def get_camera_info(self, camera_name: str) -> dict[str, any]: if camera_name not in self.cams.keys(): diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 4f976bbf67..2697356703 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -29,6 +29,7 @@ RECORD_DIR, ) from frigate.models import Recordings, ReviewSegment +from frigate.review.types import SeverityEnum from frigate.util.services import get_video_properties logger = logging.getLogger(__name__) @@ -194,6 +195,7 @@ async def move_files(self) -> None: ReviewSegment.select( ReviewSegment.start_time, ReviewSegment.end_time, + ReviewSegment.severity, ReviewSegment.data, ) .where( @@ -219,11 +221,15 @@ async def move_files(self) -> None: [r for r in recordings_to_insert if r is not None], ) + def drop_segment(self, cache_path: str) -> None: + Path(cache_path).unlink(missing_ok=True) + self.end_time_cache.pop(cache_path, None) + async def validate_and_move_segment( self, camera: str, reviews: list[ReviewSegment], recording: dict[str, any] ) -> None: - cache_path = recording["cache_path"] - start_time = recording["start_time"] + cache_path: str = recording["cache_path"] + start_time: datetime.datetime = recording["start_time"] record_config = self.config.cameras[camera].record # Just delete files if recordings are turned off @@ -231,8 +237,7 @@ async def validate_and_move_segment( camera not in self.config.cameras or not self.config.cameras[camera].record.enabled ): - Path(cache_path).unlink(missing_ok=True) - self.end_time_cache.pop(cache_path, None) + self.drop_segment(cache_path) return if cache_path in self.end_time_cache: @@ -260,24 +265,34 @@ async def validate_and_move_segment( return # if cached file's start_time is earlier than the retain days for the camera + # meaning continuous recording is not enabled if start_time <= ( datetime.datetime.now().astimezone(datetime.timezone.utc) - datetime.timedelta(days=self.config.cameras[camera].record.retain.days) ): - # if the cached segment overlaps with the events: + # if the cached segment overlaps with the review items: overlaps = False for review in reviews: - # if the event starts in the future, stop checking events + severity = SeverityEnum[review.severity] + + # if the review item starts in the future, stop checking review items # and remove this segment - if review.start_time > end_time.timestamp(): + if ( + review.start_time - record_config.get_review_pre_capture(severity) + ) > 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 - # and stop looking at events - if review.end_time is None or review.end_time >= start_time.timestamp(): + # 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 @@ -296,7 +311,7 @@ async def validate_and_move_segment( cache_path, record_mode, ) - # if it doesn't overlap with an event, 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 else: camera_info = self.object_recordings_info[camera] @@ -307,9 +322,9 @@ async def validate_and_move_segment( most_recently_processed_frame_time - record_config.event_pre_capture ).astimezone(datetime.timezone.utc) if end_time < retain_cutoff: - Path(cache_path).unlink(missing_ok=True) - self.end_time_cache.pop(cache_path, None) + self.drop_segment(cache_path) # else retain days includes this segment + # meaning continuous recording is enabled else: # assume that empty means the relevant recording info has not been received yet camera_info = self.object_recordings_info[camera] @@ -390,8 +405,7 @@ async def move_segment( # check if the segment shouldn't be stored if segment_info.should_discard_segment(store_mode): - Path(cache_path).unlink(missing_ok=True) - self.end_time_cache.pop(cache_path, None) + self.drop_segment(cache_path) return # directory will be in utc due to start_time being in utc