refactor(app): use jsonschema for config

This commit is contained in:
Paul Armstrong 2021-02-27 07:51:28 -08:00
parent 0f1bc40f00
commit 5950cf34fc
No known key found for this signature in database
GPG Key ID: 1F24DA6C38E1FE4A
3 changed files with 500 additions and 330 deletions

View File

@ -34,7 +34,7 @@ RUN apt-get -qq update \
RUN pip3 install \ RUN pip3 install \
peewee_migrate \ peewee_migrate \
zeroconf \ zeroconf \
voluptuous\ jsonschema \
Flask-Sockets \ Flask-Sockets \
gevent \ gevent \
gevent-websocket gevent-websocket

View File

@ -2,12 +2,13 @@ import base64
import json import json
import logging import logging
import os import os
import pathlib
from typing import Dict from typing import Dict
import cv2 import cv2
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import voluptuous as vol from jsonschema import Draft7Validator, validators
import yaml import yaml
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
@ -15,327 +16,10 @@ from frigate.util import create_mask
logger = logging.getLogger(__name__) 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: class DatabaseConfig:
def __init__(self, config): def __init__(self, config):
self._path = config["path"] self._path = config.get("path", os.path.join(CLIPS_DIR, 'frigate.db'))
@property @property
def path(self): def path(self):
@ -506,7 +190,7 @@ class CameraInput:
class CameraFfmpegConfig: class CameraFfmpegConfig:
def __init__(self, global_config, config): def __init__(self, global_config, config):
self._inputs = [CameraInput(config, global_config, i) for i in config["inputs"]] 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 @property
def inputs(self): def inputs(self):
@ -522,7 +206,7 @@ class CameraFfmpegConfig:
class RetainConfig: class RetainConfig:
def __init__(self, global_config, config): 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", {})) self._objects = config.get("objects", global_config.get("objects", {}))
@property @property
@ -645,9 +329,7 @@ class FilterConfig:
class ObjectConfig: class ObjectConfig:
def __init__(self, global_config, config, frame_shape): def __init__(self, global_config, config, frame_shape):
self._track = config.get( self._track = config.get("track", global_config.get("track"))
"track", global_config.get("track", DEFAULT_TRACKED_OBJECTS)
)
self._raw_mask = config.get("mask") self._raw_mask = config.get("mask")
self._filters = { self._filters = {
name: FilterConfig( name: FilterConfig(
@ -913,7 +595,7 @@ class ZoneConfig:
def __init__(self, name, config): def __init__(self, name, config):
self._coordinates = config["coordinates"] self._coordinates = config["coordinates"]
self._filters = { 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): 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)] [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
) )
else: 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._contour = np.array([])
self._color = (0, 0, 0) 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: class FrigateConfig:
def __init__(self, config_file=None, config=None): def __init__(self, config_file=None, config=None):
if config is None and config_file is None: if config is None and config_file is None:
@ -1157,7 +856,14 @@ class FrigateConfig:
elif not config_file is None: elif not config_file is None:
config = self._load_file(config_file) 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) config = self._sub_env_vars(config)
@ -1173,7 +879,7 @@ class FrigateConfig:
name: CameraConfig(name, c, config) for name, c in config["cameras"].items() name: CameraConfig(name, c, config) for name, c in config["cameras"].items()
} }
self._logger = LoggerConfig(config["logger"]) self._logger = LoggerConfig(config["logger"])
self._environment_vars = config["environment_vars"] self._environment_vars = config['environment_vars']
def _sub_env_vars(self, config): def _sub_env_vars(self, config):
frigate_env_vars = { frigate_env_vars = {
@ -1212,7 +918,7 @@ class FrigateConfig:
"snapshots": self.snapshots.to_dict(), "snapshots": self.snapshots.to_dict(),
"cameras": {k: c.to_dict() for k, c in self.cameras.items()}, "cameras": {k: c.to_dict() for k, c in self.cameras.items()},
"logger": self.logger.to_dict(), "logger": self.logger.to_dict(),
"environment_vars": self._environment_vars, "environment_vars": self._environment_vars
} }
@property @property

464
frigate/schema/config.json Normal file
View File

@ -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 `#/<topic_prefix>/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": {}
}
}
}