mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Move more things out of FrigateApp (#13897)
* Moved FrigateApp.init_config() into FrigateConfig.load() * Move frigate config loading into main * Store PlusApi in FrigateConfig * Register SIGTERM handler in main * Ensure logging is setup during config parsing * Removed pointless try * Moved config initialization out of FrigateApp * Made FrigateApp.shm_frame_count into a function * Removed log calls from signal handlers python's logging calls are not re-entrant, which caused at least one of these to deadlock randomly. * Reopen stdout/err on process fork This helps avoid deadlocks (https://github.com/python/cpython/issues/91776). * Make mypy happy * Whoops. I might have forgotten to save. Truly an amateur mistake. * Always call FrigateApp.stop()
This commit is contained in:
		
							parent
							
								
									a7ed90f042
								
							
						
					
					
						commit
						dc54981784
					
				| @ -1,10 +1,16 @@ | ||||
| import argparse | ||||
| import faulthandler | ||||
| import logging | ||||
| import signal | ||||
| import sys | ||||
| import threading | ||||
| 
 | ||||
| from flask import cli | ||||
| from pydantic import ValidationError | ||||
| 
 | ||||
| from frigate.app import FrigateApp | ||||
| from frigate.config import FrigateConfig | ||||
| from frigate.log import log_thread | ||||
| 
 | ||||
| 
 | ||||
| def main() -> None: | ||||
| @ -20,8 +26,50 @@ def main() -> None: | ||||
|     threading.current_thread().name = "frigate" | ||||
|     cli.show_server_banner = lambda *x: None | ||||
| 
 | ||||
|     # Make sure we exit cleanly on SIGTERM. | ||||
|     signal.signal(signal.SIGTERM, lambda sig, frame: sys.exit()) | ||||
| 
 | ||||
|     run() | ||||
| 
 | ||||
| 
 | ||||
| @log_thread() | ||||
| def run() -> None: | ||||
|     # Parse the cli arguments. | ||||
|     parser = argparse.ArgumentParser( | ||||
|         prog="Frigate", | ||||
|         description="An NVR with realtime local object detection for IP cameras.", | ||||
|     ) | ||||
|     parser.add_argument("--validate-config", action="store_true") | ||||
|     args = parser.parse_args() | ||||
| 
 | ||||
|     # Load the configuration. | ||||
|     try: | ||||
|         config = FrigateConfig.load() | ||||
|     except ValidationError as e: | ||||
|         print("*************************************************************") | ||||
|         print("*************************************************************") | ||||
|         print("***    Your config file is not valid!                     ***") | ||||
|         print("***    Please check the docs at                           ***") | ||||
|         print("***    https://docs.frigate.video/configuration/          ***") | ||||
|         print("*************************************************************") | ||||
|         print("*************************************************************") | ||||
|         print("***    Config Validation Errors                           ***") | ||||
|         print("*************************************************************") | ||||
|         for error in e.errors(): | ||||
|             location = ".".join(str(item) for item in error["loc"]) | ||||
|             print(f"{location}: {error['msg']}") | ||||
|         print("*************************************************************") | ||||
|         print("***    End Config Validation Errors                       ***") | ||||
|         print("*************************************************************") | ||||
|         sys.exit(1) | ||||
|     if args.validate_config: | ||||
|         print("*************************************************************") | ||||
|         print("*** Your config file is valid.                            ***") | ||||
|         print("*************************************************************") | ||||
|         sys.exit(0) | ||||
| 
 | ||||
|     # Run the main application. | ||||
|     FrigateApp().start() | ||||
|     FrigateApp(config).start() | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|  | ||||
| @ -28,7 +28,6 @@ from frigate.const import CONFIG_DIR | ||||
| from frigate.embeddings import EmbeddingsContext | ||||
| from frigate.events.external import ExternalEventProcessor | ||||
| from frigate.models import Event, Timeline | ||||
| from frigate.plus import PlusApi | ||||
| from frigate.ptz.onvif import OnvifController | ||||
| from frigate.stats.emitter import StatsEmitter | ||||
| from frigate.storage import StorageMaintainer | ||||
| @ -61,7 +60,6 @@ def create_app( | ||||
|     storage_maintainer: StorageMaintainer, | ||||
|     onvif: OnvifController, | ||||
|     external_processor: ExternalEventProcessor, | ||||
|     plus_api: PlusApi, | ||||
|     stats_emitter: StatsEmitter, | ||||
| ): | ||||
|     app = Flask(__name__) | ||||
| @ -89,7 +87,6 @@ def create_app( | ||||
|     app.storage_maintainer = storage_maintainer | ||||
|     app.onvif = onvif | ||||
|     app.external_processor = external_processor | ||||
|     app.plus_api = plus_api | ||||
|     app.camera_error_image = None | ||||
|     app.stats_emitter = stats_emitter | ||||
|     app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None | ||||
| @ -199,7 +196,7 @@ def config(): | ||||
|         for zone_name, zone in config_obj.cameras[camera_name].zones.items(): | ||||
|             camera_dict["zones"][zone_name]["color"] = zone.color | ||||
| 
 | ||||
|     config["plus"] = {"enabled": current_app.plus_api.is_active()} | ||||
|     config["plus"] = {"enabled": current_app.frigate_config.plus_api.is_active()} | ||||
|     config["model"]["colormap"] = config_obj.model.colormap | ||||
| 
 | ||||
|     for detector_config in config["detectors"].values(): | ||||
| @ -362,7 +359,7 @@ def config_set(): | ||||
| 
 | ||||
|     if json.get("requires_restart", 1) == 0: | ||||
|         current_app.frigate_config = FrigateConfig.parse_object( | ||||
|             config_obj, plus_api=current_app.plus_api | ||||
|             config_obj, plus_api=current_app.frigate_config.plus_api | ||||
|         ) | ||||
| 
 | ||||
|     return make_response( | ||||
|  | ||||
| @ -612,7 +612,7 @@ def set_retain(id): | ||||
| 
 | ||||
| @EventBp.route("/events/<id>/plus", methods=("POST",)) | ||||
| def send_to_plus(id): | ||||
|     if not current_app.plus_api.is_active(): | ||||
|     if not current_app.frigate_config.plus_api.is_active(): | ||||
|         message = "PLUS_API_KEY environment variable is not set" | ||||
|         logger.error(message) | ||||
|         return make_response( | ||||
| @ -680,7 +680,7 @@ def send_to_plus(id): | ||||
|         ) | ||||
| 
 | ||||
|     try: | ||||
|         plus_id = current_app.plus_api.upload_image(image, event.camera) | ||||
|         plus_id = current_app.frigate_config.plus_api.upload_image(image, event.camera) | ||||
|     except Exception as ex: | ||||
|         logger.exception(ex) | ||||
|         return make_response( | ||||
| @ -696,7 +696,7 @@ def send_to_plus(id): | ||||
|         box = event.data["box"] | ||||
| 
 | ||||
|         try: | ||||
|             current_app.plus_api.add_annotation( | ||||
|             current_app.frigate_config.plus_api.add_annotation( | ||||
|                 event.plus_id, | ||||
|                 box, | ||||
|                 event.label, | ||||
| @ -720,7 +720,7 @@ def send_to_plus(id): | ||||
| 
 | ||||
| @EventBp.route("/events/<id>/false_positive", methods=("PUT",)) | ||||
| def false_positive(id): | ||||
|     if not current_app.plus_api.is_active(): | ||||
|     if not current_app.frigate_config.plus_api.is_active(): | ||||
|         message = "PLUS_API_KEY environment variable is not set" | ||||
|         logger.error(message) | ||||
|         return make_response( | ||||
| @ -769,7 +769,7 @@ def false_positive(id): | ||||
|     ) | ||||
| 
 | ||||
|     try: | ||||
|         current_app.plus_api.add_false_positive( | ||||
|         current_app.frigate_config.plus_api.add_false_positive( | ||||
|             event.plus_id, | ||||
|             region, | ||||
|             box, | ||||
|  | ||||
| @ -294,7 +294,7 @@ def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str): | ||||
|             ) | ||||
| 
 | ||||
|         nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) | ||||
|         current_app.plus_api.upload_image(nd, camera_name) | ||||
|         current_app.frigate_config.plus_api.upload_image(nd, camera_name) | ||||
| 
 | ||||
|         return make_response( | ||||
|             jsonify( | ||||
|  | ||||
							
								
								
									
										151
									
								
								frigate/app.py
									
									
									
									
									
								
							
							
						
						
									
										151
									
								
								frigate/app.py
									
									
									
									
									
								
							| @ -1,23 +1,17 @@ | ||||
| import argparse | ||||
| import datetime | ||||
| import logging | ||||
| import multiprocessing as mp | ||||
| import os | ||||
| import secrets | ||||
| import shutil | ||||
| import signal | ||||
| import sys | ||||
| import traceback | ||||
| from multiprocessing import Queue | ||||
| from multiprocessing.synchronize import Event as MpEvent | ||||
| from types import FrameType | ||||
| from typing import Optional | ||||
| from typing import Any | ||||
| 
 | ||||
| import psutil | ||||
| from peewee_migrate import Router | ||||
| from playhouse.sqlite_ext import SqliteExtDatabase | ||||
| from playhouse.sqliteq import SqliteQueueDatabase | ||||
| from pydantic import ValidationError | ||||
| 
 | ||||
| from frigate.api.app import create_app | ||||
| from frigate.api.auth import hash_password | ||||
| @ -28,7 +22,6 @@ from frigate.comms.mqtt import MqttClient | ||||
| from frigate.comms.webpush import WebPushClient | ||||
| from frigate.comms.ws import WebSocketClient | ||||
| from frigate.comms.zmq_proxy import ZmqProxy | ||||
| from frigate.config import FrigateConfig | ||||
| from frigate.const import ( | ||||
|     CACHE_DIR, | ||||
|     CLIPS_DIR, | ||||
| @ -43,7 +36,6 @@ from frigate.events.audio import listen_to_audio | ||||
| from frigate.events.cleanup import EventCleanup | ||||
| from frigate.events.external import ExternalEventProcessor | ||||
| from frigate.events.maintainer import EventProcessor | ||||
| from frigate.log import log_thread | ||||
| from frigate.models import ( | ||||
|     Event, | ||||
|     Export, | ||||
| @ -58,7 +50,6 @@ from frigate.models import ( | ||||
| from frigate.object_detection import ObjectDetectProcess | ||||
| from frigate.object_processing import TrackedObjectProcessor | ||||
| from frigate.output.output import output_frames | ||||
| from frigate.plus import PlusApi | ||||
| from frigate.ptz.autotrack import PtzAutoTrackerThread | ||||
| from frigate.ptz.onvif import OnvifController | ||||
| from frigate.record.cleanup import RecordingCleanup | ||||
| @ -70,8 +61,7 @@ from frigate.stats.util import stats_init | ||||
| from frigate.storage import StorageMaintainer | ||||
| from frigate.timeline import TimelineProcessor | ||||
| from frigate.types import CameraMetricsTypes, PTZMetricsTypes | ||||
| from frigate.util.builtin import empty_and_close_queue, save_default_config | ||||
| from frigate.util.config import migrate_frigate_config | ||||
| from frigate.util.builtin import empty_and_close_queue | ||||
| from frigate.util.object import get_camera_regions_grid | ||||
| from frigate.version import VERSION | ||||
| from frigate.video import capture_camera, track_camera | ||||
| @ -81,22 +71,19 @@ logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class FrigateApp: | ||||
|     def __init__(self) -> None: | ||||
|     # TODO: Fix FrigateConfig usage, so we can properly annotate it here without mypy erroring out. | ||||
|     def __init__(self, config: Any) -> None: | ||||
|         self.stop_event: MpEvent = mp.Event() | ||||
|         self.detection_queue: Queue = mp.Queue() | ||||
|         self.detectors: dict[str, ObjectDetectProcess] = {} | ||||
|         self.detection_out_events: dict[str, MpEvent] = {} | ||||
|         self.detection_shms: list[mp.shared_memory.SharedMemory] = [] | ||||
|         self.log_queue: Queue = mp.Queue() | ||||
|         self.plus_api = PlusApi() | ||||
|         self.camera_metrics: dict[str, CameraMetricsTypes] = {} | ||||
|         self.ptz_metrics: dict[str, PTZMetricsTypes] = {} | ||||
|         self.processes: dict[str, int] = {} | ||||
|         self.region_grids: dict[str, list[list[dict[str, int]]]] = {} | ||||
| 
 | ||||
|     def set_environment_vars(self) -> None: | ||||
|         for key, value in self.config.environment_vars.items(): | ||||
|             os.environ[key] = value | ||||
|         self.config = config | ||||
| 
 | ||||
|     def ensure_dirs(self) -> None: | ||||
|         for d in [ | ||||
| @ -113,24 +100,7 @@ class FrigateApp: | ||||
|             else: | ||||
|                 logger.debug(f"Skipping directory: {d}") | ||||
| 
 | ||||
|     def init_config(self) -> None: | ||||
|         config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") | ||||
| 
 | ||||
|         # Check if we can use .yaml instead of .yml | ||||
|         config_file_yaml = config_file.replace(".yml", ".yaml") | ||||
|         if os.path.isfile(config_file_yaml): | ||||
|             config_file = config_file_yaml | ||||
| 
 | ||||
|         if not os.path.isfile(config_file): | ||||
|             print("No config file found, saving default config") | ||||
|             config_file = config_file_yaml | ||||
|             save_default_config(config_file) | ||||
| 
 | ||||
|         # check if the config file needs to be migrated | ||||
|         migrate_frigate_config(config_file) | ||||
| 
 | ||||
|         self.config = FrigateConfig.parse_file(config_file, plus_api=self.plus_api) | ||||
| 
 | ||||
|     def init_camera_metrics(self) -> None: | ||||
|         for camera_name in self.config.cameras.keys(): | ||||
|             # create camera_metrics | ||||
|             self.camera_metrics[camera_name] = { | ||||
| @ -190,17 +160,6 @@ class FrigateApp: | ||||
|             } | ||||
|             self.ptz_metrics[camera_name]["ptz_motor_stopped"].set() | ||||
| 
 | ||||
|     def set_log_levels(self) -> None: | ||||
|         logging.getLogger().setLevel(self.config.logger.default.value.upper()) | ||||
|         for log, level in self.config.logger.logs.items(): | ||||
|             logging.getLogger(log).setLevel(level.value.upper()) | ||||
| 
 | ||||
|         if "werkzeug" not in self.config.logger.logs: | ||||
|             logging.getLogger("werkzeug").setLevel("ERROR") | ||||
| 
 | ||||
|         if "ws4py" not in self.config.logger.logs: | ||||
|             logging.getLogger("ws4py").setLevel("ERROR") | ||||
| 
 | ||||
|     def init_queues(self) -> None: | ||||
|         # Queue for cameras to push tracked objects to | ||||
|         self.detected_frames_queue: Queue = mp.Queue( | ||||
| @ -374,19 +333,6 @@ class FrigateApp: | ||||
|         self.inter_config_updater = ConfigPublisher() | ||||
|         self.inter_zmq_proxy = ZmqProxy() | ||||
| 
 | ||||
|     def init_web_server(self) -> None: | ||||
|         self.flask_app = create_app( | ||||
|             self.config, | ||||
|             self.db, | ||||
|             self.embeddings, | ||||
|             self.detected_frames_processor, | ||||
|             self.storage_maintainer, | ||||
|             self.onvif_controller, | ||||
|             self.external_event_processor, | ||||
|             self.plus_api, | ||||
|             self.stats_emitter, | ||||
|         ) | ||||
| 
 | ||||
|     def init_onvif(self) -> None: | ||||
|         self.onvif_controller = OnvifController(self.config, self.ptz_metrics) | ||||
| 
 | ||||
| @ -527,7 +473,7 @@ class FrigateApp: | ||||
|             capture_process = mp.Process( | ||||
|                 target=capture_camera, | ||||
|                 name=f"camera_capture:{name}", | ||||
|                 args=(name, config, self.shm_frame_count, 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 | ||||
| @ -590,7 +536,7 @@ class FrigateApp: | ||||
|         self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event) | ||||
|         self.frigate_watchdog.start() | ||||
| 
 | ||||
|     def check_shm(self) -> None: | ||||
|     def shm_frame_count(self) -> int: | ||||
|         total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) | ||||
| 
 | ||||
|         # required for log files + nginx cache | ||||
| @ -610,17 +556,19 @@ class FrigateApp: | ||||
|                     1, | ||||
|                 ) | ||||
| 
 | ||||
|         self.shm_frame_count = min(50, int(available_shm / (cam_total_frame_size))) | ||||
|         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" | ||||
|             f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {shm_frame_count} frames for each camera in SHM" | ||||
|         ) | ||||
| 
 | ||||
|         if self.shm_frame_count < 10: | ||||
|         if 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." | ||||
|             ) | ||||
| 
 | ||||
|         return shm_frame_count | ||||
| 
 | ||||
|     def init_auth(self) -> None: | ||||
|         if self.config.auth.enabled: | ||||
|             if User.select().count() == 0: | ||||
| @ -657,49 +605,15 @@ class FrigateApp: | ||||
|                 logger.info("********************************************************") | ||||
|                 logger.info("********************************************************") | ||||
| 
 | ||||
|     @log_thread() | ||||
|     def start(self) -> None: | ||||
|         parser = argparse.ArgumentParser( | ||||
|             prog="Frigate", | ||||
|             description="An NVR with realtime local object detection for IP cameras.", | ||||
|         ) | ||||
|         parser.add_argument("--validate-config", action="store_true") | ||||
|         args = parser.parse_args() | ||||
| 
 | ||||
|         logger.info(f"Starting Frigate ({VERSION})") | ||||
| 
 | ||||
|         try: | ||||
|         # Ensure global state. | ||||
|         self.ensure_dirs() | ||||
|             try: | ||||
|                 self.init_config() | ||||
|             except Exception as e: | ||||
|                 print("*************************************************************") | ||||
|                 print("*************************************************************") | ||||
|                 print("***    Your config file is not valid!                     ***") | ||||
|                 print("***    Please check the docs at                           ***") | ||||
|                 print("***    https://docs.frigate.video/configuration/index     ***") | ||||
|                 print("*************************************************************") | ||||
|                 print("*************************************************************") | ||||
|                 print("***    Config Validation Errors                           ***") | ||||
|                 print("*************************************************************") | ||||
|                 if isinstance(e, ValidationError): | ||||
|                     for error in e.errors(): | ||||
|                         location = ".".join(str(item) for item in error["loc"]) | ||||
|                         print(f"{location}: {error['msg']}") | ||||
|                 else: | ||||
|                     print(e) | ||||
|                     print(traceback.format_exc()) | ||||
|                 print("*************************************************************") | ||||
|                 print("***    End Config Validation Errors                       ***") | ||||
|                 print("*************************************************************") | ||||
|                 sys.exit(1) | ||||
|             if args.validate_config: | ||||
|                 print("*************************************************************") | ||||
|                 print("*** Your config file is valid.                            ***") | ||||
|                 print("*************************************************************") | ||||
|                 sys.exit(0) | ||||
|             self.set_environment_vars() | ||||
|             self.set_log_levels() | ||||
|         self.config.install() | ||||
| 
 | ||||
|         # Start frigate services. | ||||
|         self.init_camera_metrics() | ||||
|         self.init_queues() | ||||
|         self.init_database() | ||||
|         self.init_onvif() | ||||
| @ -711,42 +625,37 @@ class FrigateApp: | ||||
|         self.check_db_data_migrations() | ||||
|         self.init_inter_process_communicator() | ||||
|         self.init_dispatcher() | ||||
|         except Exception as e: | ||||
|             print(e) | ||||
|             sys.exit(1) | ||||
|         self.start_detectors() | ||||
|         self.start_video_output_processor() | ||||
|         self.start_ptz_autotracker() | ||||
|         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() | ||||
|         self.init_external_event_processor() | ||||
|         self.start_stats_emitter() | ||||
|         self.init_web_server() | ||||
|         self.start_timeline_processor() | ||||
|         self.start_event_processor() | ||||
|         self.start_event_cleanup() | ||||
|         self.start_record_cleanup() | ||||
|         self.start_watchdog() | ||||
| 
 | ||||
|         self.init_auth() | ||||
| 
 | ||||
|         # Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT | ||||
|         def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: | ||||
|             os.kill(os.getpid(), signal.SIGINT) | ||||
| 
 | ||||
|         signal.signal(signal.SIGTERM, receiveSignal) | ||||
| 
 | ||||
|         try: | ||||
|             self.flask_app.run(host="127.0.0.1", port=5001, debug=False, threaded=True) | ||||
|         except KeyboardInterrupt: | ||||
|             pass | ||||
| 
 | ||||
|         logger.info("Flask has exited...") | ||||
| 
 | ||||
|             create_app( | ||||
|                 self.config, | ||||
|                 self.db, | ||||
|                 self.embeddings, | ||||
|                 self.detected_frames_processor, | ||||
|                 self.storage_maintainer, | ||||
|                 self.onvif_controller, | ||||
|                 self.external_event_processor, | ||||
|                 self.stats_emitter, | ||||
|             ).run(host="127.0.0.1", port=5001, debug=False, threaded=True) | ||||
|         finally: | ||||
|             self.stop() | ||||
| 
 | ||||
|     def stop(self) -> None: | ||||
|  | ||||
| @ -45,13 +45,18 @@ from frigate.ffmpeg_presets import ( | ||||
|     parse_preset_input, | ||||
|     parse_preset_output_record, | ||||
| ) | ||||
| from frigate.plus import PlusApi | ||||
| from frigate.util.builtin import ( | ||||
|     deep_merge, | ||||
|     escape_special_characters, | ||||
|     generate_color_palette, | ||||
|     get_ffmpeg_arg_list, | ||||
| ) | ||||
| from frigate.util.config import StreamInfoRetriever, get_relative_coordinates | ||||
| from frigate.util.config import ( | ||||
|     StreamInfoRetriever, | ||||
|     get_relative_coordinates, | ||||
|     migrate_frigate_config, | ||||
| ) | ||||
| from frigate.util.image import create_mask | ||||
| from frigate.util.services import auto_detect_hwaccel | ||||
| 
 | ||||
| @ -59,6 +64,25 @@ logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| yaml = YAML() | ||||
| 
 | ||||
| DEFAULT_CONFIG_FILES = ["/config/config.yaml", "/config/config.yml"] | ||||
| DEFAULT_CONFIG = """ | ||||
| mqtt: | ||||
|   enabled: False | ||||
| 
 | ||||
| cameras: | ||||
|   name_of_your_camera: # <------ Name the camera | ||||
|     enabled: True | ||||
|     ffmpeg: | ||||
|       inputs: | ||||
|         - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection | ||||
|           roles: | ||||
|             - detect | ||||
|     detect: | ||||
|       enabled: False # <---- disable detection until you have a working camera feed | ||||
|       width: 1280 | ||||
|       height: 720 | ||||
| """ | ||||
| 
 | ||||
| # TODO: Identify what the default format to display timestamps is | ||||
| DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S" | ||||
| # German Style: | ||||
| @ -1272,6 +1296,19 @@ class LoggerConfig(FrigateBaseModel): | ||||
|         default_factory=dict, title="Log level for specified processes." | ||||
|     ) | ||||
| 
 | ||||
|     def install(self): | ||||
|         """Install global logging state.""" | ||||
|         logging.getLogger().setLevel(self.default.value.upper()) | ||||
| 
 | ||||
|         log_levels = { | ||||
|             "werkzeug": LogLevelEnum.error, | ||||
|             "ws4py": LogLevelEnum.error, | ||||
|             **self.logs, | ||||
|         } | ||||
| 
 | ||||
|         for log, level in log_levels.items(): | ||||
|             logging.getLogger(log).setLevel(level.value.upper()) | ||||
| 
 | ||||
| 
 | ||||
| class CameraGroupConfig(FrigateBaseModel): | ||||
|     """Represents a group of cameras.""" | ||||
| @ -1492,11 +1529,22 @@ class FrigateConfig(FrigateBaseModel): | ||||
|     ) | ||||
|     version: Optional[str] = Field(default=None, title="Current config version.") | ||||
| 
 | ||||
|     _plus_api: PlusApi | ||||
| 
 | ||||
|     @property | ||||
|     def plus_api(self) -> PlusApi: | ||||
|         return self._plus_api | ||||
| 
 | ||||
|     @model_validator(mode="after") | ||||
|     def post_validation(self, info: ValidationInfo) -> Self: | ||||
|         plus_api = None | ||||
|         # Load plus api from context, if possible. | ||||
|         self._plus_api = None | ||||
|         if isinstance(info.context, dict): | ||||
|             plus_api = info.context.get("plus_api") | ||||
|             self._plus_api = info.context.get("plus_api") | ||||
| 
 | ||||
|         # Ensure self._plus_api is set, if no explicit value is provided. | ||||
|         if self._plus_api is None: | ||||
|             self._plus_api = PlusApi() | ||||
| 
 | ||||
|         # set notifications state | ||||
|         self.notifications.enabled_in_config = self.notifications.enabled | ||||
| @ -1691,7 +1739,7 @@ class FrigateConfig(FrigateBaseModel): | ||||
|             enabled_labels.update(camera.objects.track) | ||||
| 
 | ||||
|         self.model.create_colormap(sorted(enabled_labels)) | ||||
|         self.model.check_and_load_plus_model(plus_api) | ||||
|         self.model.check_and_load_plus_model(self.plus_api) | ||||
| 
 | ||||
|         for key, detector in self.detectors.items(): | ||||
|             adapter = TypeAdapter(DetectorConfig) | ||||
| @ -1726,7 +1774,7 @@ class FrigateConfig(FrigateBaseModel): | ||||
| 
 | ||||
|             detector_config.model = ModelConfig.model_validate(merged_model) | ||||
|             detector_config.model.check_and_load_plus_model( | ||||
|                 plus_api, detector_config.type | ||||
|                 self.plus_api, detector_config.type | ||||
|             ) | ||||
|             detector_config.model.compute_model_hash() | ||||
|             self.detectors[key] = detector_config | ||||
| @ -1743,8 +1791,38 @@ class FrigateConfig(FrigateBaseModel): | ||||
|         return v | ||||
| 
 | ||||
|     @classmethod | ||||
|     def parse_file(cls, config_path, **kwargs): | ||||
|         with open(config_path) as f: | ||||
|     def load(cls, **kwargs): | ||||
|         config_path = os.environ.get("CONFIG_FILE") | ||||
| 
 | ||||
|         # No explicit configuration file, try to find one in the default paths. | ||||
|         if config_path is None: | ||||
|             for path in DEFAULT_CONFIG_FILES: | ||||
|                 if os.path.isfile(path): | ||||
|                     config_path = path | ||||
|                     break | ||||
| 
 | ||||
|         # No configuration file found, create one. | ||||
|         new_config = False | ||||
|         if config_path is None: | ||||
|             logger.info("No config file found, saving default config") | ||||
|             config_path = DEFAULT_CONFIG_FILES[-1] | ||||
|             new_config = True | ||||
|         else: | ||||
|             # Check if the config file needs to be migrated. | ||||
|             migrate_frigate_config(config_path) | ||||
| 
 | ||||
|         # Finally, load the resulting configuration file. | ||||
|         with open(config_path, "a+") as f: | ||||
|             # Only write the default config if the opened file is non-empty. This can happen as | ||||
|             # a race condition. It's extremely unlikely, but eh. Might as well check it. | ||||
|             if new_config and f.tell() == 0: | ||||
|                 f.write(DEFAULT_CONFIG) | ||||
|                 logger.info( | ||||
|                     "Created default config file, see the getting started docs \ | ||||
|                     for configuration https://docs.frigate.video/guides/getting_started" | ||||
|                 ) | ||||
| 
 | ||||
|             f.seek(0) | ||||
|             return FrigateConfig.parse(f, **kwargs) | ||||
| 
 | ||||
|     @classmethod | ||||
| @ -1775,10 +1853,17 @@ class FrigateConfig(FrigateBaseModel): | ||||
|         # Validate and return the config dict. | ||||
|         return cls.parse_object(config, **context) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def parse_object(cls, obj: Any, **context): | ||||
|         return cls.model_validate(obj, context=context) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def parse_yaml(cls, config_yaml, **context): | ||||
|         return cls.parse(config_yaml, is_json=False, **context) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def parse_object(cls, obj: Any, *, plus_api: Optional[PlusApi] = None): | ||||
|         return cls.model_validate(obj, context={"plus_api": plus_api}) | ||||
| 
 | ||||
|     def install(self): | ||||
|         """Install global state from the config.""" | ||||
|         self.logger.install() | ||||
| 
 | ||||
|         for key, value in self.environment_vars.items(): | ||||
|             os.environ[key] = value | ||||
|  | ||||
| @ -76,16 +76,8 @@ def listen_to_audio( | ||||
|     stop_event = mp.Event() | ||||
|     audio_threads: list[threading.Thread] = [] | ||||
| 
 | ||||
|     def exit_process() -> None: | ||||
|         for thread in audio_threads: | ||||
|             thread.join() | ||||
| 
 | ||||
|         logger.info("Exiting audio detector...") | ||||
| 
 | ||||
|     def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: | ||||
|         logger.debug(f"Audio process received signal {signalNumber}") | ||||
|         stop_event.set() | ||||
|         exit_process() | ||||
| 
 | ||||
|     signal.signal(signal.SIGTERM, receiveSignal) | ||||
|     signal.signal(signal.SIGINT, receiveSignal) | ||||
| @ -104,6 +96,11 @@ def listen_to_audio( | ||||
|             audio_threads.append(audio) | ||||
|             audio.start() | ||||
| 
 | ||||
|     for thread in audio_threads: | ||||
|         thread.join() | ||||
| 
 | ||||
|     logger.info("Exiting audio detector...") | ||||
| 
 | ||||
| 
 | ||||
| class AudioTfl: | ||||
|     def __init__(self, stop_event: mp.Event, num_threads=2): | ||||
|  | ||||
| @ -2,6 +2,7 @@ import atexit | ||||
| import logging | ||||
| import multiprocessing as mp | ||||
| import os | ||||
| import sys | ||||
| import threading | ||||
| from collections import deque | ||||
| from contextlib import AbstractContextManager, ContextDecorator | ||||
| @ -68,6 +69,19 @@ class log_thread(AbstractContextManager, ContextDecorator): | ||||
|         self._stop_thread() | ||||
| 
 | ||||
| 
 | ||||
| # When a multiprocessing.Process exits, python tries to flush stdout and stderr. However, if the | ||||
| # process is created after a thread (for example a logging thread) is created and the process fork | ||||
| # happens while an internal lock is held, the stdout/err flush can cause a deadlock. | ||||
| # | ||||
| # https://github.com/python/cpython/issues/91776 | ||||
| def reopen_std_streams() -> None: | ||||
|     sys.stdout = os.fdopen(1, "w") | ||||
|     sys.stderr = os.fdopen(2, "w") | ||||
| 
 | ||||
| 
 | ||||
| os.register_at_fork(after_in_child=reopen_std_streams) | ||||
| 
 | ||||
| 
 | ||||
| # based on https://codereview.stackexchange.com/a/17959 | ||||
| class LogPipe(threading.Thread): | ||||
|     def __init__(self, log_name: str): | ||||
|  | ||||
| @ -92,7 +92,6 @@ def run_detector( | ||||
|     stop_event = mp.Event() | ||||
| 
 | ||||
|     def receiveSignal(signalNumber, frame): | ||||
|         logger.info("Signal to exit detection process...") | ||||
|         stop_event.set() | ||||
| 
 | ||||
|     signal.signal(signal.SIGTERM, receiveSignal) | ||||
|  | ||||
| @ -38,7 +38,6 @@ def output_frames( | ||||
|     stop_event = mp.Event() | ||||
| 
 | ||||
|     def receiveSignal(signalNumber, frame): | ||||
|         logger.debug(f"Output frames process received signal {signalNumber}") | ||||
|         stop_event.set() | ||||
| 
 | ||||
|     signal.signal(signal.SIGTERM, receiveSignal) | ||||
|  | ||||
| @ -22,7 +22,6 @@ def manage_recordings(config: FrigateConfig) -> None: | ||||
|     stop_event = mp.Event() | ||||
| 
 | ||||
|     def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: | ||||
|         logger.debug(f"Recording manager process received signal {signalNumber}") | ||||
|         stop_event.set() | ||||
| 
 | ||||
|     signal.signal(signal.SIGTERM, receiveSignal) | ||||
|  | ||||
| @ -20,7 +20,6 @@ def manage_review_segments(config: FrigateConfig) -> None: | ||||
|     stop_event = mp.Event() | ||||
| 
 | ||||
|     def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: | ||||
|         logger.debug(f"Manage review segments process received signal {signalNumber}") | ||||
|         stop_event.set() | ||||
| 
 | ||||
|     signal.signal(signal.SIGTERM, receiveSignal) | ||||
|  | ||||
| @ -13,7 +13,6 @@ from playhouse.sqliteq import SqliteQueueDatabase | ||||
| from frigate.api.app import create_app | ||||
| from frigate.config import FrigateConfig | ||||
| from frigate.models import Event, Recordings | ||||
| from frigate.plus import PlusApi | ||||
| from frigate.stats.emitter import StatsEmitter | ||||
| from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS | ||||
| 
 | ||||
| @ -121,7 +120,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -158,7 +156,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -180,7 +177,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -201,7 +197,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -224,7 +219,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -251,7 +245,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         morning_id = "123456.random" | ||||
| @ -290,7 +283,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -326,7 +318,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -352,7 +343,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
| 
 | ||||
| @ -370,7 +360,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             None, | ||||
|         ) | ||||
|         id = "123456.random" | ||||
| @ -392,7 +381,6 @@ class TestHttp(unittest.TestCase): | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             PlusApi(), | ||||
|             stats, | ||||
|         ) | ||||
| 
 | ||||
|  | ||||
| @ -258,37 +258,6 @@ def find_by_key(dictionary, target_key): | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| def save_default_config(location: str) -> None: | ||||
|     try: | ||||
|         with open(location, "w") as f: | ||||
|             f.write( | ||||
|                 """ | ||||
| mqtt: | ||||
|   enabled: False | ||||
| 
 | ||||
| cameras: | ||||
|   name_of_your_camera: # <------ Name the camera | ||||
|     enabled: True | ||||
|     ffmpeg: | ||||
|       inputs: | ||||
|         - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection | ||||
|           roles: | ||||
|             - detect | ||||
|     detect: | ||||
|       enabled: False # <---- disable detection until you have a working camera feed | ||||
|       width: 1280 | ||||
|       height: 720 | ||||
|                     """ | ||||
|             ) | ||||
|     except PermissionError: | ||||
|         logger.error("Unable to write default config to /config") | ||||
|         return | ||||
| 
 | ||||
|     logger.info( | ||||
|         "Created default config file, see the getting started docs for configuration https://docs.frigate.video/guides/getting_started" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def get_tomorrow_at_time(hour: int) -> datetime.datetime: | ||||
|     """Returns the datetime of the following day at 2am.""" | ||||
|     try: | ||||
|  | ||||
| @ -390,7 +390,6 @@ def capture_camera(name, config: CameraConfig, shm_frame_count: int, process_inf | ||||
|     stop_event = mp.Event() | ||||
| 
 | ||||
|     def receiveSignal(signalNumber, frame): | ||||
|         logger.debug(f"Capture camera received signal {signalNumber}") | ||||
|         stop_event.set() | ||||
| 
 | ||||
|     signal.signal(signal.SIGTERM, receiveSignal) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user