From d1fbd82952fb145062a0b1ce2fc818f21355b004 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 9 Jul 2024 06:44:53 -0600 Subject: [PATCH 01/15] Limited shm frame count (#12346) * Only keep 2x detect fps frames in SHM * Don't delete previous shm frames in output * Catch case where images do not exist * Ensure files are closed * Clear out all frames when shutting down * Correct the number of frames saved * Simplify empty shm error handling * Improve frame safety --- frigate/object_processing.py | 15 +++++++++----- frigate/output/output.py | 12 +++++------ frigate/util/image.py | 26 ++++++++++++++---------- frigate/video.py | 39 ++++++++++++++++++++++++------------ 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 5b7a1bedfd..4fba022b4f 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -668,6 +668,7 @@ def on(self, event_type: str, callback: Callable[[dict], None]): def update(self, frame_time, current_detections, motion_boxes, regions): # get the new frame frame_id = f"{self.name}{frame_time}" + current_frame = self.frame_manager.get( frame_id, self.camera_config.frame_shape_yuv ) @@ -702,7 +703,7 @@ def update(self, frame_time, current_detections, motion_boxes, regions): for c in self.callbacks["autotrack"]: c(self.name, updated_obj, frame_time) - if thumb_update: + if thumb_update and current_frame is not None: # ensure this frame is stored in the cache if ( updated_obj.thumbnail_data["frame_time"] == frame_time @@ -886,12 +887,16 @@ def update(self, frame_time, current_detections, motion_boxes, regions): with self.current_frame_lock: self.tracked_objects = tracked_objects - self.current_frame_time = frame_time self.motion_boxes = motion_boxes self.regions = regions - self._current_frame = current_frame - if self.previous_frame_id is not None: - self.frame_manager.close(self.previous_frame_id) + + if current_frame is not None: + self.current_frame_time = frame_time + self._current_frame = current_frame + + if self.previous_frame_id is not None: + self.frame_manager.close(self.previous_frame_id) + self.previous_frame_id = frame_id diff --git a/frigate/output/output.py b/frigate/output/output.py index b29144b009..9be50594f3 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -45,7 +45,6 @@ def receiveSignal(signalNumber, frame): signal.signal(signal.SIGINT, receiveSignal) frame_manager = SharedMemoryFrameManager() - previous_frames = {} # start a websocket server on 8082 WebSocketWSGIHandler.http_version = "1.1" @@ -99,6 +98,9 @@ def receiveSignal(signalNumber, frame): frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv) + if frame is None: + continue + # send camera frame to ffmpeg process if websockets are connected if any( ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager @@ -128,10 +130,6 @@ def receiveSignal(signalNumber, frame): ) preview_write_times[camera] = frame_time - # delete frames after they have been used for output - if camera in previous_frames: - frame_manager.delete(f"{camera}{previous_frames[camera]}") - # if another camera generated a preview, # check for any cameras that are currently offline # and need to generate a preview @@ -141,7 +139,7 @@ def receiveSignal(signalNumber, frame): preview_recorders[camera].flag_offline(frame_time) preview_write_times[camera] = frame_time - previous_frames[camera] = frame_time + frame_manager.close(frame_id) move_preview_frames("clips") @@ -161,7 +159,7 @@ def receiveSignal(signalNumber, frame): frame_id = f"{camera}{frame_time}" frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv) - frame_manager.delete(frame_id) + frame_manager.close(frame_id) detection_subscriber.stop() diff --git a/frigate/util/image.py b/frigate/util/image.py index 3962d9600e..a3619193fb 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -687,27 +687,31 @@ def delete(self, name): class SharedMemoryFrameManager(FrameManager): def __init__(self): - self.shm_store = {} + self.shm_store: dict[str, shared_memory.SharedMemory] = {} - def create(self, name, size) -> AnyStr: + def create(self, name: str, size) -> AnyStr: shm = shared_memory.SharedMemory(name=name, create=True, size=size) self.shm_store[name] = shm return shm.buf - def get(self, name, shape): - if name in self.shm_store: - shm = self.shm_store[name] - else: - shm = shared_memory.SharedMemory(name=name) - self.shm_store[name] = shm - return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf) + def get(self, name: str, shape) -> Optional[np.ndarray]: + try: + if name in self.shm_store: + shm = self.shm_store[name] + else: + shm = shared_memory.SharedMemory(name=name) + self.shm_store[name] = shm + return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf) + except FileNotFoundError: + logger.error(f"Failed to get {name} from SHM") + return None - def close(self, name): + def close(self, name: str): if name in self.shm_store: self.shm_store[name].close() del self.shm_store[name] - def delete(self, name): + def delete(self, name: str): if name in self.shm_store: self.shm_store[name].close() self.shm_store[name].unlink() diff --git a/frigate/video.py b/frigate/video.py index 1c74575dc3..3397de6e4f 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -94,7 +94,7 @@ def start_or_restart_ffmpeg( def capture_frames( ffmpeg_process, - camera_name, + config: CameraConfig, frame_shape, frame_manager: FrameManager, frame_queue, @@ -108,24 +108,36 @@ def capture_frames( frame_rate.start() skipped_eps = EventsPerSecond() skipped_eps.start() + + shm_count = max(10, config.detect.fps * 2) + shm_frames: list[str] = [] + while True: fps.value = frame_rate.eps() skipped_fps.value = skipped_eps.eps() - current_frame.value = datetime.datetime.now().timestamp() - frame_name = f"{camera_name}{current_frame.value}" + frame_name = f"{config.name}{current_frame.value}" frame_buffer = frame_manager.create(frame_name, frame_size) try: frame_buffer[:] = ffmpeg_process.stdout.read(frame_size) + + # update frame cache and cleanup existing frames + shm_frames.append(frame_name) + + if len(shm_frames) > shm_count: + expired_frame_name = shm_frames.pop(0) + frame_manager.delete(expired_frame_name) except Exception: + frame_manager.delete(frame_name) + # shutdown has been initiated if stop_event.is_set(): break - logger.error(f"{camera_name}: Unable to read frames from ffmpeg process.") + logger.error(f"{config.name}: Unable to read frames from ffmpeg process.") if ffmpeg_process.poll() is not None: logger.error( - f"{camera_name}: ffmpeg process is not running. exiting capture thread..." + f"{config.name}: ffmpeg process is not running. exiting capture thread..." ) frame_manager.delete(frame_name) break @@ -137,12 +149,13 @@ def capture_frames( try: # add to the queue frame_queue.put(current_frame.value, False) - # close the frame - frame_manager.close(frame_name) except queue.Full: # if the queue is full, skip this frame skipped_eps.update() - frame_manager.delete(frame_name) + + # clear out frames + for frame in shm_frames: + frame_manager.delete(frame) class CameraWatchdog(threading.Thread): @@ -282,7 +295,7 @@ def start_ffmpeg_detect(self): ) self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid self.capture_thread = CameraCapture( - self.camera_name, + self.config, self.ffmpeg_detect_process, self.frame_shape, self.frame_queue, @@ -321,7 +334,7 @@ def get_latest_segment_datetime(self, latest_segment: datetime.datetime) -> int: class CameraCapture(threading.Thread): def __init__( self, - camera_name, + config: CameraConfig, ffmpeg_process, frame_shape, frame_queue, @@ -330,8 +343,8 @@ def __init__( stop_event, ): threading.Thread.__init__(self) - self.name = f"capture:{camera_name}" - self.camera_name = camera_name + self.name = f"capture:{config.name}" + self.config = config self.frame_shape = frame_shape self.frame_queue = frame_queue self.fps = fps @@ -345,7 +358,7 @@ def __init__( def run(self): capture_frames( self.ffmpeg_process, - self.camera_name, + self.config, self.frame_shape, self.frame_manager, self.frame_queue, From f5d889f364b6d33eee9fc71bba2851fb4863582d Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 9 Jul 2024 09:15:04 -0600 Subject: [PATCH 02/15] Add handler logs when frame is None --- frigate/object_detection.py | 1 + frigate/object_processing.py | 3 +++ frigate/output/birdseye.py | 11 ++++++----- frigate/output/output.py | 1 + frigate/review/maintainer.py | 15 +++++++++++++++ frigate/util/image.py | 20 ++++++++++++++++++-- frigate/video.py | 7 +++++-- 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/frigate/object_detection.py b/frigate/object_detection.py index 207637ed47..22ff84f894 100644 --- a/frigate/object_detection.py +++ b/frigate/object_detection.py @@ -118,6 +118,7 @@ def receiveSignal(signalNumber, frame): ) if input_frame is None: + logger.warning(f"Failed to get frame {connection_id} from SHM") continue # detect and send the output diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 4fba022b4f..55f6196b6e 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -673,6 +673,9 @@ def update(self, frame_time, current_detections, motion_boxes, regions): frame_id, self.camera_config.frame_shape_yuv ) + if current_frame is None: + logger.warning(f"Failed to get frame {frame_id} from SHM") + tracked_objects = self.tracked_objects.copy() current_ids = set(current_detections.keys()) previous_ids = set(tracked_objects.keys()) diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 6e0e2bc22e..0e6218a703 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -357,16 +357,17 @@ def copy_to_position(self, position, camera=None, frame_time=None): frame = None channel_dims = None else: - try: - frame = self.frame_manager.get( - f"{camera}{frame_time}", self.config.cameras[camera].frame_shape_yuv - ) - except FileNotFoundError: + frame = self.frame_manager.get( + f"{camera}{frame_time}", self.config.cameras[camera].frame_shape_yuv + ) + + if frame is None: # TODO: better frame management would prevent this edge case logger.warning( f"Unable to copy frame {camera}{frame_time} to birdseye." ) return + channel_dims = self.cameras[camera]["channel_dims"] copy_yuv_to_position( diff --git a/frigate/output/output.py b/frigate/output/output.py index 9be50594f3..65d1e33234 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -99,6 +99,7 @@ def receiveSignal(signalNumber, frame): frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv) if frame is None: + logger.warning(f"Failed to get frame {frame_id} from SHM") continue # send camera frame to ffmpeg process if websockets are connected diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index fa2678d9b7..11c46262c1 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -292,6 +292,11 @@ def update_existing_segment( yuv_frame = self.frame_manager.get( frame_id, camera_config.frame_shape_yuv ) + + if yuv_frame is None: + logger.warning(f"Failed to get frame {frame_id} from SHM") + return + self.update_segment( segment, camera_config, yuv_frame, active_objects, prev_data ) @@ -305,6 +310,11 @@ def update_existing_segment( yuv_frame = self.frame_manager.get( frame_id, camera_config.frame_shape_yuv ) + + if yuv_frame is None: + logger.warning(f"Failed to get frame {frame_id} from SHM") + return + segment.save_full_frame(camera_config, yuv_frame) self.frame_manager.close(frame_id) self.update_segment(segment, camera_config, None, [], prev_data) @@ -401,6 +411,11 @@ def check_if_new_segment( yuv_frame = self.frame_manager.get( frame_id, camera_config.frame_shape_yuv ) + + if yuv_frame is None: + logger.warning(f"Failed to get frame {frame_id} from SHM") + return + self.active_review_segments[camera].update_frame( camera_config, yuv_frame, active_objects ) diff --git a/frigate/util/image.py b/frigate/util/image.py index a3619193fb..5c50cafdeb 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -1,6 +1,7 @@ """Utilities for creating and manipulating image frames.""" import datetime +import inspect import logging import subprocess as sp from abc import ABC, abstractmethod @@ -695,6 +696,7 @@ def create(self, name: str, size) -> AnyStr: return shm.buf def get(self, name: str, shape) -> Optional[np.ndarray]: + logger.info(f"retrieving {name} from {inspect.stack()[1].filename} {inspect.stack()[1].function}") try: if name in self.shm_store: shm = self.shm_store[name] @@ -703,19 +705,33 @@ def get(self, name: str, shape) -> Optional[np.ndarray]: self.shm_store[name] = shm return np.ndarray(shape, dtype=np.uint8, buffer=shm.buf) except FileNotFoundError: - logger.error(f"Failed to get {name} from SHM") return None def close(self, name: str): + logger.info(f"closing {name}") if name in self.shm_store: self.shm_store[name].close() del self.shm_store[name] def delete(self, name: str): + logger.info(f"deleting expired {name}") if name in self.shm_store: self.shm_store[name].close() - self.shm_store[name].unlink() + + try: + self.shm_store[name].unlink() + except FileNotFoundError: + pass + del self.shm_store[name] + else: + shm = shared_memory.SharedMemory(name=name) + shm.close() + + try: + shm.unlink() + except FileNotFoundError: + pass def create_mask(frame_shape, mask): diff --git a/frigate/video.py b/frigate/video.py index 3397de6e4f..97ace7d79c 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -109,7 +109,7 @@ def capture_frames( skipped_eps = EventsPerSecond() skipped_eps.start() - shm_count = max(10, config.detect.fps * 2) + shm_count = max(10, config.detect.fps) shm_frames: list[str] = [] while True: @@ -127,9 +127,11 @@ def capture_frames( if len(shm_frames) > shm_count: expired_frame_name = shm_frames.pop(0) frame_manager.delete(expired_frame_name) - except Exception: + except Exception as e: + logger.error(f"something video bad happened :: {e}") frame_manager.delete(frame_name) + # shutdown has been initiated if stop_event.is_set(): break @@ -149,6 +151,7 @@ def capture_frames( try: # add to the queue frame_queue.put(current_frame.value, False) + frame_manager.close(frame_name) except queue.Full: # if the queue is full, skip this frame skipped_eps.update() From f42a20d3db0acd7c3338c02f05df7535076555f8 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 9 Jul 2024 16:54:16 -0600 Subject: [PATCH 03/15] Don't fail on cleanup --- frigate/util/image.py | 9 ++------- frigate/video.py | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/frigate/util/image.py b/frigate/util/image.py index 5c50cafdeb..7c470a2a0c 100644 --- a/frigate/util/image.py +++ b/frigate/util/image.py @@ -1,7 +1,6 @@ """Utilities for creating and manipulating image frames.""" import datetime -import inspect import logging import subprocess as sp from abc import ABC, abstractmethod @@ -696,7 +695,6 @@ def create(self, name: str, size) -> AnyStr: return shm.buf def get(self, name: str, shape) -> Optional[np.ndarray]: - logger.info(f"retrieving {name} from {inspect.stack()[1].filename} {inspect.stack()[1].function}") try: if name in self.shm_store: shm = self.shm_store[name] @@ -708,13 +706,11 @@ def get(self, name: str, shape) -> Optional[np.ndarray]: return None def close(self, name: str): - logger.info(f"closing {name}") if name in self.shm_store: self.shm_store[name].close() del self.shm_store[name] def delete(self, name: str): - logger.info(f"deleting expired {name}") if name in self.shm_store: self.shm_store[name].close() @@ -725,10 +721,9 @@ def delete(self, name: str): del self.shm_store[name] else: - shm = shared_memory.SharedMemory(name=name) - shm.close() - try: + shm = shared_memory.SharedMemory(name=name) + shm.close() shm.unlink() except FileNotFoundError: pass diff --git a/frigate/video.py b/frigate/video.py index 97ace7d79c..aaea05d43b 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -131,7 +131,6 @@ def capture_frames( logger.error(f"something video bad happened :: {e}") frame_manager.delete(frame_name) - # shutdown has been initiated if stop_event.is_set(): break From b399bc4b6fcf9ebbcc9d5b360a0d2ff6dff5ff65 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Jul 2024 06:34:13 -0600 Subject: [PATCH 04/15] Cleanup logging --- frigate/object_processing.py | 2 +- frigate/output/output.py | 2 +- frigate/video.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 55f6196b6e..55a4d08f8b 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -674,7 +674,7 @@ def update(self, frame_time, current_detections, motion_boxes, regions): ) if current_frame is None: - logger.warning(f"Failed to get frame {frame_id} from SHM") + logger.debug(f"Failed to get frame {frame_id} from SHM") tracked_objects = self.tracked_objects.copy() current_ids = set(current_detections.keys()) diff --git a/frigate/output/output.py b/frigate/output/output.py index 65d1e33234..f351e967d3 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -99,7 +99,7 @@ def receiveSignal(signalNumber, frame): frame = frame_manager.get(frame_id, config.cameras[camera].frame_shape_yuv) if frame is None: - logger.warning(f"Failed to get frame {frame_id} from SHM") + logger.debug(f"Failed to get frame {frame_id} from SHM") continue # send camera frame to ffmpeg process if websockets are connected diff --git a/frigate/video.py b/frigate/video.py index aaea05d43b..712ac1bcdd 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -127,21 +127,22 @@ def capture_frames( if len(shm_frames) > shm_count: expired_frame_name = shm_frames.pop(0) frame_manager.delete(expired_frame_name) - except Exception as e: - logger.error(f"something video bad happened :: {e}") + except Exception: + # always delete the frame frame_manager.delete(frame_name) # shutdown has been initiated if stop_event.is_set(): break + logger.error(f"{config.name}: Unable to read frames from ffmpeg process.") if ffmpeg_process.poll() is not None: logger.error( f"{config.name}: ffmpeg process is not running. exiting capture thread..." ) - frame_manager.delete(frame_name) break + continue frame_rate.update() From c57ea6f653fba2e7b51a6b719eb62ebebc3df7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Jul 2024 06:40:31 -0600 Subject: [PATCH 05/15] Update docs --- docs/docs/frigate/installation.md | 10 +++++----- frigate/app.py | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 98f55f229e..e2f229a6a7 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -75,21 +75,21 @@ Frigate utilizes shared memory to store frames during processing. The default `s The default shm size of **64MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in docker-compose). -The Frigate container also stores logs in shm, which can take up to **30MB**, so make sure to take this into account in your math as well. +The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well. You can calculate the necessary shm size for each camera with the following formula using the resolution specified for detect: ```console # Replace and -$ python -c 'print("{:.2f}MB".format(( * * 1.5 * 9 + 270480) / 1048576))' +$ python -c 'print("{:.2f}MB".format(( * * 1.5 * 10 + 270480) / 1048576))' # Example for 1280x720 -$ python -c 'print("{:.2f}MB".format((1280 * 720 * 1.5 * 9 + 270480) / 1048576))' +$ python -c 'print("{:.2f}MB".format((1280 * 720 * 1.5 * 10 + 270480) / 1048576))' 12.12MB # Example for eight cameras detecting at 1280x720, including logs -$ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 9 + 270480) / 1048576) * 8 + 30))' -126.99MB +$ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 10 + 270480) / 1048576) * 8 + 40))' +136.99MB ``` The shm size cannot be set per container for Home Assistant add-ons. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration. diff --git a/frigate/app.py b/frigate/app.py index 9149f98546..e185f0e448 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -602,18 +602,26 @@ def start_watchdog(self) -> None: def check_shm(self) -> None: available_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) - min_req_shm = 30 - for _, camera in self.config.cameras.items(): + # required for log files + min_req_shm = 40 + + for camera in self.config.cameras.values(): min_req_shm += round( - (camera.detect.width * camera.detect.height * 1.5 * 9 + 270480) + ( + camera.detect.width + * camera.detect.height + * 1.5 + * max(10, camera.detect.fps) + + 270480 + ) / 1048576, 1, ) - if available_shm < min_req_shm: + if True: logger.warning( - f"The current SHM size of {available_shm}MB is too small, recommend increasing it to at least {min_req_shm}MB." + f"The current SHM size of {available_shm}MB is too small, recommend increasing it to at least {round(min_req_shm)}MB." ) def init_auth(self) -> None: From b66264ed9990265ea74c8900c50fc5c69d306d69 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Jul 2024 06:42:37 -0600 Subject: [PATCH 06/15] Update calculation --- docs/docs/frigate/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index e2f229a6a7..857c14f198 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -85,7 +85,7 @@ $ python -c 'print("{:.2f}MB".format(( * * 1.5 * 10 + 270480) / # Example for 1280x720 $ python -c 'print("{:.2f}MB".format((1280 * 720 * 1.5 * 10 + 270480) / 1048576))' -12.12MB +13.44MB # Example for eight cameras detecting at 1280x720, including logs $ python -c 'print("{:.2f}MB".format(((1280 * 720 * 1.5 * 10 + 270480) / 1048576) * 8 + 40))' From 4940e909be9e0024a1b11840e9d8fe544724a144 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Jul 2024 07:19:49 -0600 Subject: [PATCH 07/15] Restore condition --- frigate/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate/app.py b/frigate/app.py index e185f0e448..c708d7dee4 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -619,7 +619,7 @@ def check_shm(self) -> None: 1, ) - if True: + if available_shm < min_req_shm: logger.warning( f"The current SHM size of {available_shm}MB is too small, recommend increasing it to at least {round(min_req_shm)}MB." ) From 575494a091b7fcfe14a18113db60690d0324c747 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Jul 2024 07:23:09 -0600 Subject: [PATCH 08/15] Fix case where thumbnail is saved without frame --- frigate/object_processing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frigate/object_processing.py b/frigate/object_processing.py index 55a4d08f8b..3616d6b503 100644 --- a/frigate/object_processing.py +++ b/frigate/object_processing.py @@ -147,7 +147,7 @@ def compute_score(self): """get median of scores for object.""" return median(self.score_history) - def update(self, current_frame_time, obj_data): + def update(self, current_frame_time: float, obj_data, has_valid_frame: bool): thumb_update = False significant_change = False autotracker_update = False @@ -168,7 +168,7 @@ def update(self, current_frame_time, obj_data): self.false_positive = self._is_false_positive() self.active = self.is_active() - if not self.false_positive: + if not self.false_positive and has_valid_frame: # determine if this frame is a better thumbnail if self.thumbnail_data is None or is_better_thumbnail( self.obj_data["label"], @@ -699,7 +699,7 @@ def update(self, frame_time, current_detections, motion_boxes, regions): for id in updated_ids: updated_obj = tracked_objects[id] thumb_update, significant_update, autotracker_update = updated_obj.update( - frame_time, current_detections[id] + frame_time, current_detections[id], current_frame is not None ) if autotracker_update or significant_update: From 2c05bd1a4c74e0e4931ed8e5ab9da8a81ad60276 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 10 Jul 2024 09:33:25 -0600 Subject: [PATCH 09/15] Adjust debug logs --- frigate/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate/video.py b/frigate/video.py index 712ac1bcdd..87492c5ff7 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -583,7 +583,7 @@ def process_frames( ) if frame is None: - logger.info(f"{camera_name}: frame {frame_time} is not in memory store.") + logger.debug(f"{camera_name}: frame {frame_time} is not in memory store.") continue # look for motion if enabled From 50c28cc048bc55a7a56985beceb7677469fe9307 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 2 Sep 2024 10:41:06 -0600 Subject: [PATCH 10/15] Calculate best shm frame count --- frigate/app.py | 39 +++++++++++++++++++++--------------- frigate/output/birdseye.py | 3 +-- frigate/review/maintainer.py | 6 +++--- frigate/video.py | 13 +++++++++--- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index c708d7dee4..5023e66218 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -537,7 +537,7 @@ def start_camera_capture_processes(self) -> None: capture_process = mp.Process( target=capture_camera, name=f"camera_capture:{name}", - args=(name, config, self.camera_metrics[name]), + args=(name, config, self.shm_frame_count, self.camera_metrics[name]), ) capture_process.daemon = True self.camera_metrics[name]["capture_process"] = capture_process @@ -601,27 +601,34 @@ def start_watchdog(self) -> None: self.frigate_watchdog.start() def check_shm(self) -> None: - available_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) + total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) - # required for log files - min_req_shm = 40 + # required for log files + nginx cache + min_req_shm = 40 + 10 + + if self.config.birdseye.restream: + min_req_shm += 8 + + available_shm = total_shm - min_req_shm + cam_average_shm = 0 for camera in self.config.cameras.values(): - min_req_shm += round( - ( - camera.detect.width - * camera.detect.height - * 1.5 - * max(10, camera.detect.fps) - + 270480 - ) - / 1048576, + cam_average_shm += round( + (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, 1, ) - if available_shm < min_req_shm: + self.shm_frame_count = max( + 50, int(available_shm / (cam_average_shm / len(self.config.cameras))) + ) + + logger.info( + f"Calculated {self.shm_frame_count} frames for each camera in SHM" + ) + + if self.shm_frame_count < 10: logger.warning( - f"The current SHM size of {available_shm}MB is too small, recommend increasing it to at least {round(min_req_shm)}MB." + f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + (cam_average_shm * 10))}MB." ) def init_auth(self) -> None: @@ -726,6 +733,7 @@ def start(self) -> None: self.init_historical_regions() self.start_detected_frames_processor() self.start_camera_processors() + self.check_shm() self.start_camera_capture_processes() self.start_audio_processors() self.start_storage_maintainer() @@ -737,7 +745,6 @@ def start(self) -> None: self.start_event_cleanup() self.start_record_cleanup() self.start_watchdog() - self.check_shm() self.init_auth() # Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 0e6218a703..36376ebc53 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -362,8 +362,7 @@ def copy_to_position(self, position, camera=None, frame_time=None): ) if frame is None: - # TODO: better frame management would prevent this edge case - logger.warning( + logger.debug( f"Unable to copy frame {camera}{frame_time} to birdseye." ) return diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 11c46262c1..bea62256d5 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -294,7 +294,7 @@ def update_existing_segment( ) if yuv_frame is None: - logger.warning(f"Failed to get frame {frame_id} from SHM") + logger.debug(f"Failed to get frame {frame_id} from SHM") return self.update_segment( @@ -312,7 +312,7 @@ def update_existing_segment( ) if yuv_frame is None: - logger.warning(f"Failed to get frame {frame_id} from SHM") + logger.debug(f"Failed to get frame {frame_id} from SHM") return segment.save_full_frame(camera_config, yuv_frame) @@ -413,7 +413,7 @@ def check_if_new_segment( ) if yuv_frame is None: - logger.warning(f"Failed to get frame {frame_id} from SHM") + logger.debug(f"Failed to get frame {frame_id} from SHM") return self.active_review_segments[camera].update_frame( diff --git a/frigate/video.py b/frigate/video.py index 87492c5ff7..80749c6546 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -95,6 +95,7 @@ def start_or_restart_ffmpeg( def capture_frames( ffmpeg_process, config: CameraConfig, + shm_frame_count: int, frame_shape, frame_manager: FrameManager, frame_queue, @@ -109,7 +110,6 @@ def capture_frames( skipped_eps = EventsPerSecond() skipped_eps.start() - shm_count = max(10, config.detect.fps) shm_frames: list[str] = [] while True: @@ -124,7 +124,7 @@ def capture_frames( # update frame cache and cleanup existing frames shm_frames.append(frame_name) - if len(shm_frames) > shm_count: + if len(shm_frames) > shm_frame_count: expired_frame_name = shm_frames.pop(0) frame_manager.delete(expired_frame_name) except Exception: @@ -166,6 +166,7 @@ def __init__( self, camera_name, config: CameraConfig, + shm_frame_count: int, frame_queue, camera_fps, skipped_fps, @@ -176,6 +177,7 @@ def __init__( self.logger = logging.getLogger(f"watchdog.{camera_name}") self.camera_name = camera_name self.config = config + self.shm_frame_count = shm_frame_count self.capture_thread = None self.ffmpeg_detect_process = None self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect") @@ -299,6 +301,7 @@ def start_ffmpeg_detect(self): self.ffmpeg_pid.value = self.ffmpeg_detect_process.pid self.capture_thread = CameraCapture( self.config, + self.shm_frame_count, self.ffmpeg_detect_process, self.frame_shape, self.frame_queue, @@ -338,6 +341,7 @@ class CameraCapture(threading.Thread): def __init__( self, config: CameraConfig, + shm_frame_count: int, ffmpeg_process, frame_shape, frame_queue, @@ -348,6 +352,7 @@ def __init__( threading.Thread.__init__(self) self.name = f"capture:{config.name}" self.config = config + self.shm_frame_count = shm_frame_count self.frame_shape = frame_shape self.frame_queue = frame_queue self.fps = fps @@ -362,6 +367,7 @@ def run(self): capture_frames( self.ffmpeg_process, self.config, + self.shm_frame_count, self.frame_shape, self.frame_manager, self.frame_queue, @@ -372,7 +378,7 @@ def run(self): ) -def capture_camera(name, config: CameraConfig, process_info): +def capture_camera(name, config: CameraConfig, shm_frame_count: int, process_info): stop_event = mp.Event() def receiveSignal(signalNumber, frame): @@ -389,6 +395,7 @@ def receiveSignal(signalNumber, frame): camera_watchdog = CameraWatchdog( name, config, + shm_frame_count, frame_queue, process_info["camera_fps"], process_info["skipped_fps"], From 3a09bafb07bcded92c5026ba19fd96bd2c836ec7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 2 Sep 2024 17:58:52 -0600 Subject: [PATCH 11/15] Fix shm count calculation --- frigate/app.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 5023e66218..538c459317 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -610,16 +610,21 @@ def check_shm(self) -> None: min_req_shm += 8 available_shm = total_shm - min_req_shm - cam_average_shm = 0 + cam_total_frame_size = 0 for camera in self.config.cameras.values(): - cam_average_shm += round( - (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, - 1, - ) + if camera.enabled: + cam_total_frame_size += round( + (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, + 1, + ) - self.shm_frame_count = max( - 50, int(available_shm / (cam_average_shm / len(self.config.cameras))) + self.shm_frame_count = min( + 50, int(available_shm / (cam_total_frame_size)) + ) + + logger.info( + f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {self.shm_frame_count} frames for each camera in SHM" ) logger.info( @@ -628,7 +633,7 @@ def check_shm(self) -> None: if self.shm_frame_count < 10: logger.warning( - f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + (cam_average_shm * 10))}MB." + f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size)}MB." ) def init_auth(self) -> None: From 5d1b9836b25d1d62ad83032e01fbd17342880768 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 3 Sep 2024 07:23:32 -0600 Subject: [PATCH 12/15] Catch missing frame --- frigate/app.py | 6 +----- frigate/embeddings/maintainer.py | 8 +++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 538c459317..cdcf01b2d6 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -623,14 +623,10 @@ def check_shm(self) -> None: 50, int(available_shm / (cam_total_frame_size)) ) - logger.info( + logger.debug( f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {self.shm_frame_count} frames for each camera in SHM" ) - logger.info( - f"Calculated {self.shm_frame_count} frames for each camera in SHM" - ) - if self.shm_frame_count < 10: logger.warning( f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size)}MB." diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index aac85c01a6..a60663e7dc 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -78,9 +78,11 @@ def _process_updates(self) -> None: try: frame_id = f"{camera}{data['frame_time']}" yuv_frame = self.frame_manager.get(frame_id, camera_config.frame_shape_yuv) - data["thumbnail"] = self._create_thumbnail(yuv_frame, data["box"]) - self.tracked_events[data["id"]].append(data) - self.frame_manager.close(frame_id) + + if yuv_frame is not None: + data["thumbnail"] = self._create_thumbnail(yuv_frame, data["box"]) + self.tracked_events[data["id"]].append(data) + self.frame_manager.close(frame_id) except FileNotFoundError: pass From da9708f973fd2d2a373c80413ab9ba36ad44471c Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 3 Sep 2024 08:43:58 -0600 Subject: [PATCH 13/15] Formatting --- frigate/app.py | 7 +++---- frigate/output/birdseye.py | 4 +--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index cdcf01b2d6..68e5c872cb 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -615,13 +615,12 @@ def check_shm(self) -> None: for camera in self.config.cameras.values(): if camera.enabled: cam_total_frame_size += round( - (camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576, + (camera.detect.width * camera.detect.height * 1.5 + 270480) + / 1048576, 1, ) - self.shm_frame_count = min( - 50, int(available_shm / (cam_total_frame_size)) - ) + self.shm_frame_count = min(50, int(available_shm / (cam_total_frame_size))) logger.debug( f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {self.shm_frame_count} frames for each camera in SHM" diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 36376ebc53..e22628ecd2 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -362,9 +362,7 @@ def copy_to_position(self, position, camera=None, frame_time=None): ) if frame is None: - logger.debug( - f"Unable to copy frame {camera}{frame_time} to birdseye." - ) + logger.debug(f"Unable to copy frame {camera}{frame_time} to birdseye.") return channel_dims = self.cameras[camera]["channel_dims"] From 6dc7e9a76c61a59960ea3178c83cecd5c1e79c0a Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 3 Sep 2024 08:47:24 -0600 Subject: [PATCH 14/15] Clarify docs --- docs/docs/frigate/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 857c14f198..200b48e60a 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -73,11 +73,11 @@ Users of the Snapcraft build of Docker cannot use storage locations outside your Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**. -The default shm size of **64MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in docker-compose). +The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in docker-compose). The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well. -You can calculate the necessary shm size for each camera with the following formula using the resolution specified for detect: +You can calculate the **minimum** shm size for each camera with the following formula using the resolution specified for detect: ```console # Replace and From bcfd13ff7305c7d859dd2518bc3488612862c5d2 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 3 Sep 2024 10:14:13 -0600 Subject: [PATCH 15/15] Catch none frame in autotracking --- frigate/ptz/autotrack.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 44082a52eb..92c4141a72 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -99,6 +99,10 @@ def motion_estimator(self, detections, frame_time, camera): frame_id, self.camera_config.frame_shape_yuv ) + if yuv_frame is None: + self.coord_transformations = None + return None + frame = cv2.cvtColor(yuv_frame, cv2.COLOR_YUV2GRAY_I420) # mask out detections for better motion estimation