config.py 33 KB

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