text: import base64
  import json
  import logging
  import os
  from typing import Dict
  
  import cv2
  import matplotlib.pyplot as plt
  import numpy as np
  import voluptuous as vol
  import yaml
  
  from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
  from frigate.util import create_mask
  
  logger = logging.getLogger(__name__)
  
  DEFAULT_TRACKED_OBJECTS = ['person']
  
  DETECTORS_SCHEMA = vol.Schema(
      {
          vol.Required(str): {
              vol.Required('type', default='edgetpu'): vol.In(['cpu', 'edgetpu']),
              vol.Optional('device', default='usb'): str,
              vol.Optional('num_threads', default=3): int
          }
      }
  )
  
  DEFAULT_DETECTORS = {
      'coral': {
          'type': 'edgetpu',
          'device': 'usb'
      }
  }
  
  MQTT_SCHEMA = vol.Schema(
      {
          vol.Required('host'): str,
          vol.Optional('port', default=1883): int,
          vol.Optional('topic_prefix', default='frigate'): str,
          vol.Optional('client_id', default='frigate'): str,
          vol.Optional('stats_interval', default=60): int,
          'user': str,
          'password': str
      }
  )
  
  RETAIN_SCHEMA = vol.Schema(
      {
          vol.Required('default',default=10): int,
          'objects': {
              str: int
          }
      }
  )
  
  CLIPS_SCHEMA = vol.Schema(
      {
          vol.Optional('max_seconds', default=300): int,
          'tmpfs_cache_size': str,
          vol.Optional('retain', default={}): RETAIN_SCHEMA
      }
  )
  
  FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
  FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
      '-fflags', '+genpts+discardcorrupt',
      '-rtsp_transport', 'tcp',
      '-stimeout', '5000000',
      '-use_wallclock_as_timestamps', '1']
  DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ['-f', 'rawvideo',
      '-pix_fmt', 'yuv420p']
  RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
  SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
      "10", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
      "1", "-c", "copy", "-an"]
  RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "segment", "-segment_time",
      "60", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime",
      "1", "-c", "copy", "-an"]
  
  GLOBAL_FFMPEG_SCHEMA = vol.Schema(
      {
          vol.Optional('global_args', default=FFMPEG_GLOBAL_ARGS_DEFAULT):  vol.Any(str, [str]),
          vol.Optional('hwaccel_args', default=[]): vol.Any(str, [str]),
          vol.Optional('input_args', default=FFMPEG_INPUT_ARGS_DEFAULT): vol.Any(str, [str]),
          vol.Optional('output_args', default={}): {
              vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
              vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
              vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
              vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
          }
      }
  )
  
  MOTION_SCHEMA = vol.Schema(
      {
          'mask': vol.Any(str, [str]),
          'threshold': vol.Range(min=1, max=255),
          'contour_area': int,
          'delta_alpha': float,
          'frame_alpha': float,
          'frame_height': int
      }
  )
  
  DETECT_SCHEMA = vol.Schema(
      {
          'max_disappeared': int
      }
  )
  
  FILTER_SCHEMA = vol.Schema(
      {
          str: {
                  'min_area': int,
                  'max_area': int,
                  'threshold': float,
              }
      }
  )
  
  def filters_for_all_tracked_objects(object_config):
      for tracked_object in object_config.get('track', DEFAULT_TRACKED_OBJECTS):
          if not 'filters' in object_config:
              object_config['filters'] = {}
          if not tracked_object in object_config['filters']:
              object_config['filters'][tracked_object] = {}
      return object_config
  
  OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
      {
          'track': [str],
          vol.Optional('filters', default = {}): FILTER_SCHEMA.extend(
              { 
                  str: {
                          'min_score': float,
                          'mask': vol.Any(str, [str]),
                      }
              })
      }
  ))
  
  def each_role_used_once(inputs):
      roles = [role for i in inputs for role in i['roles']]
      roles_set = set(roles)
      if len(roles) > len(roles_set):
          raise ValueError
      return inputs
  
  def detect_is_required(inputs):
      roles = [role for i in inputs for role in i['roles']]
      if not 'detect' in roles:
          raise ValueError
      return inputs
  
  CAMERA_FFMPEG_SCHEMA = vol.Schema(
      {
          vol.Required('inputs'): vol.All([{
              vol.Required('path'): str,
              vol.Required('roles'): ['detect', 'clips', 'record', 'rtmp'],
              'global_args':  vol.Any(str, [str]),
              'hwaccel_args': vol.Any(str, [str]),
              'input_args': vol.Any(str, [str]),
          }], vol.Msg(each_role_used_once, msg="Each input role may only be used once"), 
              vol.Msg(detect_is_required, msg="The detect role is required")),
          'output_args': {
              vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
              vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
              vol.Optional('clips', default=SAVE_CLIPS_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
              vol.Optional('rtmp', default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
          }
      }
  )
  
  def ensure_zones_and_cameras_have_different_names(cameras):
      zones = [zone for camera in cameras.values() for zone in camera['zones'].keys()]
      for zone in zones:
          if zone in cameras.keys():
              raise ValueError
      return cameras
  
  CAMERAS_SCHEMA = vol.Schema(vol.All(
      {
          str: {
              vol.Required('ffmpeg'): CAMERA_FFMPEG_SCHEMA,
              vol.Required('height'): int,
              vol.Required('width'): int,
              'fps': int,
              vol.Optional('best_image_timeout', default=60): int,
              vol.Optional('zones', default={}):  {
                  str: {
                      vol.Required('coordinates'): vol.Any(str, [str]),
                      vol.Optional('filters', default={}): FILTER_SCHEMA
                  }
              },
              vol.Optional('clips', default={}): {
                  vol.Optional('enabled', default=False): bool,
                  vol.Optional('pre_capture', default=5): int,
                  vol.Optional('post_capture', default=5): int,
                  'objects': [str],
                  vol.Optional('retain', default={}): RETAIN_SCHEMA,
              },
              vol.Optional('record', default={}): {
                  'enabled': bool,
                  'retain_days': int,
              },
              vol.Optional('rtmp', default={}): {
                  vol.Required('enabled', default=True): bool,
              },
              vol.Optional('snapshots', default={}): {
                  vol.Optional('enabled', default=False): bool,
                  vol.Optional('timestamp', default=False): bool,
                  vol.Optional('bounding_box', default=False): bool,
                  vol.Optional('crop', default=False): bool,
                  'height': int,
                  vol.Optional('retain', default={}): RETAIN_SCHEMA,
              },
              vol.Optional('mqtt', default={}): {
                  vol.Optional('enabled', default=True): bool,
                  vol.Optional('timestamp', default=True): bool,
                  vol.Optional('bounding_box', default=True): bool,
                  vol.Optional('crop', default=True): bool,
                  vol.Optional('height', default=270): int
              },
              vol.Optional('objects', default={}): OBJECTS_SCHEMA,
              vol.Optional('motion', default={}): MOTION_SCHEMA,
              vol.Optional('detect', default={}): DETECT_SCHEMA.extend({
                  vol.Optional('enabled', default=True): bool
              })
          }
      }, vol.Msg(ensure_zones_and_cameras_have_different_names, msg='Zones cannot share names with cameras'))
  )
  
  FRIGATE_CONFIG_SCHEMA = vol.Schema(
      {
          vol.Optional('database', default={}): {
              vol.Optional('path', default=os.path.join(CLIPS_DIR, 'frigate.db')): str
          },
          vol.Optional('model', default={'width': 320, 'height': 320}): {
              vol.Required('width'): int,
              vol.Required('height'): int
          },
          vol.Optional('detectors', default=DEFAULT_DETECTORS): DETECTORS_SCHEMA,
          'mqtt': MQTT_SCHEMA,
          vol.Optional('logger', default={'default': 'info', 'logs': {}}): {
              vol.Optional('default', default='info'): vol.In(['info', 'debug', 'warning', 'error', 'critical']),
              vol.Optional('logs', default={}): {str: vol.In(['info', 'debug', 'warning', 'error', 'critical']) }
          },
          vol.Optional('snapshots', default={}): {
              vol.Optional('retain', default={}): RETAIN_SCHEMA
          },
          vol.Optional('clips', default={}): CLIPS_SCHEMA,
          vol.Optional('record', default={}): {
              vol.Optional('enabled', default=False): bool,
              vol.Optional('retain_days', default=30): int,
          },
          vol.Optional('ffmpeg', default={}): GLOBAL_FFMPEG_SCHEMA,
          vol.Optional('objects', default={}): OBJECTS_SCHEMA,
          vol.Optional('motion', default={}): MOTION_SCHEMA,
          vol.Optional('detect', default={}): DETECT_SCHEMA,
          vol.Required('cameras', default={}): CAMERAS_SCHEMA,
          vol.Optional('environment_vars', default={}): { str: str }
      }
  )
  
  class DatabaseConfig():
      def __init__(self, config):
          self._path = config['path']
  
      @property
      def path(self):
          return self._path
  
      def to_dict(self):
          return {
              'path': self.path
          }
  
  class ModelConfig():
      def __init__(self, config):
          self._width = config['width']
          self._height = config['height']
  
      @property
      def width(self):
          return self._width
  
      @property
      def height(self):
          return self._height
  
      def to_dict(self):
          return {
              'width': self.width,
              'height': self.height
          }
  
  class DetectorConfig():
      def __init__(self, config):
          self._type = config['type']
          self._device = config['device']
          self._num_threads = config['num_threads']
  
      @property
      def type(self):
          return self._type
  
      @property
      def device(self):
          return self._device
  
      @property
      def num_threads(self):
          return self._num_threads
  
      def to_dict(self):
          return {
              'type': self.type,
              'device': self.device,
              'num_threads': self.num_threads
          }
  
  class LoggerConfig():
      def __init__(self, config):
          self._default = config['default'].upper()
          self._logs = {k: v.upper() for k, v in config['logs'].items()}
  
      @property
      def default(self):
          return self._default
  
      @property
      def logs(self):
          return self._logs
  
      def to_dict(self):
          return {
              'default': self.default,
              'logs': self.logs
          }
  
  class MqttConfig():
      def __init__(self, config):
          self._host = config['host']
          self._port = config['port']
          self._topic_prefix = config['topic_prefix']
          self._client_id = config['client_id']
          self._user = config.get('user')
          self._password = config.get('password')
          self._stats_interval = config.get('stats_interval')
      
      @property
      def host(self):
          return self._host
  
      @property
      def port(self):
          return self._port
  
      @property
      def topic_prefix(self):
          return self._topic_prefix
  
      @property
      def client_id(self):
          return self._client_id
  
      @property
      def user(self):
          return self._user
  
      @property
      def password(self):
          return self._password
  
      @property
      def stats_interval(self):
          return self._stats_interval
  
      def to_dict(self):
          return {
              'host': self.host,
              'port': self.port,
              'topic_prefix': self.topic_prefix,
              'client_id': self.client_id,
              'user': self.user,
              'stats_interval': self.stats_interval
          }
  
  class CameraInput():
      def __init__(self, global_config, ffmpeg_input):
          self._path = ffmpeg_input['path']
          self._roles = ffmpeg_input['roles']
          self._global_args = ffmpeg_input.get('global_args', global_config['global_args'])
          self._hwaccel_args = ffmpeg_input.get('hwaccel_args', global_config['hwaccel_args'])
          self._input_args = ffmpeg_input.get('input_args', global_config['input_args'])
  
      @property
      def path(self):
          return self._path
  
      @property
      def roles(self):
          return self._roles
  
      @property
      def global_args(self):
          return self._global_args if isinstance(self._global_args, list) else self._global_args.split(' ')
  
      @property
      def hwaccel_args(self):
          return self._hwaccel_args if isinstance(self._hwaccel_args, list) else self._hwaccel_args.split(' ')
  
      @property
      def input_args(self):
          return self._input_args if isinstance(self._input_args, list) else self._input_args.split(' ')
  
  class CameraFfmpegConfig():
      def __init__(self, global_config, config):
          self._inputs = [CameraInput(global_config, i) for i in config['inputs']]
          self._output_args = config.get('output_args', global_config['output_args'])
  
      @property
      def inputs(self):
          return self._inputs
  
      @property
      def output_args(self):
          return {k: v if isinstance(v, list) else v.split(' ') for k, v in self._output_args.items()}
  
  class RetainConfig():
      def __init__(self, global_config, config):
          self._default = config.get('default', global_config.get('default'))
          self._objects = config.get('objects', global_config.get('objects', {}))
  
      @property
      def default(self):
          return self._default
  
      @property
      def objects(self):
          return self._objects
  
      def to_dict(self):
          return {
              'default': self.default,
              'objects': self.objects
          }
  
  class ClipsConfig():
      def __init__(self, config):
          self._max_seconds = config['max_seconds']
          self._tmpfs_cache_size = config.get('tmpfs_cache_size', '').strip()
          self._retain = RetainConfig(config['retain'], config['retain'])
      
      @property
      def max_seconds(self):
          return self._max_seconds
  
      @property
      def tmpfs_cache_size(self):
          return self._tmpfs_cache_size
  
      @property
      def retain(self):
          return self._retain
  
      def to_dict(self):
          return {
              'max_seconds': self.max_seconds,
              'tmpfs_cache_size': self.tmpfs_cache_size,
              'retain': self.retain.to_dict()
          }
  
  class SnapshotsConfig():
      def __init__(self, config):
          self._retain = RetainConfig(config['retain'], config['retain'])
  
      @property
      def retain(self):
          return self._retain
  
      def to_dict(self):
          return {
              'retain': self.retain.to_dict()
          }
  
  class RecordConfig():
      def __init__(self, global_config, config):
          self._enabled = config.get('enabled', global_config['enabled'])
          self._retain_days = config.get('retain_days', global_config['retain_days'])
  
      @property
      def enabled(self):
          return self._enabled
  
      @property
      def retain_days(self):
          return self._retain_days
  
      def to_dict(self):
          return {
              'enabled': self.enabled,
              'retain_days': self.retain_days,
          }
  
  class FilterConfig():
      def __init__(self, global_config, config, frame_shape=None):
          self._min_area = config.get('min_area', global_config.get('min_area', 0))
          self._max_area = config.get('max_area', global_config.get('max_area', 24000000))
          self._threshold = config.get('threshold', global_config.get('threshold', 0.7))
          self._min_score = config.get('min_score', global_config.get('min_score', 0.5))
          self._raw_mask = config.get('mask')
          self._mask = create_mask(frame_shape, self._raw_mask) if self._raw_mask else None
  
      @property
      def min_area(self):
          return self._min_area
  
      @property
      def max_area(self):
          return self._max_area
  
      @property
      def threshold(self):
          return self._threshold
  
      @property
      def min_score(self):
          return self._min_score
  
      @property
      def mask(self):
          return self._mask
  
      def to_dict(self):
          return {
              'min_area': self.min_area,
              'max_area': self.max_area,
              'threshold': self.threshold,
              'min_score': self.min_score,
              'mask': self._raw_mask
          }
  
  class ObjectConfig():
      def __init__(self, global_config, config, frame_shape):
          self._track = config.get('track', global_config.get('track', DEFAULT_TRACKED_OBJECTS))
          self._filters = { name: FilterConfig(global_config.get('filters').get(name, {}), config.get('filters').get(name, {}), frame_shape) for name in self._track }
  
      @property
      def track(self):
          return self._track
  
      @property
      def filters(self) -> Dict[str, FilterConfig]:
          return self._filters
  
      def to_dict(self):
          return {
              'track': self.track,
              'filters': { k: f.to_dict() for k, f in self.filters.items() }
          }
  
  class CameraSnapshotsConfig():
      def __init__(self, global_config, config):
          self._enabled = config['enabled']
          self._timestamp = config['timestamp']
          self._bounding_box = config['bounding_box']
          self._crop = config['crop']
          self._height = config.get('height')
          self._retain = RetainConfig(global_config['snapshots']['retain'], config['retain'])
      
      @property
      def enabled(self):
          return self._enabled
  
      @property
      def timestamp(self):
          return self._timestamp
  
      @property
      def bounding_box(self):
          return self._bounding_box
  
      @property
      def crop(self):
          return self._crop
  
      @property
      def height(self):
          return self._height
      
      @property
      def retain(self):
          return self._retain
      
      def to_dict(self):
          return {
              'enabled': self.enabled,
              'timestamp': self.timestamp,
              'bounding_box': self.bounding_box,
              'crop': self.crop,
              'height': self.height,
              'retain': self.retain.to_dict()
          }
  
  class CameraMqttConfig():
      def __init__(self, config):
          self._enabled = config['enabled']
          self._timestamp = config['timestamp']
          self._bounding_box = config['bounding_box']
          self._crop = config['crop']
          self._height = config.get('height')
  
      @property
      def enabled(self):
          return self._enabled
  
      @property
      def timestamp(self):
          return self._timestamp
  
      @property
      def bounding_box(self):
          return self._bounding_box
  
      @property
      def crop(self):
          return self._crop
  
      @property
      def height(self):
          return self._height
  
      def to_dict(self):
          return {
              'enabled': self.enabled,
              'timestamp': self.timestamp,
              'bounding_box': self.bounding_box,
              'crop': self.crop,
              'height': self.height
          }
  
  class CameraClipsConfig():
      def __init__(self, global_config, config):
          self._enabled = config['enabled']
          self._pre_capture = config['pre_capture']
          self._post_capture = config['post_capture']
          self._objects = config.get('objects')
          self._retain = RetainConfig(global_config['clips']['retain'], config['retain'])
      
      @property
      def enabled(self):
          return self._enabled
  
      @property
      def pre_capture(self):
          return self._pre_capture
  
      @property
      def post_capture(self):
          return self._post_capture
  
      @property
      def objects(self):
          return self._objects
  
      @property
      def retain(self):
          return self._retain
  
      def to_dict(self):
          return {
              'enabled': self.enabled,
              'pre_capture': self.pre_capture,
              'post_capture': self.post_capture,
              'objects': self.objects,
              'retain': self.retain.to_dict()
          }
  
  class CameraRtmpConfig():
      def __init__(self, global_config, config):
          self._enabled = config['enabled']
  
      @property
      def enabled(self):
          return self._enabled
  
      def to_dict(self):
          return {
              'enabled': self.enabled,
          }
  
  class MotionConfig():
      def __init__(self, global_config, config, frame_shape):
          self._raw_mask = config.get('mask')
          if self._raw_mask:
              self._mask = create_mask(frame_shape, self._raw_mask)
          else:
              default_mask = np.zeros(frame_shape, np.uint8)
              default_mask[:] = 255
              self._mask = default_mask
          self._threshold = config.get('threshold', global_config.get('threshold', 25))
          self._contour_area = config.get('contour_area', global_config.get('contour_area', 100))
          self._delta_alpha = config.get('delta_alpha', global_config.get('delta_alpha', 0.2))
          self._frame_alpha = config.get('frame_alpha', global_config.get('frame_alpha', 0.2))
          self._frame_height = config.get('frame_height', global_config.get('frame_height', frame_shape[0]//6))
  
      @property
      def mask(self):
          return self._mask
  
      @property
      def threshold(self):
          return self._threshold
  
      @property
      def contour_area(self):
          return self._contour_area
  
      @property
      def delta_alpha(self):
          return self._delta_alpha
  
      @property
      def frame_alpha(self):
          return self._frame_alpha
  
      @property
      def frame_height(self):
          return self._frame_height
  
      def to_dict(self):
          return {
              'mask': self._raw_mask,
              'threshold': self.threshold,
              'contour_area': self.contour_area,
              'delta_alpha': self.delta_alpha,
              'frame_alpha': self.frame_alpha,
              'frame_height': self.frame_height,
          }
  
  
  
  class DetectConfig():
      def __init__(self, global_config, config, camera_fps):
          self._enabled = config['enabled']
          self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*2))
  
      @property
      def enabled(self):
          return self._enabled
  
      @property
      def max_disappeared(self):
          return self._max_disappeared
  
      def to_dict(self):
          return {
              'enabled': self.enabled,
              'max_disappeared': self._max_disappeared,
          }
  
  class ZoneConfig():
      def __init__(self, name, config):
          self._coordinates = config['coordinates']
          self._filters = { name: FilterConfig(c, c) for name, c in config['filters'].items() }
  
          if isinstance(self._coordinates, list):
              self._contour =  np.array([[int(p.split(',')[0]), int(p.split(',')[1])] for p in self._coordinates])
          elif isinstance(self._coordinates, str):
              points = self._coordinates.split(',')
              self._contour =  np.array([[int(points[i]), int(points[i+1])] for i in range(0, len(points), 2)])
          else:
              print(f"Unable to parse zone coordinates for {name}")
              self._contour = np.array([])
  
          self._color = (0,0,0)
  
      @property
      def coordinates(self):
          return self._coordinates
  
      @property
      def contour(self):
          return self._contour
  
      @contour.setter
      def contour(self, val):
          self._contour = val
  
      @property
      def color(self):
          return self._color
  
      @color.setter
      def color(self, val):
          self._color = val
  
      @property
      def filters(self):
          return self._filters
  
      def to_dict(self):
          return {
              'filters': {k: f.to_dict() for k, f in self.filters.items()},
              'coordinates': self._coordinates
          }
  
  class CameraConfig():
      def __init__(self, name, config, global_config):
          self._name = name
          self._ffmpeg = CameraFfmpegConfig(global_config['ffmpeg'], config['ffmpeg'])
          self._height = config.get('height')
          self._width = config.get('width')
          self._frame_shape = (self._height, self._width)
          self._frame_shape_yuv = (self._frame_shape[0]*3//2, self._frame_shape[1])
          self._fps = config.get('fps')
          self._best_image_timeout = config['best_image_timeout']
          self._zones = { name: ZoneConfig(name, z) for name, z in config['zones'].items() }
          self._clips = CameraClipsConfig(global_config, config['clips'])
          self._record = RecordConfig(global_config['record'], config['record'])
          self._rtmp = CameraRtmpConfig(global_config, config['rtmp'])
          self._snapshots = CameraSnapshotsConfig(global_config, config['snapshots'])
          self._mqtt = CameraMqttConfig(config['mqtt'])
          self._objects = ObjectConfig(global_config['objects'], config.get('objects', {}), self._frame_shape)
          self._motion = MotionConfig(global_config['motion'], config['motion'], self._frame_shape)
          self._detect = DetectConfig(global_config['detect'], config['detect'], config.get('fps', 5))
  
          self._ffmpeg_cmds = []
          for ffmpeg_input in self._ffmpeg.inputs:
              ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
              if ffmpeg_cmd is None:
                  continue
  
              self._ffmpeg_cmds.append({
                  'roles': ffmpeg_input.roles,
                  'cmd': ffmpeg_cmd
              })
  
  
          self._set_zone_colors(self._zones)
  
      def _get_ffmpeg_cmd(self, ffmpeg_input):
          ffmpeg_output_args = []
          if 'detect' in ffmpeg_input.roles:
              ffmpeg_output_args = self.ffmpeg.output_args['detect'] + ffmpeg_output_args + ['pipe:']
              if self.fps:
                  ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
          if 'rtmp' in ffmpeg_input.roles and self.rtmp.enabled:
              ffmpeg_output_args = self.ffmpeg.output_args['rtmp'] + [
                  f"rtmp://127.0.0.1/live/{self.name}"
              ] + ffmpeg_output_args
          if 'clips' in ffmpeg_input.roles:
              ffmpeg_output_args = self.ffmpeg.output_args['clips'] + [
                  f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
              ] + ffmpeg_output_args
          if 'record' in ffmpeg_input.roles and self.record.enabled:
              ffmpeg_output_args = self.ffmpeg.output_args['record'] + [
                  f"{os.path.join(RECORD_DIR, self.name)}-%Y%m%d%H%M%S.mp4"
              ] + ffmpeg_output_args
  
          # if there arent any outputs enabled for this input
          if len(ffmpeg_output_args) == 0:
              return None
  
          cmd = (['ffmpeg'] +
                  ffmpeg_input.global_args +
                  ffmpeg_input.hwaccel_args +
                  ffmpeg_input.input_args +
                  ['-i', ffmpeg_input.path] +
                  ffmpeg_output_args)
  
          return [part for part in cmd if part != '']
  
      def _set_zone_colors(self, zones: Dict[str, ZoneConfig]):
          # set colors for zones
          all_zone_names = zones.keys()
          zone_colors = {}
          colors = plt.cm.get_cmap('tab10', len(all_zone_names))
          for i, zone in enumerate(all_zone_names):
              zone_colors[zone] = tuple(int(round(255 * c)) for c in colors(i)[:3])
  
          for name, zone in zones.items():
              zone.color = zone_colors[name]
  
      @property
      def name(self):
          return self._name
  
      @property
      def ffmpeg(self):
          return self._ffmpeg
  
      @property
      def height(self):
          return self._height
  
      @property
      def width(self):
          return self._width
  
      @property
      def fps(self):
          return self._fps
  
      @property
      def best_image_timeout(self):
          return self._best_image_timeout
  
      @property
      def zones(self)-> Dict[str, ZoneConfig]:
          return self._zones
  
      @property
      def clips(self):
          return self._clips
  
      @property
      def record(self):
          return self._record
  
      @property
      def rtmp(self):
          return self._rtmp
  
      @property
      def snapshots(self):
          return self._snapshots
  
      @property
      def mqtt(self):
          return self._mqtt
  
      @property
      def objects(self):
          return self._objects
  
      @property
      def motion(self):
          return self._motion
  
      @property
      def detect(self):
          return self._detect
  
      @property
      def frame_shape(self):
          return self._frame_shape
  
      @property
      def frame_shape_yuv(self):
          return self._frame_shape_yuv
  
      @property
      def ffmpeg_cmds(self):
          return self._ffmpeg_cmds
  
      def to_dict(self):
          return {
              'name': self.name,
              'height': self.height,
              'width': self.width,
              'fps': self.fps,
              'best_image_timeout': self.best_image_timeout,
              'zones': {k: z.to_dict() for k, z in self.zones.items()},
              'clips': self.clips.to_dict(),
              'record': self.record.to_dict(),
              'rtmp': self.rtmp.to_dict(),
              'snapshots': self.snapshots.to_dict(),
              'mqtt': self.mqtt.to_dict(),
              'objects': self.objects.to_dict(),
              'motion': self.motion.to_dict(),
              'detect': self.detect.to_dict(),
              'frame_shape': self.frame_shape,
              'ffmpeg_cmds': [{'roles': c['roles'], 'cmd': ' '.join(c['cmd'])} for c in self.ffmpeg_cmds],
          }
  
  
  class FrigateConfig():
      def __init__(self, config_file=None, config=None):
          if config is None and config_file is None:
              raise ValueError('config or config_file must be defined')
          elif not config_file is None:
              config = self._load_file(config_file)
  
          config = FRIGATE_CONFIG_SCHEMA(config)
  
          config = self._sub_env_vars(config)
  
          self._database = DatabaseConfig(config['database'])
          self._model = ModelConfig(config['model'])
          self._detectors = { name: DetectorConfig(d) for name, d in config['detectors'].items() }
          self._mqtt = MqttConfig(config['mqtt'])
          self._clips = ClipsConfig(config['clips'])
          self._snapshots = SnapshotsConfig(config['snapshots'])
          self._cameras = { name: CameraConfig(name, c, config) for name, c in config['cameras'].items() }
          self._logger = LoggerConfig(config['logger'])
          self._environment_vars = config['environment_vars']
  
      def _sub_env_vars(self, config):
          frigate_env_vars = {k: v for k, v in os.environ.items() if k.startswith('FRIGATE_')}
  
          if 'password' in config['mqtt']:
              config['mqtt']['password'] = config['mqtt']['password'].format(**frigate_env_vars)
  
          for camera in config['cameras'].values():
              for i in camera['ffmpeg']['inputs']:
                  i['path'] = i['path'].format(**frigate_env_vars)
  
          return config
  
      def _load_file(self, config_file):
          with open(config_file) as f:
              raw_config = f.read()
  
          if config_file.endswith(".yml"):
              config = yaml.safe_load(raw_config)
          elif config_file.endswith(".json"):
              config = json.loads(raw_config)
  
          return config
  
      def to_dict(self):
          return {
              'database': self.database.to_dict(),
              'model': self.model.to_dict(),
              'detectors': {k: d.to_dict() for k, d in self.detectors.items()},
              'mqtt': self.mqtt.to_dict(),
              'clips': self.clips.to_dict(),
              'snapshots': self.snapshots.to_dict(),
              'cameras': {k: c.to_dict() for k, c in self.cameras.items()},
              'logger': self.logger.to_dict(),
              'environment_vars': self._environment_vars
          }
  
      @property
      def database(self):
          return self._database
  
      @property
      def model(self):
          return self._model
  
      @property
      def detectors(self) -> Dict[str, DetectorConfig]:
          return self._detectors
  
      @property
      def logger(self):
          return self._logger
  
      @property
      def mqtt(self):
          return self._mqtt
  
      @property
      def clips(self):
          return self._clips
  
      @property
      def snapshots(self):
          return self._snapshots
  
      @property
      def cameras(self) -> Dict[str, CameraConfig]:
          return self._cameras
  
      @property
      def environment_vars(self):
          return self._environment_vars
  