use camera config for autotracking enabled state

Camera processes read autotracking state from a shared `autotracker_enabled` value that the dispatcher and autotracker had to keep in sync with the config by hand. Camera processes now subscribe to the `autotracking` and `onvif` config updates and read `onvif.autotracking.enabled` directly, so the shared value and the autotracker's mirroring method are gone. `_disable` now publishes its change so the camera process hears about it. Also removes `tracking_active`, which was set and cleared but never read.
This commit is contained in:
Josh Hawkins
2026-10-03 16:11:28 -05:00
parent cc1ff63b53
commit 30012f89fc
12 changed files with 53 additions and 104 deletions
+1 -5
View File
@@ -166,11 +166,7 @@ class FrigateApp:
# create camera_metrics # create camera_metrics
for camera_name in self.config.cameras.keys(): for camera_name in self.config.cameras.keys():
self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager) self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager)
self.ptz_metrics[camera_name] = PTZMetrics( self.ptz_metrics[camera_name] = PTZMetrics()
autotracker_enabled=self.config.cameras[
camera_name
].onvif.autotracking.enabled
)
def init_queues(self) -> None: def init_queues(self) -> None:
# Queue for cameras to push tracked objects to # Queue for cameras to push tracked objects to
+1 -7
View File
@@ -43,8 +43,6 @@ class CameraMetrics:
class PTZMetrics: class PTZMetrics:
autotracker_enabled: Synchronized
start_time: Synchronized start_time: Synchronized
stop_time: Synchronized stop_time: Synchronized
frame_time: Synchronized frame_time: Synchronized
@@ -52,13 +50,10 @@ class PTZMetrics:
max_zoom: Synchronized max_zoom: Synchronized
min_zoom: Synchronized min_zoom: Synchronized
tracking_active: Event
motor_stopped: Event motor_stopped: Event
reset: Event reset: Event
def __init__(self, *, autotracker_enabled: bool): def __init__(self) -> None:
self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment]
self.start_time = mp.Value("d", 0) # type: ignore[assignment] self.start_time = mp.Value("d", 0) # type: ignore[assignment]
self.stop_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] 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.max_zoom = mp.Value("d", 0) # type: ignore[assignment]
self.min_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.motor_stopped = mp.Event()
self.reset = mp.Event() self.reset = mp.Event()
+1 -3
View File
@@ -120,9 +120,7 @@ class CameraMaintainer(threading.Thread):
if runtime: if runtime:
self.camera_metrics[name] = CameraMetrics(self.metrics_manager) self.camera_metrics[name] = CameraMetrics(self.metrics_manager)
self.ptz_metrics[name] = PTZMetrics( self.ptz_metrics[name] = PTZMetrics()
autotracker_enabled=config.onvif.autotracking.enabled
)
self.region_grids[name] = get_camera_regions_grid( self.region_grids[name] = get_camera_regions_grid(
name, name,
config.detect, config.detect,
+2 -4
View File
@@ -759,15 +759,13 @@ class Dispatcher:
"Autotracking must be enabled in the config to be turned on via MQTT." "Autotracking must be enabled in the config to be turned on via MQTT."
) )
return 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}") 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 self.ptz_metrics[camera_name].start_time.value = 0
ptz_autotracker_settings.enabled = True ptz_autotracker_settings.enabled = True
elif payload == "OFF": 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}") 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 self.ptz_metrics[camera_name].start_time.value = 0
ptz_autotracker_settings.enabled = False ptz_autotracker_settings.enabled = False
+4 -3
View File
@@ -19,6 +19,7 @@ class ImprovedMotionDetector(MotionDetector):
config: RuntimeMotionConfig, config: RuntimeMotionConfig,
fps: int, fps: int,
ptz_metrics: PTZMetrics | None = None, ptz_metrics: PTZMetrics | None = None,
autotracking_enabled: bool = False,
name: str = "improved", name: str = "improved",
blur_radius: int = 1, blur_radius: int = 1,
interpolation: int = cv2.INTER_NEAREST, interpolation: int = cv2.INTER_NEAREST,
@@ -45,6 +46,7 @@ class ImprovedMotionDetector(MotionDetector):
self.contrast_values[:, 1:2] = 255 self.contrast_values[:, 1:2] = 255
self.contrast_values_index = 0 self.contrast_values_index = 0
self.ptz_metrics = ptz_metrics self.ptz_metrics = ptz_metrics
self.autotracking_enabled = autotracking_enabled
self.last_stop_time: float | None = None self.last_stop_time: float | None = None
def is_calibrating(self) -> bool: def is_calibrating(self) -> bool:
@@ -59,8 +61,7 @@ class ImprovedMotionDetector(MotionDetector):
# if ptz motor is moving from autotracking, quickly return # if ptz motor is moving from autotracking, quickly return
# a single box that is 80% of the frame # a single box that is 80% of the frame
if self.ptz_metrics is not None and ( if self.ptz_metrics is not None and (
self.ptz_metrics.autotracker_enabled.value self.autotracking_enabled and not self.ptz_metrics.motor_stopped.is_set()
and not self.ptz_metrics.motor_stopped.is_set()
): ):
return [ 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 so, reassign the average to the current frame so we begin with a new baseline
if self.ptz_metrics is not None and ( if self.ptz_metrics is not None and (
# ensure we only do this for cameras with autotracking enabled # 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.ptz_metrics.motor_stopped.is_set()
and ( and (
self.last_stop_time is None self.last_stop_time is None
+10 -29
View File
@@ -23,6 +23,7 @@ from frigate.config import CameraConfig, FrigateConfig, ZoomingModeEnum
from frigate.config.camera.updater import ( from frigate.config.camera.updater import (
CameraConfigUpdateEnum, CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber, CameraConfigUpdateSubscriber,
CameraConfigUpdateTopic,
) )
from frigate.const import ( from frigate.const import (
AUTOTRACKING_MAX_AREA_RATIO, AUTOTRACKING_MAX_AREA_RATIO,
@@ -236,7 +237,7 @@ class PtzAutoTracker(threading.Thread):
def run(self) -> None: def run(self) -> None:
while not self.stop_event.wait(1): 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()): for camera, camera_config in list(self.config.cameras.items()):
if not camera_config.enabled: if not camera_config.enabled:
@@ -257,29 +258,6 @@ class PtzAutoTracker(threading.Thread):
self.config_subscriber.stop() self.config_subscriber.stop()
logger.info("Exiting autotracker...") 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): async def _autotracker_setup(self, camera_config: CameraConfig, camera: str):
logger.debug(f"{camera}: Autotracker init") logger.debug(f"{camera}: Autotracker init")
@@ -337,14 +315,19 @@ class PtzAutoTracker(threading.Thread):
if camera_config.onvif.autotracking.calibrate_on_startup: if camera_config.onvif.autotracking.calibrate_on_startup:
await self._calibrate_camera(camera) await self._calibrate_camera(camera)
self.ptz_metrics[camera].tracking_active.clear()
self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False) self.dispatcher.publish(f"{camera}/ptz_autotracker/active", "OFF", retain=False)
self.autotracker_init[camera] = True self.autotracker_init[camera] = True
def _disable(self, camera: str, reason: str) -> None: def _disable(self, camera: str, reason: str) -> None:
logger.warning(f"Disabling autotracking for {camera}: {reason}") logger.warning(f"Disabling autotracking for {camera}: {reason}")
self.config.cameras[camera].onvif.autotracking.enabled = False autotracking_config = self.config.cameras[camera].onvif.autotracking
self.ptz_metrics[camera].autotracker_enabled.value = False 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: def _reset_tracked_object_metrics(self, camera: str) -> None:
zoom_factor = self.config.cameras[camera].onvif.autotracking.zoom_factor zoom_factor = self.config.cameras[camera].onvif.autotracking.zoom_factor
@@ -1307,7 +1290,6 @@ class PtzAutoTracker(threading.Thread):
logger.debug( logger.debug(
f"{camera}: New object: {obj.obj_data['id']} {obj.obj_data['box']} {obj.obj_data['frame_time']}" 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( self.dispatcher.publish(
f"{camera}/ptz_autotracker/active", "ON", retain=False f"{camera}/ptz_autotracker/active", "ON", retain=False
) )
@@ -1441,7 +1423,6 @@ class PtzAutoTracker(threading.Thread):
# update stored zoom level from preset # update stored zoom level from preset
await self._wait_until_stopped(camera) await self._wait_until_stopped(camera)
self.ptz_metrics[camera].tracking_active.clear()
self.dispatcher.publish( self.dispatcher.publish(
f"{camera}/ptz_autotracker/active", "OFF", retain=False f"{camera}/ptz_autotracker/active", "OFF", retain=False
) )
+3 -2
View File
@@ -566,8 +566,9 @@ class OnvifController:
return return
metrics = self.ptz_metrics.get(camera_name) 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 return
logger.debug( logger.debug(
@@ -583,7 +584,7 @@ class OnvifController:
cam["active"] = True cam["active"] = True
# only track start_time for autotracking # only track start_time for autotracking
if metrics.autotracker_enabled.value: if camera_config.onvif.autotracking.enabled:
metrics.motor_stopped.clear() metrics.motor_stopped.clear()
logger.debug(f"{camera_name}: PTZ start time: {metrics.frame_time.value}") logger.debug(f"{camera_name}: PTZ start time: {metrics.frame_time.value}")
metrics.start_time.value = metrics.frame_time.value metrics.start_time.value = metrics.frame_time.value
-1
View File
@@ -29,7 +29,6 @@ class TestImprovedMotionDetector(unittest.TestCase):
class DummyPTZ: class DummyPTZ:
def __init__(self): def __init__(self):
self.autotracker_enabled = _Stub(False)
self.motor_stopped = _Stub(False) self.motor_stopped = _Stub(False)
self.stop_time = _Stub(0) self.stop_time = _Stub(0)
+16 -41
View File
@@ -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 - autotracker_init only got an entry for cameras enabled when PtzAutoTracker was
constructed, so runtime-enabled cameras raised KeyError on lookup. constructed, so runtime-enabled cameras raised KeyError on lookup.
- ptz_metrics autotracker_enabled is what the camera processes read, but nothing - _disable only changed the main process config, so the camera process kept
updated it when autotracking was enabled through a config save, so it stayed running its motion estimator for a camera that could not autotrack.
False and the tracker never built a motion estimator.
""" """
import unittest import unittest
@@ -17,6 +16,7 @@ from unittest.mock import MagicMock
from frigate.camera import PTZMetrics from frigate.camera import PTZMetrics
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdateEnum
from frigate.ptz.autotrack import PtzAutoTracker from frigate.ptz.autotrack import PtzAutoTracker
CAMERA = "ptz_cam" 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.""" onvif over the network. Only the config/metrics state is relevant here."""
tracker = PtzAutoTracker.__new__(PtzAutoTracker) tracker = PtzAutoTracker.__new__(PtzAutoTracker)
tracker.config = _config(autotracking_enabled) tracker.config = _config(autotracking_enabled)
tracker.ptz_metrics = {CAMERA: PTZMetrics(autotracker_enabled=False)} tracker.ptz_metrics = {CAMERA: PTZMetrics()}
tracker.onvif = MagicMock() tracker.onvif = MagicMock()
tracker.dispatcher = MagicMock()
tracker.config_subscriber = MagicMock() tracker.config_subscriber = MagicMock()
tracker.autotracker_init = {} tracker.autotracker_init = {}
tracker.calibrating = {} tracker.calibrating = {}
@@ -105,47 +106,21 @@ class TestAutotrackerEnqueueMove(unittest.TestCase):
self._enqueue(0, 0, 0).assert_not_called() self._enqueue(0, 0, 0).assert_not_called()
class TestAutotrackerMetricSync(unittest.TestCase): class TestAutotrackerDisable(unittest.TestCase):
def test_metric_follows_config_when_enabled_by_update(self) -> None: def test_disable_publishes_to_camera_process(self) -> None:
# autotracking enabled via a config save: the metric was seeded False when
# the camera was added and nothing else updates it
tracker = _make_tracker(autotracking_enabled=True) 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._disable(CAMERA, "onvif connection failed")
tracker.check_for_updates()
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: publish = tracker.dispatcher.config_updater.publish_update
tracker = _make_tracker(autotracking_enabled=False) publish.assert_called_once()
metrics = tracker.ptz_metrics[CAMERA] topic, payload = publish.call_args.args
metrics.autotracker_enabled.value = True self.assertEqual(topic.update_type, CameraConfigUpdateEnum.autotracking)
self.assertEqual(topic.camera, CAMERA)
tracker.config_subscriber.check_for_updates.return_value = { self.assertIs(payload, autotracking)
"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()
if __name__ == "__main__": if __name__ == "__main__":
+1 -3
View File
@@ -121,9 +121,7 @@ def _make_move_controller(autotracking_enabled: bool) -> OnvifController:
}, },
} }
} }
controller.ptz_metrics = { controller.ptz_metrics = {CAMERA: PTZMetrics()}
CAMERA: PTZMetrics(autotracker_enabled=autotracking_enabled)
}
return controller return controller
+4 -4
View File
@@ -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.ptz_motion_estimator = PtzMotionEstimator(
self.camera_config, self.ptz_metrics self.camera_config, self.ptz_metrics
) )
@@ -515,7 +515,7 @@ class NorfairTracker(ObjectTracker):
yuv_frame: np.ndarray | None = None yuv_frame: np.ndarray | None = None
if ( if (
self.ptz_metrics.autotracker_enabled.value self.camera_config.onvif.autotracking.enabled
or self.detect_config.stationary.classifier or self.detect_config.stationary.classifier
): ):
yuv_frame = self.frame_manager.get( 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]]]) points = np.array([[obj[2][0], obj[2][1]], [obj[2][2], obj[2][3]]])
embedding = None embedding = None
if self.ptz_metrics.autotracker_enabled.value: if self.camera_config.onvif.autotracking.enabled:
embedding = get_histogram( embedding = get_histogram(
yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3] yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3]
) )
@@ -559,7 +559,7 @@ class NorfairTracker(ObjectTracker):
coord_transformations = None 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 # we must have been enabled by mqtt, so set up the estimator
if not self.ptz_motion_estimator: if not self.ptz_motion_estimator:
self.ptz_motion_estimator = PtzMotionEstimator( self.ptz_motion_estimator = PtzMotionEstimator(
+10 -2
View File
@@ -94,6 +94,7 @@ class CameraTracker(FrigateProcess):
self.config.detect.fps, self.config.detect.fps,
name=self.config.name, name=self.config.name,
ptz_metrics=self.ptz_metrics, ptz_metrics=self.ptz_metrics,
autotracking_enabled=self.config.onvif.autotracking.enabled,
) )
object_detector = RemoteObjectDetector( object_detector = RemoteObjectDetector(
self.config.name, self.config.name,
@@ -195,10 +196,12 @@ def process_frames(
None, None,
{camera_config.name: camera_config}, {camera_config.name: camera_config},
[ [
CameraConfigUpdateEnum.autotracking,
CameraConfigUpdateEnum.detect, CameraConfigUpdateEnum.detect,
CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.enabled,
CameraConfigUpdateEnum.motion, CameraConfigUpdateEnum.motion,
CameraConfigUpdateEnum.objects, CameraConfigUpdateEnum.objects,
CameraConfigUpdateEnum.onvif,
], ],
) )
@@ -235,6 +238,11 @@ def process_frames(
motion_detector.config = camera_config.motion motion_detector.config = camera_config.motion
motion_detector.update_mask() motion_detector.update_mask()
if "autotracking" in updated_configs or "onvif" in updated_configs:
motion_detector.autotracking_enabled = (
camera_config.onvif.autotracking.enabled
)
if ( if (
not camera_enabled not camera_enabled
and prev_enabled != 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 # 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 # the ptz timestamps are only maintained while autotracking is on, so gate
# on the metric rather than trusting them to be reset otherwise # on the config rather than trusting them to be reset otherwise
ptz_moving = ptz_metrics.autotracker_enabled.value and ( ptz_moving = camera_config.onvif.autotracking.enabled and (
ptz_moving_at_frame_time( ptz_moving_at_frame_time(
frame_time, frame_time,
ptz_metrics.start_time.value, ptz_metrics.start_time.value,