Merge branch 'dev' of github.com:blakeblackshear/frigate into frigate-http-api-with-fastapi

This commit is contained in:
Nicolas Mowen 2024-09-24 06:32:13 -06:00
commit 22b6aaf070
30 changed files with 272 additions and 229 deletions

View File

@ -18,12 +18,10 @@ peewee_migrate == 1.13.*
psutil == 5.9.* psutil == 5.9.*
pydantic == 2.8.* pydantic == 2.8.*
git+https://github.com/fbcotter/py3nvml#egg=py3nvml git+https://github.com/fbcotter/py3nvml#egg=py3nvml
PyYAML == 6.0.*
pytz == 2024.1 pytz == 2024.1
pyzmq == 26.2.* pyzmq == 26.2.*
ruamel.yaml == 0.18.* ruamel.yaml == 0.18.*
tzlocal == 5.2 tzlocal == 5.2
types-PyYAML == 6.0.*
requests == 2.32.* requests == 2.32.*
types-requests == 2.32.* types-requests == 2.32.*
scipy == 1.13.* scipy == 1.13.*

View File

@ -6,7 +6,7 @@ import shutil
import sys import sys
from pathlib import Path from pathlib import Path
import yaml from ruamel.yaml import YAML
sys.path.insert(0, "/opt/frigate") sys.path.insert(0, "/opt/frigate")
from frigate.const import ( from frigate.const import (
@ -18,6 +18,7 @@ from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
sys.path.remove("/opt/frigate") sys.path.remove("/opt/frigate")
yaml = YAML()
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")} FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too # read docker secret files as env vars too
@ -40,7 +41,7 @@ try:
raw_config = f.read() raw_config = f.read()
if config_file.endswith((".yaml", ".yml")): 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"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, any] = json.loads(raw_config)
except FileNotFoundError: except FileNotFoundError:

View File

@ -104,6 +104,8 @@ http {
add_header Cache-Control "no-store"; add_header Cache-Control "no-store";
expires off; expires off;
keepalive_disable safari;
} }
location /stream/ { location /stream/ {

View File

@ -3,7 +3,9 @@
import json import json
import os import os
import yaml from ruamel.yaml import YAML
yaml = YAML()
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
@ -17,7 +19,7 @@ try:
raw_config = f.read() raw_config = f.read()
if config_file.endswith((".yaml", ".yml")): 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"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, any] = json.loads(raw_config)
except FileNotFoundError: except FileNotFoundError:

View File

@ -3,7 +3,9 @@
import json import json
import os import os
import yaml from ruamel.yaml import YAML
yaml = YAML()
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
@ -17,7 +19,7 @@ try:
raw_config = f.read() raw_config = f.read()
if config_file.endswith((".yaml", ".yml")): 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"): elif config_file.endswith(".json"):
config: dict[str, any] = json.loads(raw_config) config: dict[str, any] = json.loads(raw_config)
except FileNotFoundError: except FileNotFoundError:

View File

@ -597,6 +597,8 @@ $ cat /sys/kernel/debug/rknpu/load
This detector is available for use with Hailo-8 AI Acceleration Module. 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 ### Configuration
```yaml ```yaml

View File

@ -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. 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). 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` 3. Ensure it has execution permissions with `sudo chmod +x user_installation.sh`
4. Run the script with `./install_hailo8l_driver.sh` 4. Run the script with `./user_installation.sh`
#### Setup #### Setup

View File

@ -1,8 +1,15 @@
import argparse
import faulthandler import faulthandler
import logging import logging
import signal
import sys
import threading import threading
from pydantic import ValidationError
from frigate.app import FrigateApp from frigate.app import FrigateApp
from frigate.config import FrigateConfig
from frigate.log import log_thread
def main() -> None: def main() -> None:
@ -17,8 +24,50 @@ def main() -> None:
threading.current_thread().name = "frigate" 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. # Run the main application.
FrigateApp().start() FrigateApp(config).start()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -129,7 +129,7 @@ def config(request: Request):
for zone_name, zone in config_obj.cameras[camera_name].zones.items(): for zone_name, zone in config_obj.cameras[camera_name].zones.items():
camera_dict["zones"][zone_name]["color"] = zone.color 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 config["model"]["colormap"] = config_obj.model.colormap
for detector_config in config["detectors"].values(): for detector_config in config["detectors"].values():
@ -289,7 +289,7 @@ def config_set(request: Request, body: AppConfigSetBody):
if body.requires_restart == 0: if body.requires_restart == 0:
request.app.frigate_config = FrigateConfig.parse_object( request.app.frigate_config = FrigateConfig.parse_object(
config_obj, request.app.plus_api config_obj, request.app.frigate_config.plus_api
) )
return JSONResponse( return JSONResponse(

View File

@ -617,7 +617,7 @@ def set_retain(event_id: str):
@router.post("/events/{event_id}/plus") @router.post("/events/{event_id}/plus")
def send_to_plus(request: Request, event_id: str): 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" message = "PLUS_API_KEY environment variable is not set"
logger.error(message) logger.error(message)
return JSONResponse( return JSONResponse(
@ -689,7 +689,7 @@ def send_to_plus(request: Request, event_id: str):
) )
try: 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: except Exception as ex:
logger.exception(ex) logger.exception(ex)
return JSONResponse( return JSONResponse(
@ -705,7 +705,7 @@ def send_to_plus(request: Request, event_id: str):
box = event.data["box"] box = event.data["box"]
try: try:
request.app.plus_api.add_annotation( request.app.frigate_config.plus_api.add_annotation(
event.plus_id, event.plus_id,
box, box,
event.label, event.label,
@ -731,7 +731,7 @@ def send_to_plus(request: Request, event_id: str):
@router.put("/events/{event_id}/false_positive") @router.put("/events/{event_id}/false_positive")
def false_positive(request: Request, event_id: str): 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" message = "PLUS_API_KEY environment variable is not set"
logger.error(message) logger.error(message)
return JSONResponse( return JSONResponse(
@ -786,7 +786,7 @@ def false_positive(request: Request, event_id: str):
) )
try: try:
request.app.plus_api.add_false_positive( request.app.frigate_config.plus_api.add_false_positive(
event.plus_id, event.plus_id,
region, region,
box, box,

View File

@ -310,7 +310,7 @@ def submit_recording_snapshot_to_plus(
) )
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR) 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( return JSONResponse(
content={ content={

View File

@ -1,21 +1,18 @@
import argparse
import datetime import datetime
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import secrets import secrets
import shutil import shutil
import sys
import traceback
from multiprocessing import Queue from multiprocessing import Queue
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Any
import psutil import psutil
import uvicorn import uvicorn
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError
from frigate.api.auth import hash_password from frigate.api.auth import hash_password
from frigate.api.fastapi_app import create_fastapi_app 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.webpush import WebPushClient
from frigate.comms.ws import WebSocketClient from frigate.comms.ws import WebSocketClient
from frigate.comms.zmq_proxy import ZmqProxy from frigate.comms.zmq_proxy import ZmqProxy
from frigate.config import FrigateConfig
from frigate.const import ( from frigate.const import (
CACHE_DIR, CACHE_DIR,
CLIPS_DIR, CLIPS_DIR,
@ -41,7 +37,6 @@ from frigate.events.audio import listen_to_audio
from frigate.events.cleanup import EventCleanup from frigate.events.cleanup import EventCleanup
from frigate.events.external import ExternalEventProcessor from frigate.events.external import ExternalEventProcessor
from frigate.events.maintainer import EventProcessor from frigate.events.maintainer import EventProcessor
from frigate.log import log_thread
from frigate.models import ( from frigate.models import (
Event, Event,
Export, Export,
@ -56,7 +51,6 @@ from frigate.models import (
from frigate.object_detection import ObjectDetectProcess from frigate.object_detection import ObjectDetectProcess
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.output.output import output_frames from frigate.output.output import output_frames
from frigate.plus import PlusApi
from frigate.ptz.autotrack import PtzAutoTrackerThread from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.record.cleanup import RecordingCleanup from frigate.record.cleanup import RecordingCleanup
@ -68,8 +62,7 @@ from frigate.stats.util import stats_init
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor from frigate.timeline import TimelineProcessor
from frigate.types import CameraMetricsTypes, PTZMetricsTypes from frigate.types import CameraMetricsTypes, PTZMetricsTypes
from frigate.util.builtin import empty_and_close_queue, save_default_config from frigate.util.builtin import empty_and_close_queue
from frigate.util.config import migrate_frigate_config
from frigate.util.object import get_camera_regions_grid from frigate.util.object import get_camera_regions_grid
from frigate.version import VERSION from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
@ -79,22 +72,19 @@ logger = logging.getLogger(__name__)
class FrigateApp: 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.stop_event: MpEvent = mp.Event()
self.detection_queue: Queue = mp.Queue() self.detection_queue: Queue = mp.Queue()
self.detectors: dict[str, ObjectDetectProcess] = {} self.detectors: dict[str, ObjectDetectProcess] = {}
self.detection_out_events: dict[str, MpEvent] = {} self.detection_out_events: dict[str, MpEvent] = {}
self.detection_shms: list[mp.shared_memory.SharedMemory] = [] self.detection_shms: list[mp.shared_memory.SharedMemory] = []
self.log_queue: Queue = mp.Queue() self.log_queue: Queue = mp.Queue()
self.plus_api = PlusApi()
self.camera_metrics: dict[str, CameraMetricsTypes] = {} self.camera_metrics: dict[str, CameraMetricsTypes] = {}
self.ptz_metrics: dict[str, PTZMetricsTypes] = {} self.ptz_metrics: dict[str, PTZMetricsTypes] = {}
self.processes: dict[str, int] = {} self.processes: dict[str, int] = {}
self.region_grids: dict[str, list[list[dict[str, int]]]] = {} self.region_grids: dict[str, list[list[dict[str, int]]]] = {}
self.config = config
def set_environment_vars(self) -> None:
for key, value in self.config.environment_vars.items():
os.environ[key] = value
def ensure_dirs(self) -> None: def ensure_dirs(self) -> None:
for d in [ for d in [
@ -111,24 +101,7 @@ class FrigateApp:
else: else:
logger.debug(f"Skipping directory: {d}") logger.debug(f"Skipping directory: {d}")
def init_config(self) -> None: def init_camera_metrics(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)
for camera_name in self.config.cameras.keys(): for camera_name in self.config.cameras.keys():
# create camera_metrics # create camera_metrics
self.camera_metrics[camera_name] = { self.camera_metrics[camera_name] = {
@ -188,17 +161,6 @@ class FrigateApp:
} }
self.ptz_metrics[camera_name]["ptz_motor_stopped"].set() 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: def init_queues(self) -> None:
# Queue for cameras to push tracked objects to # Queue for cameras to push tracked objects to
self.detected_frames_queue: Queue = mp.Queue( self.detected_frames_queue: Queue = mp.Queue(
@ -372,19 +334,6 @@ class FrigateApp:
self.inter_config_updater = ConfigPublisher() self.inter_config_updater = ConfigPublisher()
self.inter_zmq_proxy = ZmqProxy() 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: def init_onvif(self) -> None:
self.onvif_controller = OnvifController(self.config, self.ptz_metrics) self.onvif_controller = OnvifController(self.config, self.ptz_metrics)
@ -525,7 +474,7 @@ class FrigateApp:
capture_process = mp.Process( capture_process = mp.Process(
target=capture_camera, target=capture_camera,
name=f"camera_capture:{name}", 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 capture_process.daemon = True
self.camera_metrics[name]["capture_process"] = capture_process 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 = FrigateWatchdog(self.detectors, self.stop_event)
self.frigate_watchdog.start() 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) total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1)
# required for log files + nginx cache # required for log files + nginx cache
@ -608,17 +557,19 @@ class FrigateApp:
1, 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( 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( 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." 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: def init_auth(self) -> None:
if self.config.auth.enabled: if self.config.auth.enabled:
if User.select().count() == 0: if User.select().count() == 0:
@ -655,96 +606,64 @@ class FrigateApp:
logger.info("********************************************************") logger.info("********************************************************")
logger.info("********************************************************") logger.info("********************************************************")
@log_thread()
def start(self) -> None: 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})") logger.info(f"Starting Frigate ({VERSION})")
try: # Ensure global state.
self.ensure_dirs() self.ensure_dirs()
try: self.config.install()
self.init_config()
except Exception as e: # Start frigate services.
print("*************************************************************") self.init_camera_metrics()
print("*************************************************************") self.init_queues()
print("*** Your config file is not valid! ***") self.init_database()
print("*** Please check the docs at ***") self.init_onvif()
print("*** https://docs.frigate.video/configuration/index ***") self.init_recording_manager()
print("*************************************************************") self.init_review_segment_manager()
print("*************************************************************") self.init_embeddings_manager()
print("*** Config Validation Errors ***") self.init_go2rtc()
print("*************************************************************") self.bind_database()
if isinstance(e, ValidationError): self.check_db_data_migrations()
for error in e.errors(): self.init_inter_process_communicator()
location = ".".join(str(item) for item in error["loc"]) self.init_dispatcher()
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)
self.start_detectors() self.start_detectors()
self.start_video_output_processor() self.start_video_output_processor()
self.start_ptz_autotracker() self.start_ptz_autotracker()
self.init_historical_regions() self.init_historical_regions()
self.start_detected_frames_processor() self.start_detected_frames_processor()
self.start_camera_processors() self.start_camera_processors()
self.check_shm()
self.start_camera_capture_processes() self.start_camera_capture_processes()
self.start_audio_processors() self.start_audio_processors()
self.start_storage_maintainer() self.start_storage_maintainer()
self.init_external_event_processor() self.init_external_event_processor()
self.start_stats_emitter() self.start_stats_emitter()
self.init_web_server()
self.start_timeline_processor() self.start_timeline_processor()
self.start_event_processor() self.start_event_processor()
self.start_event_cleanup() self.start_event_cleanup()
self.start_record_cleanup() self.start_record_cleanup()
self.start_watchdog() self.start_watchdog()
self.init_auth() self.init_auth()
try: try:
uvicorn.run( 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", host="127.0.0.1",
port=5001, port=5001,
log_level="error", log_level="error",
) )
except KeyboardInterrupt: finally:
pass self.stop()
logger.info("FastAPI has exited...")
self.stop()
def stop(self) -> None: def stop(self) -> None:
logger.info("Stopping...") logger.info("Stopping...")

View File

@ -45,13 +45,18 @@ from frigate.ffmpeg_presets import (
parse_preset_input, parse_preset_input,
parse_preset_output_record, parse_preset_output_record,
) )
from frigate.plus import PlusApi
from frigate.util.builtin import ( from frigate.util.builtin import (
deep_merge, deep_merge,
escape_special_characters, escape_special_characters,
generate_color_palette, generate_color_palette,
get_ffmpeg_arg_list, 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.image import create_mask
from frigate.util.services import auto_detect_hwaccel from frigate.util.services import auto_detect_hwaccel
@ -59,6 +64,25 @@ logger = logging.getLogger(__name__)
yaml = YAML() 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 # TODO: Identify what the default format to display timestamps is
DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S" DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
# German Style: # German Style:
@ -1272,6 +1296,19 @@ class LoggerConfig(FrigateBaseModel):
default_factory=dict, title="Log level for specified processes." 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): class CameraGroupConfig(FrigateBaseModel):
"""Represents a group of cameras.""" """Represents a group of cameras."""
@ -1492,11 +1529,22 @@ class FrigateConfig(FrigateBaseModel):
) )
version: Optional[str] = Field(default=None, title="Current config version.") 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") @model_validator(mode="after")
def post_validation(self, info: ValidationInfo) -> Self: 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): 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 # set notifications state
self.notifications.enabled_in_config = self.notifications.enabled self.notifications.enabled_in_config = self.notifications.enabled
@ -1691,7 +1739,7 @@ class FrigateConfig(FrigateBaseModel):
enabled_labels.update(camera.objects.track) enabled_labels.update(camera.objects.track)
self.model.create_colormap(sorted(enabled_labels)) 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(): for key, detector in self.detectors.items():
adapter = TypeAdapter(DetectorConfig) adapter = TypeAdapter(DetectorConfig)
@ -1726,7 +1774,7 @@ class FrigateConfig(FrigateBaseModel):
detector_config.model = ModelConfig.model_validate(merged_model) detector_config.model = ModelConfig.model_validate(merged_model)
detector_config.model.check_and_load_plus_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() detector_config.model.compute_model_hash()
self.detectors[key] = detector_config self.detectors[key] = detector_config
@ -1743,8 +1791,38 @@ class FrigateConfig(FrigateBaseModel):
return v return v
@classmethod @classmethod
def parse_file(cls, config_path, **kwargs): def load(cls, **kwargs):
with open(config_path) as f: 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) return FrigateConfig.parse(f, **kwargs)
@classmethod @classmethod
@ -1762,7 +1840,7 @@ class FrigateConfig(FrigateBaseModel):
elif ext == ".json": elif ext == ".json":
is_json = True 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: if is_json is None:
is_json = REGEX_JSON.match(config) is not None is_json = REGEX_JSON.match(config) is not None
@ -1775,10 +1853,17 @@ class FrigateConfig(FrigateBaseModel):
# Validate and return the config dict. # Validate and return the config dict.
return cls.parse_object(config, **context) return cls.parse_object(config, **context)
@classmethod
def parse_object(cls, obj: Any, **context):
return cls.model_validate(obj, context=context)
@classmethod @classmethod
def parse_yaml(cls, config_yaml, **context): def parse_yaml(cls, config_yaml, **context):
return cls.parse(config_yaml, is_json=False, **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

View File

@ -126,9 +126,9 @@ class Embeddings:
thumbnails["ids"].append(event.id) thumbnails["ids"].append(event.id)
thumbnails["images"].append(img) thumbnails["images"].append(img)
thumbnails["metadatas"].append(metadata) 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["ids"].append(event.id)
descriptions["documents"].append(event.data["description"]) descriptions["documents"].append(description)
descriptions["metadatas"].append(metadata) descriptions["metadatas"].append(metadata)
if len(thumbnails["ids"]) > 0: if len(thumbnails["ids"]) > 0:

View File

@ -177,7 +177,7 @@ class EmbeddingMaintainer(threading.Thread):
camera_config, thumbnails, metadata camera_config, thumbnails, metadata
) )
if description is None: if not description:
logger.debug("Failed to generate description for %s", event.id) logger.debug("Failed to generate description for %s", event.id)
return return

View File

@ -76,16 +76,8 @@ def listen_to_audio(
stop_event = mp.Event() stop_event = mp.Event()
audio_threads: list[threading.Thread] = [] 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: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
logger.debug(f"Audio process received signal {signalNumber}")
stop_event.set() stop_event.set()
exit_process()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)
signal.signal(signal.SIGINT, receiveSignal) signal.signal(signal.SIGINT, receiveSignal)
@ -104,6 +96,11 @@ def listen_to_audio(
audio_threads.append(audio) audio_threads.append(audio)
audio.start() audio.start()
for thread in audio_threads:
thread.join()
logger.info("Exiting audio detector...")
class AudioTfl: class AudioTfl:
def __init__(self, stop_event: mp.Event, num_threads=2): def __init__(self, stop_event: mp.Event, num_threads=2):

View File

@ -230,7 +230,15 @@ class EventCleanup(threading.Thread):
Event.delete().where(Event.id << chunk).execute() Event.delete().where(Event.id << chunk).execute()
if self.config.semantic_search.enabled: if self.config.semantic_search.enabled:
self.embeddings.thumbnail.delete(ids=chunk) for collection in [
self.embeddings.description.delete(ids=chunk) 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...") logger.info("Exiting event cleanup...")

View File

@ -2,6 +2,7 @@ import atexit
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import sys
import threading import threading
from collections import deque from collections import deque
from contextlib import AbstractContextManager, ContextDecorator from contextlib import AbstractContextManager, ContextDecorator
@ -68,6 +69,19 @@ class log_thread(AbstractContextManager, ContextDecorator):
self._stop_thread() 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 # based on https://codereview.stackexchange.com/a/17959
class LogPipe(threading.Thread): class LogPipe(threading.Thread):
def __init__(self, log_name: str): def __init__(self, log_name: str):

View File

@ -92,7 +92,6 @@ def run_detector(
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber, frame):
logger.info("Signal to exit detection process...")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -38,7 +38,6 @@ def output_frames(
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber, frame):
logger.debug(f"Output frames process received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -210,7 +210,13 @@ class OnvifController:
"RelativeZoomTranslationSpace" "RelativeZoomTranslationSpace"
][zoom_space_id]["URI"] ][zoom_space_id]["URI"]
else: 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: except Exception:
self.config.cameras[ self.config.cameras[
camera_name camera_name
@ -569,7 +575,7 @@ class OnvifController:
def get_camera_info(self, camera_name: str) -> dict[str, any]: def get_camera_info(self, camera_name: str) -> dict[str, any]:
if camera_name not in self.cams.keys(): 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 {} return {}
if not self.cams[camera_name]["init"]: if not self.cams[camera_name]["init"]:

View File

@ -22,7 +22,6 @@ def manage_recordings(config: FrigateConfig) -> None:
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
logger.debug(f"Recording manager process received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -20,7 +20,6 @@ def manage_review_segments(config: FrigateConfig) -> None:
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None: def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
logger.debug(f"Manage review segments process received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -13,7 +13,6 @@ from playhouse.sqliteq import SqliteQueueDatabase
from frigate.api.fastapi_app import create_fastapi_app from frigate.api.fastapi_app import create_fastapi_app
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.models import Event, Recordings, Timeline from frigate.models import Event, Recordings, Timeline
from frigate.plus import PlusApi
from frigate.stats.emitter import StatsEmitter from frigate.stats.emitter import StatsEmitter
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
@ -121,7 +120,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -158,7 +156,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -180,7 +177,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -201,7 +197,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -224,7 +219,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -251,7 +245,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
morning_id = "123456.random" morning_id = "123456.random"
@ -290,7 +283,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -326,7 +318,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -351,7 +342,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
@ -369,7 +359,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
None, None,
) )
id = "123456.random" id = "123456.random"
@ -393,7 +382,6 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
PlusApi(),
stats, stats,
) )

View File

@ -258,37 +258,6 @@ def find_by_key(dictionary, target_key):
return None 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: def get_tomorrow_at_time(hour: int) -> datetime.datetime:
"""Returns the datetime of the following day at 2am.""" """Returns the datetime of the following day at 2am."""
try: try:

View File

@ -390,7 +390,6 @@ def capture_camera(name, config: CameraConfig, shm_frame_count: int, process_inf
stop_event = mp.Event() stop_event = mp.Event()
def receiveSignal(signalNumber, frame): def receiveSignal(signalNumber, frame):
logger.debug(f"Capture camera received signal {signalNumber}")
stop_event.set() stop_event.set()
signal.signal(signal.SIGTERM, receiveSignal) signal.signal(signal.SIGTERM, receiveSignal)

View File

@ -86,7 +86,7 @@ export function CameraLineGraph({
size: 0, size: 0,
}, },
xaxis: { xaxis: {
tickAmount: isMobileOnly ? 3 : 4, tickAmount: isMobileOnly ? 2 : 3,
tickPlacement: "on", tickPlacement: "on",
labels: { labels: {
rotate: 0, rotate: 0,

View File

@ -121,7 +121,7 @@ export function ThresholdBarGraph({
size: 0, size: 0,
}, },
xaxis: { xaxis: {
tickAmount: isMobileOnly ? 3 : 4, tickAmount: isMobileOnly ? 2 : 3,
tickPlacement: "on", tickPlacement: "on",
labels: { labels: {
rotate: 0, rotate: 0,

View File

@ -445,7 +445,7 @@ export default function InputWithTags({
onFocus={handleInputFocus} onFocus={handleInputFocus}
onBlur={handleInputBlur} onBlur={handleInputBlur}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
className="text-md h-10 pr-24" className="text-md h-9 pr-24"
placeholder="Search..." placeholder="Search..."
/> />
<div className="absolute right-3 top-0 flex h-full flex-row items-center justify-center gap-5"> <div className="absolute right-3 top-0 flex h-full flex-row items-center justify-center gap-5">

View File

@ -22,6 +22,7 @@ import useKeyboardListener, {
} from "@/hooks/use-keyboard-listener"; } from "@/hooks/use-keyboard-listener";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import InputWithTags from "@/components/input/InputWithTags"; import InputWithTags from "@/components/input/InputWithTags";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
type SearchViewProps = { type SearchViewProps = {
search: string; search: string;
@ -276,13 +277,18 @@ export default function SearchView({
)} )}
{hasExistingSearch && ( {hasExistingSearch && (
<SearchFilterGroup <ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]">
className={cn( <div className="flex flex-row">
"w-full justify-between md:justify-start lg:justify-end", <SearchFilterGroup
)} className={cn(
filter={searchFilter} "w-full justify-between md:justify-start lg:justify-end",
onUpdateFilter={onUpdateFilter} )}
/> filter={searchFilter}
onUpdateFilter={onUpdateFilter}
/>
<ScrollBar orientation="horizontal" className="h-0" />
</div>
</ScrollArea>
)} )}
</div> </div>