From cfb87f97444dac4887a7fb8039e91ac424f861b9 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 18 Apr 2026 08:10:50 -0500 Subject: [PATCH] Miscellaneous fixes (#22913) * add log when probing detect stream on startup when users don't explicitly set detect.width and detect.height, we probe for them. sometimes the probe hangs (camera doesn't support UDP, like some Reolinks), so this log message will make that clearer * add faq about probing detect stream * fix stuck activity ring when tracked object transitions to stationary * drop cache segments past retain cutoff regardless of retention mode * add maintainer test --- docs/docs/troubleshooting/faqs.md | 24 ++++++++++++++++ frigate/config/config.py | 3 ++ frigate/record/maintainer.py | 10 ++++--- frigate/test/test_maintainer.py | 41 ++++++++++++++++++++++++++++ web/src/hooks/use-camera-activity.ts | 8 ++++-- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/docs/docs/troubleshooting/faqs.md b/docs/docs/troubleshooting/faqs.md index ff2379ea7..6cd67ba88 100644 --- a/docs/docs/troubleshooting/faqs.md +++ b/docs/docs/troubleshooting/faqs.md @@ -110,3 +110,27 @@ No. Frigate uses the TCP protocol to connect to your camera's RTSP URL. VLC auto TCP ensures that all data packets arrive in the correct order. This is crucial for video recording, decoding, and stream processing, which is why Frigate enforces a TCP connection. UDP is faster but less reliable, as it does not guarantee packet delivery or order, and VLC does not have the same requirements as Frigate. You can still configure Frigate to use UDP by using ffmpeg input args or the preset `preset-rtsp-udp`. See the [ffmpeg presets](/configuration/ffmpeg_presets) documentation. + +### Frigate hangs on startup with a "probing detect stream" message in the logs + +On startup, Frigate probes each camera's detect stream with OpenCV to auto-detect its resolution. OpenCV's FFmpeg backend may attempt RTSP over UDP during this probe regardless of the `-rtsp_transport tcp` in your `input_args` or preset. For cameras that do not respond to UDP (common on some Reolink models and others behind firewalls that block UDP), the probe can hang indefinitely and block Frigate from finishing startup, or it can return zeroed-out dimensions that show up as width `0` and height `0` in Camera Probe Info under System Metrics. + +There are two ways to avoid this: + +1. Set `detect.width` and `detect.height` explicitly in your camera config. When both are set, Frigate skips the auto-detect probe entirely: + + ```yaml + cameras: + my_camera: + detect: + width: 1280 + height: 720 + ``` + +2. Force OpenCV's FFmpeg backend to use TCP for RTSP by setting the environment variable on your Frigate container: + + ``` + OPENCV_FFMPEG_CAPTURE_OPTIONS=rtsp_transport;tcp + ``` + + This is a process-wide setting and applies to all cameras. If you have any cameras that require `preset-rtsp-udp`, use option 1 instead. diff --git a/frigate/config/config.py b/frigate/config/config.py index 1d09016f6..de3438cd0 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -730,6 +730,9 @@ class FrigateConfig(FrigateBaseModel): ) if need_detect_dimensions: + logger.info( + f"detect.width and detect.height not set for {camera_config.name}, probing detect stream to determine resolution." + ) stream_info = {"width": 0, "height": 0, "fourcc": None} try: stream_info = stream_info_retriever.get_stream_info( diff --git a/frigate/record/maintainer.py b/frigate/record/maintainer.py index 73868ea24..6d25622f4 100644 --- a/frigate/record/maintainer.py +++ b/frigate/record/maintainer.py @@ -464,10 +464,12 @@ class RecordingMaintainer(threading.Thread): self.drop_segment(cache_path) return None - # 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 - # BUT only if continuous/motion is NOT enabled (otherwise wait for processing) - elif highest is None: + # if it doesn't overlap with a review item, drop the segment once it + # ends more than event_pre_capture before the most recently processed + # frame. at this point we've already decided not to keep it for + # continuous/motion retention (either disabled or segment_stats said + # discard), so waiting longer just fills the cache. + else: camera_info = self.object_recordings_info[camera] most_recently_processed_frame_time = ( camera_info[-1][0] if len(camera_info) > 0 else 0 diff --git a/frigate/test/test_maintainer.py b/frigate/test/test_maintainer.py index 49712749e..3ac4d8a07 100644 --- a/frigate/test/test_maintainer.py +++ b/frigate/test/test_maintainer.py @@ -1,3 +1,4 @@ +import datetime import sys import unittest from unittest.mock import MagicMock, patch @@ -74,6 +75,46 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase): f"Expected a single warning for unexpected files, got {len(matching)}", ) + async def test_drops_quiet_segment_when_only_motion_retention(self): + # Regression: when motion retention is enabled but a segment has no + # motion and no review overlaps it, the segment must still be dropped. + # Otherwise it sits in cache forever, accumulates, and triggers the + # "Unable to keep up with recording segments in cache" warning every + # ~10s as the overflow trim in move_files discards the oldest one. + config = MagicMock(spec=FrigateConfig) + + camera_config = MagicMock() + camera_config.record.enabled = True + camera_config.record.continuous.days = 0 + camera_config.record.motion.days = 1 + camera_config.record.event_pre_capture = 5 + config.cameras = {"test_cam": camera_config} + + stop_event = MagicMock() + maintainer = RecordingMaintainer(config, stop_event) + + now = datetime.datetime.now(datetime.timezone.utc) + start_time = now - datetime.timedelta(seconds=20) + end_time = now - datetime.timedelta(seconds=10) + cache_path = "/tmp/cache/test_cam@20260417150000+0000.mp4" + + maintainer.end_time_cache = {cache_path: (end_time, 10.0)} + # Single processed frame well past end_time with no motion/objects. + maintainer.object_recordings_info["test_cam"] = [(now.timestamp(), [], [], [])] + maintainer.audio_recordings_info["test_cam"] = [] + + maintainer.drop_segment = MagicMock() + maintainer.recordings_publisher = MagicMock() + + result = await maintainer.validate_and_move_segment( + "test_cam", + reviews=[], + recording={"start_time": start_time, "cache_path": cache_path}, + ) + + self.assertIsNone(result) + maintainer.drop_segment.assert_called_once_with(cache_path) + if __name__ == "__main__": unittest.main() diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 76a982725..9900f277f 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -137,9 +137,11 @@ export function useCameraActivity( } } - newObjects[updatedEventIndex].label = label; - newObjects[updatedEventIndex].stationary = - updatedEvent.after.stationary; + newObjects[updatedEventIndex] = { + ...newObjects[updatedEventIndex], + label, + stationary: updatedEvent.after.stationary, + }; } }