config.py 24 KB

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