config.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. import base64
  2. import json
  3. import os
  4. from typing import Dict
  5. import cv2
  6. import matplotlib.pyplot as plt
  7. import numpy as np
  8. import voluptuous as vol
  9. import yaml
  10. from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
  11. DETECTORS_SCHEMA = vol.Schema(
  12. {
  13. vol.Required(str): {
  14. vol.Required('type', default='edgetpu'): vol.In(['cpu', 'edgetpu']),
  15. vol.Optional('device', default='usb'): str
  16. }
  17. }
  18. )
  19. DEFAULT_DETECTORS = {
  20. 'coral': {
  21. 'type': 'edgetpu',
  22. 'device': 'usb'
  23. }
  24. }
  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. 'user': str,
  32. 'password': str
  33. }
  34. )
  35. SAVE_CLIPS_RETAIN_SCHEMA = vol.Schema(
  36. {
  37. vol.Required('default',default=10): int,
  38. 'objects': {
  39. str: int
  40. }
  41. }
  42. )
  43. SAVE_CLIPS_SCHEMA = vol.Schema(
  44. {
  45. vol.Optional('max_seconds', default=300): int,
  46. vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA
  47. }
  48. )
  49. FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
  50. FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
  51. '-fflags', '+genpts+discardcorrupt'
  52. '-rtsp_transport', 'tcp',
  53. '-stimeout', '5000000',
  54. '-use_wallclock_as_timestamps', '1']
  55. DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ['-f', 'rawvideo',
  56. '-pix_fmt', 'yuv420p']
  57. RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
  58. SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
  59. "10", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
  60. "1", "-c", "copy", "-an"]
  61. RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
  62. "60", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
  63. "1", "-c", "copy", "-an"]
  64. GLOBAL_FFMPEG_SCHEMA = vol.Schema(
  65. {
  66. vol.Optional('global_args', default=FFMPEG_GLOBAL_ARGS_DEFAULT): vol.Any(str, [str]),
  67. vol.Optional('hwaccel_args', default=[]): vol.Any(str, [str]),
  68. vol.Optional('input_args', default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  69. vol.Optional('output_args', default={}): {
  70. vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  71. vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  72. vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  73. vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  74. }
  75. }
  76. )
  77. FILTER_SCHEMA = vol.Schema(
  78. {
  79. str: {
  80. vol.Optional('min_area', default=0): int,
  81. vol.Optional('max_area', default=24000000): int,
  82. vol.Optional('threshold', default=0.85): float
  83. }
  84. }
  85. )
  86. def filters_for_all_tracked_objects(object_config):
  87. for tracked_object in object_config.get('track', ['person']):
  88. if not 'filters' in object_config:
  89. object_config['filters'] = {}
  90. if not tracked_object in object_config['filters']:
  91. object_config['filters'][tracked_object] = {}
  92. return object_config
  93. OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
  94. {
  95. vol.Optional('track', default=['person']): [str],
  96. vol.Optional('filters', default = {}): FILTER_SCHEMA.extend({ str: {vol.Optional('min_score', default=0.5): float}})
  97. }
  98. ))
  99. DEFAULT_CAMERA_SAVE_CLIPS = {
  100. 'enabled': False
  101. }
  102. DEFAULT_CAMERA_SNAPSHOTS = {
  103. 'show_timestamp': True,
  104. 'draw_zones': False,
  105. 'draw_bounding_boxes': True,
  106. 'crop_to_region': True
  107. }
  108. def each_role_used_once(inputs):
  109. roles = [role for i in inputs for role in i['roles']]
  110. roles_set = set(roles)
  111. if len(roles) > len(roles_set):
  112. raise ValueError
  113. return inputs
  114. CAMERA_FFMPEG_SCHEMA = vol.Schema(
  115. {
  116. vol.Required('inputs'): vol.All([{
  117. vol.Required('path'): str,
  118. vol.Required('roles'): ['detect', 'clips', 'record', 'rtmp'],
  119. 'global_args': vol.Any(str, [str]),
  120. 'hwaccel_args': vol.Any(str, [str]),
  121. 'input_args': vol.Any(str, [str]),
  122. }], vol.Msg(each_role_used_once, msg="Each input role may only be used once")),
  123. 'output_args': {
  124. vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  125. vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  126. vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  127. vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
  128. }
  129. }
  130. )
  131. def ensure_zones_and_cameras_have_different_names(cameras):
  132. zones = [zone for camera in cameras.values() for zone in camera['zones'].keys()]
  133. for zone in zones:
  134. if zone in cameras.keys():
  135. raise ValueError
  136. return cameras
  137. CAMERAS_SCHEMA = vol.Schema(vol.All(
  138. {
  139. str: {
  140. vol.Required('ffmpeg'): CAMERA_FFMPEG_SCHEMA,
  141. vol.Required('height'): int,
  142. vol.Required('width'): int,
  143. 'fps': int,
  144. 'mask': str,
  145. vol.Optional('best_image_timeout', default=60): int,
  146. vol.Optional('zones', default={}): {
  147. str: {
  148. vol.Required('coordinates'): vol.Any(str, [str]),
  149. vol.Optional('filters', default={}): FILTER_SCHEMA
  150. }
  151. },
  152. vol.Optional('save_clips', default=DEFAULT_CAMERA_SAVE_CLIPS): {
  153. vol.Optional('enabled', default=False): bool,
  154. vol.Optional('pre_capture', default=30): int,
  155. 'objects': [str],
  156. vol.Optional('retain', default={}): SAVE_CLIPS_RETAIN_SCHEMA,
  157. },
  158. vol.Optional('record', default={}): {
  159. 'enabled': bool,
  160. 'retain_days': int,
  161. },
  162. vol.Optional('rtmp', default={}): {
  163. vol.Required('enabled', default=True): bool,
  164. },
  165. vol.Optional('snapshots', default=DEFAULT_CAMERA_SNAPSHOTS): {
  166. vol.Optional('show_timestamp', default=True): bool,
  167. vol.Optional('draw_zones', default=False): bool,
  168. vol.Optional('draw_bounding_boxes', default=True): bool,
  169. vol.Optional('crop_to_region', default=True): bool,
  170. vol.Optional('height', default=175): int
  171. },
  172. 'objects': OBJECTS_SCHEMA
  173. }
  174. }, vol.Msg(ensure_zones_and_cameras_have_different_names, msg='Zones cannot share names with cameras'))
  175. )
  176. FRIGATE_CONFIG_SCHEMA = vol.Schema(
  177. {
  178. vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
  179. 'mqtt': MQTT_SCHEMA,
  180. vol.Optional('logger', default={'default': 'info', 'logs': {}}): {
  181. vol.Optional('default', default='info'): vol.In(['info', 'debug', 'warning', 'error', 'critical']),
  182. vol.Optional('logs', default={}): {str: vol.In(['info', 'debug', 'warning', 'error', 'critical']) }
  183. },
  184. vol.Optional('save_clips', default={}): SAVE_CLIPS_SCHEMA,
  185. vol.Optional('record', default={}): {
  186. vol.Optional('enabled', default=False): bool,
  187. vol.Optional('retain_days', default=30): int,
  188. },
  189. vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
  190. vol.Optional('objects', default={}): OBJECTS_SCHEMA,
  191. vol.Required('cameras', default={}): CAMERAS_SCHEMA
  192. }
  193. )
  194. class DetectorConfig():
  195. def __init__(self, config):
  196. self._type = config['type']
  197. self._device = config['device']
  198. @property
  199. def type(self):
  200. return self._type
  201. @property
  202. def device(self):
  203. return self._device
  204. def to_dict(self):
  205. return {
  206. 'type': self.type,
  207. 'device': self.device
  208. }
  209. class LoggerConfig():
  210. def __init__(self, config):
  211. self._default = config['default'].upper()
  212. self._logs = {k: v.upper() for k, v in config['logs'].items()}
  213. @property
  214. def default(self):
  215. return self._default
  216. @property
  217. def logs(self):
  218. return self._logs
  219. def to_dict(self):
  220. return {
  221. 'default': self.default,
  222. 'logs': self.logs
  223. }
  224. class MqttConfig():
  225. def __init__(self, config):
  226. self._host = config['host']
  227. self._port = config['port']
  228. self._topic_prefix = config['topic_prefix']
  229. self._client_id = config['client_id']
  230. self._user = config.get('user')
  231. self._password = config.get('password')
  232. @property
  233. def host(self):
  234. return self._host
  235. @property
  236. def port(self):
  237. return self._port
  238. @property
  239. def topic_prefix(self):
  240. return self._topic_prefix
  241. @property
  242. def client_id(self):
  243. return self._client_id
  244. @property
  245. def user(self):
  246. return self._user
  247. @property
  248. def password(self):
  249. return self._password
  250. def to_dict(self):
  251. return {
  252. 'host': self.host,
  253. 'port': self.port,
  254. 'topic_prefix': self.topic_prefix,
  255. 'client_id': self.client_id,
  256. 'user': self.user
  257. }
  258. class CameraInput():
  259. def __init__(self, global_config, ffmpeg_input):
  260. self._path = ffmpeg_input['path']
  261. self._roles = ffmpeg_input['roles']
  262. self._global_args = ffmpeg_input.get('global_args', global_config['global_args'])
  263. self._hwaccel_args = ffmpeg_input.get('hwaccel_args', global_config['hwaccel_args'])
  264. self._input_args = ffmpeg_input.get('input_args', global_config['input_args'])
  265. @property
  266. def path(self):
  267. return self._path
  268. @property
  269. def roles(self):
  270. return self._roles
  271. @property
  272. def global_args(self):
  273. return self._global_args if isinstance(self._global_args, list) else self._global_args.split(' ')
  274. @property
  275. def hwaccel_args(self):
  276. return self._hwaccel_args if isinstance(self._hwaccel_args, list) else self._hwaccel_args.split(' ')
  277. @property
  278. def input_args(self):
  279. return self._input_args if isinstance(self._input_args, list) else self._input_args.split(' ')
  280. class CameraFfmpegConfig():
  281. def __init__(self, global_config, config):
  282. self._inputs = [CameraInput(global_config, i) for i in config['inputs']]
  283. self._output_args = config.get('output_args', global_config['output_args'])
  284. @property
  285. def inputs(self):
  286. return self._inputs
  287. @property
  288. def output_args(self):
  289. return {k: v if isinstance(v, list) else v.split(' ') for k, v in self._output_args.items()}
  290. class SaveClipsRetainConfig():
  291. def __init__(self, global_config, config):
  292. self._default = config.get('default', global_config.get('default'))
  293. self._objects = config.get('objects', global_config.get('objects', {}))
  294. @property
  295. def default(self):
  296. return self._default
  297. @property
  298. def objects(self):
  299. return self._objects
  300. def to_dict(self):
  301. return {
  302. 'default': self.default,
  303. 'objects': self.objects
  304. }
  305. class SaveClipsConfig():
  306. def __init__(self, config):
  307. self._max_seconds = config['max_seconds']
  308. self._retain = SaveClipsRetainConfig(config['retain'], config['retain'])
  309. @property
  310. def max_seconds(self):
  311. return self._max_seconds
  312. @property
  313. def retain(self):
  314. return self._retain
  315. def to_dict(self):
  316. return {
  317. 'max_seconds': self.max_seconds,
  318. 'retain': self.retain.to_dict()
  319. }
  320. class RecordConfig():
  321. def __init__(self, global_config, config):
  322. self._enabled = config.get('enabled', global_config['enabled'])
  323. self._retain_days = config.get('retain_days', global_config['retain_days'])
  324. @property
  325. def enabled(self):
  326. return self._enabled
  327. @property
  328. def retain_days(self):
  329. return self._retain_days
  330. def to_dict(self):
  331. return {
  332. 'enabled': self.enabled,
  333. 'retain_days': self.retain_days,
  334. }
  335. class FilterConfig():
  336. def __init__(self, config):
  337. self._min_area = config['min_area']
  338. self._max_area = config['max_area']
  339. self._threshold = config['threshold']
  340. self._min_score = config.get('min_score')
  341. @property
  342. def min_area(self):
  343. return self._min_area
  344. @property
  345. def max_area(self):
  346. return self._max_area
  347. @property
  348. def threshold(self):
  349. return self._threshold
  350. @property
  351. def min_score(self):
  352. return self._min_score
  353. def to_dict(self):
  354. return {
  355. 'min_area': self.min_area,
  356. 'max_area': self.max_area,
  357. 'threshold': self.threshold,
  358. 'min_score': self.min_score
  359. }
  360. class ObjectConfig():
  361. def __init__(self, global_config, config):
  362. self._track = config.get('track', global_config['track'])
  363. if 'filters' in config:
  364. self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
  365. else:
  366. self._filters = { name: FilterConfig(c) for name, c in global_config['filters'].items() }
  367. @property
  368. def track(self):
  369. return self._track
  370. @property
  371. def filters(self) -> Dict[str, FilterConfig]:
  372. return self._filters
  373. def to_dict(self):
  374. return {
  375. 'track': self.track,
  376. 'filters': { k: f.to_dict() for k, f in self.filters.items() }
  377. }
  378. class CameraSnapshotsConfig():
  379. def __init__(self, config):
  380. self._show_timestamp = config['show_timestamp']
  381. self._draw_zones = config['draw_zones']
  382. self._draw_bounding_boxes = config['draw_bounding_boxes']
  383. self._crop_to_region = config['crop_to_region']
  384. self._height = config.get('height')
  385. @property
  386. def show_timestamp(self):
  387. return self._show_timestamp
  388. @property
  389. def draw_zones(self):
  390. return self._draw_zones
  391. @property
  392. def draw_bounding_boxes(self):
  393. return self._draw_bounding_boxes
  394. @property
  395. def crop_to_region(self):
  396. return self._crop_to_region
  397. @property
  398. def height(self):
  399. return self._height
  400. def to_dict(self):
  401. return {
  402. 'show_timestamp': self.show_timestamp,
  403. 'draw_zones': self.draw_zones,
  404. 'draw_bounding_boxes': self.draw_bounding_boxes,
  405. 'crop_to_region': self.crop_to_region,
  406. 'height': self.height
  407. }
  408. class CameraSaveClipsConfig():
  409. def __init__(self, global_config, config):
  410. self._enabled = config['enabled']
  411. self._pre_capture = config['pre_capture']
  412. self._objects = config.get('objects', global_config['objects']['track'])
  413. self._retain = SaveClipsRetainConfig(global_config['save_clips']['retain'], config['retain'])
  414. @property
  415. def enabled(self):
  416. return self._enabled
  417. @property
  418. def pre_capture(self):
  419. return self._pre_capture
  420. @property
  421. def objects(self):
  422. return self._objects
  423. @property
  424. def retain(self):
  425. return self._retain
  426. def to_dict(self):
  427. return {
  428. 'enabled': self.enabled,
  429. 'pre_capture': self.pre_capture,
  430. 'objects': self.objects,
  431. 'retain': self.retain.to_dict()
  432. }
  433. class CameraRtmpConfig():
  434. def __init__(self, global_config, config):
  435. self._enabled = config['enabled']
  436. @property
  437. def enabled(self):
  438. return self._enabled
  439. def to_dict(self):
  440. return {
  441. 'enabled': self.enabled,
  442. }
  443. class ZoneConfig():
  444. def __init__(self, name, config):
  445. self._coordinates = config['coordinates']
  446. self._filters = { name: FilterConfig(c) for name, c in config['filters'].items() }
  447. if isinstance(self._coordinates, list):
  448. self._contour = np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in self._coordinates])
  449. elif isinstance(self._coordinates, str):
  450. points = self._coordinates.split(',')
  451. self._contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  452. else:
  453. print(f"Unable to parse zone coordinates for {name}")
  454. self._contour = np.array([])
  455. self._color = (0,0,0)
  456. @property
  457. def coordinates(self):
  458. return self._coordinates
  459. @property
  460. def contour(self):
  461. return self._contour
  462. @contour.setter
  463. def contour(self, val):
  464. self._contour = val
  465. @property
  466. def color(self):
  467. return self._color
  468. @color.setter
  469. def color(self, val):
  470. self._color = val
  471. @property
  472. def filters(self):
  473. return self._filters
  474. def to_dict(self):
  475. return {
  476. 'filters': {k: f.to_dict() for k, f in self.filters.items()}
  477. }
  478. class CameraConfig():
  479. def __init__(self, name, config, global_config):
  480. self._name = name
  481. self._ffmpeg = CameraFfmpegConfig(global_config['ffmpeg'], config['ffmpeg'])
  482. self._height = config.get('height')
  483. self._width = config.get('width')
  484. self._frame_shape = (self._height, self._width)
  485. self._frame_shape_yuv = (self._frame_shape[0]*3//2, self._frame_shape[1])
  486. self._fps = config.get('fps')
  487. self._mask = self._create_mask(config.get('mask'))
  488. self._best_image_timeout = config['best_image_timeout']
  489. self._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() }
  490. self._save_clips = CameraSaveClipsConfig(global_config, config['save_clips'])
  491. self._record = RecordConfig(global_config['record'], config['record'])
  492. self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
  493. self._snapshots = CameraSnapshotsConfig(config['snapshots'])
  494. self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}))
  495. self._ffmpeg_cmds = []
  496. for ffmpeg_input in self._ffmpeg.inputs:
  497. self._ffmpeg_cmds.append({
  498. 'roles': ffmpeg_input.roles,
  499. 'cmd': self._get_ffmpeg_cmd(ffmpeg_input)
  500. })
  501. self._set_zone_colors(self._zones)
  502. def _create_mask(self, mask):
  503. if mask:
  504. if mask.startswith('base64,'):
  505. img = base64.b64decode(mask[7:])
  506. np_img = np.fromstring(img, dtype=np.uint8)
  507. mask_img = cv2.imdecode(np_img, cv2.IMREAD_GRAYSCALE)
  508. elif mask.startswith('poly,'):
  509. points = mask.split(',')[1:]
  510. contour = np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
  511. mask_img = np.zeros(self.frame_shape, np.uint8)
  512. mask_img[:] = 255
  513. cv2.fillPoly(mask_img, pts=[contour], color=(0))
  514. else:
  515. mask_img = cv2.imread(f"/config/{mask}", cv2.IMREAD_GRAYSCALE)
  516. else:
  517. mask_img = None
  518. if mask_img is None or mask_img.size == 0:
  519. mask_img = np.zeros(self.frame_shape, np.uint8)
  520. mask_img[:] = 255
  521. return mask_img
  522. def _get_ffmpeg_cmd(self, ffmpeg_input):
  523. ffmpeg_output_args = []
  524. if 'detect' in ffmpeg_input.roles:
  525. ffmpeg_output_args = self.ffmpeg.output_args['detect'] + ffmpeg_output_args + ['pipe:']
  526. if self.fps:
  527. ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
  528. if 'rtmp' in ffmpeg_input.roles and self.rtmp.enabled:
  529. ffmpeg_output_args = self.ffmpeg.output_args['rtmp'] + [
  530. f"rtmp://127.0.0.1/live/{self.name}"
  531. ] + ffmpeg_output_args
  532. if 'clips' in ffmpeg_input.roles and self.save_clips.enabled:
  533. ffmpeg_output_args = self.ffmpeg.output_args['clips'] + [
  534. f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
  535. ] + ffmpeg_output_args
  536. if 'record' in ffmpeg_input.roles and self.record.enabled:
  537. ffmpeg_output_args = self.ffmpeg.output_args['record'] + [
  538. f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
  539. ] + ffmpeg_output_args
  540. return (['ffmpeg'] +
  541. ffmpeg_input.global_args +
  542. ffmpeg_input.hwaccel_args +
  543. ffmpeg_input.input_args +
  544. ['-i', ffmpeg_input.path] +
  545. ffmpeg_output_args)
  546. def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
  547. # set colors for zones
  548. all_zone_names = zones.keys()
  549. zone_colors = {}
  550. colors = plt.cm.get_cmap('tab10', len(all_zone_names))
  551. for i, zone in enumerate(all_zone_names):
  552. zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
  553. for name, zone in zones.items():
  554. zone.color = zone_colors[name]
  555. @property
  556. def name(self):
  557. return self._name
  558. @property
  559. def ffmpeg(self):
  560. return self._ffmpeg
  561. @property
  562. def height(self):
  563. return self._height
  564. @property
  565. def width(self):
  566. return self._width
  567. @property
  568. def fps(self):
  569. return self._fps
  570. @property
  571. def mask(self):
  572. return self._mask
  573. @property
  574. def best_image_timeout(self):
  575. return self._best_image_timeout
  576. @property
  577. def zones(self)-> Dict[str, ZoneConfig]:
  578. return self._zones
  579. @property
  580. def save_clips(self):
  581. return self._save_clips
  582. @property
  583. def record(self):
  584. return self._record
  585. @property
  586. def rtmp(self):
  587. return self._rtmp
  588. @property
  589. def snapshots(self):
  590. return self._snapshots
  591. @property
  592. def objects(self):
  593. return self._objects
  594. @property
  595. def frame_shape(self):
  596. return self._frame_shape
  597. @property
  598. def frame_shape_yuv(self):
  599. return self._frame_shape_yuv
  600. @property
  601. def ffmpeg_cmds(self):
  602. return self._ffmpeg_cmds
  603. def to_dict(self):
  604. return {
  605. 'name': self.name,
  606. 'height': self.height,
  607. 'width': self.width,
  608. 'fps': self.fps,
  609. 'best_image_timeout': self.best_image_timeout,
  610. 'zones': {k: z.to_dict() for k, z in self.zones.items()},
  611. 'save_clips': self.save_clips.to_dict(),
  612. 'record': self.record.to_dict(),
  613. 'rtmp': self.rtmp.to_dict(),
  614. 'snapshots': self.snapshots.to_dict(),
  615. 'objects': self.objects.to_dict(),
  616. 'frame_shape': self.frame_shape,
  617. 'ffmpeg_cmds': [{'roles': c['roles'], 'cmd': ' '.join(c['cmd'])} for c in self.ffmpeg_cmds],
  618. }
  619. class FrigateConfig():
  620. def __init__(self, config_file=None, config=None):
  621. if config is None and config_file is None:
  622. raise ValueError('config or config_file must be defined')
  623. elif not config_file is None:
  624. config = self._load_file(config_file)
  625. config = FRIGATE_CONFIG_SCHEMA(config)
  626. config = self._sub_env_vars(config)
  627. self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() }
  628. self._mqtt = MqttConfig(config['mqtt'])
  629. self._save_clips = SaveClipsConfig(config['save_clips'])
  630. self._cameras = { name: CameraConfig(name, c, config) for name, c in config['cameras'].items() }
  631. self._logger = LoggerConfig(config['logger'])
  632. def _sub_env_vars(self, config):
  633. frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
  634. if 'password' in config['mqtt']:
  635. config['mqtt']['password'] = config['mqtt']['password'].format(**frigate_env_vars)
  636. for camera in config['cameras'].values():
  637. for i in camera['ffmpeg']['inputs']:
  638. i['path'] = i['path'].format(**frigate_env_vars)
  639. return config
  640. def _load_file(self, config_file):
  641. with open(config_file) as f:
  642. raw_config = f.read()
  643. if config_file.endswith(".yml"):
  644. config = yaml.safe_load(raw_config)
  645. elif config_file.endswith(".json"):
  646. config = json.loads(raw_config)
  647. return config
  648. def to_dict(self):
  649. return {
  650. 'detectors': {k: d.to_dict() for k, d in self.detectors.items()},
  651. 'mqtt': self.mqtt.to_dict(),
  652. 'save_clips': self.save_clips.to_dict(),
  653. 'cameras': {k: c.to_dict() for k, c in self.cameras.items()},
  654. 'logger': self.logger.to_dict()
  655. }
  656. @property
  657. def detectors(self) -> Dict[str, DetectorConfig]:
  658. return self._detectors
  659. @property
  660. def logger(self):
  661. return self._logger
  662. @property
  663. def mqtt(self):
  664. return self._mqtt
  665. @property
  666. def save_clips(self):
  667. return self._save_clips
  668. @property
  669. def cameras(self) -> Dict[str, CameraConfig]:
  670. return self._cameras