diff --git a/frigate/test/test_camera_watchdog.py b/frigate/test/test_camera_watchdog.py index 286b3b459a..153da6b334 100644 --- a/frigate/test/test_camera_watchdog.py +++ b/frigate/test/test_camera_watchdog.py @@ -1,8 +1,9 @@ """Tests for per stream recording health tracking in the camera watchdog.""" +import subprocess as sp import unittest from datetime import UTC, datetime, timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch from frigate.config import FrigateConfig from frigate.const import STREAM_TYPE_MAIN, STREAM_TYPE_SUB @@ -62,6 +63,39 @@ def build_watchdog( return watchdog +class TestCameraWatchdogReset(unittest.TestCase): + def _build_resettable_watchdog(self) -> CameraWatchdog: + watchdog = build_watchdog() + watchdog.ffmpeg_detect_process = MagicMock() + watchdog.capture_thread = MagicMock() + watchdog.capture_thread.is_alive.return_value = False + watchdog.start_ffmpeg_detect = MagicMock() + return watchdog + + def test_reset_does_not_buffer_raw_frames(self): + watchdog = self._build_resettable_watchdog() + process = watchdog.ffmpeg_detect_process + + watchdog.reset_capture_thread() + + process.terminate.assert_called_once() + process.wait.assert_called_once_with(timeout=30) + process.communicate.assert_not_called() + watchdog.start_ffmpeg_detect.assert_called_once() + + def test_timed_out_reset_kills_and_reaps_without_reading_stdout(self): + watchdog = self._build_resettable_watchdog() + process = watchdog.ffmpeg_detect_process + process.wait.side_effect = [sp.TimeoutExpired("ffmpeg", 30), 0] + + watchdog.reset_capture_thread() + + process.kill.assert_called_once() + self.assertEqual(process.wait.call_args_list, [call(timeout=30), call()]) + process.communicate.assert_not_called() + watchdog.start_ffmpeg_detect.assert_called_once() + + class TestCameraWatchdogStreamHealth(unittest.TestCase): def _build_watchdog( self, sub_enabled: bool = True, output_args: dict | None = None diff --git a/frigate/video/ffmpeg.py b/frigate/video/ffmpeg.py index d56671b520..8de45743fb 100644 --- a/frigate/video/ffmpeg.py +++ b/frigate/video/ffmpeg.py @@ -310,26 +310,20 @@ class CameraWatchdog(threading.Thread): self._check_config_updates() return self.config.enabled - def reset_capture_thread( - self, terminate: bool = True, drain_output: bool = True - ) -> None: + def reset_capture_thread(self, terminate: bool = True) -> None: if terminate: + # The capture thread keeps draining stdout, so wait() is enough here. + # communicate() would also buffer every frame ffmpeg writes before it + # exits, which can be many duplicates after a stall. self.ffmpeg_detect_process.terminate() try: self.logger.info("Waiting for ffmpeg to exit gracefully...") - - if drain_output: - self.ffmpeg_detect_process.communicate(timeout=30) - else: - self.ffmpeg_detect_process.wait(timeout=30) + self.ffmpeg_detect_process.wait(timeout=30) except sp.TimeoutExpired: self.logger.info("FFmpeg did not exit. Force killing...") self.ffmpeg_detect_process.kill() - if drain_output: - self.ffmpeg_detect_process.communicate() - else: - self.ffmpeg_detect_process.wait() + self.ffmpeg_detect_process.wait() # Update reconnects now = datetime.now().timestamp() @@ -497,7 +491,7 @@ class CameraWatchdog(threading.Thread): f"{self.config.name} exceeded fps limit. Exiting ffmpeg..." ) if can_restart: - self.reset_capture_thread(drain_output=False) + self.reset_capture_thread() last_restart_time = now elif now - self.capture_thread.current_frame.value > 20: self.detect_status.send("offline", now)