1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183 |
- from __future__ import annotations
- import base64
- import dataclasses
- import datetime
- import json
- import logging
- import os
- from typing import Any, Dict, List, Optional, Tuple, Union
- import matplotlib.pyplot as plt
- import numpy as np
- import voluptuous as vol
- import yaml
- from frigate.const import BASE_DIR, RECORD_DIR, CLIPS_DIR, CACHE_DIR
- from frigate.util import create_mask
- logger = logging.getLogger(__name__)
- # TODO: Identify what the default format to display timestamps is
- DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
- # German Style:
- # DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
- DEFAULT_TRACKED_OBJECTS = ["person"]
- DEFAULT_RGB_COLOR = {"red": 255, "green": 255, "blue": 255}
- DEFAULT_DETECTORS = {"coral": {"type": "edgetpu", "device": "usb"}}
- DETECTORS_SCHEMA = vol.Schema(
- {
- vol.Required(str): {
- vol.Required("type", default="edgetpu"): vol.In(["cpu", "edgetpu"]),
- vol.Optional("device", default="usb"): str,
- vol.Optional("num_threads", default=3): int,
- }
- }
- )
- @dataclasses.dataclass(frozen=True)
- class DetectorConfig:
- type: str
- device: str
- num_threads: int
- @classmethod
- def build(cls, config) -> DetectorConfig:
- return DetectorConfig(config["type"], config["device"], config["num_threads"])
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- MQTT_SCHEMA = vol.Schema(
- {
- vol.Required("host"): str,
- vol.Optional("port", default=1883): int,
- vol.Optional("topic_prefix", default="frigate"): str,
- vol.Optional("client_id", default="frigate"): str,
- vol.Optional("stats_interval", default=60): int,
- vol.Inclusive("user", "auth"): str,
- vol.Inclusive("password", "auth"): str,
- vol.Optional("tls_ca_certs"): str,
- vol.Optional("tls_client_cert"): str,
- vol.Optional("tls_client_key"): str,
- vol.Optional("tls_insecure"): bool,
- }
- )
- @dataclasses.dataclass(frozen=True)
- class MqttConfig:
- host: str
- port: int
- topic_prefix: str
- client_id: str
- stats_interval: int
- user: Optional[str]
- password: Optional[str]
- tls_ca_certs: Optional[str]
- tls_client_cert: Optional[str]
- tls_client_key: Optional[str]
- tls_insecure: Optional[bool]
- @classmethod
- def build(cls, config) -> MqttConfig:
- return MqttConfig(
- config["host"],
- config["port"],
- config["topic_prefix"],
- config["client_id"],
- config["stats_interval"],
- config.get("user"),
- config.get("password"),
- config.get("tls_ca_certs"),
- config.get("tls_client_cert"),
- config.get("tls_client_key"),
- config.get("tls_insecure"),
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- RETAIN_SCHEMA = vol.Schema(
- {vol.Required("default", default=10): int, "objects": {str: int}}
- )
- @dataclasses.dataclass(frozen=True)
- class RetainConfig:
- default: int
- objects: Dict[str, int]
- @classmethod
- def build(cls, config, global_config={}) -> RetainConfig:
- return RetainConfig(
- config.get("default", global_config.get("default")),
- config.get("objects", global_config.get("objects", {})),
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- CLIPS_SCHEMA = vol.Schema(
- {
- vol.Optional("max_seconds", default=300): int,
- vol.Optional("retain", default={}): RETAIN_SCHEMA,
- }
- )
- @dataclasses.dataclass(frozen=True)
- class ClipsConfig:
- max_seconds: int
- retain: RetainConfig
- @classmethod
- def build(cls, config) -> ClipsConfig:
- return ClipsConfig(
- config["max_seconds"],
- RetainConfig.build(config["retain"]),
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "max_seconds": self.max_seconds,
- "retain": self.retain.to_dict(),
- }
- MOTION_SCHEMA = vol.Schema(
- {
- "mask": vol.Any(str, [str]),
- "threshold": vol.Range(min=1, max=255),
- "contour_area": int,
- "delta_alpha": float,
- "frame_alpha": float,
- "frame_height": int,
- }
- )
- @dataclasses.dataclass(frozen=True)
- class MotionConfig:
- raw_mask: Union[str, List[str]]
- mask: np.ndarray
- threshold: int
- contour_area: int
- delta_alpha: float
- frame_alpha: float
- frame_height: int
- @classmethod
- def build(cls, config, global_config, frame_shape) -> MotionConfig:
- raw_mask = config.get("mask")
- if raw_mask:
- mask = create_mask(frame_shape, raw_mask)
- else:
- mask = np.zeros(frame_shape, np.uint8)
- mask[:] = 255
- return MotionConfig(
- raw_mask,
- mask,
- config.get("threshold", global_config.get("threshold", 25)),
- config.get("contour_area", global_config.get("contour_area", 100)),
- config.get("delta_alpha", global_config.get("delta_alpha", 0.2)),
- config.get("frame_alpha", global_config.get("frame_alpha", 0.2)),
- config.get(
- "frame_height", global_config.get("frame_height", frame_shape[0] // 6)
- ),
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "mask": self.raw_mask,
- "threshold": self.threshold,
- "contour_area": self.contour_area,
- "delta_alpha": self.delta_alpha,
- "frame_alpha": self.frame_alpha,
- "frame_height": self.frame_height,
- }
- GLOBAL_DETECT_SCHEMA = vol.Schema({"max_disappeared": int})
- DETECT_SCHEMA = GLOBAL_DETECT_SCHEMA.extend(
- {vol.Optional("enabled", default=True): bool}
- )
- @dataclasses.dataclass
- class DetectConfig:
- enabled: bool
- max_disappeared: int
- @classmethod
- def build(cls, config, global_config, camera_fps) -> DetectConfig:
- return DetectConfig(
- config["enabled"],
- config.get(
- "max_disappeared", global_config.get("max_disappeared", camera_fps * 5)
- ),
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "enabled": self.enabled,
- "max_disappeared": self.max_disappeared,
- }
- ZONE_FILTER_SCHEMA = vol.Schema(
- {
- str: {
- "min_area": int,
- "max_area": int,
- "threshold": float,
- }
- }
- )
- FILTER_SCHEMA = ZONE_FILTER_SCHEMA.extend(
- {
- str: {
- "min_score": float,
- "mask": vol.Any(str, [str]),
- }
- }
- )
- @dataclasses.dataclass(frozen=True)
- class FilterConfig:
- min_area: int
- max_area: int
- threshold: float
- min_score: float
- mask: Optional[np.ndarray]
- raw_mask: Union[str, List[str]]
- @classmethod
- def build(
- cls, config, global_config={}, global_mask=None, frame_shape=None
- ) -> FilterConfig:
- raw_mask = []
- if global_mask:
- if isinstance(global_mask, list):
- raw_mask += global_mask
- elif isinstance(global_mask, str):
- raw_mask += [global_mask]
- config_mask = config.get("mask")
- if config_mask:
- if isinstance(config_mask, list):
- raw_mask += config_mask
- elif isinstance(config_mask, str):
- raw_mask += [config_mask]
- mask = create_mask(frame_shape, raw_mask) if raw_mask else None
- return FilterConfig(
- min_area=config.get("min_area", global_config.get("min_area", 0)),
- max_area=config.get("max_area", global_config.get("max_area", 24000000)),
- threshold=config.get("threshold", global_config.get("threshold", 0.7)),
- min_score=config.get("min_score", global_config.get("min_score", 0.5)),
- mask=mask,
- raw_mask=raw_mask,
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "min_area": self.min_area,
- "max_area": self.max_area,
- "threshold": self.threshold,
- "min_score": self.min_score,
- "mask": self.raw_mask,
- }
- ZONE_SCHEMA = {
- str: {
- vol.Required("coordinates"): vol.Any(str, [str]),
- vol.Optional("filters", default={}): ZONE_FILTER_SCHEMA,
- }
- }
- @dataclasses.dataclass(frozen=True)
- class ZoneConfig:
- filters: Dict[str, FilterConfig]
- coordinates: Union[str, List[str]]
- contour: np.ndarray
- color: Tuple[int, int, int]
- @classmethod
- def build(cls, config, color: Tuple[int, int, int]) -> ZoneConfig:
- coordinates = config["coordinates"]
- if isinstance(coordinates, list):
- contour = np.array(
- [[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates]
- )
- elif isinstance(coordinates, str):
- points = coordinates.split(",")
- contour = np.array(
- [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
- )
- else:
- contour = np.array([])
- return ZoneConfig(
- {name: FilterConfig.build(c) for name, c in config["filters"].items()},
- coordinates,
- contour,
- color=color,
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "filters": {k: f.to_dict() for k, f in self.filters.items()},
- "coordinates": self.coordinates,
- }
- def filters_for_all_tracked_objects(object_config):
- for tracked_object in object_config.get("track", DEFAULT_TRACKED_OBJECTS):
- if not "filters" in object_config:
- object_config["filters"] = {}
- if not tracked_object in object_config["filters"]:
- object_config["filters"][tracked_object] = {}
- return object_config
- OBJECTS_SCHEMA = vol.Schema(
- vol.All(
- filters_for_all_tracked_objects,
- {
- "track": [str],
- "mask": vol.Any(str, [str]),
- vol.Optional("filters", default={}): FILTER_SCHEMA,
- },
- )
- )
- @dataclasses.dataclass(frozen=True)
- class ObjectConfig:
- track: List[str]
- filters: Dict[str, FilterConfig]
- raw_mask: Optional[Union[str, List[str]]]
- @classmethod
- def build(cls, config, global_config, frame_shape) -> ObjectConfig:
- track = config.get("track", global_config.get("track", DEFAULT_TRACKED_OBJECTS))
- raw_mask = config.get("mask")
- return ObjectConfig(
- track,
- {
- name: FilterConfig.build(
- config["filters"].get(name, {}),
- global_config["filters"].get(name, {}),
- raw_mask,
- frame_shape,
- )
- for name in track
- },
- raw_mask,
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "track": self.track,
- "mask": self.raw_mask,
- "filters": {k: f.to_dict() for k, f in self.filters.items()},
- }
- BIRDSEYE_SCHEMA = vol.Schema(
- {
- vol.Optional("enabled", default=True): bool,
- vol.Optional("width", default=1280): int,
- vol.Optional("height", default=720): int,
- vol.Optional("quality", default=8): vol.Range(min=1, max=31),
- vol.Optional("mode", default="objects"): vol.In(
- ["objects", "motion", "continuous"]
- ),
- }
- )
- @dataclasses.dataclass(frozen=True)
- class BirdseyeConfig:
- enabled: bool
- width: int
- height: int
- quality: int
- mode: str
- @classmethod
- def build(cls, config) -> BirdseyeConfig:
- return BirdseyeConfig(
- config["enabled"],
- config["width"],
- config["height"],
- config["quality"],
- config["mode"],
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
- FFMPEG_INPUT_ARGS_DEFAULT = [
- "-avoid_negative_ts",
- "make_zero",
- "-fflags",
- "+genpts+discardcorrupt",
- "-rtsp_transport",
- "tcp",
- "-stimeout",
- "5000000",
- "-use_wallclock_as_timestamps",
- "1",
- ]
- DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "rawvideo", "-pix_fmt", "yuv420p"]
- RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
- SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = [
- "-f",
- "segment",
- "-segment_time",
- "10",
- "-segment_format",
- "mp4",
- "-reset_timestamps",
- "1",
- "-strftime",
- "1",
- "-c",
- "copy",
- "-an",
- ]
- RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
- "-f",
- "segment",
- "-segment_time",
- "60",
- "-segment_format",
- "mp4",
- "-reset_timestamps",
- "1",
- "-strftime",
- "1",
- "-c",
- "copy",
- "-an",
- ]
- GLOBAL_FFMPEG_SCHEMA = vol.Schema(
- {
- vol.Optional("global_args", default=FFMPEG_GLOBAL_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- vol.Optional("hwaccel_args", default=[]): vol.Any(str, [str]),
- vol.Optional("input_args", default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- vol.Optional("output_args", default={}): {
- vol.Optional("detect", default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- vol.Optional("record", default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- vol.Optional(
- "clips", default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT
- ): vol.Any(str, [str]),
- vol.Optional("rtmp", default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- },
- }
- )
- def each_role_used_once(inputs):
- roles = [role for i in inputs for role in i["roles"]]
- roles_set = set(roles)
- if len(roles) > len(roles_set):
- raise ValueError
- return inputs
- def detect_is_required(inputs):
- roles = [role for i in inputs for role in i["roles"]]
- if not "detect" in roles:
- raise ValueError
- return inputs
- CAMERA_FFMPEG_SCHEMA = vol.Schema(
- {
- vol.Required("inputs"): vol.All(
- [
- {
- vol.Required("path"): str,
- vol.Required("roles"): ["detect", "clips", "record", "rtmp"],
- "global_args": vol.Any(str, [str]),
- "hwaccel_args": vol.Any(str, [str]),
- "input_args": vol.Any(str, [str]),
- }
- ],
- vol.Msg(each_role_used_once, msg="Each input role may only be used once"),
- vol.Msg(detect_is_required, msg="The detect role is required"),
- ),
- "global_args": vol.Any(str, [str]),
- "hwaccel_args": vol.Any(str, [str]),
- "input_args": vol.Any(str, [str]),
- "output_args": {
- vol.Optional("detect", default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- vol.Optional("record", default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- vol.Optional(
- "clips", default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT
- ): vol.Any(str, [str]),
- vol.Optional("rtmp", default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
- str, [str]
- ),
- },
- }
- )
- @dataclasses.dataclass(frozen=True)
- class CameraFfmpegConfig:
- inputs: List[CameraInput]
- output_args: Dict[str, List[str]]
- @classmethod
- def build(cls, config, global_config):
- output_args = config.get("output_args", global_config["output_args"])
- output_args = {
- k: v if isinstance(v, list) else v.split(" ")
- for k, v in output_args.items()
- }
- return CameraFfmpegConfig(
- [CameraInput.build(i, config, global_config) for i in config["inputs"]],
- output_args,
- )
- @dataclasses.dataclass(frozen=True)
- class CameraInput:
- path: str
- roles: List[str]
- global_args: List[str]
- hwaccel_args: List[str]
- input_args: List[str]
- @classmethod
- def build(cls, ffmpeg_input, camera_config, global_config) -> CameraInput:
- return CameraInput(
- ffmpeg_input["path"],
- ffmpeg_input["roles"],
- CameraInput._extract_args(
- "global_args", ffmpeg_input, camera_config, global_config
- ),
- CameraInput._extract_args(
- "hwaccel_args", ffmpeg_input, camera_config, global_config
- ),
- CameraInput._extract_args(
- "input_args", ffmpeg_input, camera_config, global_config
- ),
- )
- @staticmethod
- def _extract_args(name, ffmpeg_input, camera_config, global_config):
- args = ffmpeg_input.get(name, camera_config.get(name, global_config[name]))
- return args if isinstance(args, list) else args.split(" ")
- def ensure_zones_and_cameras_have_different_names(cameras):
- zones = [zone for camera in cameras.values() for zone in camera["zones"].keys()]
- for zone in zones:
- if zone in cameras.keys():
- raise ValueError
- return cameras
- def ensure_timeformat_is_legit(format_string):
- datetime.datetime.now().strftime(format_string)
- return format_string
- RGB_COLOR_SCHEMA = vol.Schema(
- {
- vol.Required("red"): vol.Range(min=0, max=255),
- vol.Required("green"): vol.Range(min=0, max=255),
- vol.Required("blue"): vol.Range(min=0, max=255),
- }
- )
- CAMERAS_SCHEMA = vol.Schema(
- vol.All(
- {
- str: {
- vol.Required("ffmpeg"): CAMERA_FFMPEG_SCHEMA,
- vol.Required("height"): int,
- vol.Required("width"): int,
- "fps": int,
- vol.Optional("best_image_timeout", default=60): int,
- vol.Optional("zones", default={}): ZONE_SCHEMA,
- vol.Optional("clips", default={}): {
- vol.Optional("enabled", default=False): bool,
- vol.Optional("pre_capture", default=5): int,
- vol.Optional("post_capture", default=5): int,
- vol.Optional("required_zones", default=[]): [str],
- "objects": [str],
- vol.Optional("retain", default={}): RETAIN_SCHEMA,
- },
- vol.Optional("record", default={}): {
- "enabled": bool,
- "retain_days": int,
- },
- vol.Optional("rtmp", default={}): {
- vol.Required("enabled", default=True): bool,
- },
- vol.Optional("snapshots", default={}): {
- vol.Optional("enabled", default=False): bool,
- vol.Optional("clean_copy", default=True): bool,
- vol.Optional("timestamp", default=False): bool,
- vol.Optional("bounding_box", default=False): bool,
- vol.Optional("crop", default=False): bool,
- vol.Optional("required_zones", default=[]): [str],
- "height": int,
- vol.Optional("retain", default={}): RETAIN_SCHEMA,
- },
- vol.Optional("mqtt", default={}): {
- vol.Optional("enabled", default=True): bool,
- vol.Optional("timestamp", default=True): bool,
- vol.Optional("bounding_box", default=True): bool,
- vol.Optional("crop", default=True): bool,
- vol.Optional("height", default=270): int,
- vol.Optional("required_zones", default=[]): [str],
- },
- vol.Optional("objects", default={}): OBJECTS_SCHEMA,
- vol.Optional("motion", default={}): MOTION_SCHEMA,
- vol.Optional("detect", default={}): DETECT_SCHEMA,
- vol.Optional("timestamp_style", default={}): {
- vol.Optional("position", default="tl"): vol.In(
- ["tl", "tr", "bl", "br"]
- ),
- vol.Optional(
- "format", default=DEFAULT_TIME_FORMAT
- ): ensure_timeformat_is_legit,
- vol.Optional("color", default=DEFAULT_RGB_COLOR): RGB_COLOR_SCHEMA,
- vol.Optional("scale", default=1.0): float,
- vol.Optional("thickness", default=2): int,
- vol.Optional("effect", default=None): vol.In(
- [None, "solid", "shadow"]
- ),
- },
- }
- },
- vol.Msg(
- ensure_zones_and_cameras_have_different_names,
- msg="Zones cannot share names with cameras",
- ),
- )
- )
- @dataclasses.dataclass
- class CameraSnapshotsConfig:
- enabled: bool
- clean_copy: bool
- timestamp: bool
- bounding_box: bool
- crop: bool
- required_zones: List[str]
- height: Optional[int]
- retain: RetainConfig
- @classmethod
- def build(cls, config, global_config) -> CameraSnapshotsConfig:
- return CameraSnapshotsConfig(
- enabled=config["enabled"],
- clean_copy=config["clean_copy"],
- timestamp=config["timestamp"],
- bounding_box=config["bounding_box"],
- crop=config["crop"],
- required_zones=config["required_zones"],
- height=config.get("height"),
- retain=RetainConfig.build(
- config["retain"], global_config["snapshots"]["retain"]
- ),
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "enabled": self.enabled,
- "clean_copy": self.clean_copy,
- "timestamp": self.timestamp,
- "bounding_box": self.bounding_box,
- "crop": self.crop,
- "height": self.height,
- "retain": self.retain.to_dict(),
- "required_zones": self.required_zones,
- }
- @dataclasses.dataclass
- class CameraMqttConfig:
- enabled: bool
- timestamp: bool
- bounding_box: bool
- crop: bool
- height: int
- required_zones: List[str]
- @classmethod
- def build(cls, config) -> CameraMqttConfig:
- return CameraMqttConfig(
- config["enabled"],
- config["timestamp"],
- config["bounding_box"],
- config["crop"],
- config.get("height"),
- config["required_zones"],
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- @dataclasses.dataclass
- class TimestampStyleConfig:
- position: str
- format: str
- color: Tuple[int, int, int]
- scale: float
- thickness: int
- effect: str
- @classmethod
- def build(cls, config) -> TimestampStyleConfig:
- return TimestampStyleConfig(
- config["position"],
- config["format"],
- (config["color"]["red"], config["color"]["green"], config["color"]["blue"]),
- config["scale"],
- config["thickness"],
- config["effect"],
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- @dataclasses.dataclass
- class CameraClipsConfig:
- enabled: bool
- pre_capture: int
- post_capture: int
- required_zones: List[str]
- objects: Optional[List[str]]
- retain: RetainConfig
- @classmethod
- def build(cls, config, global_config) -> CameraClipsConfig:
- return CameraClipsConfig(
- enabled=config["enabled"],
- pre_capture=config["pre_capture"],
- post_capture=config["post_capture"],
- required_zones=config["required_zones"],
- objects=config.get("objects"),
- retain=RetainConfig.build(
- config["retain"],
- global_config["clips"]["retain"],
- ),
- )
- def to_dict(self) -> Dict[str, Any]:
- return {
- "enabled": self.enabled,
- "pre_capture": self.pre_capture,
- "post_capture": self.post_capture,
- "objects": self.objects,
- "retain": self.retain.to_dict(),
- "required_zones": self.required_zones,
- }
- @dataclasses.dataclass
- class CameraRtmpConfig:
- enabled: bool
- @classmethod
- def build(cls, config) -> CameraRtmpConfig:
- return CameraRtmpConfig(config["enabled"])
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- @dataclasses.dataclass(frozen=True)
- class CameraConfig:
- name: str
- ffmpeg: CameraFfmpegConfig
- height: int
- width: int
- fps: Optional[int]
- best_image_timeout: int
- zones: Dict[str, ZoneConfig]
- clips: CameraClipsConfig
- record: RecordConfig
- rtmp: CameraRtmpConfig
- snapshots: CameraSnapshotsConfig
- mqtt: CameraMqttConfig
- objects: ObjectConfig
- motion: MotionConfig
- detect: DetectConfig
- timestamp_style: TimestampStyleConfig
- @property
- def frame_shape(self) -> Tuple[int, int]:
- return self.height, self.width
- @property
- def frame_shape_yuv(self) -> Tuple[int, int]:
- return self.height * 3 // 2, self.width
- @property
- def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
- ffmpeg_cmds = []
- for ffmpeg_input in self.ffmpeg.inputs:
- ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
- if ffmpeg_cmd is None:
- continue
- ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
- return ffmpeg_cmds
- @classmethod
- def build(cls, name, config, global_config) -> CameraConfig:
- colors = plt.cm.get_cmap("tab10", len(config["zones"]))
- zones = {
- name: ZoneConfig.build(z, tuple(round(255 * c) for c in colors(idx)[:3]))
- for idx, (name, z) in enumerate(config["zones"].items())
- }
- frame_shape = config["height"], config["width"]
- return CameraConfig(
- name=name,
- ffmpeg=CameraFfmpegConfig.build(config["ffmpeg"], global_config["ffmpeg"]),
- height=config["height"],
- width=config["width"],
- fps=config.get("fps"),
- best_image_timeout=config["best_image_timeout"],
- zones=zones,
- clips=CameraClipsConfig.build(config["clips"], global_config),
- record=RecordConfig.build(config["record"], global_config["record"]),
- rtmp=CameraRtmpConfig.build(config["rtmp"]),
- snapshots=CameraSnapshotsConfig.build(config["snapshots"], global_config),
- mqtt=CameraMqttConfig.build(config["mqtt"]),
- objects=ObjectConfig.build(
- config.get("objects", {}), global_config["objects"], frame_shape
- ),
- motion=MotionConfig.build(
- config["motion"], global_config["motion"], frame_shape
- ),
- detect=DetectConfig.build(
- config["detect"], global_config["detect"], config.get("fps", 5)
- ),
- timestamp_style=TimestampStyleConfig.build(config["timestamp_style"]),
- )
- def _get_ffmpeg_cmd(self, ffmpeg_input):
- ffmpeg_output_args = []
- if "detect" in ffmpeg_input.roles:
- ffmpeg_output_args = (
- self.ffmpeg.output_args["detect"] + ffmpeg_output_args + ["pipe:"]
- )
- if self.fps:
- ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
- if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
- ffmpeg_output_args = (
- self.ffmpeg.output_args["rtmp"]
- + [f"rtmp://127.0.0.1/live/{self.name}"]
- + ffmpeg_output_args
- )
- if "clips" in ffmpeg_input.roles:
- ffmpeg_output_args = (
- self.ffmpeg.output_args["clips"]
- + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
- + ffmpeg_output_args
- )
- if "record" in ffmpeg_input.roles and self.record.enabled:
- ffmpeg_output_args = (
- self.ffmpeg.output_args["record"]
- + [f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
- + ffmpeg_output_args
- )
- # if there arent any outputs enabled for this input
- if len(ffmpeg_output_args) == 0:
- return None
- cmd = (
- ["ffmpeg"]
- + ffmpeg_input.global_args
- + ffmpeg_input.hwaccel_args
- + ffmpeg_input.input_args
- + ["-i", ffmpeg_input.path]
- + ffmpeg_output_args
- )
- return [part for part in cmd if part != ""]
- def to_dict(self) -> Dict[str, Any]:
- return {
- "name": self.name,
- "height": self.height,
- "width": self.width,
- "fps": self.fps,
- "best_image_timeout": self.best_image_timeout,
- "zones": {k: z.to_dict() for k, z in self.zones.items()},
- "clips": self.clips.to_dict(),
- "record": self.record.to_dict(),
- "rtmp": self.rtmp.to_dict(),
- "snapshots": self.snapshots.to_dict(),
- "mqtt": self.mqtt.to_dict(),
- "objects": self.objects.to_dict(),
- "motion": self.motion.to_dict(),
- "detect": self.detect.to_dict(),
- "frame_shape": self.frame_shape,
- "ffmpeg_cmds": [
- {"roles": c["roles"], "cmd": " ".join(c["cmd"])}
- for c in self.ffmpeg_cmds
- ],
- "timestamp_style": self.timestamp_style.to_dict(),
- }
- FRIGATE_CONFIG_SCHEMA = vol.Schema(
- {
- vol.Optional("database", default={}): {
- vol.Optional("path", default=os.path.join(BASE_DIR, "frigate.db")): str
- },
- vol.Optional("model", default={"width": 320, "height": 320}): {
- vol.Required("width"): int,
- vol.Required("height"): int,
- },
- vol.Optional("detectors", default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
- "mqtt": MQTT_SCHEMA,
- vol.Optional("logger", default={}): {
- vol.Optional("default", default="info"): vol.In(
- ["info", "debug", "warning", "error", "critical"]
- ),
- vol.Optional("logs", default={}): {
- str: vol.In(["info", "debug", "warning", "error", "critical"])
- },
- },
- vol.Optional("snapshots", default={}): {
- vol.Optional("retain", default={}): RETAIN_SCHEMA
- },
- vol.Optional("clips", default={}): CLIPS_SCHEMA,
- vol.Optional("record", default={}): {
- vol.Optional("enabled", default=False): bool,
- vol.Optional("retain_days", default=30): int,
- },
- vol.Optional("ffmpeg", default={}): GLOBAL_FFMPEG_SCHEMA,
- vol.Optional("objects", default={}): OBJECTS_SCHEMA,
- vol.Optional("motion", default={}): MOTION_SCHEMA,
- vol.Optional("detect", default={}): GLOBAL_DETECT_SCHEMA,
- vol.Optional("birdseye", default={}): BIRDSEYE_SCHEMA,
- vol.Required("cameras", default={}): CAMERAS_SCHEMA,
- vol.Optional("environment_vars", default={}): {str: str},
- }
- )
- @dataclasses.dataclass(frozen=True)
- class DatabaseConfig:
- path: str
- @classmethod
- def build(cls, config) -> DatabaseConfig:
- return DatabaseConfig(config["path"])
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- @dataclasses.dataclass(frozen=True)
- class ModelConfig:
- width: int
- height: int
- @classmethod
- def build(cls, config) -> ModelConfig:
- return ModelConfig(config["width"], config["height"])
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- @dataclasses.dataclass(frozen=True)
- class LoggerConfig:
- default: str
- logs: Dict[str, str]
- @classmethod
- def build(cls, config) -> LoggerConfig:
- return LoggerConfig(
- config["default"].upper(),
- {k: v.upper() for k, v in config["logs"].items()},
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- @dataclasses.dataclass(frozen=True)
- class SnapshotsConfig:
- retain: RetainConfig
- @classmethod
- def build(cls, config) -> SnapshotsConfig:
- return SnapshotsConfig(RetainConfig.build(config["retain"]))
- def to_dict(self) -> Dict[str, Any]:
- return {"retain": self.retain.to_dict()}
- @dataclasses.dataclass
- class RecordConfig:
- enabled: bool
- retain_days: int
- @classmethod
- def build(cls, config, global_config) -> RecordConfig:
- return RecordConfig(
- config.get("enabled", global_config["enabled"]),
- config.get("retain_days", global_config["retain_days"]),
- )
- def to_dict(self) -> Dict[str, Any]:
- return dataclasses.asdict(self)
- class FrigateConfig:
- def __init__(self, config_file=None, config=None) -> None:
- if config is None and config_file is None:
- raise ValueError("config or config_file must be defined")
- elif not config_file is None:
- config = self._load_file(config_file)
- config = FRIGATE_CONFIG_SCHEMA(config)
- config = self._sub_env_vars(config)
- self._database = DatabaseConfig.build(config["database"])
- self._model = ModelConfig.build(config["model"])
- self._detectors = {
- name: DetectorConfig.build(d) for name, d in config["detectors"].items()
- }
- self._mqtt = MqttConfig.build(config["mqtt"])
- self._clips = ClipsConfig.build(config["clips"])
- self._snapshots = SnapshotsConfig.build(config["snapshots"])
- self._birdseye = BirdseyeConfig.build(config["birdseye"])
- self._cameras = {
- name: CameraConfig.build(name, c, config)
- for name, c in config["cameras"].items()
- }
- self._logger = LoggerConfig.build(config["logger"])
- self._environment_vars = config["environment_vars"]
- def _sub_env_vars(self, config):
- frigate_env_vars = {
- k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")
- }
- if "password" in config["mqtt"]:
- config["mqtt"]["password"] = config["mqtt"]["password"].format(
- **frigate_env_vars
- )
- for camera in config["cameras"].values():
- for i in camera["ffmpeg"]["inputs"]:
- i["path"] = i["path"].format(**frigate_env_vars)
- return config
- def _load_file(self, config_file):
- with open(config_file) as f:
- raw_config = f.read()
- if config_file.endswith(".yml"):
- config = yaml.safe_load(raw_config)
- elif config_file.endswith(".json"):
- config = json.loads(raw_config)
- return config
- def to_dict(self) -> Dict[str, Any]:
- return {
- "database": self.database.to_dict(),
- "model": self.model.to_dict(),
- "detectors": {k: d.to_dict() for k, d in self.detectors.items()},
- "mqtt": self.mqtt.to_dict(),
- "clips": self.clips.to_dict(),
- "snapshots": self.snapshots.to_dict(),
- "birdseye": self.birdseye.to_dict(),
- "cameras": {k: c.to_dict() for k, c in self.cameras.items()},
- "logger": self.logger.to_dict(),
- "environment_vars": self._environment_vars,
- }
- @property
- def database(self) -> DatabaseConfig:
- return self._database
- @property
- def model(self) -> ModelConfig:
- return self._model
- @property
- def detectors(self) -> Dict[str, DetectorConfig]:
- return self._detectors
- @property
- def logger(self) -> LoggerConfig:
- return self._logger
- @property
- def mqtt(self) -> MqttConfig:
- return self._mqtt
- @property
- def clips(self) -> ClipsConfig:
- return self._clips
- @property
- def snapshots(self) -> SnapshotsConfig:
- return self._snapshots
- @property
- def birdseye(self) -> BirdseyeConfig:
- return self._birdseye
- @property
- def cameras(self) -> Dict[str, CameraConfig]:
- return self._cameras
- @property
- def environment_vars(self):
- return self._environment_vars
|