diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index 20d2fb9d7..814d6ef8d 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -18,12 +18,10 @@ peewee_migrate == 1.13.* psutil == 5.9.* pydantic == 2.8.* git+https://github.com/fbcotter/py3nvml#egg=py3nvml -PyYAML == 6.0.* pytz == 2024.1 pyzmq == 26.2.* ruamel.yaml == 0.18.* tzlocal == 5.2 -types-PyYAML == 6.0.* requests == 2.32.* types-requests == 2.32.* scipy == 1.13.* diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index a9abbe1d1..ae2c0128e 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -6,7 +6,7 @@ import shutil import sys from pathlib import Path -import yaml +from ruamel.yaml import YAML sys.path.insert(0, "/opt/frigate") from frigate.const import ( @@ -18,6 +18,7 @@ from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode sys.path.remove("/opt/frigate") +yaml = YAML() FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} # read docker secret files as env vars too @@ -40,7 +41,7 @@ try: raw_config = f.read() if config_file.endswith((".yaml", ".yml")): - config: dict[str, any] = yaml.safe_load(raw_config) + config: dict[str, any] = yaml.load(raw_config) elif config_file.endswith(".json"): config: dict[str, any] = json.loads(raw_config) except FileNotFoundError: diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index ebfe6a210..75527bf53 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -104,6 +104,8 @@ http { add_header Cache-Control "no-store"; expires off; + + keepalive_disable safari; } location /stream/ { diff --git a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py b/docker/main/rootfs/usr/local/nginx/get_tls_settings.py index a96a829df..f1a4c85de 100644 --- a/docker/main/rootfs/usr/local/nginx/get_tls_settings.py +++ b/docker/main/rootfs/usr/local/nginx/get_tls_settings.py @@ -3,7 +3,9 @@ import json import os -import yaml +from ruamel.yaml import YAML + +yaml = YAML() config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") @@ -17,7 +19,7 @@ try: raw_config = f.read() if config_file.endswith((".yaml", ".yml")): - config: dict[str, any] = yaml.safe_load(raw_config) + config: dict[str, any] = yaml.load(raw_config) elif config_file.endswith(".json"): config: dict[str, any] = json.loads(raw_config) except FileNotFoundError: diff --git a/docker/main/rootfs/usr/local/semantic_search/get_search_settings.py b/docker/main/rootfs/usr/local/semantic_search/get_search_settings.py index e4ec4ea1c..ec3c9c1fa 100644 --- a/docker/main/rootfs/usr/local/semantic_search/get_search_settings.py +++ b/docker/main/rootfs/usr/local/semantic_search/get_search_settings.py @@ -3,7 +3,9 @@ import json import os -import yaml +from ruamel.yaml import YAML + +yaml = YAML() config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") @@ -17,7 +19,7 @@ try: raw_config = f.read() if config_file.endswith((".yaml", ".yml")): - config: dict[str, any] = yaml.safe_load(raw_config) + config: dict[str, any] = yaml.load(raw_config) elif config_file.endswith(".json"): config: dict[str, any] = json.loads(raw_config) except FileNotFoundError: diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index 174d7a572..c500ff78e 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -597,6 +597,8 @@ $ cat /sys/kernel/debug/rknpu/load This detector is available for use with Hailo-8 AI Acceleration Module. +See the [installation docs](../frigate/installation.md#hailo-8l) for information on configuring the hailo8. + ### Configuration ```yaml diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 200b48e60..5fa09f13d 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -112,8 +112,8 @@ For other installations, follow these steps for installation: 1. Install the driver from the [Hailo GitHub repository](https://github.com/hailo-ai/hailort-drivers). A convenient script for Linux is available to clone the repository, build the driver, and install it. 2. Copy or download [this script](https://github.com/blakeblackshear/frigate/blob/41c9b13d2fffce508b32dfc971fa529b49295fbd/docker/hailo8l/user_installation.sh). -3. Ensure it has execution permissions with `sudo chmod +x install_hailo8l_driver.sh` -4. Run the script with `./install_hailo8l_driver.sh` +3. Ensure it has execution permissions with `sudo chmod +x user_installation.sh` +4. Run the script with `./user_installation.sh` #### Setup diff --git a/frigate/__main__.py b/frigate/__main__.py index 29db356ea..ccd2594e2 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -1,8 +1,15 @@ +import argparse import faulthandler import logging +import signal +import sys import threading +from pydantic import ValidationError + from frigate.app import FrigateApp +from frigate.config import FrigateConfig +from frigate.log import log_thread def main() -> None: @@ -17,8 +24,50 @@ def main() -> None: threading.current_thread().name = "frigate" + # 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__": diff --git a/frigate/api/app.py b/frigate/api/app.py index 9b5bec0e1..8ffcc789b 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -129,7 +129,7 @@ def config(request: Request): for zone_name, zone in config_obj.cameras[camera_name].zones.items(): camera_dict["zones"][zone_name]["color"] = zone.color - config["plus"] = {"enabled": request.app.plus_api.is_active()} + config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()} config["model"]["colormap"] = config_obj.model.colormap for detector_config in config["detectors"].values(): @@ -289,7 +289,7 @@ def config_set(request: Request, body: AppConfigSetBody): if body.requires_restart == 0: request.app.frigate_config = FrigateConfig.parse_object( - config_obj, request.app.plus_api + config_obj, request.app.frigate_config.plus_api ) return JSONResponse( diff --git a/frigate/api/event.py b/frigate/api/event.py index 750fca6d3..8c56a465d 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -617,7 +617,7 @@ def set_retain(event_id: str): @router.post("/events/{event_id}/plus") def send_to_plus(request: Request, event_id: str): - if not request.app.plus_api.is_active(): + if not request.app.frigate_config.plus_api.is_active(): message = "PLUS_API_KEY environment variable is not set" logger.error(message) return JSONResponse( @@ -689,7 +689,7 @@ def send_to_plus(request: Request, event_id: str): ) try: - plus_id = request.app.plus_api.upload_image(image, event.camera) + plus_id = request.app.frigate_config.plus_api.upload_image(image, event.camera) except Exception as ex: logger.exception(ex) return JSONResponse( @@ -705,7 +705,7 @@ def send_to_plus(request: Request, event_id: str): box = event.data["box"] try: - request.app.plus_api.add_annotation( + request.app.frigate_config.plus_api.add_annotation( event.plus_id, box, event.label, @@ -731,7 +731,7 @@ def send_to_plus(request: Request, event_id: str): @router.put("/events/{event_id}/false_positive") def false_positive(request: Request, event_id: str): - if not request.app.plus_api.is_active(): + if not request.app.frigate_config.plus_api.is_active(): message = "PLUS_API_KEY environment variable is not set" logger.error(message) return JSONResponse( @@ -786,7 +786,7 @@ def false_positive(request: Request, event_id: str): ) try: - request.app.plus_api.add_false_positive( + request.app.frigate_config.plus_api.add_false_positive( event.plus_id, region, box, diff --git a/frigate/api/media.py b/frigate/api/media.py index 1627c9cd9..3a57780d5 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -310,7 +310,7 @@ def submit_recording_snapshot_to_plus( ) nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) - request.app.plus_api.upload_image(nd, camera_name) + request.app.frigate_config.plus_api.upload_image(nd, camera_name) return JSONResponse( content={ diff --git a/frigate/app.py b/frigate/app.py index a2c88cdde..86119ebd8 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -1,21 +1,18 @@ -import argparse import datetime import logging import multiprocessing as mp import os import secrets import shutil -import sys -import traceback from multiprocessing import Queue from multiprocessing.synchronize import Event as MpEvent +from typing import Any import psutil import uvicorn from peewee_migrate import Router from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqliteq import SqliteQueueDatabase -from pydantic import ValidationError from frigate.api.auth import hash_password from frigate.api.fastapi_app import create_fastapi_app @@ -26,7 +23,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, @@ -41,7 +37,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, @@ -56,7 +51,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 @@ -68,8 +62,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 @@ -79,22 +72,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 [ @@ -111,24 +101,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] = { @@ -188,17 +161,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( @@ -372,19 +334,6 @@ class FrigateApp: self.inter_config_updater = ConfigPublisher() self.inter_zmq_proxy = ZmqProxy() - def init_web_server(self) -> None: - self.fastapi_app = create_fastapi_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) @@ -525,7 +474,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 @@ -588,7 +537,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 @@ -608,17 +557,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: @@ -655,96 +606,64 @@ 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: - 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.init_queues() - self.init_database() - self.init_onvif() - self.init_recording_manager() - self.init_review_segment_manager() - self.init_embeddings_manager() - self.init_go2rtc() - self.bind_database() - self.check_db_data_migrations() - self.init_inter_process_communicator() - self.init_dispatcher() - except Exception as e: - print(e) - sys.exit(1) + # Ensure global state. + self.ensure_dirs() + self.config.install() + + # Start frigate services. + self.init_camera_metrics() + self.init_queues() + self.init_database() + self.init_onvif() + self.init_recording_manager() + self.init_review_segment_manager() + self.init_embeddings_manager() + self.init_go2rtc() + self.bind_database() + self.check_db_data_migrations() + self.init_inter_process_communicator() + self.init_dispatcher() 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() try: uvicorn.run( - self.fastapi_app, + create_fastapi_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, + ), host="127.0.0.1", port=5001, log_level="error", ) - except KeyboardInterrupt: - pass - - logger.info("FastAPI has exited...") - - self.stop() + finally: + self.stop() def stop(self) -> None: logger.info("Stopping...") diff --git a/frigate/config.py b/frigate/config.py index aec525076..ffd5afb02 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -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 @@ -1762,7 +1840,7 @@ class FrigateConfig(FrigateBaseModel): elif ext == ".json": is_json = True - # At this point, ry to sniff the config string, to guess if it is json or not. + # At this point, try to sniff the config string, to guess if it is json or not. if is_json is None: is_json = REGEX_JSON.match(config) is not None @@ -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 diff --git a/frigate/embeddings/embeddings.py b/frigate/embeddings/embeddings.py index 8179de15e..ba5363cab 100644 --- a/frigate/embeddings/embeddings.py +++ b/frigate/embeddings/embeddings.py @@ -126,9 +126,9 @@ class Embeddings: thumbnails["ids"].append(event.id) thumbnails["images"].append(img) thumbnails["metadatas"].append(metadata) - if event.data.get("description") is not None: + if description := event.data.get("description", "").strip(): descriptions["ids"].append(event.id) - descriptions["documents"].append(event.data["description"]) + descriptions["documents"].append(description) descriptions["metadatas"].append(metadata) if len(thumbnails["ids"]) > 0: diff --git a/frigate/embeddings/maintainer.py b/frigate/embeddings/maintainer.py index 8e4309d5e..eee0d2994 100644 --- a/frigate/embeddings/maintainer.py +++ b/frigate/embeddings/maintainer.py @@ -177,7 +177,7 @@ class EmbeddingMaintainer(threading.Thread): camera_config, thumbnails, metadata ) - if description is None: + if not description: logger.debug("Failed to generate description for %s", event.id) return diff --git a/frigate/events/audio.py b/frigate/events/audio.py index 662eb5189..5875dca84 100644 --- a/frigate/events/audio.py +++ b/frigate/events/audio.py @@ -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): diff --git a/frigate/events/cleanup.py b/frigate/events/cleanup.py index e4c571be7..35edf4195 100644 --- a/frigate/events/cleanup.py +++ b/frigate/events/cleanup.py @@ -230,7 +230,15 @@ class EventCleanup(threading.Thread): Event.delete().where(Event.id << chunk).execute() if self.config.semantic_search.enabled: - self.embeddings.thumbnail.delete(ids=chunk) - self.embeddings.description.delete(ids=chunk) + for collection in [ + self.embeddings.thumbnail, + self.embeddings.description, + ]: + existing_ids = collection.get(ids=chunk, include=[])["ids"] + if existing_ids: + collection.delete(ids=existing_ids) + logger.debug( + f"Deleted {len(existing_ids)} embeddings from {collection.__class__.__name__}" + ) logger.info("Exiting event cleanup...") diff --git a/frigate/log.py b/frigate/log.py index 475be50d4..ec60b1b71 100644 --- a/frigate/log.py +++ b/frigate/log.py @@ -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): diff --git a/frigate/object_detection.py b/frigate/object_detection.py index de383dffa..d5b8b0cfe 100644 --- a/frigate/object_detection.py +++ b/frigate/object_detection.py @@ -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) diff --git a/frigate/output/output.py b/frigate/output/output.py index c1be7154c..7d5b6d39a 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -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) diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index d13b7bbc3..dc7a00e78 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -210,7 +210,13 @@ class OnvifController: "RelativeZoomTranslationSpace" ][zoom_space_id]["URI"] else: - move_request.Translation.Zoom = [] + if "Zoom" in move_request["Translation"]: + del move_request["Translation"]["Zoom"] + if "Zoom" in move_request["Speed"]: + del move_request["Speed"]["Zoom"] + logger.debug( + f"{camera_name}: Relative move request after deleting zoom: {move_request}" + ) except Exception: self.config.cameras[ camera_name @@ -569,7 +575,7 @@ class OnvifController: def get_camera_info(self, camera_name: str) -> dict[str, any]: if camera_name not in self.cams.keys(): - logger.error(f"Onvif is not setup for {camera_name}") + logger.debug(f"Onvif is not setup for {camera_name}") return {} if not self.cams[camera_name]["init"]: diff --git a/frigate/record/record.py b/frigate/record/record.py index 00634f157..252b80545 100644 --- a/frigate/record/record.py +++ b/frigate/record/record.py @@ -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) diff --git a/frigate/review/review.py b/frigate/review/review.py index d0e3a163f..dafa6c802 100644 --- a/frigate/review/review.py +++ b/frigate/review/review.py @@ -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) diff --git a/frigate/test/test_http.py b/frigate/test/test_http.py index 030edc1bf..b4ffa121b 100644 --- a/frigate/test/test_http.py +++ b/frigate/test/test_http.py @@ -13,7 +13,6 @@ from playhouse.sqliteq import SqliteQueueDatabase from frigate.api.fastapi_app import create_fastapi_app from frigate.config import FrigateConfig from frigate.models import Event, Recordings, Timeline -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" @@ -351,7 +342,6 @@ class TestHttp(unittest.TestCase): None, None, None, - PlusApi(), None, ) @@ -369,7 +359,6 @@ class TestHttp(unittest.TestCase): None, None, None, - PlusApi(), None, ) id = "123456.random" @@ -393,7 +382,6 @@ class TestHttp(unittest.TestCase): None, None, None, - PlusApi(), stats, ) diff --git a/frigate/util/builtin.py b/frigate/util/builtin.py index e50882e10..164c34091 100644 --- a/frigate/util/builtin.py +++ b/frigate/util/builtin.py @@ -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: diff --git a/frigate/video.py b/frigate/video.py index f31b1d267..5c3c8a054 100755 --- a/frigate/video.py +++ b/frigate/video.py @@ -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) diff --git a/web/src/components/graph/CameraGraph.tsx b/web/src/components/graph/CameraGraph.tsx index f2b327da1..3289887c5 100644 --- a/web/src/components/graph/CameraGraph.tsx +++ b/web/src/components/graph/CameraGraph.tsx @@ -86,7 +86,7 @@ export function CameraLineGraph({ size: 0, }, xaxis: { - tickAmount: isMobileOnly ? 3 : 4, + tickAmount: isMobileOnly ? 2 : 3, tickPlacement: "on", labels: { rotate: 0, diff --git a/web/src/components/graph/SystemGraph.tsx b/web/src/components/graph/SystemGraph.tsx index 098a1957e..572eae5cd 100644 --- a/web/src/components/graph/SystemGraph.tsx +++ b/web/src/components/graph/SystemGraph.tsx @@ -121,7 +121,7 @@ export function ThresholdBarGraph({ size: 0, }, xaxis: { - tickAmount: isMobileOnly ? 3 : 4, + tickAmount: isMobileOnly ? 2 : 3, tickPlacement: "on", labels: { rotate: 0, diff --git a/web/src/components/input/InputWithTags.tsx b/web/src/components/input/InputWithTags.tsx index c21dba733..802e4acaa 100644 --- a/web/src/components/input/InputWithTags.tsx +++ b/web/src/components/input/InputWithTags.tsx @@ -445,7 +445,7 @@ export default function InputWithTags({ onFocus={handleInputFocus} onBlur={handleInputBlur} onKeyDown={handleInputKeyDown} - className="text-md h-10 pr-24" + className="text-md h-9 pr-24" placeholder="Search..." />
diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 56257ee3e..06ad73b3e 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -22,6 +22,7 @@ import useKeyboardListener, { } from "@/hooks/use-keyboard-listener"; import scrollIntoView from "scroll-into-view-if-needed"; import InputWithTags from "@/components/input/InputWithTags"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; type SearchViewProps = { search: string; @@ -276,13 +277,18 @@ export default function SearchView({ )} {hasExistingSearch && ( - + +
+ + +
+
)}