diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py index cc5b9a32b..3d8b18105 100644 --- a/frigate/video/ffmpeg.py +++ b/frigate/video/ffmpeg.py @@ -24,7 +24,7 @@ from frigate.config.camera.updater import ( ) from frigate.const import PROCESS_PRIORITY_HIGH from frigate.log import LogPipe -from frigate.util.builtin import EventsPerSecond +from frigate.util.builtin import EventsPerSecond, get_ffmpeg_arg_list from frigate.util.ffmpeg import start_or_restart_ffmpeg, stop_ffmpeg from frigate.util.image import ( FrameManager, @@ -34,6 +34,23 @@ from frigate.util.process import FrigateProcess logger = logging.getLogger(__name__) +# all built-in record presets use this segment_time +DEFAULT_RECORD_SEGMENT_TIME = 10 + + +def _get_record_segment_time(config: CameraConfig) -> int: + """Extract -segment_time from the camera's record output args.""" + record_args = get_ffmpeg_arg_list(config.ffmpeg.output_args.record) + + if record_args and record_args[0].startswith("preset"): + return DEFAULT_RECORD_SEGMENT_TIME + + try: + idx = record_args.index("-segment_time") + return int(record_args[idx + 1]) + except (ValueError, IndexError): + return DEFAULT_RECORD_SEGMENT_TIME + def capture_frames( ffmpeg_process: sp.Popen[Any], @@ -164,6 +181,12 @@ class CameraWatchdog(threading.Thread): self.latest_cache_segment_time: float = 0 self.record_enable_time: datetime | None = None + # `valid` segments are published with the segment's start time, so the + # gap between consecutive publishes can reach 2 * segment_time. Pad the + # staleness threshold so it's never tighter than that worst case. + segment_time = _get_record_segment_time(self.config) + self.record_stale_threshold = max(120, 2 * segment_time + 30) + # Stall tracking (based on last processed frame) self._stall_timestamps: deque[float] = deque() self._stall_active: bool = False @@ -413,16 +436,17 @@ class CameraWatchdog(threading.Thread): # ensure segments are still being created and that they have valid video data # Skip checks during grace period to allow segments to start being created + stale_window = timedelta(seconds=self.record_stale_threshold) cache_stale = not in_grace_period and now_utc > ( - latest_cache_dt + timedelta(seconds=120) + latest_cache_dt + stale_window ) valid_stale = not in_grace_period and now_utc > ( - latest_valid_dt + timedelta(seconds=120) + latest_valid_dt + stale_window ) invalid_stale_condition = ( self.latest_invalid_segment_time > 0 and not in_grace_period - and now_utc > (latest_invalid_dt + timedelta(seconds=120)) + and now_utc > (latest_invalid_dt + stale_window) and self.latest_valid_segment_time <= self.latest_invalid_segment_time ) @@ -439,7 +463,7 @@ class CameraWatchdog(threading.Thread): ) self.logger.error( - f"{reason} for {self.config.name} in the last 120s. Restarting the ffmpeg record process..." + f"{reason} for {self.config.name} in the last {self.record_stale_threshold}s. Restarting the ffmpeg record process..." ) p["process"] = start_or_restart_ffmpeg( p["cmd"],