diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base index 794e81dac..ffc6bbe68 100644 --- a/docker/Dockerfile.base +++ b/docker/Dockerfile.base @@ -34,7 +34,7 @@ RUN apt-get -qq update \ RUN pip3 install \ peewee_migrate \ zeroconf \ - voluptuous\ + jsonschema \ Flask-Sockets \ gevent \ gevent-websocket diff --git a/frigate/config.py b/frigate/config.py index a12106c29..0119ef487 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -2,12 +2,13 @@ import base64 import json import logging import os +import pathlib from typing import Dict import cv2 import matplotlib.pyplot as plt import numpy as np -import voluptuous as vol +from jsonschema import Draft7Validator, validators import yaml from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR @@ -15,327 +16,10 @@ 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, - vol.Optional("retain", default={}): RETAIN_SCHEMA, - } -) - -FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"] -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], - "mask": vol.Any(str, [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"), - ), - "global_args": vol.Any(str, [str]), - "hwaccel_args": vol.Any(str, [str]), - "input_args": vol.Any(str, [str]), - "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, - vol.Optional("required_zones", default=[]): [str], - "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, - vol.Optional("required_zones", default=[]): [str], - "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("required_zones", default=[]): [str], - }, - 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"] + self._path = config.get("path", os.path.join(CLIPS_DIR, 'frigate.db')) @property def path(self): @@ -506,7 +190,7 @@ class CameraInput: class CameraFfmpegConfig: def __init__(self, global_config, config): self._inputs = [CameraInput(config, global_config, i) for i in config["inputs"]] - self._output_args = config.get("output_args", global_config["output_args"]) + self._output_args = config["output_args"] @property def inputs(self): @@ -522,7 +206,7 @@ class CameraFfmpegConfig: class RetainConfig: def __init__(self, global_config, config): - self._default = config.get("default", global_config.get("default")) + self._default = config["default"] self._objects = config.get("objects", global_config.get("objects", {})) @property @@ -645,9 +329,7 @@ class FilterConfig: class ObjectConfig: def __init__(self, global_config, config, frame_shape): - self._track = config.get( - "track", global_config.get("track", DEFAULT_TRACKED_OBJECTS) - ) + self._track = config.get("track", global_config.get("track")) self._raw_mask = config.get("mask") self._filters = { name: FilterConfig( @@ -913,7 +595,7 @@ class ZoneConfig: def __init__(self, name, config): self._coordinates = config["coordinates"] self._filters = { - name: FilterConfig(c, c) for name, c in config["filters"].items() + name: FilterConfig(c, c) for name, c in config.get("filters", {}).items() } if isinstance(self._coordinates, list): @@ -929,7 +611,7 @@ class ZoneConfig: [[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}") + logger.error(f"Unable to parse zone coordinates for {name}") self._contour = np.array([]) self._color = (0, 0, 0) @@ -1150,6 +832,23 @@ class CameraConfig: } +def extend_with_default(validator_class): + validate_properties = validator_class.VALIDATORS["properties"] + + def set_defaults(validator, properties, instance, schema): + for prop, subschema in properties.items(): + if "default" in subschema and isinstance(instance, dict): + instance.setdefault(prop, subschema["default"]) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {"properties": set_defaults}, + ) + class FrigateConfig: def __init__(self, config_file=None, config=None): if config is None and config_file is None: @@ -1157,7 +856,14 @@ class FrigateConfig: elif not config_file is None: config = self._load_file(config_file) - config = FRIGATE_CONFIG_SCHEMA(config) + dir = pathlib.Path(__file__).parent.absolute() + schema = self._load_file(os.path.join(dir, './schema/config.json')) + + SchemaValidator = extend_with_default(Draft7Validator) + validator = SchemaValidator(schema) + errors = sorted(validator.iter_errors(config), key=lambda e: e.path) + for error in errors: + logger.error(f"{error.path} - {error.message}") config = self._sub_env_vars(config) @@ -1173,7 +879,7 @@ class FrigateConfig: name: CameraConfig(name, c, config) for name, c in config["cameras"].items() } self._logger = LoggerConfig(config["logger"]) - self._environment_vars = config["environment_vars"] + self._environment_vars = config['environment_vars'] def _sub_env_vars(self, config): frigate_env_vars = { @@ -1212,7 +918,7 @@ class FrigateConfig: "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, + "environment_vars": self._environment_vars } @property diff --git a/frigate/schema/config.json b/frigate/schema/config.json new file mode 100644 index 000000000..4deda9d6d --- /dev/null +++ b/frigate/schema/config.json @@ -0,0 +1,464 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://blakeblackshear.github.io/frigate", + + "type": "object", + + "title": "frigate.yml", + + "description": "NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}. eg `password: '{FRIGATE_MQTT_PASSWORD}`", + + "properties": { + "mqtt": { + "title": "MQTT", + "type": "object", + "properties": { + "host": { "type": "string", "description": "MQTT host name", "examples": ["mqtt.server.com", "10.0.1.123"] }, + "port": { "type": "number", "default": 1883, "description": "MQTT port" }, + "topic_prefix": { + "type": "string", + "default": "frigate", + "description": "MQTT topic prefix – must be unique if you are running multiple instances" + }, + "client_id": { + "type": "string", + "default": "frigate", + "description": "MQTT client ID – must be unique if you are running multiple instances" + }, + "stats_interval": { + "type": "number", + "default": 60, + "description": "Interval in seconds for publishing Frigate internal stats to MQTT. Available at `#//stats`" + }, + "user": { "type": "string" }, + "password": { "type": "string" } + }, + "additionalProperties": false, + "required": ["host"], + "default": {} + }, + + "database": { + "type": "object", + "default": {}, + "properties": { + "path": { "type": "string" } + }, + "additionalProperties": false + }, + + "model": { + "type": "object", + "properties": { + "width": { "type": "integer", "default": 320 }, + "height": { "type": "integer", "default": 320 } + }, + "default": {}, + "additionalProperties": false, + "required": ["width", "height"] + }, + + "detectors": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["edgetpu", "cpu"], + "default": "edgetpu" + }, + "device": { "type": "string", "default": "usb" }, + "num_threads": { "type": "number", "default": 3 } + }, + "required": ["type"] + }, + "default": { + "coral": { + "type": "edgetpu", + "device": "usb" + } + } + }, + + "cameras": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "object", + "properties": { + "ffmpeg": { + "allOf": [ + { + "type": "object", + "properties": { + "inputs": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "#/definitions/ffmpeg" }, + { + "type": "object", + "properties": { + "path": { "type": "string" }, + "roles": { + "type": "array", + "items": { "type": "string", "enum": ["detect", "rtmp", "record", "clips"] }, + "uniqueItems": true + }, + "required": ["path", "roles"] + } + } + ] + } + } + }, + "required": ["inputs"] + }, + { "$ref": "#/definitions/ffmpeg" } + ] + }, + "width": { "type": "integer" }, + "height": { "type": "integer" }, + "fps": { "type": "integer" }, + "motion": { + "type": "object", + "properties": { + "mask": { "$ref": "#/definitions/string-or-array" } + }, + "default": {}, + "additionalProperties": false + }, + "objects": { + "type": "object", + "properties": { + "mask": { "type": "string", "examples": ["0,0,1000,0,1000,200,0,200"] }, + "track": { "type": "array", "items": { "type": "string" }, "default": ["person"] }, + "filters": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "object", + "properties": { + "mask": { "$ref": "#/definitions/string-or-array" }, + "filters": { "$ref": "#/definitions/filters" } + }, + "additionalProperties": false + }, + "default": {} + } + }, + "additionalProperties": false, + "default": {} + }, + "best_image_timeout": { "type": "integer", "default": 60 }, + "zones": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "object", + "additionalProperties": { + "coordinates": { "$ref": "#/definitions/string-or-array" }, + "filters": { + "$ref": "#/definitions/filters", + "default": {} + } + }, + "required": ["coordinates"], + "default": {} + }, + "default": {} + }, + "clips": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": true }, + "pre_capture": { "type": "integer", "default": 5 }, + "post_capture": { "type": "integer", "default": 5 }, + "required_zones": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] }, + "objects": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": ["person"] }, + "retain": { "$ref": "#/definitions/retain", "default": {} } + }, + "additionalProperties": false, + "default": {} + }, + "record": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": false }, + "retain_days": { "type": "integer", "default": 30 } + }, + "additionalProperties": false, + "default": {} + }, + "rtmp": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": true } + }, + "default": {}, + "additionalProperties": false + }, + "snapshots": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": true }, + "timestamp": { "type": "boolean", "default": false }, + "bounding_box": { "type": "boolean", "default": false }, + "crop": { "type": "boolean", "default": false }, + "height": { "type": "integer" }, + "required_zones": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] }, + "retain": { "$ref": "#/definitions/retain", "default": {} } + }, + "additionalProperties": false, + "default": {} + }, + "mqtt": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": true }, + "timestamp": { "type": "boolean", "default": true }, + "bounding_box": { "type": "boolean", "default": true }, + "crop": { "type": "boolean", "default": true }, + "height": { "type": "integer", "default": 270 }, + "required_zones": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] } + }, + "additionalProperties": false, + "default": {} + } + }, + "required": ["ffmpeg", "width", "height"] + } + }, + + "record": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": false }, + "retain_days": { "type": "integer", "default": 30 } + }, + "additionalProperties": false, + "default": {} + }, + + "motion": { + "$ref": "#/definitions/motion", + "default": {} + }, + + "detect": { + "$ref": "#/definitions/detect", + "default": {} + }, + + "clips": { + "type": "object", + "properties": { + "max_seconds": { + "type": "integer", + "description": "Maximum length of time to retain video during long events. If an object is being tracked for longer than this amount of time, the cache will begin to expire and the resulting clip will be the last x seconds of the event.", + "default": 300 + }, + "tmpfs_cache_size": { + "type": "string", + "description": "Size of tmpfs mount to create for cache files, eg `mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache`.\n\n> Addon users must have Protection mode disabled for the addon when using this setting. \n\n> Also, if you have mounted a tmpfs volume through docker, this value should not be set in your config.", + "examples": ["256m", "512m"] + }, + "retain": { "$ref": "#/definitions/retain", "default": {} } + }, + "default": {}, + "additionalProperties": false + }, + + "snapshots": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "defualt": true }, + "retain": { "$ref": "#/definitions/retain", "default": {} } + }, + "default": {}, + "additionalProperties": false + }, + + "ffmpeg": { + "title": "ffmpeg", + "description": "Arguments to apply as a default to all FFMPEG processes", + "$ref": "#/definitions/ffmpeg" + }, + + "objects": { + "$ref": "#/definitions/objects", + "default": {} + }, + + "logger": { + "type": "object", + "description": "Change the default log levels for troubleshooting purposes.", + "properties": { + "default": { "$ref": "#/definitions/log-level" }, + "logs": { + "type": "object", + "properties": {}, + "additionalProperties": { "$ref": "#/definitions/log-level" } + } + }, + "additionalProperties": false + }, + + "environment_vars": { + "type": "object", + "properties": {}, + "additionalProperties": { "type": "string" }, + "default": {} + } + }, + + "additionalProperties": false, + + "required": ["mqtt", "detectors", "cameras"], + + "examples": [ + { + "mqtt": { + "host": "mqtt.server.com" + }, + "cameras": { + "back": { + "ffmpeg": { + "inputs": [ + { + "path": "rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2", + "roles": ["detect", "rtmp"] + } + ] + }, + "width": 1280, + "height": 720, + "fps": 5 + } + } + } + ], + + "definitions": { + "log-level": { "type": "string", "enum": ["debug", "info", "warning", "error", "critical"] }, + + "filters": { + "$id": "#filters", + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "object", + "properties": { + "min_area": { + "type": "integer", + "description": "minimum width*height of the bounding box for the detected object", + "default": 0 + }, + "max_area": { + "type": "integer", + "description": "maximum width*height of the bounding box for the detected object", + "default": 24000000 + }, + "min_score": { + "type": "number", + "description": "minimum score for the object to initiate tracking", + "default": 0.5 + }, + "threshold": { + "type": "number", + "description": "minimum decimal percentage for tracked object's computed score to be considered a true positive", + "default": 0.7 + } + }, + "default": { + "min_area": 0, + "max_area": 24000000, + "min_score": 0.5, + "threshold": 0.7 + } + }, + "default": {} + }, + + "string-or-array": { + "$id": "#string-or-array", + "anyOf": [{ "type": "array", "items": { "type": "string" } }, { "type": "string" }] + }, + + "motion": { + "$id": "#motion", + "type": "object", + "properties": { + "mask": { "type": "string" }, + "threshold": { "type": "integer", "minimum": 0, "maximum": 255 }, + "contour_area": { "type": "integer" }, + "delta_alpha": { "type": "number" }, + "frame_alpha": { "type": "number" }, + "frame_height": { "type": "integer" } + } + }, + + "detect": { + "$id": "#detect", + "type": "object", + "properties": { + "max_disappeared": { "type": "integer" } + } + }, + + "retain": { + "$id": "#retain", + "type": "object", + "properties": { + "default": { "type": "number", "default": 10 }, + "objects": { "type": "object", "properties": {}, "additionalProperties": { "type": "number" } } + } + }, + + "ffmpeg": { + "$id": "#ffmpeg", + "description": "Configure FFMPEG process arguments", + "properties": { + "global_args": { "$ref": "#/definitions/string-or-array", "default": "-hide_banner -loglevel warning" }, + "hwaccel_args": { "$ref": "#/definitions/string-or-array", "default": "" }, + "input_args": { + "$ref": "#/definitions/string-or-array", + "default": "-avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1" + }, + + "output_args": { + "type": "object", + "properties": { + "detect": { "$ref": "#/definitions/string-or-array", "default": "-f rawvideo -pix_fmt yuv420p" }, + "record": { + "$ref": "#/definitions/string-or-array", + "default": "-f segment -segment_time 60 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an" + }, + "clips": { + "$ref": "#/definitions/string-or-array", + "default": "-f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an" + }, + "rtmp": { "$ref": "#/definitions/string-or-array", "default": "-c copy -f flv" } + } + } + } + }, + + "objects": { + "$id": "#objects", + "description": "Track specific objects and apply filters to each", + "type": "object", + "properties": { + "track": { "type": "array", "items": { "type": "string" }, "default": ["person"] }, + "filters": { + "$ref": "#/definitions/filters", + "default": {} + } + }, + "additionalProperties": false, + "default": {} + } + } +}