config.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. from __future__ import annotations
  2. import json
  3. import logging
  4. import os
  5. from enum import Enum
  6. from typing import Dict, List, Optional, Tuple, Union
  7. import matplotlib.pyplot as plt
  8. import numpy as np
  9. import yaml
  10. from pydantic import BaseModel, Extra, Field, validator
  11. from pydantic.fields import PrivateAttr
  12. from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT
  13. from frigate.util import create_mask, deep_merge, load_labels
  14. logger = logging.getLogger(__name__)
  15. # TODO: Identify what the default format to display timestamps is
  16. DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
  17. # German Style:
  18. # DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
  19. FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
  20. DEFAULT_TRACKED_OBJECTS = ["person"]
  21. DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
  22. class FrigateBaseModel(BaseModel):
  23. class Config:
  24. extra = Extra.forbid
  25. class DetectorTypeEnum(str, Enum):
  26. edgetpu = "edgetpu"
  27. cpu = "cpu"
  28. class DetectorConfig(FrigateBaseModel):
  29. type: DetectorTypeEnum = Field(default=DetectorTypeEnum.cpu, title="Detector Type")
  30. device: str = Field(default="usb", title="Device Type")
  31. num_threads: int = Field(default=3, title="Number of detection threads")
  32. class MqttConfig(FrigateBaseModel):
  33. host: str = Field(title="MQTT Host")
  34. port: int = Field(default=1883, title="MQTT Port")
  35. topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix")
  36. client_id: str = Field(default="frigate", title="MQTT Client ID")
  37. stats_interval: int = Field(default=60, title="MQTT Camera Stats Interval")
  38. user: Optional[str] = Field(title="MQTT Username")
  39. password: Optional[str] = Field(title="MQTT Password")
  40. tls_ca_certs: Optional[str] = Field(title="MQTT TLS CA Certificates")
  41. tls_client_cert: Optional[str] = Field(title="MQTT TLS Client Certificate")
  42. tls_client_key: Optional[str] = Field(title="MQTT TLS Client Key")
  43. tls_insecure: Optional[bool] = Field(title="MQTT TLS Insecure")
  44. @validator("password", pre=True, always=True)
  45. def validate_password(cls, v, values):
  46. if (v is None) != (values["user"] is None):
  47. raise ValueError("Password must be provided with username.")
  48. return v
  49. class RetainModeEnum(str, Enum):
  50. all = "all"
  51. motion = "motion"
  52. active_objects = "active_objects"
  53. class RetainConfig(FrigateBaseModel):
  54. default: float = Field(default=10, title="Default retention period.")
  55. mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.")
  56. objects: Dict[str, float] = Field(
  57. default_factory=dict, title="Object retention period."
  58. )
  59. class EventsConfig(FrigateBaseModel):
  60. max_seconds: int = Field(default=300, title="Maximum event duration.")
  61. pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
  62. post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
  63. required_zones: List[str] = Field(
  64. default_factory=list,
  65. title="List of required zones to be entered in order to save the event.",
  66. )
  67. objects: Optional[List[str]] = Field(
  68. title="List of objects to be detected in order to save the event.",
  69. )
  70. retain: RetainConfig = Field(
  71. default_factory=RetainConfig, title="Event retention settings."
  72. )
  73. class RecordRetainConfig(FrigateBaseModel):
  74. days: float = Field(default=0, title="Default retention period.")
  75. mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
  76. class RecordConfig(FrigateBaseModel):
  77. enabled: bool = Field(default=False, title="Enable record on all cameras.")
  78. expire_interval: int = Field(
  79. default=60,
  80. title="Number of minutes to wait between cleanup runs.",
  81. )
  82. # deprecated - to be removed in a future version
  83. retain_days: Optional[float] = Field(title="Recording retention period in days.")
  84. retain: RecordRetainConfig = Field(
  85. default_factory=RecordRetainConfig, title="Record retention settings."
  86. )
  87. events: EventsConfig = Field(
  88. default_factory=EventsConfig, title="Event specific settings."
  89. )
  90. class MotionConfig(FrigateBaseModel):
  91. threshold: int = Field(
  92. default=25,
  93. title="Motion detection threshold (1-255).",
  94. ge=1,
  95. le=255,
  96. )
  97. improve_contrast: bool = Field(default=False, title="Improve Contrast")
  98. contour_area: Optional[int] = Field(default=30, title="Contour Area")
  99. delta_alpha: float = Field(default=0.2, title="Delta Alpha")
  100. frame_alpha: float = Field(default=0.2, title="Frame Alpha")
  101. frame_height: Optional[int] = Field(default=50, title="Frame Height")
  102. mask: Union[str, List[str]] = Field(
  103. default="", title="Coordinates polygon for the motion mask."
  104. )
  105. class RuntimeMotionConfig(MotionConfig):
  106. raw_mask: Union[str, List[str]] = ""
  107. mask: np.ndarray = None
  108. def __init__(self, **config):
  109. frame_shape = config.get("frame_shape", (1, 1))
  110. mask = config.get("mask", "")
  111. config["raw_mask"] = mask
  112. if mask:
  113. config["mask"] = create_mask(frame_shape, mask)
  114. else:
  115. empty_mask = np.zeros(frame_shape, np.uint8)
  116. empty_mask[:] = 255
  117. config["mask"] = empty_mask
  118. super().__init__(**config)
  119. def dict(self, **kwargs):
  120. ret = super().dict(**kwargs)
  121. if "mask" in ret:
  122. ret["mask"] = ret["raw_mask"]
  123. ret.pop("raw_mask")
  124. return ret
  125. class Config:
  126. arbitrary_types_allowed = True
  127. extra = Extra.ignore
  128. class StationaryMaxFramesConfig(FrigateBaseModel):
  129. default: Optional[int] = Field(title="Default max frames.", ge=1)
  130. objects: Dict[str, int] = Field(
  131. default_factory=dict, title="Object specific max frames."
  132. )
  133. class StationaryConfig(FrigateBaseModel):
  134. interval: Optional[int] = Field(
  135. default=0,
  136. title="Frame interval for checking stationary objects.",
  137. ge=0,
  138. )
  139. threshold: Optional[int] = Field(
  140. title="Number of frames without a position change for an object to be considered stationary",
  141. ge=1,
  142. )
  143. max_frames: StationaryMaxFramesConfig = Field(
  144. default_factory=StationaryMaxFramesConfig,
  145. title="Max frames for stationary objects.",
  146. )
  147. class DetectConfig(FrigateBaseModel):
  148. height: int = Field(default=720, title="Height of the stream for the detect role.")
  149. width: int = Field(default=1280, title="Width of the stream for the detect role.")
  150. fps: int = Field(
  151. default=5, title="Number of frames per second to process through detection."
  152. )
  153. enabled: bool = Field(default=True, title="Detection Enabled.")
  154. max_disappeared: Optional[int] = Field(
  155. title="Maximum number of frames the object can dissapear before detection ends."
  156. )
  157. stationary: StationaryConfig = Field(
  158. default_factory=StationaryConfig,
  159. title="Stationary objects config.",
  160. )
  161. class FilterConfig(FrigateBaseModel):
  162. min_area: int = Field(
  163. default=0, title="Minimum area of bounding box for object to be counted."
  164. )
  165. max_area: int = Field(
  166. default=24000000, title="Maximum area of bounding box for object to be counted."
  167. )
  168. threshold: float = Field(
  169. default=0.7,
  170. title="Average detection confidence threshold for object to be counted.",
  171. )
  172. min_score: float = Field(
  173. default=0.5, title="Minimum detection confidence for object to be counted."
  174. )
  175. mask: Optional[Union[str, List[str]]] = Field(
  176. title="Detection area polygon mask for this filter configuration.",
  177. )
  178. class RuntimeFilterConfig(FilterConfig):
  179. mask: Optional[np.ndarray]
  180. raw_mask: Optional[Union[str, List[str]]]
  181. def __init__(self, **config):
  182. mask = config.get("mask")
  183. config["raw_mask"] = mask
  184. if mask is not None:
  185. config["mask"] = create_mask(config.get("frame_shape", (1, 1)), mask)
  186. super().__init__(**config)
  187. def dict(self, **kwargs):
  188. ret = super().dict(**kwargs)
  189. if "mask" in ret:
  190. ret["mask"] = ret["raw_mask"]
  191. ret.pop("raw_mask")
  192. return ret
  193. class Config:
  194. arbitrary_types_allowed = True
  195. extra = Extra.ignore
  196. # this uses the base model because the color is an extra attribute
  197. class ZoneConfig(BaseModel):
  198. filters: Dict[str, FilterConfig] = Field(
  199. default_factory=dict, title="Zone filters."
  200. )
  201. coordinates: Union[str, List[str]] = Field(
  202. title="Coordinates polygon for the defined zone."
  203. )
  204. objects: List[str] = Field(
  205. default_factory=list,
  206. title="List of objects that can trigger the zone.",
  207. )
  208. _color: Optional[Tuple[int, int, int]] = PrivateAttr()
  209. _contour: np.ndarray = PrivateAttr()
  210. @property
  211. def color(self) -> Tuple[int, int, int]:
  212. return self._color
  213. @property
  214. def contour(self) -> np.ndarray:
  215. return self._contour
  216. def __init__(self, **config):
  217. super().__init__(**config)
  218. self._color = config.get("color", (0, 0, 0))
  219. coordinates = config["coordinates"]
  220. if isinstance(coordinates, list):
  221. self._contour = np.array(
  222. [[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates]
  223. )
  224. elif isinstance(coordinates, str):
  225. points = coordinates.split(",")
  226. self._contour = np.array(
  227. [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
  228. )
  229. else:
  230. self._contour = np.array([])
  231. class ObjectConfig(FrigateBaseModel):
  232. track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
  233. filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.")
  234. mask: Union[str, List[str]] = Field(default="", title="Object mask.")
  235. class BirdseyeModeEnum(str, Enum):
  236. objects = "objects"
  237. motion = "motion"
  238. continuous = "continuous"
  239. class BirdseyeConfig(FrigateBaseModel):
  240. enabled: bool = Field(default=True, title="Enable birdseye view.")
  241. width: int = Field(default=1280, title="Birdseye width.")
  242. height: int = Field(default=720, title="Birdseye height.")
  243. quality: int = Field(
  244. default=8,
  245. title="Encoding quality.",
  246. ge=1,
  247. le=31,
  248. )
  249. mode: BirdseyeModeEnum = Field(
  250. default=BirdseyeModeEnum.objects, title="Tracking mode."
  251. )
  252. FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
  253. FFMPEG_INPUT_ARGS_DEFAULT = [
  254. "-avoid_negative_ts",
  255. "make_zero",
  256. "-fflags",
  257. "+genpts+discardcorrupt",
  258. "-rtsp_transport",
  259. "tcp",
  260. "-stimeout",
  261. "5000000",
  262. "-use_wallclock_as_timestamps",
  263. "1",
  264. ]
  265. DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "rawvideo", "-pix_fmt", "yuv420p"]
  266. RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
  267. RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
  268. "-f",
  269. "segment",
  270. "-segment_time",
  271. "10",
  272. "-segment_format",
  273. "mp4",
  274. "-reset_timestamps",
  275. "1",
  276. "-strftime",
  277. "1",
  278. "-c",
  279. "copy",
  280. "-an",
  281. ]
  282. class FfmpegOutputArgsConfig(FrigateBaseModel):
  283. detect: Union[str, List[str]] = Field(
  284. default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
  285. title="Detect role FFmpeg output arguments.",
  286. )
  287. record: Union[str, List[str]] = Field(
  288. default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
  289. title="Record role FFmpeg output arguments.",
  290. )
  291. rtmp: Union[str, List[str]] = Field(
  292. default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT,
  293. title="RTMP role FFmpeg output arguments.",
  294. )
  295. class FfmpegConfig(FrigateBaseModel):
  296. global_args: Union[str, List[str]] = Field(
  297. default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments."
  298. )
  299. hwaccel_args: Union[str, List[str]] = Field(
  300. default_factory=list, title="FFmpeg hardware acceleration arguments."
  301. )
  302. input_args: Union[str, List[str]] = Field(
  303. default=FFMPEG_INPUT_ARGS_DEFAULT, title="FFmpeg input arguments."
  304. )
  305. output_args: FfmpegOutputArgsConfig = Field(
  306. default_factory=FfmpegOutputArgsConfig,
  307. title="FFmpeg output arguments per role.",
  308. )
  309. class CameraRoleEnum(str, Enum):
  310. record = "record"
  311. rtmp = "rtmp"
  312. detect = "detect"
  313. class CameraInput(FrigateBaseModel):
  314. path: str = Field(title="Camera input path.")
  315. roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.")
  316. global_args: Union[str, List[str]] = Field(
  317. default_factory=list, title="FFmpeg global arguments."
  318. )
  319. hwaccel_args: Union[str, List[str]] = Field(
  320. default_factory=list, title="FFmpeg hardware acceleration arguments."
  321. )
  322. input_args: Union[str, List[str]] = Field(
  323. default_factory=list, title="FFmpeg input arguments."
  324. )
  325. class CameraFfmpegConfig(FfmpegConfig):
  326. inputs: List[CameraInput] = Field(title="Camera inputs.")
  327. @validator("inputs")
  328. def validate_roles(cls, v):
  329. roles = [role for i in v for role in i.roles]
  330. roles_set = set(roles)
  331. if len(roles) > len(roles_set):
  332. raise ValueError("Each input role may only be used once.")
  333. if not "detect" in roles:
  334. raise ValueError("The detect role is required.")
  335. return v
  336. class SnapshotsConfig(FrigateBaseModel):
  337. enabled: bool = Field(default=False, title="Snapshots enabled.")
  338. clean_copy: bool = Field(
  339. default=True, title="Create a clean copy of the snapshot image."
  340. )
  341. timestamp: bool = Field(
  342. default=False, title="Add a timestamp overlay on the snapshot."
  343. )
  344. bounding_box: bool = Field(
  345. default=True, title="Add a bounding box overlay on the snapshot."
  346. )
  347. crop: bool = Field(default=False, title="Crop the snapshot to the detected object.")
  348. required_zones: List[str] = Field(
  349. default_factory=list,
  350. title="List of required zones to be entered in order to save a snapshot.",
  351. )
  352. height: Optional[int] = Field(title="Snapshot image height.")
  353. retain: RetainConfig = Field(
  354. default_factory=RetainConfig, title="Snapshot retention."
  355. )
  356. quality: int = Field(
  357. default=70,
  358. title="Quality of the encoded jpeg (0-100).",
  359. ge=0,
  360. le=100,
  361. )
  362. class ColorConfig(FrigateBaseModel):
  363. red: int = Field(default=255, ge=0, le=255, title="Red")
  364. green: int = Field(default=255, ge=0, le=255, title="Green")
  365. blue: int = Field(default=255, ge=0, le=255, title="Blue")
  366. class TimestampPositionEnum(str, Enum):
  367. tl = "tl"
  368. tr = "tr"
  369. bl = "bl"
  370. br = "br"
  371. class TimestampEffectEnum(str, Enum):
  372. solid = "solid"
  373. shadow = "shadow"
  374. class TimestampStyleConfig(FrigateBaseModel):
  375. position: TimestampPositionEnum = Field(
  376. default=TimestampPositionEnum.tl, title="Timestamp position."
  377. )
  378. format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.")
  379. color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.")
  380. thickness: int = Field(default=2, title="Timestamp thickness.")
  381. effect: Optional[TimestampEffectEnum] = Field(title="Timestamp effect.")
  382. class CameraMqttConfig(FrigateBaseModel):
  383. enabled: bool = Field(default=True, title="Send image over MQTT.")
  384. timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.")
  385. bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.")
  386. crop: bool = Field(default=True, title="Crop MQTT image to detected object.")
  387. height: int = Field(default=270, title="MQTT image height.")
  388. required_zones: List[str] = Field(
  389. default_factory=list,
  390. title="List of required zones to be entered in order to send the image.",
  391. )
  392. quality: int = Field(
  393. default=70,
  394. title="Quality of the encoded jpeg (0-100).",
  395. ge=0,
  396. le=100,
  397. )
  398. class RtmpConfig(FrigateBaseModel):
  399. enabled: bool = Field(default=True, title="RTMP restreaming enabled.")
  400. class CameraLiveConfig(FrigateBaseModel):
  401. height: int = Field(default=720, title="Live camera view height")
  402. quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
  403. class CameraConfig(FrigateBaseModel):
  404. name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
  405. ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
  406. best_image_timeout: int = Field(
  407. default=60,
  408. title="How long to wait for the image with the highest confidence score.",
  409. )
  410. zones: Dict[str, ZoneConfig] = Field(
  411. default_factory=dict, title="Zone configuration."
  412. )
  413. record: RecordConfig = Field(
  414. default_factory=RecordConfig, title="Record configuration."
  415. )
  416. rtmp: RtmpConfig = Field(
  417. default_factory=RtmpConfig, title="RTMP restreaming configuration."
  418. )
  419. live: CameraLiveConfig = Field(
  420. default_factory=CameraLiveConfig, title="Live playback settings."
  421. )
  422. snapshots: SnapshotsConfig = Field(
  423. default_factory=SnapshotsConfig, title="Snapshot configuration."
  424. )
  425. mqtt: CameraMqttConfig = Field(
  426. default_factory=CameraMqttConfig, title="MQTT configuration."
  427. )
  428. objects: ObjectConfig = Field(
  429. default_factory=ObjectConfig, title="Object configuration."
  430. )
  431. motion: Optional[MotionConfig] = Field(title="Motion detection configuration.")
  432. detect: DetectConfig = Field(
  433. default_factory=DetectConfig, title="Object detection configuration."
  434. )
  435. timestamp_style: TimestampStyleConfig = Field(
  436. default_factory=TimestampStyleConfig, title="Timestamp style configuration."
  437. )
  438. _ffmpeg_cmds: List[Dict[str, List[str]]] = PrivateAttr()
  439. def __init__(self, **config):
  440. # Set zone colors
  441. if "zones" in config:
  442. colors = plt.cm.get_cmap("tab10", len(config["zones"]))
  443. config["zones"] = {
  444. name: {**z, "color": tuple(round(255 * c) for c in colors(idx)[:3])}
  445. for idx, (name, z) in enumerate(config["zones"].items())
  446. }
  447. # add roles to the input if there is only one
  448. if len(config["ffmpeg"]["inputs"]) == 1:
  449. config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
  450. super().__init__(**config)
  451. @property
  452. def frame_shape(self) -> Tuple[int, int]:
  453. return self.detect.height, self.detect.width
  454. @property
  455. def frame_shape_yuv(self) -> Tuple[int, int]:
  456. return self.detect.height * 3 // 2, self.detect.width
  457. @property
  458. def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
  459. return self._ffmpeg_cmds
  460. def create_ffmpeg_cmds(self):
  461. if "_ffmpeg_cmds" in self:
  462. return
  463. ffmpeg_cmds = []
  464. for ffmpeg_input in self.ffmpeg.inputs:
  465. ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
  466. if ffmpeg_cmd is None:
  467. continue
  468. ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
  469. self._ffmpeg_cmds = ffmpeg_cmds
  470. def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
  471. ffmpeg_output_args = []
  472. if "detect" in ffmpeg_input.roles:
  473. detect_args = (
  474. self.ffmpeg.output_args.detect
  475. if isinstance(self.ffmpeg.output_args.detect, list)
  476. else self.ffmpeg.output_args.detect.split(" ")
  477. )
  478. ffmpeg_output_args = (
  479. [
  480. "-r",
  481. str(self.detect.fps),
  482. "-s",
  483. f"{self.detect.width}x{self.detect.height}",
  484. ]
  485. + detect_args
  486. + ffmpeg_output_args
  487. + ["pipe:"]
  488. )
  489. if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
  490. rtmp_args = (
  491. self.ffmpeg.output_args.rtmp
  492. if isinstance(self.ffmpeg.output_args.rtmp, list)
  493. else self.ffmpeg.output_args.rtmp.split(" ")
  494. )
  495. ffmpeg_output_args = (
  496. rtmp_args + [f"rtmp://127.0.0.1/live/{self.name}"] + ffmpeg_output_args
  497. )
  498. if "record" in ffmpeg_input.roles and self.record.enabled:
  499. record_args = (
  500. self.ffmpeg.output_args.record
  501. if isinstance(self.ffmpeg.output_args.record, list)
  502. else self.ffmpeg.output_args.record.split(" ")
  503. )
  504. ffmpeg_output_args = (
  505. record_args
  506. + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
  507. + ffmpeg_output_args
  508. )
  509. # if there arent any outputs enabled for this input
  510. if len(ffmpeg_output_args) == 0:
  511. return None
  512. global_args = ffmpeg_input.global_args or self.ffmpeg.global_args
  513. hwaccel_args = ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args
  514. input_args = ffmpeg_input.input_args or self.ffmpeg.input_args
  515. global_args = (
  516. global_args if isinstance(global_args, list) else global_args.split(" ")
  517. )
  518. hwaccel_args = (
  519. hwaccel_args if isinstance(hwaccel_args, list) else hwaccel_args.split(" ")
  520. )
  521. input_args = (
  522. input_args if isinstance(input_args, list) else input_args.split(" ")
  523. )
  524. cmd = (
  525. ["ffmpeg"]
  526. + global_args
  527. + hwaccel_args
  528. + input_args
  529. + ["-i", ffmpeg_input.path]
  530. + ffmpeg_output_args
  531. )
  532. return [part for part in cmd if part != ""]
  533. class DatabaseConfig(FrigateBaseModel):
  534. path: str = Field(
  535. default=os.path.join(BASE_DIR, "frigate.db"), title="Database path."
  536. )
  537. class ModelConfig(FrigateBaseModel):
  538. path: Optional[str] = Field(title="Custom Object detection model path.")
  539. labelmap_path: Optional[str] = Field(title="Label map for custom object detector.")
  540. width: int = Field(default=320, title="Object detection model input width.")
  541. height: int = Field(default=320, title="Object detection model input height.")
  542. labelmap: Dict[int, str] = Field(
  543. default_factory=dict, title="Labelmap customization."
  544. )
  545. _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
  546. _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()
  547. @property
  548. def merged_labelmap(self) -> Dict[int, str]:
  549. return self._merged_labelmap
  550. @property
  551. def colormap(self) -> Dict[int, Tuple[int, int, int]]:
  552. return self._colormap
  553. def __init__(self, **config):
  554. super().__init__(**config)
  555. self._merged_labelmap = {
  556. **load_labels(config.get("labelmap_path", "/labelmap.txt")),
  557. **config.get("labelmap", {}),
  558. }
  559. cmap = plt.cm.get_cmap("tab10", len(self._merged_labelmap.keys()))
  560. self._colormap = {}
  561. for key, val in self._merged_labelmap.items():
  562. self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
  563. class LogLevelEnum(str, Enum):
  564. debug = "debug"
  565. info = "info"
  566. warning = "warning"
  567. error = "error"
  568. critical = "critical"
  569. class LoggerConfig(FrigateBaseModel):
  570. default: LogLevelEnum = Field(
  571. default=LogLevelEnum.info, title="Default logging level."
  572. )
  573. logs: Dict[str, LogLevelEnum] = Field(
  574. default_factory=dict, title="Log level for specified processes."
  575. )
  576. class FrigateConfig(FrigateBaseModel):
  577. mqtt: MqttConfig = Field(title="MQTT Configuration.")
  578. database: DatabaseConfig = Field(
  579. default_factory=DatabaseConfig, title="Database configuration."
  580. )
  581. environment_vars: Dict[str, str] = Field(
  582. default_factory=dict, title="Frigate environment variables."
  583. )
  584. model: ModelConfig = Field(
  585. default_factory=ModelConfig, title="Detection model configuration."
  586. )
  587. detectors: Dict[str, DetectorConfig] = Field(
  588. default={name: DetectorConfig(**d) for name, d in DEFAULT_DETECTORS.items()},
  589. title="Detector hardware configuration.",
  590. )
  591. logger: LoggerConfig = Field(
  592. default_factory=LoggerConfig, title="Logging configuration."
  593. )
  594. record: RecordConfig = Field(
  595. default_factory=RecordConfig, title="Global record configuration."
  596. )
  597. snapshots: SnapshotsConfig = Field(
  598. default_factory=SnapshotsConfig, title="Global snapshots configuration."
  599. )
  600. live: CameraLiveConfig = Field(
  601. default_factory=CameraLiveConfig, title="Global live configuration."
  602. )
  603. rtmp: RtmpConfig = Field(
  604. default_factory=RtmpConfig, title="Global RTMP restreaming configuration."
  605. )
  606. birdseye: BirdseyeConfig = Field(
  607. default_factory=BirdseyeConfig, title="Birdseye configuration."
  608. )
  609. ffmpeg: FfmpegConfig = Field(
  610. default_factory=FfmpegConfig, title="Global FFmpeg configuration."
  611. )
  612. objects: ObjectConfig = Field(
  613. default_factory=ObjectConfig, title="Global object configuration."
  614. )
  615. motion: Optional[MotionConfig] = Field(
  616. title="Global motion detection configuration."
  617. )
  618. detect: DetectConfig = Field(
  619. default_factory=DetectConfig, title="Global object tracking configuration."
  620. )
  621. cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
  622. timestamp_style: TimestampStyleConfig = Field(
  623. default_factory=TimestampStyleConfig,
  624. title="Global timestamp style configuration.",
  625. )
  626. @property
  627. def runtime_config(self) -> FrigateConfig:
  628. """Merge camera config with globals."""
  629. config = self.copy(deep=True)
  630. # MQTT password substitution
  631. if config.mqtt.password:
  632. config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS)
  633. # Global config to propegate down to camera level
  634. global_config = config.dict(
  635. include={
  636. "record": ...,
  637. "snapshots": ...,
  638. "live": ...,
  639. "rtmp": ...,
  640. "objects": ...,
  641. "motion": ...,
  642. "detect": ...,
  643. "ffmpeg": ...,
  644. "timestamp_style": ...,
  645. },
  646. exclude_unset=True,
  647. )
  648. for name, camera in config.cameras.items():
  649. merged_config = deep_merge(camera.dict(exclude_unset=True), global_config)
  650. camera_config: CameraConfig = CameraConfig.parse_obj(
  651. {"name": name, **merged_config}
  652. )
  653. # Default max_disappeared configuration
  654. max_disappeared = camera_config.detect.fps * 5
  655. if camera_config.detect.max_disappeared is None:
  656. camera_config.detect.max_disappeared = max_disappeared
  657. # Default stationary_threshold configuration
  658. stationary_threshold = camera_config.detect.fps * 10
  659. if camera_config.detect.stationary.threshold is None:
  660. camera_config.detect.stationary.threshold = stationary_threshold
  661. # FFMPEG input substitution
  662. for input in camera_config.ffmpeg.inputs:
  663. input.path = input.path.format(**FRIGATE_ENV_VARS)
  664. # Add default filters
  665. object_keys = camera_config.objects.track
  666. if camera_config.objects.filters is None:
  667. camera_config.objects.filters = {}
  668. object_keys = object_keys - camera_config.objects.filters.keys()
  669. for key in object_keys:
  670. camera_config.objects.filters[key] = FilterConfig()
  671. # Apply global object masks and convert masks to numpy array
  672. for object, filter in camera_config.objects.filters.items():
  673. if camera_config.objects.mask:
  674. filter_mask = []
  675. if filter.mask is not None:
  676. filter_mask = (
  677. filter.mask
  678. if isinstance(filter.mask, list)
  679. else [filter.mask]
  680. )
  681. object_mask = (
  682. camera_config.objects.mask
  683. if isinstance(camera_config.objects.mask, list)
  684. else [camera_config.objects.mask]
  685. )
  686. filter.mask = filter_mask + object_mask
  687. # Set runtime filter to create masks
  688. camera_config.objects.filters[object] = RuntimeFilterConfig(
  689. frame_shape=camera_config.frame_shape,
  690. **filter.dict(exclude_unset=True),
  691. )
  692. # Convert motion configuration
  693. if camera_config.motion is None:
  694. camera_config.motion = RuntimeMotionConfig(
  695. frame_shape=camera_config.frame_shape
  696. )
  697. else:
  698. camera_config.motion = RuntimeMotionConfig(
  699. frame_shape=camera_config.frame_shape,
  700. raw_mask=camera_config.motion.mask,
  701. **camera_config.motion.dict(exclude_unset=True),
  702. )
  703. # check runtime config
  704. assigned_roles = list(
  705. set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
  706. )
  707. if camera_config.record.enabled and not "record" in assigned_roles:
  708. raise ValueError(
  709. f"Camera {name} has record enabled, but record is not assigned to an input."
  710. )
  711. if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
  712. raise ValueError(
  713. f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
  714. )
  715. # backwards compatibility for retain_days
  716. if not camera_config.record.retain_days is None:
  717. logger.warning(
  718. "The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'"
  719. )
  720. if camera_config.record.retain.days == 0:
  721. camera_config.record.retain.days = camera_config.record.retain_days
  722. # warning if the higher level record mode is potentially more restrictive than the events
  723. rank_map = {
  724. RetainModeEnum.all: 0,
  725. RetainModeEnum.motion: 1,
  726. RetainModeEnum.active_objects: 2,
  727. }
  728. if (
  729. camera_config.record.retain.days != 0
  730. and rank_map[camera_config.record.retain.mode]
  731. > rank_map[camera_config.record.events.retain.mode]
  732. ):
  733. logger.warning(
  734. f"{name}: Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied."
  735. )
  736. # generage the ffmpeg commands
  737. camera_config.create_ffmpeg_cmds()
  738. config.cameras[name] = camera_config
  739. return config
  740. @validator("cameras")
  741. def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
  742. zones = [zone for camera in v.values() for zone in camera.zones.keys()]
  743. for zone in zones:
  744. if zone in v.keys():
  745. raise ValueError("Zones cannot share names with cameras")
  746. return v
  747. @classmethod
  748. def parse_file(cls, config_file):
  749. with open(config_file) as f:
  750. raw_config = f.read()
  751. if config_file.endswith(YAML_EXT):
  752. config = yaml.safe_load(raw_config)
  753. elif config_file.endswith(".json"):
  754. config = json.loads(raw_config)
  755. return cls.parse_obj(config)