mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-26 19:06: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(
|
||||
|
177
frigate/app.py
177
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,97 +605,58 @@ 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()
|
||||
|
||||
# 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...")
|
||||
|
||||
self.stop()
|
||||
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:
|
||||
logger.info("Stopping...")
|
||||
|
@ -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