config.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259
  1. import base64
  2. import json
  3. import logging
  4. import os
  5. from typing import Dict
  6. import cv2
  7. import matplotlib.pyplot as plt
  8. import numpy as np
  9. import voluptuous as vol
  10. import yaml
  11. from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
  12. from frigate.util import create_mask
  13. logger = logging.getLogger(__name__)
  14. DEFAULT_TRACKED_OBJECTS = ["person"]
  15. DETECTORS_SCHEMA = vol.Schema(
  16. {
  17. vol.Required(str): {
  18. vol.Required("type", default="edgetpu"): vol.In(["cpu", "edgetpu"]),
  19. vol.Optional("device", default="usb"): str,
  20. vol.Optional("num_threads", default=3): int,
  21. }
  22. }
  23. )
  24. DEFAULT_DETECTORS = {"coral": {"type": "edgetpu", "device": "usb"}}
  25. MQTT_SCHEMA = vol.Schema(
  26. {
  27. vol.Required("host"): str,
  28. vol.Optional("port", default=1883): int,
  29. vol.Optional("topic_prefix", default="frigate"): str,
  30. vol.Optional("client_id", default="frigate"): str,
  31. vol.Optional("stats_interval", default=60): int,
  32. "user": str,
  33. "password": str,
  34. }
  35. )
  36. RETAIN_SCHEMA = vol.Schema(
  37. {vol.Required("default", default=10): int, "objects": {str: int}}
  38. )
  39. CLIPS_SCHEMA = vol.Schema(
  40. {
  41. vol.Optional("max_seconds", default=300): int,
  42. "tmpfs_cache_size": str,
  43. vol.Optional("retain", default={}): RETAIN_SCHEMA,
  44. }
  45. )
  46. FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
  47. FFMPEG_INPUT_ARGS_DEFAULT = [
  48. "-avoid_negative_ts",
  49. "make_zero",
  50. "-fflags",
  51. "+genpts+discardcorrupt",
  52. "-rtsp_transport",
  53. "tcp",
  54. "-stimeout",
  55. "5000000",
  56. "-use_wallclock_as_timestamps",
  57. "1",
  58. ]
  59. DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "rawvideo", "-pix_fmt", "yuv420p"]
  60. RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
  61. SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = [
  62. "-f",
  63. "segment",
  64. "-segment_time",
  65. "10",
  66. "-segment_format",
  67. "mp4",
  68. "-reset_timestamps",
  69. "1",
  70. "-strftime",
  71. "1",
  72. "-c",
  73. "copy",
  74. "-an",
  75. ]
  76. RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
  77. "-f",
  78. "segment",
  79. "-segment_time",
  80. "60",
  81. "-segment_format",
  82. "mp4",
  83. "-reset_timestamps",
  84. "1",
  85. "-strftime",
  86. "1",
  87. "-c",
  88. "copy",
  89. "-an",
  90. ]
  91. GLOBAL_FFMPEG_SCHEMA = vol.Schema(
  92. {
  93. vol.Optional("global_args", default=FFMPEG_GLOBAL_ARGS_DEFAULT): vol.Any(
  94. str, [str]
  95. ),
  96. vol.Optional("hwaccel_args", default=[]): vol.Any(str, [str]),
  97. vol.Optional("input_args", default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(
  98. str, [str]
  99. ),
  100. vol.Optional("output_args", default={}): {
  101. vol.Optional("detect", default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  102. str, [str]
  103. ),
  104. vol.Optional("record", default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  105. str, [str]
  106. ),
  107. vol.Optional(
  108. "clips", default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT
  109. ): vol.Any(str, [str]),
  110. vol.Optional("rtmp", default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  111. str, [str]
  112. ),
  113. },
  114. }
  115. )
  116. MOTION_SCHEMA = vol.Schema(
  117. {
  118. "mask": vol.Any(str, [str]),
  119. "threshold": vol.Range(min=1, max=255),
  120. "contour_area": int,
  121. "delta_alpha": float,
  122. "frame_alpha": float,
  123. "frame_height": int,
  124. }
  125. )
  126. DETECT_SCHEMA = vol.Schema({"max_disappeared": int})
  127. FILTER_SCHEMA = vol.Schema(
  128. {
  129. str: {
  130. "min_area": int,
  131. "max_area": int,
  132. "threshold": float,
  133. }
  134. }
  135. )
  136. def filters_for_all_tracked_objects(object_config):
  137. for tracked_object in object_config.get("track", DEFAULT_TRACKED_OBJECTS):
  138. if not "filters" in object_config:
  139. object_config["filters"] = {}
  140. if not tracked_object in object_config["filters"]:
  141. object_config["filters"][tracked_object] = {}
  142. return object_config
  143. OBJECTS_SCHEMA = vol.Schema(
  144. vol.All(
  145. filters_for_all_tracked_objects,
  146. {
  147. "track": [str],
  148. "mask": vol.Any(str, [str]),
  149. vol.Optional("filters", default={}): FILTER_SCHEMA.extend(
  150. {
  151. str: {
  152. "min_score": float,
  153. "mask": vol.Any(str, [str]),
  154. }
  155. }
  156. ),
  157. },
  158. )
  159. )
  160. def each_role_used_once(inputs):
  161. roles = [role for i in inputs for role in i["roles"]]
  162. roles_set = set(roles)
  163. if len(roles) > len(roles_set):
  164. raise ValueError
  165. return inputs
  166. def detect_is_required(inputs):
  167. roles = [role for i in inputs for role in i["roles"]]
  168. if not "detect" in roles:
  169. raise ValueError
  170. return inputs
  171. CAMERA_FFMPEG_SCHEMA = vol.Schema(
  172. {
  173. vol.Required("inputs"): vol.All(
  174. [
  175. {
  176. vol.Required("path"): str,
  177. vol.Required("roles"): ["detect", "clips", "record", "rtmp"],
  178. "global_args": vol.Any(str, [str]),
  179. "hwaccel_args": vol.Any(str, [str]),
  180. "input_args": vol.Any(str, [str]),
  181. }
  182. ],
  183. vol.Msg(each_role_used_once, msg="Each input role may only be used once"),
  184. vol.Msg(detect_is_required, msg="The detect role is required"),
  185. ),
  186. "global_args": vol.Any(str, [str]),
  187. "hwaccel_args": vol.Any(str, [str]),
  188. "input_args": vol.Any(str, [str]),
  189. "output_args": {
  190. vol.Optional("detect", default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  191. str, [str]
  192. ),
  193. vol.Optional("record", default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  194. str, [str]
  195. ),
  196. vol.Optional(
  197. "clips", default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT
  198. ): vol.Any(str, [str]),
  199. vol.Optional("rtmp", default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(
  200. str, [str]
  201. ),
  202. },
  203. }
  204. )
  205. def ensure_zones_and_cameras_have_different_names(cameras):
  206. zones = [zone for camera in cameras.values() for zone in camera["zones"].keys()]
  207. for zone in zones:
  208. if zone in cameras.keys():
  209. raise ValueError
  210. return cameras
  211. CAMERAS_SCHEMA = vol.Schema(
  212. vol.All(
  213. {
  214. str: {
  215. vol.Required("ffmpeg"): CAMERA_FFMPEG_SCHEMA,
  216. vol.Required("height"): int,
  217. vol.Required("width"): int,
  218. "fps": int,
  219. vol.Optional("best_image_timeout", default=60): int,
  220. vol.Optional("zones", default={}): {
  221. str: {
  222. vol.Required("coordinates"): vol.Any(str, [str]),
  223. vol.Optional("filters", default={}): FILTER_SCHEMA,
  224. }
  225. },
  226. vol.Optional("clips", default={}): {
  227. vol.Optional("enabled", default=False): bool,
  228. vol.Optional("pre_capture", default=5): int,
  229. vol.Optional("post_capture", default=5): int,
  230. vol.Optional("required_zones", default=[]): [str],
  231. "objects": [str],
  232. vol.Optional("retain", default={}): RETAIN_SCHEMA,
  233. },
  234. vol.Optional("record", default={}): {
  235. "enabled": bool,
  236. "retain_days": int,
  237. },
  238. vol.Optional("rtmp", default={}): {
  239. vol.Required("enabled", default=True): bool,
  240. },
  241. vol.Optional("snapshots", default={}): {
  242. vol.Optional("enabled", default=False): bool,
  243. vol.Optional("timestamp", default=False): bool,
  244. vol.Optional("bounding_box", default=False): bool,
  245. vol.Optional("crop", default=False): bool,
  246. vol.Optional("required_zones", default=[]): [str],
  247. "height": int,
  248. vol.Optional("retain", default={}): RETAIN_SCHEMA,
  249. },
  250. vol.Optional("mqtt", default={}): {
  251. vol.Optional("enabled", default=True): bool,
  252. vol.Optional("timestamp", default=True): bool,
  253. vol.Optional("bounding_box", default=True): bool,
  254. vol.Optional("crop", default=True): bool,
  255. vol.Optional("height", default=270): int,
  256. vol.Optional("required_zones", default=[]): [str],
  257. },
  258. vol.Optional("objects", default={}): OBJECTS_SCHEMA,
  259. vol.Optional("motion", default={}): MOTION_SCHEMA,
  260. vol.Optional("detect", default={}): DETECT_SCHEMA.extend(
  261. {vol.Optional("enabled", default=True): bool}
  262. ),
  263. }
  264. },
  265. vol.Msg(
  266. ensure_zones_and_cameras_have_different_names,
  267. msg="Zones cannot share names with cameras",
  268. ),
  269. )
  270. )
  271. FRIGATE_CONFIG_SCHEMA = vol.Schema(
  272. {
  273. vol.Optional("database", default={}): {
  274. vol.Optional("path", default=os.path.join(CLIPS_DIR, "frigate.db")): str
  275. },
  276. vol.Optional("model", default={"width": 320, "height": 320}): {
  277. vol.Required("width"): int,
  278. vol.Required("height"): int,
  279. },
  280. vol.Optional("detectors", default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
  281. "mqtt": MQTT_SCHEMA,
  282. vol.Optional("logger", default={"default": "info", "logs": {}}): {
  283. vol.Optional("default", default="info"): vol.In(
  284. ["info", "debug", "warning", "error", "critical"]
  285. ),
  286. vol.Optional("logs", default={}): {
  287. str: vol.In(["info", "debug", "warning", "error", "critical"])
  288. },
  289. },
  290. vol.Optional("snapshots", default={}): {
  291. vol.Optional("retain", default={}): RETAIN_SCHEMA
  292. },
  293. vol.Optional("clips", default={}): CLIPS_SCHEMA,
  294. vol.Optional("record", default={}): {
  295. vol.Optional("enabled", default=False): bool,
  296. vol.Optional("retain_days", default=30): int,
  297. },
  298. vol.Optional("ffmpeg", default={}): GLOBAL_FFMPEG_SCHEMA,
  299. vol.Optional("objects", default={}): OBJECTS_SCHEMA,
  300. vol.Optional("motion", default={}): MOTION_SCHEMA,
  301. vol.Optional("detect", default={}): DETECT_SCHEMA,
  302. vol.Required("cameras", default={}): CAMERAS_SCHEMA,
  303. vol.Optional("environment_vars", default={}): {str: str},
  304. }
  305. )
  306. class DatabaseConfig:
  307. def __init__(self, config):
  308. self._path = config["path"]
  309. @property
  310. def path(self):
  311. return self._path
  312. def to_dict(self):
  313. return {"path": self.path}
  314. class ModelConfig:
  315. def __init__(self, config):
  316. self._width = config["width"]
  317. self._height = config["height"]
  318. @property
  319. def width(self):
  320. return self._width
  321. @property
  322. def height(self):
  323. return self._height
  324. def to_dict(self):
  325. return {"width": self.width, "height": self.height}
  326. class DetectorConfig:
  327. def __init__(self, config):
  328. self._type = config["type"]
  329. self._device = config["device"]
  330. self._num_threads = config["num_threads"]
  331. @property
  332. def type(self):
  333. return self._type
  334. @property
  335. def device(self):
  336. return self._device
  337. @property
  338. def num_threads(self):
  339. return self._num_threads
  340. def to_dict(self):
  341. return {
  342. "type": self.type,
  343. "device": self.device,
  344. "num_threads": self.num_threads,
  345. }
  346. class LoggerConfig:
  347. def __init__(self, config):
  348. self._default = config["default"].upper()
  349. self._logs = {k: v.upper() for k, v in config["logs"].items()}
  350. @property
  351. def default(self):
  352. return self._default
  353. @property
  354. def logs(self):
  355. return self._logs
  356. def to_dict(self):
  357. return {"default": self.default, "logs": self.logs}
  358. class MqttConfig:
  359. def __init__(self, config):
  360. self._host = config["host"]
  361. self._port = config["port"]
  362. self._topic_prefix = config["topic_prefix"]
  363. self._client_id = config["client_id"]
  364. self._user = config.get("user")
  365. self._password = config.get("password")
  366. self._stats_interval = config.get("stats_interval")
  367. @property
  368. def host(self):
  369. return self._host
  370. @property
  371. def port(self):
  372. return self._port
  373. @property
  374. def topic_prefix(self):
  375. return self._topic_prefix
  376. @property
  377. def client_id(self):
  378. return self._client_id
  379. @property
  380. def user(self):
  381. return self._user
  382. @property
  383. def password(self):
  384. return self._password
  385. @property
  386. def stats_interval(self):
  387. return self._stats_interval
  388. def to_dict(self):
  389. return {
  390. "host": self.host,
  391. "port": self.port,
  392. "topic_prefix": self.topic_prefix,
  393. "client_id": self.client_id,
  394. "user": self.user,
  395. "stats_interval": self.stats_interval,
  396. }
  397. class CameraInput:
  398. def __init__(self, camera_config, global_config, ffmpeg_input):
  399. self._path = ffmpeg_input["path"]
  400. self._roles = ffmpeg_input["roles"]
  401. self._global_args = ffmpeg_input.get(
  402. "global_args",
  403. camera_config.get("global_args", global_config["global_args"]),
  404. )
  405. self._hwaccel_args = ffmpeg_input.get(
  406. "hwaccel_args",
  407. camera_config.get("hwaccel_args", global_config["hwaccel_args"]),
  408. )
  409. self._input_args = ffmpeg_input.get(
  410. "input_args", camera_config.get("input_args", global_config["input_args"])
  411. )
  412. @property
  413. def path(self):
  414. return self._path
  415. @property
  416. def roles(self):
  417. return self._roles
  418. @property
  419. def global_args(self):
  420. return (
  421. self._global_args
  422. if isinstance(self._global_args, list)
  423. else self._global_args.split(" ")
  424. )
  425. @property
  426. def hwaccel_args(self):
  427. return (
  428. self._hwaccel_args
  429. if isinstance(self._hwaccel_args, list)
  430. else self._hwaccel_args.split(" ")
  431. )
  432. @property
  433. def input_args(self):
  434. return (
  435. self._input_args
  436. if isinstance(self._input_args, list)
  437. else self._input_args.split(" ")
  438. )
  439. class CameraFfmpegConfig:
  440. def __init__(self, global_config, config):
  441. self._inputs = [CameraInput(config, global_config, i) for i in config["inputs"]]
  442. self._output_args = config.get("output_args", global_config["output_args"])
  443. @property
  444. def inputs(self):
  445. return self._inputs
  446. @property
  447. def output_args(self):
  448. return {
  449. k: v if isinstance(v, list) else v.split(" ")
  450. for k, v in self._output_args.items()
  451. }
  452. class RetainConfig:
  453. def __init__(self, global_config, config):
  454. self._default = config.get("default", global_config.get("default"))
  455. self._objects = config.get("objects", global_config.get("objects", {}))
  456. @property
  457. def default(self):
  458. return self._default
  459. @property
  460. def objects(self):
  461. return self._objects
  462. def to_dict(self):
  463. return {"default": self.default, "objects": self.objects}
  464. class ClipsConfig:
  465. def __init__(self, config):
  466. self._max_seconds = config["max_seconds"]
  467. self._tmpfs_cache_size = config.get("tmpfs_cache_size", "").strip()
  468. self._retain = RetainConfig(config["retain"], config["retain"])
  469. @property
  470. def max_seconds(self):
  471. return self._max_seconds
  472. @property
  473. def tmpfs_cache_size(self):
  474. return self._tmpfs_cache_size
  475. @property
  476. def retain(self):
  477. return self._retain
  478. def to_dict(self):
  479. return {
  480. "max_seconds": self.max_seconds,
  481. "tmpfs_cache_size": self.tmpfs_cache_size,
  482. "retain": self.retain.to_dict(),
  483. }
  484. class SnapshotsConfig:
  485. def __init__(self, config):
  486. self._retain = RetainConfig(config["retain"], config["retain"])
  487. @property
  488. def retain(self):
  489. return self._retain
  490. def to_dict(self):
  491. return {"retain": self.retain.to_dict()}
  492. class RecordConfig:
  493. def __init__(self, global_config, config):
  494. self._enabled = config.get("enabled", global_config["enabled"])
  495. self._retain_days = config.get("retain_days", global_config["retain_days"])
  496. @property
  497. def enabled(self):
  498. return self._enabled
  499. @property
  500. def retain_days(self):
  501. return self._retain_days
  502. def to_dict(self):
  503. return {
  504. "enabled": self.enabled,
  505. "retain_days": self.retain_days,
  506. }
  507. class FilterConfig:
  508. def __init__(self, global_config, config, global_mask=None, frame_shape=None):
  509. self._min_area = config.get("min_area", global_config.get("min_area", 0))
  510. self._max_area = config.get("max_area", global_config.get("max_area", 24000000))
  511. self._threshold = config.get("threshold", global_config.get("threshold", 0.7))
  512. self._min_score = config.get("min_score", global_config.get("min_score", 0.5))
  513. self._raw_mask = []
  514. if global_mask:
  515. if isinstance(global_mask, list):
  516. self._raw_mask += global_mask
  517. elif isinstance(global_mask, str):
  518. self._raw_mask += [global_mask]
  519. mask = config.get("mask")
  520. if mask:
  521. if isinstance(mask, list):
  522. self._raw_mask += mask
  523. elif isinstance(mask, str):
  524. self._raw_mask += [mask]
  525. self._mask = (
  526. create_mask(frame_shape, self._raw_mask) if self._raw_mask else None
  527. )
  528. @property
  529. def min_area(self):
  530. return self._min_area
  531. @property
  532. def max_area(self):
  533. return self._max_area
  534. @property
  535. def threshold(self):
  536. return self._threshold
  537. @property
  538. def min_score(self):
  539. return self._min_score
  540. @property
  541. def mask(self):
  542. return self._mask
  543. def to_dict(self):
  544. return {
  545. "min_area": self.min_area,
  546. "max_area": self.max_area,
  547. "threshold": self.threshold,
  548. "min_score": self.min_score,
  549. "mask": self._raw_mask,
  550. }
  551. class ObjectConfig:
  552. def __init__(self, global_config, config, frame_shape):
  553. self._track = config.get(
  554. "track", global_config.get("track", DEFAULT_TRACKED_OBJECTS)
  555. )
  556. self._raw_mask = config.get("mask")
  557. self._filters = {
  558. name: FilterConfig(
  559. global_config["filters"].get(name, {}),
  560. config["filters"].get(name, {}),
  561. self._raw_mask,
  562. frame_shape,
  563. )
  564. for name in self._track
  565. }
  566. @property
  567. def track(self):
  568. return self._track
  569. @property
  570. def filters(self) -> Dict[str, FilterConfig]:
  571. return self._filters
  572. def to_dict(self):
  573. return {
  574. "track": self.track,
  575. "mask": self._raw_mask,
  576. "filters": {k: f.to_dict() for k, f in self.filters.items()},
  577. }
  578. class CameraSnapshotsConfig:
  579. def __init__(self, global_config, config):
  580. self._enabled = config["enabled"]
  581. self._timestamp = config["timestamp"]
  582. self._bounding_box = config["bounding_box"]
  583. self._crop = config["crop"]
  584. self._height = config.get("height")
  585. self._retain = RetainConfig(
  586. global_config["snapshots"]["retain"], config["retain"]
  587. )
  588. self._required_zones = config["required_zones"]
  589. @property
  590. def enabled(self):
  591. return self._enabled
  592. @property
  593. def timestamp(self):
  594. return self._timestamp
  595. @property
  596. def bounding_box(self):
  597. return self._bounding_box
  598. @property
  599. def crop(self):
  600. return self._crop
  601. @property
  602. def height(self):
  603. return self._height
  604. @property
  605. def retain(self):
  606. return self._retain
  607. @property
  608. def required_zones(self):
  609. return self._required_zones
  610. def to_dict(self):
  611. return {
  612. "enabled": self.enabled,
  613. "timestamp": self.timestamp,
  614. "bounding_box": self.bounding_box,
  615. "crop": self.crop,
  616. "height": self.height,
  617. "retain": self.retain.to_dict(),
  618. "required_zones": self.required_zones,
  619. }
  620. class CameraMqttConfig:
  621. def __init__(self, config):
  622. self._enabled = config["enabled"]
  623. self._timestamp = config["timestamp"]
  624. self._bounding_box = config["bounding_box"]
  625. self._crop = config["crop"]
  626. self._height = config.get("height")
  627. self._required_zones = config["required_zones"]
  628. @property
  629. def enabled(self):
  630. return self._enabled
  631. @property
  632. def timestamp(self):
  633. return self._timestamp
  634. @property
  635. def bounding_box(self):
  636. return self._bounding_box
  637. @property
  638. def crop(self):
  639. return self._crop
  640. @property
  641. def height(self):
  642. return self._height
  643. @property
  644. def required_zones(self):
  645. return self._required_zones
  646. def to_dict(self):
  647. return {
  648. "enabled": self.enabled,
  649. "timestamp": self.timestamp,
  650. "bounding_box": self.bounding_box,
  651. "crop": self.crop,
  652. "height": self.height,
  653. "required_zones": self.required_zones,
  654. }
  655. class CameraClipsConfig:
  656. def __init__(self, global_config, config):
  657. self._enabled = config["enabled"]
  658. self._pre_capture = config["pre_capture"]
  659. self._post_capture = config["post_capture"]
  660. self._objects = config.get("objects")
  661. self._retain = RetainConfig(global_config["clips"]["retain"], config["retain"])
  662. self._required_zones = config["required_zones"]
  663. @property
  664. def enabled(self):
  665. return self._enabled
  666. @property
  667. def pre_capture(self):
  668. return self._pre_capture
  669. @property
  670. def post_capture(self):
  671. return self._post_capture
  672. @property
  673. def objects(self):
  674. return self._objects
  675. @property
  676. def retain(self):
  677. return self._retain
  678. @property
  679. def required_zones(self):
  680. return self._required_zones
  681. def to_dict(self):
  682. return {
  683. "enabled": self.enabled,
  684. "pre_capture": self.pre_capture,
  685. "post_capture": self.post_capture,
  686. "objects": self.objects,
  687. "retain": self.retain.to_dict(),
  688. "required_zones": self.required_zones,
  689. }
  690. class CameraRtmpConfig:
  691. def __init__(self, global_config, config):
  692. self._enabled = config["enabled"]
  693. @property
  694. def enabled(self):
  695. return self._enabled
  696. def to_dict(self):
  697. return {
  698. "enabled": self.enabled,
  699. }
  700. class MotionConfig:
  701. def __init__(self, global_config, config, frame_shape):
  702. self._raw_mask = config.get("mask")
  703. if self._raw_mask:
  704. self._mask = create_mask(frame_shape, self._raw_mask)
  705. else:
  706. default_mask = np.zeros(frame_shape, np.uint8)
  707. default_mask[:] = 255
  708. self._mask = default_mask
  709. self._threshold = config.get("threshold", global_config.get("threshold", 25))
  710. self._contour_area = config.get(
  711. "contour_area", global_config.get("contour_area", 100)
  712. )
  713. self._delta_alpha = config.get(
  714. "delta_alpha", global_config.get("delta_alpha", 0.2)
  715. )
  716. self._frame_alpha = config.get(
  717. "frame_alpha", global_config.get("frame_alpha", 0.2)
  718. )
  719. self._frame_height = config.get(
  720. "frame_height", global_config.get("frame_height", frame_shape[0] // 6)
  721. )
  722. @property
  723. def mask(self):
  724. return self._mask
  725. @property
  726. def threshold(self):
  727. return self._threshold
  728. @property
  729. def contour_area(self):
  730. return self._contour_area
  731. @property
  732. def delta_alpha(self):
  733. return self._delta_alpha
  734. @property
  735. def frame_alpha(self):
  736. return self._frame_alpha
  737. @property
  738. def frame_height(self):
  739. return self._frame_height
  740. def to_dict(self):
  741. return {
  742. "mask": self._raw_mask,
  743. "threshold": self.threshold,
  744. "contour_area": self.contour_area,
  745. "delta_alpha": self.delta_alpha,
  746. "frame_alpha": self.frame_alpha,
  747. "frame_height": self.frame_height,
  748. }
  749. class DetectConfig:
  750. def __init__(self, global_config, config, camera_fps):
  751. self._enabled = config["enabled"]
  752. self._max_disappeared = config.get(
  753. "max_disappeared", global_config.get("max_disappeared", camera_fps * 5)
  754. )
  755. @property
  756. def enabled(self):
  757. return self._enabled
  758. @property
  759. def max_disappeared(self):
  760. return self._max_disappeared
  761. def to_dict(self):
  762. return {
  763. "enabled": self.enabled,
  764. "max_disappeared": self._max_disappeared,
  765. }
  766. class ZoneConfig:
  767. def __init__(self, name, config):
  768. self._coordinates = config["coordinates"]
  769. self._filters = {
  770. name: FilterConfig(c, c) for name, c in config["filters"].items()
  771. }
  772. if isinstance(self._coordinates, list):
  773. self._contour = np.array(
  774. [
  775. [int(p.split(",")[0]), int(p.split(",")[1])]
  776. for p in self._coordinates
  777. ]
  778. )
  779. elif isinstance(self._coordinates, str):
  780. points = self._coordinates.split(",")
  781. self._contour = np.array(
  782. [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
  783. )
  784. else:
  785. print(f"Unable to parse zone coordinates for {name}")
  786. self._contour = np.array([])
  787. self._color = (0, 0, 0)
  788. @property
  789. def coordinates(self):
  790. return self._coordinates
  791. @property
  792. def contour(self):
  793. return self._contour
  794. @contour.setter
  795. def contour(self, val):
  796. self._contour = val
  797. @property
  798. def color(self):
  799. return self._color
  800. @color.setter
  801. def color(self, val):
  802. self._color = val
  803. @property
  804. def filters(self):
  805. return self._filters
  806. def to_dict(self):
  807. return {
  808. "filters": {k: f.to_dict() for k, f in self.filters.items()},
  809. "coordinates": self._coordinates,
  810. }
  811. class CameraConfig:
  812. def __init__(self, name, config, global_config):
  813. self._name = name
  814. self._ffmpeg = CameraFfmpegConfig(global_config["ffmpeg"], config["ffmpeg"])
  815. self._height = config.get("height")
  816. self._width = config.get("width")
  817. self._frame_shape = (self._height, self._width)
  818. self._frame_shape_yuv = (self._frame_shape[0] * 3 // 2, self._frame_shape[1])
  819. self._fps = config.get("fps")
  820. self._best_image_timeout = config["best_image_timeout"]
  821. self._zones = {name: ZoneConfig(name, z) for name, z in config["zones"].items()}
  822. self._clips = CameraClipsConfig(global_config, config["clips"])
  823. self._record = RecordConfig(global_config["record"], config["record"])
  824. self._rtmp = CameraRtmpConfig(global_config, config["rtmp"])
  825. self._snapshots = CameraSnapshotsConfig(global_config, config["snapshots"])
  826. self._mqtt = CameraMqttConfig(config["mqtt"])
  827. self._objects = ObjectConfig(
  828. global_config["objects"], config.get("objects", {}), self._frame_shape
  829. )
  830. self._motion = MotionConfig(
  831. global_config["motion"], config["motion"], self._frame_shape
  832. )
  833. self._detect = DetectConfig(
  834. global_config["detect"], config["detect"], config.get("fps", 5)
  835. )
  836. self._ffmpeg_cmds = []
  837. for ffmpeg_input in self._ffmpeg.inputs:
  838. ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
  839. if ffmpeg_cmd is None:
  840. continue
  841. self._ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
  842. self._set_zone_colors(self._zones)
  843. def _get_ffmpeg_cmd(self, ffmpeg_input):
  844. ffmpeg_output_args = []
  845. if "detect" in ffmpeg_input.roles:
  846. ffmpeg_output_args = (
  847. self.ffmpeg.output_args["detect"] + ffmpeg_output_args + ["pipe:"]
  848. )
  849. if self.fps:
  850. ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
  851. if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
  852. ffmpeg_output_args = (
  853. self.ffmpeg.output_args["rtmp"]
  854. + [f"rtmp://127.0.0.1/live/{self.name}"]
  855. + ffmpeg_output_args
  856. )
  857. if "clips" in ffmpeg_input.roles:
  858. ffmpeg_output_args = (
  859. self.ffmpeg.output_args["clips"]
  860. + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
  861. + ffmpeg_output_args
  862. )
  863. if "record" in ffmpeg_input.roles and self.record.enabled:
  864. ffmpeg_output_args = (
  865. self.ffmpeg.output_args["record"]
  866. + [f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
  867. + ffmpeg_output_args
  868. )
  869. # if there arent any outputs enabled for this input
  870. if len(ffmpeg_output_args) == 0:
  871. return None
  872. cmd = (
  873. ["ffmpeg"]
  874. + ffmpeg_input.global_args
  875. + ffmpeg_input.hwaccel_args
  876. + ffmpeg_input.input_args
  877. + ["-i", ffmpeg_input.path]
  878. + ffmpeg_output_args
  879. )
  880. return [part for part in cmd if part != ""]
  881. def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
  882. # set colors for zones
  883. all_zone_names = zones.keys()
  884. zone_colors = {}
  885. colors = plt.cm.get_cmap("tab10", len(all_zone_names))
  886. for i, zone in enumerate(all_zone_names):
  887. zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
  888. for name, zone in zones.items():
  889. zone.color = zone_colors[name]
  890. @property
  891. def name(self):
  892. return self._name
  893. @property
  894. def ffmpeg(self):
  895. return self._ffmpeg
  896. @property
  897. def height(self):
  898. return self._height
  899. @property
  900. def width(self):
  901. return self._width
  902. @property
  903. def fps(self):
  904. return self._fps
  905. @property
  906. def best_image_timeout(self):
  907. return self._best_image_timeout
  908. @property
  909. def zones(self) -> Dict[str, ZoneConfig]:
  910. return self._zones
  911. @property
  912. def clips(self):
  913. return self._clips
  914. @property
  915. def record(self):
  916. return self._record
  917. @property
  918. def rtmp(self):
  919. return self._rtmp
  920. @property
  921. def snapshots(self):
  922. return self._snapshots
  923. @property
  924. def mqtt(self):
  925. return self._mqtt
  926. @property
  927. def objects(self):
  928. return self._objects
  929. @property
  930. def motion(self):
  931. return self._motion
  932. @property
  933. def detect(self):
  934. return self._detect
  935. @property
  936. def frame_shape(self):
  937. return self._frame_shape
  938. @property
  939. def frame_shape_yuv(self):
  940. return self._frame_shape_yuv
  941. @property
  942. def ffmpeg_cmds(self):
  943. return self._ffmpeg_cmds
  944. def to_dict(self):
  945. return {
  946. "name": self.name,
  947. "height": self.height,
  948. "width": self.width,
  949. "fps": self.fps,
  950. "best_image_timeout": self.best_image_timeout,
  951. "zones": {k: z.to_dict() for k, z in self.zones.items()},
  952. "clips": self.clips.to_dict(),
  953. "record": self.record.to_dict(),
  954. "rtmp": self.rtmp.to_dict(),
  955. "snapshots": self.snapshots.to_dict(),
  956. "mqtt": self.mqtt.to_dict(),
  957. "objects": self.objects.to_dict(),
  958. "motion": self.motion.to_dict(),
  959. "detect": self.detect.to_dict(),
  960. "frame_shape": self.frame_shape,
  961. "ffmpeg_cmds": [
  962. {"roles": c["roles"], "cmd": " ".join(c["cmd"])}
  963. for c in self.ffmpeg_cmds
  964. ],
  965. }
  966. class FrigateConfig:
  967. def __init__(self, config_file=None, config=None):
  968. if config is None and config_file is None:
  969. raise ValueError("config or config_file must be defined")
  970. elif not config_file is None:
  971. config = self._load_file(config_file)
  972. config = FRIGATE_CONFIG_SCHEMA(config)
  973. config = self._sub_env_vars(config)
  974. self._database = DatabaseConfig(config["database"])
  975. self._model = ModelConfig(config["model"])
  976. self._detectors = {
  977. name: DetectorConfig(d) for name, d in config["detectors"].items()
  978. }
  979. self._mqtt = MqttConfig(config["mqtt"])
  980. self._clips = ClipsConfig(config["clips"])
  981. self._snapshots = SnapshotsConfig(config["snapshots"])
  982. self._cameras = {
  983. name: CameraConfig(name, c, config) for name, c in config["cameras"].items()
  984. }
  985. self._logger = LoggerConfig(config["logger"])
  986. self._environment_vars = config["environment_vars"]
  987. def _sub_env_vars(self, config):
  988. frigate_env_vars = {
  989. k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")
  990. }
  991. if "password" in config["mqtt"]:
  992. config["mqtt"]["password"] = config["mqtt"]["password"].format(
  993. **frigate_env_vars
  994. )
  995. for camera in config["cameras"].values():
  996. for i in camera["ffmpeg"]["inputs"]:
  997. i["path"] = i["path"].format(**frigate_env_vars)
  998. return config
  999. def _load_file(self, config_file):
  1000. with open(config_file) as f:
  1001. raw_config = f.read()
  1002. if config_file.endswith(".yml"):
  1003. config = yaml.safe_load(raw_config)
  1004. elif config_file.endswith(".json"):
  1005. config = json.loads(raw_config)
  1006. return config
  1007. def to_dict(self):
  1008. return {
  1009. "database": self.database.to_dict(),
  1010. "model": self.model.to_dict(),
  1011. "detectors": {k: d.to_dict() for k, d in self.detectors.items()},
  1012. "mqtt": self.mqtt.to_dict(),
  1013. "clips": self.clips.to_dict(),
  1014. "snapshots": self.snapshots.to_dict(),
  1015. "cameras": {k: c.to_dict() for k, c in self.cameras.items()},
  1016. "logger": self.logger.to_dict(),
  1017. "environment_vars": self._environment_vars,
  1018. }
  1019. @property
  1020. def database(self):
  1021. return self._database
  1022. @property
  1023. def model(self):
  1024. return self._model
  1025. @property
  1026. def detectors(self) -> Dict[str, DetectorConfig]:
  1027. return self._detectors
  1028. @property
  1029. def logger(self):
  1030. return self._logger
  1031. @property
  1032. def mqtt(self):
  1033. return self._mqtt
  1034. @property
  1035. def clips(self):
  1036. return self._clips
  1037. @property
  1038. def snapshots(self):
  1039. return self._snapshots
  1040. @property
  1041. def cameras(self) -> Dict[str, CameraConfig]:
  1042. return self._cameras
  1043. @property
  1044. def environment_vars(self):
  1045. return self._environment_vars