diff --git a/frigate/app.py b/frigate/app.py index 0185a0c382..3491f06b3d 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -166,11 +166,7 @@ class FrigateApp: # create camera_metrics for camera_name in self.config.cameras.keys(): self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager) - self.ptz_metrics[camera_name] = PTZMetrics( - autotracker_enabled=self.config.cameras[ - camera_name - ].onvif.autotracking.enabled - ) + self.ptz_metrics[camera_name] = PTZMetrics() def init_queues(self) -> None: # Queue for cameras to push tracked objects to diff --git a/frigate/camera/__init__.py b/frigate/camera/__init__.py index 85831653e1..c20e150041 100644 --- a/frigate/camera/__init__.py +++ b/frigate/camera/__init__.py @@ -43,8 +43,6 @@ class CameraMetrics: class PTZMetrics: - autotracker_enabled: Synchronized - start_time: Synchronized stop_time: Synchronized frame_time: Synchronized @@ -52,13 +50,10 @@ class PTZMetrics: max_zoom: Synchronized min_zoom: Synchronized - tracking_active: Event motor_stopped: Event reset: Event - def __init__(self, *, autotracker_enabled: bool): - self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment] - + def __init__(self) -> None: self.start_time = mp.Value("d", 0) # type: ignore[assignment] self.stop_time = mp.Value("d", 0) # type: ignore[assignment] self.frame_time = mp.Value("d", 0) # type: ignore[assignment] @@ -66,7 +61,6 @@ class PTZMetrics: self.max_zoom = mp.Value("d", 0) # type: ignore[assignment] self.min_zoom = mp.Value("d", 0) # type: ignore[assignment] - self.tracking_active = mp.Event() self.motor_stopped = mp.Event() self.reset = mp.Event() diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index a7e7ca5ecb..9634b3803c 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -120,9 +120,7 @@ class CameraMaintainer(threading.Thread): if runtime: self.camera_metrics[name] = CameraMetrics(self.metrics_manager) - self.ptz_metrics[name] = PTZMetrics( - autotracker_enabled=config.onvif.autotracking.enabled - ) + self.ptz_metrics[name] = PTZMetrics() self.region_grids[name] = get_camera_regions_grid( name, config.detect, diff --git a/frigate/comms/dispatcher.py b/frigate/comms/dispatcher.py index 4d9cbc0470..085f133413 100644 --- a/frigate/comms/dispatcher.py +++ b/frigate/comms/dispatcher.py @@ -759,15 +759,13 @@ class Dispatcher: "Autotracking must be enabled in the config to be turned on via MQTT." ) return - if not self.ptz_metrics[camera_name].autotracker_enabled.value: + if not ptz_autotracker_settings.enabled: logger.info(f"Turning on ptz autotracker for {camera_name}") - self.ptz_metrics[camera_name].autotracker_enabled.value = True self.ptz_metrics[camera_name].start_time.value = 0 ptz_autotracker_settings.enabled = True elif payload == "OFF": - if self.ptz_metrics[camera_name].autotracker_enabled.value: + if ptz_autotracker_settings.enabled: logger.info(f"Turning off ptz autotracker for {camera_name}") - self.ptz_metrics[camera_name].autotracker_enabled.value = False self.ptz_metrics[camera_name].start_time.value = 0 ptz_autotracker_settings.enabled = False diff --git a/frigate/motion/improved_motion.py b/frigate/motion/improved_motion.py index bbcaee23af..18115825fc 100644 --- a/frigate/motion/improved_motion.py +++ b/frigate/motion/improved_motion.py @@ -19,6 +19,7 @@ class ImprovedMotionDetector(MotionDetector): config: RuntimeMotionConfig, fps: int, ptz_metrics: PTZMetrics | None = None, + autotracking_enabled: bool = False, name: str = "improved", blur_radius: int = 1, interpolation: int = cv2.INTER_NEAREST, @@ -45,6 +46,7 @@ class ImprovedMotionDetector(MotionDetector): self.contrast_values[:, 1:2] = 255 self.contrast_values_index = 0 self.ptz_metrics = ptz_metrics + self.autotracking_enabled = autotracking_enabled self.last_stop_time: float | None = None def is_calibrating(self) -> bool: @@ -59,8 +61,7 @@ class ImprovedMotionDetector(MotionDetector): # if ptz motor is moving from autotracking, quickly return # a single box that is 80% of the frame if self.ptz_metrics is not None and ( - self.ptz_metrics.autotracker_enabled.value - and not self.ptz_metrics.motor_stopped.is_set() + self.autotracking_enabled and not self.ptz_metrics.motor_stopped.is_set() ): return [ ( @@ -162,7 +163,7 @@ class ImprovedMotionDetector(MotionDetector): # if so, reassign the average to the current frame so we begin with a new baseline if self.ptz_metrics is not None and ( # ensure we only do this for cameras with autotracking enabled - self.ptz_metrics.autotracker_enabled.value + self.autotracking_enabled and self.ptz_metrics.motor_stopped.is_set() and ( self.last_stop_time is None diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 14d78bc7fd..335a3d9b6b 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -23,6 +23,7 @@ from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum from frigate.config.camera.updater import ( CameraConfigUpdateEnum, CameraConfigUpdateSubscriber, + CameraConfigUpdateTopic, ) from frigate.const import ( AUTOTRACKING_MAX_AREA_RATIO, @@ -236,7 +237,7 @@ class PtzAutoTracker(threading.Thread): def run(self) -> None: while not self.stop_event.wait(1): - self.check_for_updates() + self.config_subscriber.check_for_updates() for camera, camera_config in list(self.config.cameras.items()): if not camera_config.enabled: @@ -257,29 +258,6 @@ class PtzAutoTracker(threading.Thread): self.config_subscriber.stop() logger.info("Exiting autotracker...") - def check_for_updates(self) -> None: - """Apply camera config updates and mirror autotracking state to ptz metrics. - - The camera processes read autotracker_enabled rather than the config, so it - has to follow every path that can change autotracking, not just the mqtt - toggle that writes it directly. - """ - updates = self.config_subscriber.check_for_updates() - - for cameras in updates.values(): - for camera in cameras: - camera_config = self.config.cameras.get(camera) - metrics = self.ptz_metrics.get(camera) - - # a camera added at runtime gets its metrics from the maintainer on - # another thread, which seeds them from this same config value - if camera_config is None or metrics is None: - continue - - metrics.autotracker_enabled.value = ( - camera_config.onvif.autotracking.enabled - ) - async def _autotracker_setup(self, camera_config: CameraConfig, camera: str): logger.debug(f"{camera}: Autotracker init") @@ -337,14 +315,19 @@ class PtzAutoTracker(threading.Thread): if camera_config.onvif.autotracking.calibrate_on_startup: await self._calibrate_camera(camera) - self.ptz_metrics[camera].tracking_active.clear() self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False) self.autotracker_init[camera] = True def _disable(self, camera: str, reason: str) -> None: logger.warning(f"Disabling autotracking for {camera}: {reason}") - self.config.cameras[camera].onvif.autotracking.enabled = False - self.ptz_metrics[camera].autotracker_enabled.value = False + autotracking_config = self.config.cameras[camera].onvif.autotracking + autotracking_config.enabled = False + + # the camera process holds its own copy of the config + self.dispatcher.config_updater.publish_update( + CameraConfigUpdateTopic(CameraConfigUpdateEnum.autotracking, camera), + autotracking_config, + ) def _reset_tracked_object_metrics(self, camera: str) -> None: zoom_factor = self.config.cameras[camera].onvif.autotracking.zoom_factor @@ -1307,7 +1290,6 @@ class PtzAutoTracker(threading.Thread): logger.debug( f"{camera}: New object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" ) - self.ptz_metrics[camera].tracking_active.set() self.dispatcher.publish( f"{camera}/ptz_autotracker/active", "ON", retain=False ) @@ -1441,7 +1423,6 @@ class PtzAutoTracker(threading.Thread): # update stored zoom level from preset await self._wait_until_stopped(camera) - self.ptz_metrics[camera].tracking_active.clear() self.dispatcher.publish( f"{camera}/ptz_autotracker/active", "OFF", retain=False ) diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index f828aa5f93..67403b02ab 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -566,8 +566,9 @@ class OnvifController: return metrics = self.ptz_metrics.get(camera_name) + camera_config = self.config.cameras.get(camera_name) - if metrics is None: + if metrics is None or camera_config is None: return logger.debug( @@ -583,7 +584,7 @@ class OnvifController: cam["active"] = True # only track start_time for autotracking - if metrics.autotracker_enabled.value: + if camera_config.onvif.autotracking.enabled: metrics.motor_stopped.clear() logger.debug(f"{camera_name}: PTZ start time: {metrics.frame_time.value}") metrics.start_time.value = metrics.frame_time.value diff --git a/frigate/test/test_motion_detector.py b/frigate/test/test_motion_detector.py index 6fb7a68efc..b5fe126a6c 100644 --- a/frigate/test/test_motion_detector.py +++ b/frigate/test/test_motion_detector.py @@ -29,7 +29,6 @@ class TestImprovedMotionDetector(unittest.TestCase): class DummyPTZ: def __init__(self): - self.autotracker_enabled = _Stub(False) self.motor_stopped = _Stub(False) self.stop_time = _Stub(0) diff --git a/frigate/test/test_ptz_autotrack.py b/frigate/test/test_ptz_autotrack.py index 97e3a935c0..78b0aa0c78 100644 --- a/frigate/test/test_ptz_autotrack.py +++ b/frigate/test/test_ptz_autotrack.py @@ -7,9 +7,8 @@ KeyError on the autotracker thread or silently keep the wrong state: - autotracker_init only got an entry for cameras enabled when PtzAutoTracker was constructed, so runtime-enabled cameras raised KeyError on lookup. -- ptz_metrics autotracker_enabled is what the camera processes read, but nothing - updated it when autotracking was enabled through a config save, so it stayed - False and the tracker never built a motion estimator. +- _disable only changed the main process config, so the camera process kept + running its motion estimator for a camera that could not autotrack. """ import unittest @@ -17,6 +16,7 @@ from unittest.mock import MagicMock from frigate.camera import PTZMetrics from frigate.config import FrigateConfig +from frigate.config.camera.updater import CameraConfigUpdateEnum from frigate.ptz.autotrack import PtzAutoTracker CAMERA = "ptz_cam" @@ -53,8 +53,9 @@ def _make_tracker(autotracking_enabled: bool = True) -> PtzAutoTracker: onvif over the network. Only the config/metrics state is relevant here.""" tracker = PtzAutoTracker.__new__(PtzAutoTracker) tracker.config = _config(autotracking_enabled) - tracker.ptz_metrics = {CAMERA: PTZMetrics(autotracker_enabled=False)} + tracker.ptz_metrics = {CAMERA: PTZMetrics()} tracker.onvif = MagicMock() + tracker.dispatcher = MagicMock() tracker.config_subscriber = MagicMock() tracker.autotracker_init = {} tracker.calibrating = {} @@ -105,47 +106,21 @@ class TestAutotrackerEnqueueMove(unittest.TestCase): self._enqueue(0, 0, 0).assert_not_called() -class TestAutotrackerMetricSync(unittest.TestCase): - def test_metric_follows_config_when_enabled_by_update(self) -> None: - # autotracking enabled via a config save: the metric was seeded False when - # the camera was added and nothing else updates it +class TestAutotrackerDisable(unittest.TestCase): + def test_disable_publishes_to_camera_process(self) -> None: tracker = _make_tracker(autotracking_enabled=True) - metrics = tracker.ptz_metrics[CAMERA] - self.assertFalse(metrics.autotracker_enabled.value) - tracker.config_subscriber.check_for_updates.return_value = {"onvif": [CAMERA]} - tracker.check_for_updates() + tracker._disable(CAMERA, "onvif connection failed") - self.assertTrue(metrics.autotracker_enabled.value) + autotracking = tracker.config.cameras[CAMERA].onvif.autotracking + self.assertFalse(autotracking.enabled) - def test_metric_follows_config_when_disabled_by_update(self) -> None: - tracker = _make_tracker(autotracking_enabled=False) - metrics = tracker.ptz_metrics[CAMERA] - metrics.autotracker_enabled.value = True - - tracker.config_subscriber.check_for_updates.return_value = { - "autotracking": [CAMERA] - } - tracker.check_for_updates() - - self.assertFalse(metrics.autotracker_enabled.value) - - def test_metric_sync_skips_camera_without_metrics(self) -> None: - # `add` reaches the maintainer and the autotracker on separate threads with - # no ordering guarantee, so the metrics may not exist yet - tracker = _make_tracker() - tracker.ptz_metrics = {} - tracker.config_subscriber.check_for_updates.return_value = {"add": [CAMERA]} - - tracker.check_for_updates() - - def test_metric_sync_skips_unknown_camera(self) -> None: - tracker = _make_tracker() - tracker.config_subscriber.check_for_updates.return_value = { - "add": ["not_in_config"] - } - - tracker.check_for_updates() + publish = tracker.dispatcher.config_updater.publish_update + publish.assert_called_once() + topic, payload = publish.call_args.args + self.assertEqual(topic.update_type, CameraConfigUpdateEnum.autotracking) + self.assertEqual(topic.camera, CAMERA) + self.assertIs(payload, autotracking) if __name__ == "__main__": diff --git a/frigate/test/test_ptz_onvif.py b/frigate/test/test_ptz_onvif.py index daaf44ecc7..5b6971d63d 100644 --- a/frigate/test/test_ptz_onvif.py +++ b/frigate/test/test_ptz_onvif.py @@ -121,9 +121,7 @@ def _make_move_controller(autotracking_enabled: bool) -> OnvifController: }, } } - controller.ptz_metrics = { - CAMERA: PTZMetrics(autotracker_enabled=autotracking_enabled) - } + controller.ptz_metrics = {CAMERA: PTZMetrics()} return controller diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index b2e8221f27..e69e1305a3 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -223,7 +223,7 @@ class NorfairTracker(ObjectTracker): ), } - if self.ptz_metrics.autotracker_enabled.value: + if self.camera_config.onvif.autotracking.enabled: self.ptz_motion_estimator = PtzMotionEstimator( self.camera_config, self.ptz_metrics ) @@ -515,7 +515,7 @@ class NorfairTracker(ObjectTracker): yuv_frame: np.ndarray | None = None if ( - self.ptz_metrics.autotracker_enabled.value + self.camera_config.onvif.autotracking.enabled or self.detect_config.stationary.classifier ): yuv_frame = self.frame_manager.get( @@ -534,7 +534,7 @@ class NorfairTracker(ObjectTracker): points = np.array([[obj[2][0], obj[2][1]], [obj[2][2], obj[2][3]]]) embedding = None - if self.ptz_metrics.autotracker_enabled.value: + if self.camera_config.onvif.autotracking.enabled: embedding = get_histogram( yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3] ) @@ -559,7 +559,7 @@ class NorfairTracker(ObjectTracker): coord_transformations = None - if self.ptz_metrics.autotracker_enabled.value: + if self.camera_config.onvif.autotracking.enabled: # we must have been enabled by mqtt, so set up the estimator if not self.ptz_motion_estimator: self.ptz_motion_estimator = PtzMotionEstimator( diff --git a/frigate/video/detect.py b/frigate/video/detect.py index 2fca30debb..4bdcfde227 100644 --- a/frigate/video/detect.py +++ b/frigate/video/detect.py @@ -94,6 +94,7 @@ class CameraTracker(FrigateProcess): self.config.detect.fps, name=self.config.name, ptz_metrics=self.ptz_metrics, + autotracking_enabled=self.config.onvif.autotracking.enabled, ) object_detector = RemoteObjectDetector( self.config.name, @@ -195,10 +196,12 @@ def process_frames( None, {camera_config.name: camera_config}, [ + CameraConfigUpdateEnum.autotracking, CameraConfigUpdateEnum.detect, CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.motion, CameraConfigUpdateEnum.objects, + CameraConfigUpdateEnum.onvif, ], ) @@ -235,6 +238,11 @@ def process_frames( motion_detector.config = camera_config.motion motion_detector.update_mask() + if "autotracking" in updated_configs or "onvif" in updated_configs: + motion_detector.autotracking_enabled = ( + camera_config.onvif.autotracking.enabled + ) + if ( not camera_enabled and prev_enabled != camera_enabled @@ -349,8 +357,8 @@ def process_frames( # only add in the motion boxes when not calibrating and a ptz is not moving via autotracking # the ptz timestamps are only maintained while autotracking is on, so gate - # on the metric rather than trusting them to be reset otherwise - ptz_moving = ptz_metrics.autotracker_enabled.value and ( + # on the config rather than trusting them to be reset otherwise + ptz_moving = camera_config.onvif.autotracking.enabled and ( ptz_moving_at_frame_time( frame_time, ptz_metrics.start_time.value,