2020-11-04 06:26:39 +03:00
|
|
|
import logging
|
2020-11-04 15:31:25 +03:00
|
|
|
import queue
|
2021-10-31 19:12:44 +03:00
|
|
|
import subprocess as sp
|
2020-11-04 15:31:25 +03:00
|
|
|
import threading
|
|
|
|
|
import time
|
2025-09-28 19:52:14 +03:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2025-04-24 02:06:06 +03:00
|
|
|
from multiprocessing import Queue, Value
|
|
|
|
|
from multiprocessing.synchronize import Event as MpEvent
|
|
|
|
|
from typing import Any
|
2020-11-04 15:31:25 +03:00
|
|
|
|
2022-11-04 05:23:09 +03:00
|
|
|
import cv2
|
2020-11-04 15:31:25 +03:00
|
|
|
|
2024-09-27 15:53:23 +03:00
|
|
|
from frigate.camera import CameraMetrics, PTZMetrics
|
2024-02-15 03:24:36 +03:00
|
|
|
from frigate.comms.inter_process import InterProcessRequestor
|
2025-09-28 19:52:14 +03:00
|
|
|
from frigate.comms.recordings_updater import (
|
|
|
|
|
RecordingsDataSubscriber,
|
|
|
|
|
RecordingsDataTypeEnum,
|
|
|
|
|
)
|
2025-11-17 17:12:05 +03:00
|
|
|
from frigate.config import CameraConfig, DetectConfig, LoggerConfig, ModelConfig
|
2025-03-30 16:43:24 +03:00
|
|
|
from frigate.config.camera.camera import CameraTypeEnum
|
2025-05-22 21:16:51 +03:00
|
|
|
from frigate.config.camera.updater import (
|
|
|
|
|
CameraConfigUpdateEnum,
|
|
|
|
|
CameraConfigUpdateSubscriber,
|
|
|
|
|
)
|
2023-10-19 02:21:52 +03:00
|
|
|
from frigate.const import (
|
2025-07-18 20:23:06 +03:00
|
|
|
PROCESS_PRIORITY_HIGH,
|
2025-07-29 18:11:52 +03:00
|
|
|
REQUEST_REGION_GRID,
|
2023-10-19 02:21:52 +03:00
|
|
|
)
|
2020-12-04 15:59:03 +03:00
|
|
|
from frigate.log import LogPipe
|
2020-02-16 06:07:54 +03:00
|
|
|
from frigate.motion import MotionDetector
|
2023-06-11 16:45:11 +03:00
|
|
|
from frigate.motion.improved_motion import ImprovedMotionDetector
|
2025-04-15 16:55:38 +03:00
|
|
|
from frigate.object_detection.base import RemoteObjectDetector
|
2023-10-25 02:25:22 +03:00
|
|
|
from frigate.ptz.autotrack import ptz_moving_at_frame_time
|
2023-05-31 17:12:43 +03:00
|
|
|
from frigate.track import ObjectTracker
|
|
|
|
|
from frigate.track.norfair_tracker import NorfairTracker
|
2024-10-17 19:02:27 +03:00
|
|
|
from frigate.track.tracked_object import TrackedObjectAttribute
|
2025-11-03 16:34:47 +03:00
|
|
|
from frigate.util.builtin import EventsPerSecond
|
2023-07-06 17:28:50 +03:00
|
|
|
from frigate.util.image import (
|
2021-02-17 16:23:32 +03:00
|
|
|
FrameManager,
|
|
|
|
|
SharedMemoryFrameManager,
|
2023-06-11 16:45:11 +03:00
|
|
|
draw_box_with_label,
|
2023-10-19 02:21:52 +03:00
|
|
|
)
|
|
|
|
|
from frigate.util.object import (
|
|
|
|
|
create_tensor_input,
|
|
|
|
|
get_cluster_candidates,
|
|
|
|
|
get_cluster_region,
|
|
|
|
|
get_cluster_region_from_grid,
|
|
|
|
|
get_min_region_size,
|
|
|
|
|
get_startup_regions,
|
|
|
|
|
inside_any,
|
|
|
|
|
intersects_any,
|
|
|
|
|
is_object_filtered,
|
2023-10-24 03:20:21 +03:00
|
|
|
reduce_detections,
|
2021-02-17 16:23:32 +03:00
|
|
|
)
|
2025-06-13 20:09:51 +03:00
|
|
|
from frigate.util.process import FrigateProcess
|
2025-11-03 16:34:47 +03:00
|
|
|
from frigate.util.time import get_tomorrow_at_time
|
2019-02-26 05:27:02 +03:00
|
|
|
|
2020-11-04 06:26:39 +03:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
2025-06-24 00:55:57 +03:00
|
|
|
def stop_ffmpeg(ffmpeg_process: sp.Popen[Any], logger: logging.Logger):
|
2020-11-30 01:19:59 +03:00
|
|
|
logger.info("Terminating the existing ffmpeg process...")
|
|
|
|
|
ffmpeg_process.terminate()
|
|
|
|
|
try:
|
|
|
|
|
logger.info("Waiting for ffmpeg to exit gracefully...")
|
|
|
|
|
ffmpeg_process.communicate(timeout=30)
|
|
|
|
|
except sp.TimeoutExpired:
|
2024-04-20 14:16:43 +03:00
|
|
|
logger.info("FFmpeg didn't exit. Force killing...")
|
2020-11-30 01:19:59 +03:00
|
|
|
ffmpeg_process.kill()
|
|
|
|
|
ffmpeg_process.communicate()
|
|
|
|
|
ffmpeg_process = None
|
|
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
|
|
|
|
def start_or_restart_ffmpeg(
|
|
|
|
|
ffmpeg_cmd, logger, logpipe: LogPipe, frame_size=None, ffmpeg_process=None
|
2025-08-22 15:42:36 +03:00
|
|
|
) -> sp.Popen[Any]:
|
Use dataclasses for config handling
Use config data classes to eliminate some of the boilerplate associated
with setting up the configuration. In particular, using dataclasses
removes a lot of the boilerplate around assigning properties to the
object and allows these to be easily immutable by freezing them. In the
case of simple, non-nested dataclasses, this also provides more
convenient `asdict` helpers.
To set this up, where previously the objects would be parsed from the
config via the `__init__` method, create a `build` classmethod that does
this and calls the dataclass initializer.
Some of the objects are mutated at runtime, in particular some of the
zones are mutated to set the color (this might be able to be refactored
out) and some of the camera functionality can be enabled/disabled. Some
of the configs with `enabled` properties don't seem to have mqtt hooks
to be able to toggle this, in particular, the clips, snapshots, and
detect can be toggled but rtmp and record configs do not, but all of
these configs are still not frozen in case there is some other
functionality I am missing.
There are a couple other minor fixes here, one that was introduced
by me recently where `max_seconds` was not defined, the other to
properly `get()` the message payload when handling publishing mqtt
messages sent via websocket.
2021-05-23 01:28:15 +03:00
|
|
|
if ffmpeg_process is not None:
|
2020-12-04 15:59:03 +03:00
|
|
|
stop_ffmpeg(ffmpeg_process, logger)
|
2020-02-27 04:02:12 +03:00
|
|
|
|
2020-11-30 00:55:53 +03:00
|
|
|
if frame_size is None:
|
2021-02-17 16:23:32 +03:00
|
|
|
process = sp.Popen(
|
|
|
|
|
ffmpeg_cmd,
|
|
|
|
|
stdout=sp.DEVNULL,
|
|
|
|
|
stderr=logpipe,
|
|
|
|
|
stdin=sp.DEVNULL,
|
|
|
|
|
start_new_session=True,
|
|
|
|
|
)
|
2020-11-30 00:55:53 +03:00
|
|
|
else:
|
2021-02-17 16:23:32 +03:00
|
|
|
process = sp.Popen(
|
|
|
|
|
ffmpeg_cmd,
|
|
|
|
|
stdout=sp.PIPE,
|
|
|
|
|
stderr=logpipe,
|
|
|
|
|
stdin=sp.DEVNULL,
|
|
|
|
|
bufsize=frame_size * 10,
|
|
|
|
|
start_new_session=True,
|
|
|
|
|
)
|
2020-03-10 05:12:19 +03:00
|
|
|
return process
|
2020-02-27 04:02:12 +03:00
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
|
|
|
|
def capture_frames(
|
2025-08-22 15:42:36 +03:00
|
|
|
ffmpeg_process: sp.Popen[Any],
|
2024-09-03 19:22:30 +03:00
|
|
|
config: CameraConfig,
|
|
|
|
|
shm_frame_count: int,
|
2024-11-17 20:25:49 +03:00
|
|
|
frame_index: int,
|
|
|
|
|
frame_shape: tuple[int, int],
|
2021-02-17 16:23:32 +03:00
|
|
|
frame_manager: FrameManager,
|
|
|
|
|
frame_queue,
|
2025-04-24 02:06:06 +03:00
|
|
|
fps: Value,
|
|
|
|
|
skipped_fps: Value,
|
|
|
|
|
current_frame: Value,
|
|
|
|
|
stop_event: MpEvent,
|
2025-08-22 15:42:36 +03:00
|
|
|
) -> None:
|
2020-11-03 17:15:58 +03:00
|
|
|
frame_size = frame_shape[0] * frame_shape[1]
|
2020-10-25 18:05:21 +03:00
|
|
|
frame_rate = EventsPerSecond()
|
2020-10-26 15:59:22 +03:00
|
|
|
frame_rate.start()
|
2020-10-25 18:05:21 +03:00
|
|
|
skipped_eps = EventsPerSecond()
|
|
|
|
|
skipped_eps.start()
|
2025-05-22 21:16:51 +03:00
|
|
|
config_subscriber = CameraConfigUpdateSubscriber(
|
2025-06-11 20:25:30 +03:00
|
|
|
None, {config.name: config}, [CameraConfigUpdateEnum.enabled]
|
2025-05-22 21:16:51 +03:00
|
|
|
)
|
2025-03-03 18:30:52 +03:00
|
|
|
|
|
|
|
|
def get_enabled_state():
|
|
|
|
|
"""Fetch the latest enabled state from ZMQ."""
|
2025-05-22 21:16:51 +03:00
|
|
|
config_subscriber.check_for_updates()
|
2025-03-03 18:30:52 +03:00
|
|
|
return config.enabled
|
|
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
try:
|
|
|
|
|
while not stop_event.is_set():
|
|
|
|
|
if not get_enabled_state():
|
|
|
|
|
logger.debug(f"Stopping capture thread for disabled {config.name}")
|
2023-02-04 17:58:45 +03:00
|
|
|
break
|
2024-09-03 19:22:30 +03:00
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
fps.value = frame_rate.eps()
|
|
|
|
|
skipped_fps.value = skipped_eps.eps()
|
|
|
|
|
current_frame.value = datetime.now().timestamp()
|
|
|
|
|
frame_name = f"{config.name}_frame{frame_index}"
|
|
|
|
|
frame_buffer = frame_manager.write(frame_name)
|
|
|
|
|
try:
|
|
|
|
|
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
|
|
|
|
|
except Exception:
|
|
|
|
|
# shutdown has been initiated
|
|
|
|
|
if stop_event.is_set():
|
|
|
|
|
break
|
2020-12-12 18:12:15 +03:00
|
|
|
|
2022-02-06 17:46:41 +03:00
|
|
|
logger.error(
|
2025-11-30 15:54:42 +03:00
|
|
|
f"{config.name}: Unable to read frames from ffmpeg process."
|
2021-02-17 16:23:32 +03:00
|
|
|
)
|
2024-09-03 19:22:30 +03:00
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
if ffmpeg_process.poll() is not None:
|
|
|
|
|
logger.error(
|
|
|
|
|
f"{config.name}: ffmpeg process is not running. exiting capture thread..."
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
continue
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
frame_rate.update()
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
# don't lock the queue to check, just try since it should rarely be full
|
|
|
|
|
try:
|
|
|
|
|
# add to the queue
|
|
|
|
|
frame_queue.put((frame_name, current_frame.value), False)
|
|
|
|
|
frame_manager.close(frame_name)
|
|
|
|
|
except queue.Full:
|
|
|
|
|
# if the queue is full, skip this frame
|
|
|
|
|
skipped_eps.update()
|
2024-09-03 19:22:30 +03:00
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
frame_index = 0 if frame_index == shm_frame_count - 1 else frame_index + 1
|
|
|
|
|
finally:
|
|
|
|
|
config_subscriber.stop()
|
2024-11-17 02:00:19 +03:00
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
2020-10-25 18:05:21 +03:00
|
|
|
class CameraWatchdog(threading.Thread):
|
2021-02-17 16:23:32 +03:00
|
|
|
def __init__(
|
2022-12-09 06:03:54 +03:00
|
|
|
self,
|
|
|
|
|
config: CameraConfig,
|
2024-09-03 19:22:30 +03:00
|
|
|
shm_frame_count: int,
|
2025-04-24 02:06:06 +03:00
|
|
|
frame_queue: Queue,
|
2022-12-09 06:03:54 +03:00
|
|
|
camera_fps,
|
2023-06-28 13:53:28 +03:00
|
|
|
skipped_fps,
|
2022-12-09 06:03:54 +03:00
|
|
|
ffmpeg_pid,
|
|
|
|
|
stop_event,
|
2021-02-17 16:23:32 +03:00
|
|
|
):
|
2020-10-25 18:05:21 +03:00
|
|
|
threading.Thread.__init__(self)
|
2025-05-22 21:16:51 +03:00
|
|
|
self.logger = logging.getLogger(f"watchdog.{config.name}")
|
2020-10-25 18:05:21 +03:00
|
|
|
self.config = config
|
2024-09-03 19:22:30 +03:00
|
|
|
self.shm_frame_count = shm_frame_count
|
2020-10-25 18:05:21 +03:00
|
|
|
self.capture_thread = None
|
2020-11-30 00:55:53 +03:00
|
|
|
self.ffmpeg_detect_process = None
|
2025-05-22 21:16:51 +03:00
|
|
|
self.logpipe = LogPipe(f"ffmpeg.{self.config.name}.detect")
|
2025-05-13 17:27:20 +03:00
|
|
|
self.ffmpeg_other_processes: list[dict[str, Any]] = []
|
2020-10-25 18:05:21 +03:00
|
|
|
self.camera_fps = camera_fps
|
2023-06-28 13:53:28 +03:00
|
|
|
self.skipped_fps = skipped_fps
|
2020-10-26 15:59:05 +03:00
|
|
|
self.ffmpeg_pid = ffmpeg_pid
|
2020-10-25 18:05:21 +03:00
|
|
|
self.frame_queue = frame_queue
|
2020-11-03 17:15:58 +03:00
|
|
|
self.frame_shape = self.config.frame_shape_yuv
|
|
|
|
|
self.frame_size = self.frame_shape[0] * self.frame_shape[1]
|
2024-09-10 16:39:37 +03:00
|
|
|
self.fps_overflow_count = 0
|
2024-11-17 20:25:49 +03:00
|
|
|
self.frame_index = 0
|
2020-11-30 01:19:59 +03:00
|
|
|
self.stop_event = stop_event
|
2023-06-30 15:14:39 +03:00
|
|
|
self.sleeptime = self.config.ffmpeg.retry_interval
|
2020-10-25 18:05:21 +03:00
|
|
|
|
2025-05-22 21:16:51 +03:00
|
|
|
self.config_subscriber = CameraConfigUpdateSubscriber(
|
2025-11-06 17:21:07 +03:00
|
|
|
None,
|
|
|
|
|
{config.name: config},
|
|
|
|
|
[CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.record],
|
2025-05-22 21:16:51 +03:00
|
|
|
)
|
2025-08-22 15:42:36 +03:00
|
|
|
self.requestor = InterProcessRequestor()
|
2025-03-03 18:30:52 +03:00
|
|
|
self.was_enabled = self.config.enabled
|
2020-11-30 00:55:53 +03:00
|
|
|
|
2025-09-28 19:52:14 +03:00
|
|
|
self.segment_subscriber = RecordingsDataSubscriber(RecordingsDataTypeEnum.all)
|
|
|
|
|
self.latest_valid_segment_time: float = 0
|
|
|
|
|
self.latest_invalid_segment_time: float = 0
|
|
|
|
|
self.latest_cache_segment_time: float = 0
|
|
|
|
|
|
2025-03-03 18:30:52 +03:00
|
|
|
def _update_enabled_state(self) -> bool:
|
|
|
|
|
"""Fetch the latest config and update enabled state."""
|
2025-05-22 21:16:51 +03:00
|
|
|
self.config_subscriber.check_for_updates()
|
2025-03-05 16:30:23 +03:00
|
|
|
return self.config.enabled
|
2025-03-03 18:30:52 +03:00
|
|
|
|
2025-07-11 22:19:43 +03:00
|
|
|
def reset_capture_thread(
|
|
|
|
|
self, terminate: bool = True, drain_output: bool = True
|
|
|
|
|
) -> None:
|
|
|
|
|
if terminate:
|
|
|
|
|
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)
|
|
|
|
|
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()
|
|
|
|
|
|
2025-11-30 15:54:42 +03:00
|
|
|
# Wait for old capture thread to fully exit before starting a new one
|
|
|
|
|
if self.capture_thread is not None and self.capture_thread.is_alive():
|
|
|
|
|
self.logger.info("Waiting for capture thread to exit...")
|
|
|
|
|
self.capture_thread.join(timeout=5)
|
|
|
|
|
|
|
|
|
|
if self.capture_thread.is_alive():
|
|
|
|
|
self.logger.warning(
|
|
|
|
|
f"Capture thread for {self.config.name} did not exit in time"
|
|
|
|
|
)
|
|
|
|
|
|
2025-07-11 22:19:43 +03:00
|
|
|
self.logger.error(
|
|
|
|
|
"The following ffmpeg logs include the last 100 lines prior to exit."
|
|
|
|
|
)
|
|
|
|
|
self.logpipe.dump()
|
|
|
|
|
self.logger.info("Restarting ffmpeg...")
|
|
|
|
|
self.start_ffmpeg_detect()
|
|
|
|
|
|
|
|
|
|
def run(self) -> None:
|
2025-03-03 18:30:52 +03:00
|
|
|
if self._update_enabled_state():
|
|
|
|
|
self.start_all_ffmpeg()
|
2021-02-17 16:23:32 +03:00
|
|
|
|
2023-06-30 15:14:39 +03:00
|
|
|
time.sleep(self.sleeptime)
|
|
|
|
|
while not self.stop_event.wait(self.sleeptime):
|
2025-03-03 18:30:52 +03:00
|
|
|
enabled = self._update_enabled_state()
|
|
|
|
|
if enabled != self.was_enabled:
|
|
|
|
|
if enabled:
|
2025-05-22 21:16:51 +03:00
|
|
|
self.logger.debug(f"Enabling camera {self.config.name}")
|
2025-03-03 18:30:52 +03:00
|
|
|
self.start_all_ffmpeg()
|
2025-09-28 19:52:14 +03:00
|
|
|
|
|
|
|
|
# reset all timestamps
|
|
|
|
|
self.latest_valid_segment_time = 0
|
|
|
|
|
self.latest_invalid_segment_time = 0
|
|
|
|
|
self.latest_cache_segment_time = 0
|
2025-03-03 18:30:52 +03:00
|
|
|
else:
|
2025-05-22 21:16:51 +03:00
|
|
|
self.logger.debug(f"Disabling camera {self.config.name}")
|
2025-03-03 18:30:52 +03:00
|
|
|
self.stop_all_ffmpeg()
|
2025-08-22 15:42:36 +03:00
|
|
|
|
|
|
|
|
# update camera status
|
|
|
|
|
self.requestor.send_data(
|
|
|
|
|
f"{self.config.name}/status/detect", "disabled"
|
|
|
|
|
)
|
|
|
|
|
self.requestor.send_data(
|
|
|
|
|
f"{self.config.name}/status/record", "disabled"
|
|
|
|
|
)
|
2025-03-03 18:30:52 +03:00
|
|
|
self.was_enabled = enabled
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if not enabled:
|
|
|
|
|
continue
|
|
|
|
|
|
2025-09-28 19:52:14 +03:00
|
|
|
while True:
|
|
|
|
|
update = self.segment_subscriber.check_for_update(timeout=0)
|
|
|
|
|
|
|
|
|
|
if update == (None, None):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
raw_topic, payload = update
|
|
|
|
|
if raw_topic and payload:
|
|
|
|
|
topic = str(raw_topic)
|
|
|
|
|
camera, segment_time, _ = payload
|
|
|
|
|
|
|
|
|
|
if camera != self.config.name:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if topic.endswith(RecordingsDataTypeEnum.valid.value):
|
|
|
|
|
self.logger.debug(
|
|
|
|
|
f"Latest valid recording segment time on {camera}: {segment_time}"
|
|
|
|
|
)
|
|
|
|
|
self.latest_valid_segment_time = segment_time
|
|
|
|
|
elif topic.endswith(RecordingsDataTypeEnum.invalid.value):
|
|
|
|
|
self.logger.warning(
|
|
|
|
|
f"Invalid recording segment detected for {camera} at {segment_time}"
|
|
|
|
|
)
|
|
|
|
|
self.latest_invalid_segment_time = segment_time
|
|
|
|
|
elif topic.endswith(RecordingsDataTypeEnum.latest.value):
|
|
|
|
|
if segment_time is not None:
|
|
|
|
|
self.latest_cache_segment_time = segment_time
|
|
|
|
|
else:
|
|
|
|
|
self.latest_cache_segment_time = 0
|
|
|
|
|
|
|
|
|
|
now = datetime.now().timestamp()
|
2020-10-25 18:05:21 +03:00
|
|
|
|
|
|
|
|
if not self.capture_thread.is_alive():
|
2025-08-22 15:42:36 +03:00
|
|
|
self.requestor.send_data(f"{self.config.name}/status/detect", "offline")
|
2022-11-29 06:47:20 +03:00
|
|
|
self.camera_fps.value = 0
|
2021-08-14 22:04:00 +03:00
|
|
|
self.logger.error(
|
2025-05-22 21:16:51 +03:00
|
|
|
f"Ffmpeg process crashed unexpectedly for {self.config.name}."
|
2021-08-14 22:04:00 +03:00
|
|
|
)
|
2025-07-11 22:19:43 +03:00
|
|
|
self.reset_capture_thread(terminate=False)
|
2023-01-30 02:20:42 +03:00
|
|
|
elif self.camera_fps.value >= (self.config.detect.fps + 10):
|
2024-09-10 16:39:37 +03:00
|
|
|
self.fps_overflow_count += 1
|
|
|
|
|
|
|
|
|
|
if self.fps_overflow_count == 3:
|
2025-08-22 15:42:36 +03:00
|
|
|
self.requestor.send_data(
|
|
|
|
|
f"{self.config.name}/status/detect", "offline"
|
|
|
|
|
)
|
2024-09-10 16:39:37 +03:00
|
|
|
self.fps_overflow_count = 0
|
|
|
|
|
self.camera_fps.value = 0
|
|
|
|
|
self.logger.info(
|
2025-05-22 21:16:51 +03:00
|
|
|
f"{self.config.name} exceeded fps limit. Exiting ffmpeg..."
|
2024-09-10 16:39:37 +03:00
|
|
|
)
|
2025-07-11 22:19:43 +03:00
|
|
|
self.reset_capture_thread(drain_output=False)
|
|
|
|
|
elif now - self.capture_thread.current_frame.value > 20:
|
2025-08-22 15:42:36 +03:00
|
|
|
self.requestor.send_data(f"{self.config.name}/status/detect", "offline")
|
2025-07-11 22:19:43 +03:00
|
|
|
self.camera_fps.value = 0
|
|
|
|
|
self.logger.info(
|
2025-05-22 21:16:51 +03:00
|
|
|
f"No frames received from {self.config.name} in 20 seconds. Exiting ffmpeg..."
|
2025-07-11 22:19:43 +03:00
|
|
|
)
|
|
|
|
|
self.reset_capture_thread()
|
2024-09-10 16:39:37 +03:00
|
|
|
else:
|
|
|
|
|
# process is running normally
|
2025-08-22 15:42:36 +03:00
|
|
|
self.requestor.send_data(f"{self.config.name}/status/detect", "online")
|
2024-09-10 16:39:37 +03:00
|
|
|
self.fps_overflow_count = 0
|
2021-02-17 16:23:32 +03:00
|
|
|
|
2020-11-30 00:55:53 +03:00
|
|
|
for p in self.ffmpeg_other_processes:
|
2021-02-17 16:23:32 +03:00
|
|
|
poll = p["process"].poll()
|
2022-12-09 06:03:54 +03:00
|
|
|
|
|
|
|
|
if self.config.record.enabled and "record" in p["roles"]:
|
2025-09-28 19:52:14 +03:00
|
|
|
now_utc = datetime.now().astimezone(timezone.utc)
|
|
|
|
|
|
|
|
|
|
latest_cache_dt = (
|
|
|
|
|
datetime.fromtimestamp(
|
|
|
|
|
self.latest_cache_segment_time, tz=timezone.utc
|
|
|
|
|
)
|
|
|
|
|
if self.latest_cache_segment_time > 0
|
|
|
|
|
else now_utc - timedelta(seconds=1)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
latest_valid_dt = (
|
|
|
|
|
datetime.fromtimestamp(
|
|
|
|
|
self.latest_valid_segment_time, tz=timezone.utc
|
|
|
|
|
)
|
|
|
|
|
if self.latest_valid_segment_time > 0
|
|
|
|
|
else now_utc - timedelta(seconds=1)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
latest_invalid_dt = (
|
|
|
|
|
datetime.fromtimestamp(
|
|
|
|
|
self.latest_invalid_segment_time, tz=timezone.utc
|
2022-12-09 06:03:54 +03:00
|
|
|
)
|
2025-09-28 19:52:14 +03:00
|
|
|
if self.latest_invalid_segment_time > 0
|
|
|
|
|
else now_utc - timedelta(seconds=1)
|
2022-12-09 06:03:54 +03:00
|
|
|
)
|
|
|
|
|
|
2025-09-28 19:52:14 +03:00
|
|
|
# ensure segments are still being created and that they have valid video data
|
|
|
|
|
cache_stale = now_utc > (latest_cache_dt + timedelta(seconds=120))
|
|
|
|
|
valid_stale = now_utc > (latest_valid_dt + timedelta(seconds=120))
|
|
|
|
|
invalid_stale_condition = (
|
|
|
|
|
self.latest_invalid_segment_time > 0
|
|
|
|
|
and now_utc > (latest_invalid_dt + timedelta(seconds=120))
|
|
|
|
|
and self.latest_valid_segment_time
|
|
|
|
|
<= self.latest_invalid_segment_time
|
|
|
|
|
)
|
|
|
|
|
invalid_stale = invalid_stale_condition
|
|
|
|
|
|
|
|
|
|
if cache_stale or valid_stale or invalid_stale:
|
|
|
|
|
if cache_stale:
|
|
|
|
|
reason = "No new recording segments were created"
|
|
|
|
|
elif valid_stale:
|
|
|
|
|
reason = "No new valid recording segments were created"
|
|
|
|
|
else: # invalid_stale
|
|
|
|
|
reason = (
|
|
|
|
|
"No valid segments created since last invalid segment"
|
|
|
|
|
)
|
|
|
|
|
|
2022-12-09 06:03:54 +03:00
|
|
|
self.logger.error(
|
2025-09-28 19:52:14 +03:00
|
|
|
f"{reason} for {self.config.name} in the last 120s. Restarting the ffmpeg record process..."
|
2022-12-09 06:03:54 +03:00
|
|
|
)
|
|
|
|
|
p["process"] = start_or_restart_ffmpeg(
|
|
|
|
|
p["cmd"],
|
|
|
|
|
self.logger,
|
|
|
|
|
p["logpipe"],
|
|
|
|
|
ffmpeg_process=p["process"],
|
|
|
|
|
)
|
2025-08-22 15:42:36 +03:00
|
|
|
|
|
|
|
|
for role in p["roles"]:
|
|
|
|
|
self.requestor.send_data(
|
|
|
|
|
f"{self.config.name}/status/{role}", "offline"
|
|
|
|
|
)
|
|
|
|
|
|
2022-12-09 06:03:54 +03:00
|
|
|
continue
|
|
|
|
|
else:
|
2025-08-22 15:42:36 +03:00
|
|
|
self.requestor.send_data(
|
|
|
|
|
f"{self.config.name}/status/record", "online"
|
|
|
|
|
)
|
2025-09-28 19:52:14 +03:00
|
|
|
p["latest_segment_time"] = self.latest_cache_segment_time
|
2022-12-09 06:03:54 +03:00
|
|
|
|
2021-06-25 19:37:21 +03:00
|
|
|
if poll is None:
|
2020-11-30 00:55:53 +03:00
|
|
|
continue
|
2022-12-09 06:03:54 +03:00
|
|
|
|
2025-08-22 15:42:36 +03:00
|
|
|
for role in p["roles"]:
|
|
|
|
|
self.requestor.send_data(
|
|
|
|
|
f"{self.config.name}/status/{role}", "offline"
|
|
|
|
|
)
|
|
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
p["logpipe"].dump()
|
|
|
|
|
p["process"] = start_or_restart_ffmpeg(
|
|
|
|
|
p["cmd"], self.logger, p["logpipe"], ffmpeg_process=p["process"]
|
|
|
|
|
)
|
|
|
|
|
|
2025-03-03 18:30:52 +03:00
|
|
|
self.stop_all_ffmpeg()
|
2021-05-21 18:39:14 +03:00
|
|
|
self.logpipe.close()
|
2025-03-03 18:30:52 +03:00
|
|
|
self.config_subscriber.stop()
|
2025-09-28 19:52:14 +03:00
|
|
|
self.segment_subscriber.stop()
|
2021-02-17 16:23:32 +03:00
|
|
|
|
2020-11-30 00:55:53 +03:00
|
|
|
def start_ffmpeg_detect(self):
|
2021-02-17 16:23:32 +03:00
|
|
|
ffmpeg_cmd = [
|
|
|
|
|
c["cmd"] for c in self.config.ffmpeg_cmds if "detect" in c["roles"]
|
|
|
|
|
][0]
|
|
|
|
|
self.ffmpeg_detect_process = start_or_restart_ffmpeg(
|
|
|
|
|
ffmpeg_cmd, self.logger, self.logpipe, self.frame_size
|
|
|
|
|
)
|
2020-11-30 00:55:53 +03:00
|
|
|
self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
self.capture_thread = CameraCaptureRunner(
|
2024-09-03 19:22:30 +03:00
|
|
|
self.config,
|
|
|
|
|
self.shm_frame_count,
|
2024-11-17 20:25:49 +03:00
|
|
|
self.frame_index,
|
2021-02-17 16:23:32 +03:00
|
|
|
self.ffmpeg_detect_process,
|
|
|
|
|
self.frame_shape,
|
|
|
|
|
self.frame_queue,
|
|
|
|
|
self.camera_fps,
|
2023-06-28 13:53:28 +03:00
|
|
|
self.skipped_fps,
|
2023-02-04 17:58:45 +03:00
|
|
|
self.stop_event,
|
2021-02-17 16:23:32 +03:00
|
|
|
)
|
2020-11-01 19:55:11 +03:00
|
|
|
self.capture_thread.start()
|
2020-10-25 18:05:21 +03:00
|
|
|
|
2025-03-03 18:30:52 +03:00
|
|
|
def start_all_ffmpeg(self):
|
|
|
|
|
"""Start all ffmpeg processes (detection and others)."""
|
2025-05-22 21:16:51 +03:00
|
|
|
logger.debug(f"Starting all ffmpeg processes for {self.config.name}")
|
2025-03-03 18:30:52 +03:00
|
|
|
self.start_ffmpeg_detect()
|
|
|
|
|
for c in self.config.ffmpeg_cmds:
|
|
|
|
|
if "detect" in c["roles"]:
|
|
|
|
|
continue
|
|
|
|
|
logpipe = LogPipe(
|
2025-05-22 21:16:51 +03:00
|
|
|
f"ffmpeg.{self.config.name}.{'_'.join(sorted(c['roles']))}"
|
2025-03-03 18:30:52 +03:00
|
|
|
)
|
|
|
|
|
self.ffmpeg_other_processes.append(
|
|
|
|
|
{
|
|
|
|
|
"cmd": c["cmd"],
|
|
|
|
|
"roles": c["roles"],
|
|
|
|
|
"logpipe": logpipe,
|
|
|
|
|
"process": start_or_restart_ffmpeg(c["cmd"], self.logger, logpipe),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def stop_all_ffmpeg(self):
|
|
|
|
|
"""Stop all ffmpeg processes (detection and others)."""
|
2025-05-22 21:16:51 +03:00
|
|
|
logger.debug(f"Stopping all ffmpeg processes for {self.config.name}")
|
2025-03-03 18:30:52 +03:00
|
|
|
if self.capture_thread is not None and self.capture_thread.is_alive():
|
|
|
|
|
self.capture_thread.join(timeout=5)
|
|
|
|
|
if self.capture_thread.is_alive():
|
|
|
|
|
self.logger.warning(
|
2025-05-22 21:16:51 +03:00
|
|
|
f"Capture thread for {self.config.name} did not stop gracefully."
|
2025-03-03 18:30:52 +03:00
|
|
|
)
|
|
|
|
|
if self.ffmpeg_detect_process is not None:
|
|
|
|
|
stop_ffmpeg(self.ffmpeg_detect_process, self.logger)
|
|
|
|
|
self.ffmpeg_detect_process = None
|
|
|
|
|
for p in self.ffmpeg_other_processes[:]:
|
|
|
|
|
if p["process"] is not None:
|
|
|
|
|
stop_ffmpeg(p["process"], self.logger)
|
|
|
|
|
p["logpipe"].close()
|
|
|
|
|
self.ffmpeg_other_processes.clear()
|
|
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
class CameraCaptureRunner(threading.Thread):
|
2023-02-04 17:58:45 +03:00
|
|
|
def __init__(
|
2023-06-28 13:53:28 +03:00
|
|
|
self,
|
2024-09-03 19:22:30 +03:00
|
|
|
config: CameraConfig,
|
|
|
|
|
shm_frame_count: int,
|
2024-11-17 20:25:49 +03:00
|
|
|
frame_index: int,
|
2023-06-28 13:53:28 +03:00
|
|
|
ffmpeg_process,
|
2024-11-17 20:25:49 +03:00
|
|
|
frame_shape: tuple[int, int],
|
2025-04-24 02:06:06 +03:00
|
|
|
frame_queue: Queue,
|
|
|
|
|
fps: Value,
|
|
|
|
|
skipped_fps: Value,
|
|
|
|
|
stop_event: MpEvent,
|
2023-02-04 17:58:45 +03:00
|
|
|
):
|
2020-03-14 23:32:51 +03:00
|
|
|
threading.Thread.__init__(self)
|
2024-09-03 19:22:30 +03:00
|
|
|
self.name = f"capture:{config.name}"
|
|
|
|
|
self.config = config
|
|
|
|
|
self.shm_frame_count = shm_frame_count
|
2024-11-17 20:25:49 +03:00
|
|
|
self.frame_index = frame_index
|
2020-03-14 23:32:51 +03:00
|
|
|
self.frame_shape = frame_shape
|
|
|
|
|
self.frame_queue = frame_queue
|
|
|
|
|
self.fps = fps
|
2023-02-04 17:58:45 +03:00
|
|
|
self.stop_event = stop_event
|
2023-06-28 13:53:28 +03:00
|
|
|
self.skipped_fps = skipped_fps
|
2020-09-22 05:02:00 +03:00
|
|
|
self.frame_manager = SharedMemoryFrameManager()
|
2020-03-14 23:32:51 +03:00
|
|
|
self.ffmpeg_process = ffmpeg_process
|
2025-04-24 02:06:06 +03:00
|
|
|
self.current_frame = Value("d", 0.0)
|
2020-04-19 18:07:27 +03:00
|
|
|
self.last_frame = 0
|
2020-03-14 23:32:51 +03:00
|
|
|
|
|
|
|
|
def run(self):
|
2021-02-17 16:23:32 +03:00
|
|
|
capture_frames(
|
|
|
|
|
self.ffmpeg_process,
|
2024-09-03 19:22:30 +03:00
|
|
|
self.config,
|
|
|
|
|
self.shm_frame_count,
|
2024-11-17 20:25:49 +03:00
|
|
|
self.frame_index,
|
2021-02-17 16:23:32 +03:00
|
|
|
self.frame_shape,
|
|
|
|
|
self.frame_manager,
|
|
|
|
|
self.frame_queue,
|
|
|
|
|
self.fps,
|
|
|
|
|
self.skipped_fps,
|
|
|
|
|
self.current_frame,
|
2023-02-04 17:58:45 +03:00
|
|
|
self.stop_event,
|
2021-02-17 16:23:32 +03:00
|
|
|
)
|
|
|
|
|
|
2020-03-14 23:32:51 +03:00
|
|
|
|
2025-06-13 20:09:51 +03:00
|
|
|
class CameraCapture(FrigateProcess):
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
def __init__(
|
2025-06-24 20:41:11 +03:00
|
|
|
self,
|
|
|
|
|
config: CameraConfig,
|
|
|
|
|
shm_frame_count: int,
|
|
|
|
|
camera_metrics: CameraMetrics,
|
|
|
|
|
stop_event: MpEvent,
|
2025-11-17 17:12:05 +03:00
|
|
|
log_config: LoggerConfig | None = None,
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
) -> None:
|
2025-07-18 20:23:06 +03:00
|
|
|
super().__init__(
|
|
|
|
|
stop_event,
|
|
|
|
|
PROCESS_PRIORITY_HIGH,
|
|
|
|
|
name=f"frigate.capture:{config.name}",
|
|
|
|
|
daemon=True,
|
|
|
|
|
)
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
self.config = config
|
|
|
|
|
self.shm_frame_count = shm_frame_count
|
|
|
|
|
self.camera_metrics = camera_metrics
|
2025-11-17 17:12:05 +03:00
|
|
|
self.log_config = log_config
|
2020-11-30 01:19:59 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
def run(self) -> None:
|
2025-11-17 17:12:05 +03:00
|
|
|
self.pre_run_setup(self.log_config)
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
camera_watchdog = CameraWatchdog(
|
|
|
|
|
self.config,
|
|
|
|
|
self.shm_frame_count,
|
|
|
|
|
self.camera_metrics.frame_queue,
|
|
|
|
|
self.camera_metrics.camera_fps,
|
|
|
|
|
self.camera_metrics.skipped_fps,
|
|
|
|
|
self.camera_metrics.ffmpeg_pid,
|
|
|
|
|
self.stop_event,
|
|
|
|
|
)
|
|
|
|
|
camera_watchdog.start()
|
|
|
|
|
camera_watchdog.join()
|
2020-02-16 06:07:54 +03:00
|
|
|
|
2020-10-25 18:05:21 +03:00
|
|
|
|
2025-06-13 20:09:51 +03:00
|
|
|
class CameraTracker(FrigateProcess):
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
config: CameraConfig,
|
|
|
|
|
model_config: ModelConfig,
|
|
|
|
|
labelmap: dict[int, str],
|
|
|
|
|
detection_queue: Queue,
|
|
|
|
|
detected_objects_queue,
|
|
|
|
|
camera_metrics: CameraMetrics,
|
|
|
|
|
ptz_metrics: PTZMetrics,
|
|
|
|
|
region_grid: list[list[dict[str, Any]]],
|
2025-06-24 20:41:11 +03:00
|
|
|
stop_event: MpEvent,
|
2025-11-17 17:12:05 +03:00
|
|
|
log_config: LoggerConfig | None = None,
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
) -> None:
|
2025-07-18 20:23:06 +03:00
|
|
|
super().__init__(
|
|
|
|
|
stop_event,
|
|
|
|
|
PROCESS_PRIORITY_HIGH,
|
|
|
|
|
name=f"frigate.process:{config.name}",
|
|
|
|
|
daemon=True,
|
|
|
|
|
)
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
self.config = config
|
|
|
|
|
self.model_config = model_config
|
|
|
|
|
self.labelmap = labelmap
|
|
|
|
|
self.detection_queue = detection_queue
|
|
|
|
|
self.detected_objects_queue = detected_objects_queue
|
|
|
|
|
self.camera_metrics = camera_metrics
|
|
|
|
|
self.ptz_metrics = ptz_metrics
|
|
|
|
|
self.region_grid = region_grid
|
2025-11-17 17:12:05 +03:00
|
|
|
self.log_config = log_config
|
2020-02-16 06:07:54 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
def run(self) -> None:
|
2025-11-17 17:12:05 +03:00
|
|
|
self.pre_run_setup(self.log_config)
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
frame_queue = self.camera_metrics.frame_queue
|
|
|
|
|
frame_shape = self.config.frame_shape
|
|
|
|
|
|
|
|
|
|
motion_detector = ImprovedMotionDetector(
|
|
|
|
|
frame_shape,
|
|
|
|
|
self.config.motion,
|
|
|
|
|
self.config.detect.fps,
|
|
|
|
|
name=self.config.name,
|
|
|
|
|
ptz_metrics=self.ptz_metrics,
|
|
|
|
|
)
|
|
|
|
|
object_detector = RemoteObjectDetector(
|
|
|
|
|
self.config.name,
|
|
|
|
|
self.labelmap,
|
|
|
|
|
self.detection_queue,
|
|
|
|
|
self.model_config,
|
|
|
|
|
self.stop_event,
|
|
|
|
|
)
|
2020-02-16 06:07:54 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
object_tracker = NorfairTracker(self.config, self.ptz_metrics)
|
2020-03-14 23:32:51 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
frame_manager = SharedMemoryFrameManager()
|
2020-08-22 15:05:20 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
# create communication for region grid updates
|
|
|
|
|
requestor = InterProcessRequestor()
|
2024-02-15 03:24:36 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
process_frames(
|
|
|
|
|
requestor,
|
|
|
|
|
frame_queue,
|
|
|
|
|
frame_shape,
|
|
|
|
|
self.model_config,
|
|
|
|
|
self.config,
|
|
|
|
|
frame_manager,
|
|
|
|
|
motion_detector,
|
|
|
|
|
object_detector,
|
|
|
|
|
object_tracker,
|
|
|
|
|
self.detected_objects_queue,
|
|
|
|
|
self.camera_metrics,
|
|
|
|
|
self.stop_event,
|
|
|
|
|
self.ptz_metrics,
|
|
|
|
|
self.region_grid,
|
|
|
|
|
)
|
2020-08-22 15:05:20 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
# empty the frame queue
|
|
|
|
|
logger.info(f"{self.config.name}: emptying frame queue")
|
|
|
|
|
while not frame_queue.empty():
|
|
|
|
|
(frame_name, _) = frame_queue.get(False)
|
|
|
|
|
frame_manager.delete(frame_name)
|
2024-06-07 02:54:38 +03:00
|
|
|
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
logger.info(f"{self.config.name}: exiting subprocess")
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
|
|
|
|
def detect(
|
2022-09-12 19:54:50 +03:00
|
|
|
detect_config: DetectConfig,
|
|
|
|
|
object_detector,
|
|
|
|
|
frame,
|
2025-02-11 00:00:12 +03:00
|
|
|
model_config: ModelConfig,
|
2022-09-12 19:54:50 +03:00
|
|
|
region,
|
|
|
|
|
objects_to_track,
|
|
|
|
|
object_filters,
|
2021-02-17 16:23:32 +03:00
|
|
|
):
|
2022-11-04 05:23:09 +03:00
|
|
|
tensor_input = create_tensor_input(frame, model_config, region)
|
2020-08-22 15:05:20 +03:00
|
|
|
|
|
|
|
|
detections = []
|
|
|
|
|
region_detections = object_detector.detect(tensor_input)
|
|
|
|
|
for d in region_detections:
|
|
|
|
|
box = d[2]
|
2021-02-17 16:23:32 +03:00
|
|
|
size = region[2] - region[0]
|
2022-09-12 19:54:50 +03:00
|
|
|
x_min = int(max(0, (box[1] * size) + region[0]))
|
|
|
|
|
y_min = int(max(0, (box[0] * size) + region[1]))
|
2022-09-22 16:07:16 +03:00
|
|
|
x_max = int(min(detect_config.width - 1, (box[3] * size) + region[0]))
|
|
|
|
|
y_max = int(min(detect_config.height - 1, (box[2] * size) + region[1]))
|
|
|
|
|
|
|
|
|
|
# ignore objects that were detected outside the frame
|
|
|
|
|
if (x_min >= detect_config.width - 1) or (y_min >= detect_config.height - 1):
|
|
|
|
|
continue
|
|
|
|
|
|
2022-04-10 16:25:18 +03:00
|
|
|
width = x_max - x_min
|
|
|
|
|
height = y_max - y_min
|
|
|
|
|
area = width * height
|
2023-08-10 13:51:30 +03:00
|
|
|
ratio = width / max(1, height)
|
2025-02-11 18:37:58 +03:00
|
|
|
det = (d[0], d[1], (x_min, y_min, x_max, y_max), area, ratio, region)
|
2020-08-22 15:05:20 +03:00
|
|
|
# apply object filters
|
2023-10-19 02:21:52 +03:00
|
|
|
if is_object_filtered(det, objects_to_track, object_filters):
|
2020-08-22 15:05:20 +03:00
|
|
|
continue
|
|
|
|
|
detections.append(det)
|
|
|
|
|
return detections
|
|
|
|
|
|
2021-02-17 16:23:32 +03:00
|
|
|
|
|
|
|
|
def process_frames(
|
2024-02-15 03:24:36 +03:00
|
|
|
requestor: InterProcessRequestor,
|
2025-04-24 02:06:06 +03:00
|
|
|
frame_queue: Queue,
|
|
|
|
|
frame_shape: tuple[int, int],
|
2023-07-06 15:25:37 +03:00
|
|
|
model_config: ModelConfig,
|
2025-03-30 16:43:24 +03:00
|
|
|
camera_config: CameraConfig,
|
2021-02-17 16:23:32 +03:00
|
|
|
frame_manager: FrameManager,
|
|
|
|
|
motion_detector: MotionDetector,
|
|
|
|
|
object_detector: RemoteObjectDetector,
|
|
|
|
|
object_tracker: ObjectTracker,
|
2025-04-24 02:06:06 +03:00
|
|
|
detected_objects_queue: Queue,
|
2024-09-27 15:53:23 +03:00
|
|
|
camera_metrics: CameraMetrics,
|
2025-04-24 02:06:06 +03:00
|
|
|
stop_event: MpEvent,
|
2024-09-27 15:53:23 +03:00
|
|
|
ptz_metrics: PTZMetrics,
|
2025-04-24 02:06:06 +03:00
|
|
|
region_grid: list[list[dict[str, Any]]],
|
2021-02-17 16:23:32 +03:00
|
|
|
exit_on_empty: bool = False,
|
|
|
|
|
):
|
2023-11-04 05:21:29 +03:00
|
|
|
next_region_update = get_tomorrow_at_time(2)
|
2025-05-22 21:16:51 +03:00
|
|
|
config_subscriber = CameraConfigUpdateSubscriber(
|
2025-06-11 20:25:30 +03:00
|
|
|
None,
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
{camera_config.name: camera_config},
|
2025-05-22 21:16:51 +03:00
|
|
|
[
|
|
|
|
|
CameraConfigUpdateEnum.detect,
|
|
|
|
|
CameraConfigUpdateEnum.enabled,
|
|
|
|
|
CameraConfigUpdateEnum.motion,
|
2025-05-23 05:51:23 +03:00
|
|
|
CameraConfigUpdateEnum.objects,
|
2025-05-22 21:16:51 +03:00
|
|
|
],
|
|
|
|
|
)
|
2020-10-25 18:05:21 +03:00
|
|
|
|
2020-02-16 06:07:54 +03:00
|
|
|
fps_tracker = EventsPerSecond()
|
|
|
|
|
fps_tracker.start()
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2023-10-19 02:21:52 +03:00
|
|
|
startup_scan = True
|
2023-10-20 01:14:33 +03:00
|
|
|
stationary_frame_counter = 0
|
2025-03-12 15:09:09 +03:00
|
|
|
camera_enabled = True
|
2022-02-05 16:10:00 +03:00
|
|
|
|
2023-07-06 15:25:37 +03:00
|
|
|
region_min_size = get_min_region_size(model_config)
|
2023-06-11 16:45:11 +03:00
|
|
|
|
2025-03-30 16:43:24 +03:00
|
|
|
attributes_map = model_config.attributes_map
|
|
|
|
|
all_attributes = model_config.all_attributes
|
|
|
|
|
|
|
|
|
|
# remove license_plate from attributes if this camera is a dedicated LPR cam
|
|
|
|
|
if camera_config.type == CameraTypeEnum.lpr:
|
|
|
|
|
modified_attributes_map = model_config.attributes_map.copy()
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
"car" in modified_attributes_map
|
|
|
|
|
and "license_plate" in modified_attributes_map["car"]
|
|
|
|
|
):
|
|
|
|
|
modified_attributes_map["car"] = [
|
|
|
|
|
attr
|
|
|
|
|
for attr in modified_attributes_map["car"]
|
|
|
|
|
if attr != "license_plate"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
attributes_map = modified_attributes_map
|
|
|
|
|
|
|
|
|
|
all_attributes = [
|
|
|
|
|
attr for attr in model_config.all_attributes if attr != "license_plate"
|
|
|
|
|
]
|
|
|
|
|
|
2021-05-21 18:39:14 +03:00
|
|
|
while not stop_event.is_set():
|
2025-05-22 21:16:51 +03:00
|
|
|
updated_configs = config_subscriber.check_for_updates()
|
2025-03-12 15:09:09 +03:00
|
|
|
|
2025-05-22 21:16:51 +03:00
|
|
|
if "enabled" in updated_configs:
|
2025-03-12 15:09:09 +03:00
|
|
|
prev_enabled = camera_enabled
|
2025-05-22 21:16:51 +03:00
|
|
|
camera_enabled = camera_config.enabled
|
2025-03-03 18:30:52 +03:00
|
|
|
|
2025-05-23 05:51:23 +03:00
|
|
|
if "motion" in updated_configs:
|
|
|
|
|
motion_detector.update_mask()
|
|
|
|
|
|
2025-03-12 15:09:09 +03:00
|
|
|
if (
|
|
|
|
|
not camera_enabled
|
|
|
|
|
and prev_enabled != camera_enabled
|
|
|
|
|
and camera_metrics.frame_queue.empty()
|
|
|
|
|
):
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
logger.debug(
|
|
|
|
|
f"Camera {camera_config.name} disabled, clearing tracked objects"
|
|
|
|
|
)
|
2025-03-12 15:09:09 +03:00
|
|
|
prev_enabled = camera_enabled
|
2025-03-03 18:30:52 +03:00
|
|
|
|
|
|
|
|
# Clear norfair's dictionaries
|
|
|
|
|
object_tracker.tracked_objects.clear()
|
|
|
|
|
object_tracker.disappeared.clear()
|
|
|
|
|
object_tracker.stationary_box_history.clear()
|
|
|
|
|
object_tracker.positions.clear()
|
|
|
|
|
object_tracker.track_id_map.clear()
|
|
|
|
|
|
|
|
|
|
# Clear internal norfair states
|
|
|
|
|
for trackers_by_type in object_tracker.trackers.values():
|
|
|
|
|
for tracker in trackers_by_type.values():
|
|
|
|
|
tracker.tracked_objects = []
|
|
|
|
|
for tracker in object_tracker.default_tracker.values():
|
|
|
|
|
tracker.tracked_objects = []
|
|
|
|
|
|
2025-03-12 15:09:09 +03:00
|
|
|
if not camera_enabled:
|
2025-03-03 18:30:52 +03:00
|
|
|
time.sleep(0.1)
|
|
|
|
|
continue
|
|
|
|
|
|
2025-09-28 19:52:14 +03:00
|
|
|
if datetime.now().astimezone(timezone.utc) > next_region_update:
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
region_grid = requestor.send_data(REQUEST_REGION_GRID, camera_config.name)
|
2023-11-04 05:21:29 +03:00
|
|
|
next_region_update = get_tomorrow_at_time(2)
|
2023-10-19 02:21:52 +03:00
|
|
|
|
2020-08-22 15:05:20 +03:00
|
|
|
try:
|
2023-07-06 16:18:39 +03:00
|
|
|
if exit_on_empty:
|
2024-11-17 02:00:19 +03:00
|
|
|
frame_name, frame_time = frame_queue.get(False)
|
2023-07-06 16:18:39 +03:00
|
|
|
else:
|
2024-11-17 02:00:19 +03:00
|
|
|
frame_name, frame_time = frame_queue.get(True, 1)
|
2020-08-22 15:05:20 +03:00
|
|
|
except queue.Empty:
|
2023-07-06 16:18:39 +03:00
|
|
|
if exit_on_empty:
|
|
|
|
|
logger.info("Exiting track_objects...")
|
|
|
|
|
break
|
2020-03-14 23:32:51 +03:00
|
|
|
continue
|
2020-04-19 18:07:27 +03:00
|
|
|
|
2024-09-27 15:53:23 +03:00
|
|
|
camera_metrics.detection_frame.value = frame_time
|
|
|
|
|
ptz_metrics.frame_time.value = frame_time
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2024-11-17 02:00:19 +03:00
|
|
|
frame = frame_manager.get(frame_name, (frame_shape[0] * 3 // 2, frame_shape[1]))
|
2020-09-14 15:40:26 +03:00
|
|
|
|
|
|
|
|
if frame is None:
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
logger.debug(
|
|
|
|
|
f"{camera_config.name}: frame {frame_time} is not in memory store."
|
|
|
|
|
)
|
2020-09-14 15:40:26 +03:00
|
|
|
continue
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2023-10-14 15:05:44 +03:00
|
|
|
# look for motion if enabled
|
2024-02-19 16:26:59 +03:00
|
|
|
motion_boxes = motion_detector.detect(frame)
|
2020-02-16 06:07:54 +03:00
|
|
|
|
2022-02-06 23:49:54 +03:00
|
|
|
regions = []
|
2023-06-17 17:56:22 +03:00
|
|
|
consolidated_detections = []
|
2020-08-22 15:05:20 +03:00
|
|
|
|
2022-02-06 23:49:54 +03:00
|
|
|
# if detection is disabled
|
2025-05-22 21:16:51 +03:00
|
|
|
if not camera_config.detect.enabled:
|
2024-11-17 02:00:19 +03:00
|
|
|
object_tracker.match_and_update(frame_name, frame_time, [])
|
2022-02-06 23:49:54 +03:00
|
|
|
else:
|
|
|
|
|
# get stationary object ids
|
|
|
|
|
# check every Nth frame for stationary objects
|
|
|
|
|
# disappeared objects are not stationary
|
|
|
|
|
# also check for overlapping motion boxes
|
2025-05-22 21:16:51 +03:00
|
|
|
if stationary_frame_counter == camera_config.detect.stationary.interval:
|
2023-10-20 01:14:33 +03:00
|
|
|
stationary_frame_counter = 0
|
|
|
|
|
stationary_object_ids = []
|
|
|
|
|
else:
|
|
|
|
|
stationary_frame_counter += 1
|
|
|
|
|
stationary_object_ids = [
|
|
|
|
|
obj["id"]
|
|
|
|
|
for obj in object_tracker.tracked_objects.values()
|
|
|
|
|
# if it has exceeded the stationary threshold
|
2025-05-22 21:16:51 +03:00
|
|
|
if obj["motionless_count"]
|
|
|
|
|
>= camera_config.detect.stationary.threshold
|
2023-10-20 01:14:33 +03:00
|
|
|
# and it hasn't disappeared
|
|
|
|
|
and object_tracker.disappeared[obj["id"]] == 0
|
|
|
|
|
# and it doesn't overlap with any current motion boxes when not calibrating
|
|
|
|
|
and not intersects_any(
|
|
|
|
|
obj["box"],
|
|
|
|
|
[] if motion_detector.is_calibrating() else motion_boxes,
|
|
|
|
|
)
|
|
|
|
|
]
|
2022-02-06 23:49:54 +03:00
|
|
|
|
|
|
|
|
# get tracked object boxes that aren't stationary
|
|
|
|
|
tracked_object_boxes = [
|
2023-10-21 02:21:34 +03:00
|
|
|
(
|
|
|
|
|
# use existing object box for stationary objects
|
|
|
|
|
obj["estimate"]
|
2025-05-22 21:16:51 +03:00
|
|
|
if obj["motionless_count"]
|
|
|
|
|
< camera_config.detect.stationary.threshold
|
2023-10-21 02:21:34 +03:00
|
|
|
else obj["box"]
|
|
|
|
|
)
|
2022-02-06 23:49:54 +03:00
|
|
|
for obj in object_tracker.tracked_objects.values()
|
2023-05-29 13:31:17 +03:00
|
|
|
if obj["id"] not in stationary_object_ids
|
2022-02-06 23:49:54 +03:00
|
|
|
]
|
2023-10-24 03:50:22 +03:00
|
|
|
object_boxes = tracked_object_boxes + object_tracker.untracked_object_boxes
|
2022-02-06 23:49:54 +03:00
|
|
|
|
2023-10-19 02:21:52 +03:00
|
|
|
# get consolidated regions for tracked objects
|
2022-02-06 23:49:54 +03:00
|
|
|
regions = [
|
2023-06-11 16:45:11 +03:00
|
|
|
get_cluster_region(
|
2023-10-24 03:50:22 +03:00
|
|
|
frame_shape, region_min_size, candidate, object_boxes
|
2023-10-19 02:21:52 +03:00
|
|
|
)
|
|
|
|
|
for candidate in get_cluster_candidates(
|
2023-10-24 03:50:22 +03:00
|
|
|
frame_shape, region_min_size, object_boxes
|
2021-02-17 16:23:32 +03:00
|
|
|
)
|
2022-02-06 23:49:54 +03:00
|
|
|
]
|
|
|
|
|
|
2023-10-25 02:25:22 +03:00
|
|
|
# only add in the motion boxes when not calibrating and a ptz is not moving via autotracking
|
|
|
|
|
# ptz_moving_at_frame_time() always returns False for non-autotracking cameras
|
|
|
|
|
if not motion_detector.is_calibrating() and not ptz_moving_at_frame_time(
|
|
|
|
|
frame_time,
|
2024-09-27 15:53:23 +03:00
|
|
|
ptz_metrics.start_time.value,
|
|
|
|
|
ptz_metrics.stop_time.value,
|
2023-10-25 02:25:22 +03:00
|
|
|
):
|
2023-10-19 02:21:52 +03:00
|
|
|
# find motion boxes that are not inside tracked object regions
|
|
|
|
|
standalone_motion_boxes = [
|
|
|
|
|
b for b in motion_boxes if not inside_any(b, regions)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
if standalone_motion_boxes:
|
|
|
|
|
motion_clusters = get_cluster_candidates(
|
2022-02-06 23:49:54 +03:00
|
|
|
frame_shape,
|
|
|
|
|
region_min_size,
|
2023-10-19 02:21:52 +03:00
|
|
|
standalone_motion_boxes,
|
2022-02-06 23:49:54 +03:00
|
|
|
)
|
2023-10-19 02:21:52 +03:00
|
|
|
motion_regions = [
|
|
|
|
|
get_cluster_region_from_grid(
|
|
|
|
|
frame_shape,
|
|
|
|
|
region_min_size,
|
|
|
|
|
candidate,
|
|
|
|
|
standalone_motion_boxes,
|
|
|
|
|
region_grid,
|
|
|
|
|
)
|
|
|
|
|
for candidate in motion_clusters
|
|
|
|
|
]
|
|
|
|
|
regions += motion_regions
|
|
|
|
|
|
|
|
|
|
# if starting up, get the next startup scan region
|
|
|
|
|
if startup_scan:
|
|
|
|
|
for region in get_startup_regions(
|
|
|
|
|
frame_shape, region_min_size, region_grid
|
|
|
|
|
):
|
|
|
|
|
regions.append(region)
|
|
|
|
|
startup_scan = False
|
2021-02-17 16:23:32 +03:00
|
|
|
|
2022-02-06 23:49:54 +03:00
|
|
|
# resize regions and detect
|
|
|
|
|
# seed with stationary objects
|
|
|
|
|
detections = [
|
|
|
|
|
(
|
|
|
|
|
obj["label"],
|
|
|
|
|
obj["score"],
|
|
|
|
|
obj["box"],
|
|
|
|
|
obj["area"],
|
2022-04-10 16:25:18 +03:00
|
|
|
obj["ratio"],
|
2022-02-06 23:49:54 +03:00
|
|
|
obj["region"],
|
|
|
|
|
)
|
|
|
|
|
for obj in object_tracker.tracked_objects.values()
|
|
|
|
|
if obj["id"] in stationary_object_ids
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for region in regions:
|
|
|
|
|
detections.extend(
|
|
|
|
|
detect(
|
2025-05-22 21:16:51 +03:00
|
|
|
camera_config.detect,
|
2022-02-06 23:49:54 +03:00
|
|
|
object_detector,
|
|
|
|
|
frame,
|
2022-11-04 05:23:09 +03:00
|
|
|
model_config,
|
2022-02-06 23:49:54 +03:00
|
|
|
region,
|
2025-05-23 05:51:23 +03:00
|
|
|
camera_config.objects.track,
|
|
|
|
|
camera_config.objects.filters,
|
2022-02-06 23:49:54 +03:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2023-10-24 03:20:21 +03:00
|
|
|
consolidated_detections = reduce_detections(frame_shape, detections)
|
2022-02-06 23:49:54 +03:00
|
|
|
|
|
|
|
|
# if detection was run on this frame, consolidate
|
|
|
|
|
if len(regions) > 0:
|
2023-06-17 17:56:22 +03:00
|
|
|
tracked_detections = [
|
2025-03-30 16:43:24 +03:00
|
|
|
d for d in consolidated_detections if d[0] not in all_attributes
|
2023-06-17 17:56:22 +03:00
|
|
|
]
|
2022-02-06 23:49:54 +03:00
|
|
|
# now that we have refined our detections, we need to track objects
|
2024-11-17 02:00:19 +03:00
|
|
|
object_tracker.match_and_update(
|
|
|
|
|
frame_name, frame_time, tracked_detections
|
|
|
|
|
)
|
2022-02-06 23:49:54 +03:00
|
|
|
# else, just update the frame times for the stationary objects
|
|
|
|
|
else:
|
2024-11-17 02:00:19 +03:00
|
|
|
object_tracker.update_frame_times(frame_name, frame_time)
|
2020-02-16 06:07:54 +03:00
|
|
|
|
2023-06-17 17:56:22 +03:00
|
|
|
# group the attribute detections based on what label they apply to
|
2024-10-17 19:02:27 +03:00
|
|
|
attribute_detections: dict[str, list[TrackedObjectAttribute]] = {}
|
2025-03-30 16:43:24 +03:00
|
|
|
for label, attribute_labels in attributes_map.items():
|
2023-06-17 17:56:22 +03:00
|
|
|
attribute_detections[label] = [
|
2024-10-17 19:02:27 +03:00
|
|
|
TrackedObjectAttribute(d)
|
2024-10-01 16:31:03 +03:00
|
|
|
for d in consolidated_detections
|
|
|
|
|
if d[0] in attribute_labels
|
2023-06-17 17:56:22 +03:00
|
|
|
]
|
|
|
|
|
|
2024-10-01 16:31:03 +03:00
|
|
|
# build detections
|
2023-06-17 17:56:22 +03:00
|
|
|
detections = {}
|
|
|
|
|
for obj in object_tracker.tracked_objects.values():
|
2024-10-01 16:31:03 +03:00
|
|
|
detections[obj["id"]] = {**obj, "attributes": []}
|
|
|
|
|
|
|
|
|
|
# find the best object for each attribute to be assigned to
|
2025-05-13 17:27:20 +03:00
|
|
|
all_objects: list[dict[str, Any]] = object_tracker.tracked_objects.values()
|
2024-10-01 16:31:03 +03:00
|
|
|
for attributes in attribute_detections.values():
|
|
|
|
|
for attribute in attributes:
|
|
|
|
|
filtered_objects = filter(
|
2025-03-30 16:43:24 +03:00
|
|
|
lambda o: attribute.label in attributes_map.get(o["label"], []),
|
2024-10-01 16:54:27 +03:00
|
|
|
all_objects,
|
2024-10-01 16:31:03 +03:00
|
|
|
)
|
|
|
|
|
selected_object_id = attribute.find_best_object(filtered_objects)
|
|
|
|
|
|
|
|
|
|
if selected_object_id is not None:
|
|
|
|
|
detections[selected_object_id]["attributes"].append(
|
|
|
|
|
attribute.get_tracking_data()
|
|
|
|
|
)
|
2023-06-17 17:56:22 +03:00
|
|
|
|
2023-06-11 16:45:11 +03:00
|
|
|
# debug object tracking
|
2023-05-31 17:12:43 +03:00
|
|
|
if False:
|
|
|
|
|
bgr_frame = cv2.cvtColor(
|
|
|
|
|
frame,
|
|
|
|
|
cv2.COLOR_YUV2BGR_I420,
|
|
|
|
|
)
|
|
|
|
|
object_tracker.debug_draw(bgr_frame, frame_time)
|
|
|
|
|
cv2.imwrite(
|
|
|
|
|
f"debug/frames/track-{'{:.6f}'.format(frame_time)}.jpg", bgr_frame
|
|
|
|
|
)
|
2023-06-11 16:45:11 +03:00
|
|
|
# debug
|
|
|
|
|
if False:
|
|
|
|
|
bgr_frame = cv2.cvtColor(
|
|
|
|
|
frame,
|
|
|
|
|
cv2.COLOR_YUV2BGR_I420,
|
|
|
|
|
)
|
2023-05-31 17:12:43 +03:00
|
|
|
|
2023-06-11 16:45:11 +03:00
|
|
|
for m_box in motion_boxes:
|
|
|
|
|
cv2.rectangle(
|
|
|
|
|
bgr_frame,
|
|
|
|
|
(m_box[0], m_box[1]),
|
|
|
|
|
(m_box[2], m_box[3]),
|
|
|
|
|
(0, 0, 255),
|
|
|
|
|
2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for b in tracked_object_boxes:
|
|
|
|
|
cv2.rectangle(
|
|
|
|
|
bgr_frame,
|
|
|
|
|
(b[0], b[1]),
|
|
|
|
|
(b[2], b[3]),
|
|
|
|
|
(255, 0, 0),
|
|
|
|
|
2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for obj in object_tracker.tracked_objects.values():
|
|
|
|
|
if obj["frame_time"] == frame_time:
|
|
|
|
|
thickness = 2
|
2025-03-30 16:43:24 +03:00
|
|
|
color = model_config.colormap.get(obj["label"], (255, 255, 255))
|
2023-06-11 16:45:11 +03:00
|
|
|
else:
|
|
|
|
|
thickness = 1
|
|
|
|
|
color = (255, 0, 0)
|
|
|
|
|
|
|
|
|
|
# draw the bounding boxes on the frame
|
|
|
|
|
box = obj["box"]
|
|
|
|
|
|
|
|
|
|
draw_box_with_label(
|
|
|
|
|
bgr_frame,
|
|
|
|
|
box[0],
|
|
|
|
|
box[1],
|
|
|
|
|
box[2],
|
|
|
|
|
box[3],
|
|
|
|
|
obj["label"],
|
|
|
|
|
obj["id"],
|
|
|
|
|
thickness=thickness,
|
|
|
|
|
color=color,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for region in regions:
|
|
|
|
|
cv2.rectangle(
|
|
|
|
|
bgr_frame,
|
|
|
|
|
(region[0], region[1]),
|
|
|
|
|
(region[2], region[3]),
|
|
|
|
|
(0, 255, 0),
|
|
|
|
|
2,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
cv2.imwrite(
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
f"debug/frames/{camera_config.name}-{'{:.6f}'.format(frame_time)}.jpg",
|
2023-06-11 16:45:11 +03:00
|
|
|
bgr_frame,
|
|
|
|
|
)
|
2020-10-24 19:36:04 +03:00
|
|
|
# add to the queue if not full
|
2021-02-17 16:23:32 +03:00
|
|
|
if detected_objects_queue.full():
|
2024-11-17 02:00:19 +03:00
|
|
|
frame_manager.close(frame_name)
|
2021-01-16 05:52:59 +03:00
|
|
|
continue
|
2020-10-24 19:36:04 +03:00
|
|
|
else:
|
2021-01-16 05:52:59 +03:00
|
|
|
fps_tracker.update()
|
2024-09-27 15:53:23 +03:00
|
|
|
camera_metrics.process_fps.value = fps_tracker.eps()
|
2021-02-17 16:23:32 +03:00
|
|
|
detected_objects_queue.put(
|
|
|
|
|
(
|
Use Fork-Server As Spawn Method (#18682)
* Set runtime
* Use count correctly
* Don't assume camera sizes
* Use separate zmq proxy for object detection
* Correct order
* Use forkserver
* Only store PID instead of entire process reference
* Cleanup
* Catch correct errors
* Fix typing
* Remove before_run from process util
The before_run never actually ran because:
You're right to suspect an issue with before_run not being called and a potential deadlock. The way you've implemented the run_wrapper using __getattribute__ for the run method of BaseProcess is a common pitfall in Python's multiprocessing, especially when combined with how multiprocessing.Process works internally.
Here's a breakdown of why before_run isn't being called and why you might be experiencing a deadlock:
The Problem: __getattribute__ and Process Serialization
When you create a multiprocessing.Process object and call start(), the multiprocessing module needs to serialize the process object (or at least enough of it to re-create the process in the new interpreter). It then pickles this serialized object and sends it to the newly spawned process.
The issue with your __getattribute__ implementation for run is that:
run is retrieved during serialization: When multiprocessing tries to pickle your Process object to send to the new process, it will likely access the run attribute. This triggers your __getattribute__ wrapper, which then tries to bind run_wrapper to self.
run_wrapper is bound to the parent process's self: The run_wrapper closure, when created in the parent process, captures the self (the Process instance) from the parent's memory space.
Deserialization creates a new object: In the child process, a new Process object is created by deserializing the pickled data. However, the run_wrapper method that was pickled still holds a reference to the self from the parent process. This is a subtle but critical distinction.
The child's run is not your wrapped run: When the child process starts, it internally calls its own run method. Because of the serialization and deserialization process, the run method that's ultimately executed in the child process is the original multiprocessing.Process.run or the Process.run if you had directly overridden it. Your __getattribute__ magic, which wraps run, isn't correctly applied to the Process object within the child's context.
* Cleanup
* Logging bugfix (#18465)
* use mp Manager to handle logging queues
A Python bug (https://github.com/python/cpython/issues/91555) was preventing logs from the embeddings maintainer process from printing. The bug is fixed in Python 3.14, but a viable workaround is to use the multiprocessing Manager, which better manages mp queues and causes the logging to work correctly.
* consolidate
* fix typing
* Fix typing
* Use global log queue
* Move to using process for logging
* Convert camera tracking to process
* Add more processes
* Finalize process
* Cleanup
* Cleanup typing
* Formatting
* Remove daemon
---------
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-06-12 21:12:34 +03:00
|
|
|
camera_config.name,
|
2024-11-17 02:00:19 +03:00
|
|
|
frame_name,
|
2021-02-17 16:23:32 +03:00
|
|
|
frame_time,
|
2023-06-17 17:56:22 +03:00
|
|
|
detections,
|
2021-02-17 16:23:32 +03:00
|
|
|
motion_boxes,
|
|
|
|
|
regions,
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-09-27 15:53:23 +03:00
|
|
|
camera_metrics.detection_fps.value = object_detector.fps.eps()
|
2024-11-17 02:00:19 +03:00
|
|
|
frame_manager.close(frame_name)
|
2024-02-15 03:24:36 +03:00
|
|
|
|
2024-02-19 16:26:59 +03:00
|
|
|
motion_detector.stop()
|
2024-02-15 03:24:36 +03:00
|
|
|
requestor.stop()
|
2025-05-22 21:16:51 +03:00
|
|
|
config_subscriber.stop()
|