diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..f687072fe
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,27 @@
+{
+ "name": "Frigate Dev",
+ "dockerComposeFile": "../docker-compose.yml",
+ "service": "dev",
+ "workspaceFolder": "/lab/frigate",
+ "extensions": [
+ "ms-python.python",
+ "visualstudioexptteam.vscodeintellicode",
+ "mhutchie.git-graph",
+ "ms-azuretools.vscode-docker",
+ "streetsidesoftware.code-spell-checker",
+ "eamodio.gitlens",
+ "esbenp.prettier-vscode",
+ "ms-python.vscode-pylance"
+ ],
+ "settings": {
+ "python.pythonPath": "/usr/bin/python3",
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.formatting.provider": "black",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true,
+ "terminal.integrated.shell.linux": "/bin/bash"
+ }
+}
diff --git a/.dockerignore b/.dockerignore
index c6f4105f3..5cad5e7d4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,4 +4,7 @@ docs/
debug
config/
*.pyc
-.git
\ No newline at end of file
+.git
+core
+*.mp4
+*.db
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a1dc92a01..85caded13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.DS_Store
*.pyc
+*.swp
debug
.vscode
config/config.yml
@@ -10,3 +11,4 @@ frigate/version.py
web/build
web/node_modules
web/coverage
+core
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 000000000..bf205daa6
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,588 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=10.0
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ non-ascii-bytes-literal,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ deprecated-itertools-function,
+ deprecated-types-field,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ deprecated-operator-function,
+ deprecated-urllib-function,
+ xreadlines-attribute,
+ deprecated-sys-function,
+ exception-escape,
+ comprehension-escape
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it work,
+# install the python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )??$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=fstr
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+ Exception
diff --git a/Makefile b/Makefile
index eb6e9b49a..3cf7d0fb8 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version:
- echo "VERSION='0.8.4-$(COMMIT_HASH)'" > frigate/version.py
+ echo "VERSION='0.9.0-$(COMMIT_HASH)'" > frigate/version.py
web:
docker build --tag frigate-web --file docker/Dockerfile.web web/
@@ -12,11 +12,14 @@ amd64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64 --file docker/Dockerfile.wheels .
amd64_ffmpeg:
- docker build --tag blakeblackshear/frigate-ffmpeg:1.1.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
+ docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
+
+nginx_frigate:
+ docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate-nginx:1.0.2 --file docker/Dockerfile.nginx .
amd64_frigate: version web
- docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
- docker build --tag frigate --file docker/Dockerfile.amd64 .
+ docker build --no-cache --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
+ docker build --no-cache --tag frigate --file docker/Dockerfile.amd64 .
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
@@ -24,11 +27,11 @@ amd64nvidia_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64nvidia --file docker/Dockerfile.wheels .
amd64nvidia_ffmpeg:
- docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
+ docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
amd64nvidia_frigate: version web
- docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
- docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
+ docker build --no-cache --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
+ docker build --no-cache --tag frigate --file docker/Dockerfile.amd64nvidia .
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
@@ -36,11 +39,11 @@ aarch64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels .
aarch64_ffmpeg:
- docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
+ docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
aarch64_frigate: version web
- docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
- docker build --tag frigate --file docker/Dockerfile.aarch64 .
+ docker build --no-cache --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
+ docker build --no-cache --tag frigate --file docker/Dockerfile.aarch64 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
@@ -48,11 +51,11 @@ armv7_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels .
armv7_ffmpeg:
- docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
+ docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
armv7_frigate: version web
- docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
- docker build --tag frigate --file docker/Dockerfile.armv7 .
+ docker build --no-cache --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --build-arg NGINX_VERSION=1.0.2 --file docker/Dockerfile.base .
+ docker build --no-cache --tag frigate --file docker/Dockerfile.armv7 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
diff --git a/README.md b/README.md
index 012a856af..94f80db2d 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
- Uses a very low overhead motion detection to determine where to run object detection
- Object detection with TensorFlow runs in separate processes for maximum FPS
- Communicates over MQTT for easy integration into other systems
-- Records video clips of detected objects
+- Records video with retention settings based on detected objects
- 24/7 recording
- Re-streaming via RTMP to reduce the number of connections to your camera
@@ -23,16 +23,20 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
View the documentation at https://blakeblackshear.github.io/frigate
## Donations
+
If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear).
## Screenshots
+
Integration into Home Assistant
+
Also comes with a builtin UI:
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 000000000..197d9e11e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,29 @@
+version: "3"
+services:
+ dev:
+ container_name: frigate-dev
+ user: vscode
+ privileged: true
+ shm_size: "256mb"
+ build:
+ context: .
+ dockerfile: docker/Dockerfile.dev
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+ - .:/lab/frigate:cached
+ - ./config/config.yml:/config/config.yml:ro
+ - ./debug:/media/frigate
+ - /dev/bus/usb:/dev/bus/usb
+ - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
+ ports:
+ - "1935:1935"
+ - "5000:5000"
+ - "5001:5001"
+ - "8080:8080"
+ entrypoint: ["sudo", "/init"]
+ command: /bin/sh -c "while sleep 1000; do :; done"
+ mqtt:
+ container_name: mqtt
+ image: eclipse-mosquitto:1.6
+ ports:
+ - "1883:1883"
diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64
index 5ce548f2d..30d69fc83 100644
--- a/docker/Dockerfile.aarch64
+++ b/docker/Dockerfile.aarch64
@@ -5,18 +5,24 @@ ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
- # ffmpeg runtime dependencies
- libgomp1 \
- # runtime dependencies
- libopenexr24 \
- libgstreamer1.0-0 \
- libgstreamer-plugins-base1.0-0 \
- libopenblas-base \
- libjpeg-turbo8 \
- libpng16-16 \
- libtiff5 \
- libdc1394-22 \
- ## Tensorflow lite
- && pip3 install https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_aarch64.whl \
+ # ffmpeg runtime dependencies
+ libgomp1 \
+ # runtime dependencies
+ libopenexr24 \
+ libgstreamer1.0-0 \
+ libgstreamer-plugins-base1.0-0 \
+ libopenblas-base \
+ libjpeg-turbo8 \
+ libpng16-16 \
+ libtiff5 \
+ libdc1394-22 \
&& rm -rf /var/lib/apt/lists/* \
- && (apt-get autoremove -y; apt-get autoclean -y)
\ No newline at end of file
+ && (apt-get autoremove -y; apt-get autoclean -y)
+
+# s6-overlay
+ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-aarch64-installer /tmp/
+RUN chmod +x /tmp/s6-overlay-aarch64-installer && /tmp/s6-overlay-aarch64-installer /
+
+ENTRYPOINT ["/init"]
+
+CMD ["python3", "-u", "-m", "frigate"]
\ No newline at end of file
diff --git a/docker/Dockerfile.amd64 b/docker/Dockerfile.amd64
index 56f9839e8..d583e43f8 100644
--- a/docker/Dockerfile.amd64
+++ b/docker/Dockerfile.amd64
@@ -4,15 +4,25 @@ LABEL maintainer "blakeb@blakeshome.com"
# By default, use the i965 driver
ENV LIBVA_DRIVER_NAME=i965
# Install packages for apt repo
+
+RUN wget -qO - https://repositories.intel.com/graphics/intel-graphics.key | apt-key add - \
+ && echo 'deb [arch=amd64] https://repositories.intel.com/graphics/ubuntu focal main' > /etc/apt/sources.list.d/intel-graphics.list \
+ && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F63F0F2B90935439 \
+ && echo 'deb http://ppa.launchpad.net/kisak/kisak-mesa/ubuntu focal main' > /etc/apt/sources.list.d/kisak-mesa-focal.list
+
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
- # ffmpeg dependencies
- libgomp1 \
- # VAAPI drivers for Intel hardware accel
- libva-drm2 libva2 libmfx1 i965-va-driver vainfo intel-media-va-driver mesa-va-drivers \
- ## Tensorflow lite
- && wget -q https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
- && python3.8 -m pip install tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
- && rm tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
+ # ffmpeg dependencies
+ libgomp1 \
+ # VAAPI drivers for Intel hardware accel
+ libva-drm2 libva2 libmfx1 i965-va-driver vainfo intel-media-va-driver-non-free mesa-vdpau-drivers mesa-va-drivers mesa-vdpau-drivers libdrm-radeon1 \
&& rm -rf /var/lib/apt/lists/* \
- && (apt-get autoremove -y; apt-get autoclean -y)
\ No newline at end of file
+ && (apt-get autoremove -y; apt-get autoclean -y)
+
+# s6-overlay
+ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64-installer /tmp/
+RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer /
+
+ENTRYPOINT ["/init"]
+
+CMD ["python3", "-u", "-m", "frigate"]
\ No newline at end of file
diff --git a/docker/Dockerfile.amd64nvidia b/docker/Dockerfile.amd64nvidia
index 8714f70ad..f893d684f 100644
--- a/docker/Dockerfile.amd64nvidia
+++ b/docker/Dockerfile.amd64nvidia
@@ -4,12 +4,8 @@ LABEL maintainer "blakeb@blakeshome.com"
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
- # ffmpeg dependencies
- libgomp1 \
- ## Tensorflow lite
- && wget -q https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
- && python3.8 -m pip install tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
- && rm tflite_runtime-2.5.0-cp38-cp38-linux_x86_64.whl \
+ # ffmpeg dependencies
+ libgomp1 \
&& rm -rf /var/lib/apt/lists/* \
&& (apt-get autoremove -y; apt-get autoclean -y)
@@ -45,3 +41,11 @@ ENV LD_LIBRARY_PATH /usr/local/nvidia/lib:/usr/local/nvidia/lib64
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES compute,utility,video
ENV NVIDIA_REQUIRE_CUDA "cuda>=11.1 brand=tesla,driver>=418,driver<419 brand=tesla,driver>=440,driver<441 brand=tesla,driver>=450,driver<451"
+
+# s6-overlay
+ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64-installer /tmp/
+RUN chmod +x /tmp/s6-overlay-amd64-installer && /tmp/s6-overlay-amd64-installer /
+
+ENTRYPOINT ["/init"]
+
+CMD ["python3", "-u", "-m", "frigate"]
\ No newline at end of file
diff --git a/docker/Dockerfile.armv7 b/docker/Dockerfile.armv7
index 2e50cd22c..af44301b8 100644
--- a/docker/Dockerfile.armv7
+++ b/docker/Dockerfile.armv7
@@ -5,20 +5,26 @@ ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get -qq install --no-install-recommends -y \
- # ffmpeg runtime dependencies
- libgomp1 \
- # runtime dependencies
- libopenexr24 \
- libgstreamer1.0-0 \
- libgstreamer-plugins-base1.0-0 \
- libopenblas-base \
- libjpeg-turbo8 \
- libpng16-16 \
- libtiff5 \
- libdc1394-22 \
- libaom0 \
- libx265-179 \
- ## Tensorflow lite
- && pip3 install https://github.com/google-coral/pycoral/releases/download/release-frogfish/tflite_runtime-2.5.0-cp38-cp38-linux_armv7l.whl \
+ # ffmpeg runtime dependencies
+ libgomp1 \
+ # runtime dependencies
+ libopenexr24 \
+ libgstreamer1.0-0 \
+ libgstreamer-plugins-base1.0-0 \
+ libopenblas-base \
+ libjpeg-turbo8 \
+ libpng16-16 \
+ libtiff5 \
+ libdc1394-22 \
+ libaom0 \
+ libx265-179 \
&& rm -rf /var/lib/apt/lists/* \
- && (apt-get autoremove -y; apt-get autoclean -y)
\ No newline at end of file
+ && (apt-get autoremove -y; apt-get autoclean -y)
+
+# s6-overlay
+ADD https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-armhf-installer /tmp/
+RUN chmod +x /tmp/s6-overlay-armhf-installer && /tmp/s6-overlay-armhf-installer /
+
+ENTRYPOINT ["/init"]
+
+CMD ["python3", "-u", "-m", "frigate"]
\ No newline at end of file
diff --git a/docker/Dockerfile.base b/docker/Dockerfile.base
index 794e81dac..8b025a23c 100644
--- a/docker/Dockerfile.base
+++ b/docker/Dockerfile.base
@@ -1,8 +1,10 @@
ARG ARCH=amd64
ARG WHEELS_VERSION
ARG FFMPEG_VERSION
+ARG NGINX_VERSION
FROM blakeblackshear/frigate-wheels:${WHEELS_VERSION}-${ARCH} as wheels
FROM blakeblackshear/frigate-ffmpeg:${FFMPEG_VERSION}-${ARCH} as ffmpeg
+FROM blakeblackshear/frigate-nginx:${NGINX_VERSION} as nginx
FROM frigate-web as web
FROM ubuntu:20.04
@@ -18,28 +20,23 @@ ENV DEBIAN_FRONTEND=noninteractive
# Install packages for apt repo
RUN apt-get -qq update \
&& apt-get upgrade -y \
- && apt-get -qq install --no-install-recommends -y \
- gnupg wget unzip tzdata nginx libnginx-mod-rtmp \
- && apt-get -qq install --no-install-recommends -y \
- python3-pip \
+ && apt-get -qq install --no-install-recommends -y gnupg wget unzip tzdata libxml2 \
+ && apt-get -qq install --no-install-recommends -y python3-pip \
&& pip3 install -U /wheels/*.whl \
&& APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
- && apt-get -qq update && apt-get -qq install --no-install-recommends -y \
- libedgetpu1-max=15.0 \
+ && apt-get -qq update && apt-get -qq install --no-install-recommends -y libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& rm -rf /var/lib/apt/lists/* /wheels \
&& (apt-get autoremove -y; apt-get autoclean -y)
RUN pip3 install \
peewee_migrate \
+ pydantic \
zeroconf \
- voluptuous\
- Flask-Sockets \
- gevent \
- gevent-websocket
+ ws4py
-COPY nginx/nginx.conf /etc/nginx/nginx.conf
+COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
# get model and labels
COPY labelmap.txt /labelmap.txt
@@ -52,10 +49,7 @@ ADD migrations migrations/
COPY --from=web /opt/frigate/build web/
-COPY run.sh /run.sh
-RUN chmod +x /run.sh
+COPY docker/rootfs/ /
EXPOSE 5000
EXPOSE 1935
-
-CMD ["/run.sh"]
diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev
new file mode 100644
index 000000000..41a3296c9
--- /dev/null
+++ b/docker/Dockerfile.dev
@@ -0,0 +1,24 @@
+FROM frigate:latest
+
+ARG USERNAME=vscode
+ARG USER_UID=1000
+ARG USER_GID=$USER_UID
+
+# Create the user
+RUN groupadd --gid $USER_GID $USERNAME \
+ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
+ #
+ # [Optional] Add sudo support. Omit if you don't need to install software after connecting.
+ && apt-get update \
+ && apt-get install -y sudo \
+ && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
+ && chmod 0440 /etc/sudoers.d/$USERNAME
+
+RUN apt-get update \
+ && apt-get install -y git curl vim htop
+
+RUN pip3 install pylint black
+
+# Install Node 14
+RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
+ && apt-get install -y nodejs
diff --git a/docker/Dockerfile.ffmpeg.aarch64 b/docker/Dockerfile.ffmpeg.aarch64
index 3fa630a65..c376f63a3 100644
--- a/docker/Dockerfile.ffmpeg.aarch64
+++ b/docker/Dockerfile.ffmpeg.aarch64
@@ -15,33 +15,33 @@ RUN apt-get -yqq update && \
FROM base as build
-ENV FFMPEG_VERSION=4.3.1 \
- AOM_VERSION=v1.0.0 \
- FDKAAC_VERSION=0.1.5 \
- FREETYPE_VERSION=2.5.5 \
- FRIBIDI_VERSION=0.19.7 \
- KVAZAAR_VERSION=1.2.0 \
- LAME_VERSION=3.100 \
- LIBPTHREAD_STUBS_VERSION=0.4 \
- LIBVIDSTAB_VERSION=1.1.0 \
- LIBXCB_VERSION=1.13.1 \
- XCBPROTO_VERSION=1.13 \
- OGG_VERSION=1.3.2 \
- OPENCOREAMR_VERSION=0.1.5 \
- OPUS_VERSION=1.2 \
- OPENJPEG_VERSION=2.1.2 \
- THEORA_VERSION=1.1.1 \
- VORBIS_VERSION=1.3.5 \
- VPX_VERSION=1.8.0 \
- WEBP_VERSION=1.0.2 \
- X264_VERSION=20170226-2245-stable \
- X265_VERSION=3.1.1 \
- XAU_VERSION=1.0.9 \
- XORG_MACROS_VERSION=1.19.2 \
- XPROTO_VERSION=7.0.31 \
- XVID_VERSION=1.3.4 \
- LIBZMQ_VERSION=4.3.2 \
- SRC=/usr/local
+ENV FFMPEG_VERSION=4.3.2 \
+ AOM_VERSION=v1.0.0 \
+ FDKAAC_VERSION=0.1.5 \
+ FREETYPE_VERSION=2.5.5 \
+ FRIBIDI_VERSION=0.19.7 \
+ KVAZAAR_VERSION=1.2.0 \
+ LAME_VERSION=3.100 \
+ LIBPTHREAD_STUBS_VERSION=0.4 \
+ LIBVIDSTAB_VERSION=1.1.0 \
+ LIBXCB_VERSION=1.13.1 \
+ XCBPROTO_VERSION=1.13 \
+ OGG_VERSION=1.3.2 \
+ OPENCOREAMR_VERSION=0.1.5 \
+ OPUS_VERSION=1.2 \
+ OPENJPEG_VERSION=2.1.2 \
+ THEORA_VERSION=1.1.1 \
+ VORBIS_VERSION=1.3.5 \
+ VPX_VERSION=1.8.0 \
+ WEBP_VERSION=1.0.2 \
+ X264_VERSION=20170226-2245-stable \
+ X265_VERSION=3.1.1 \
+ XAU_VERSION=1.0.9 \
+ XORG_MACROS_VERSION=1.19.2 \
+ XPROTO_VERSION=7.0.31 \
+ XVID_VERSION=1.3.4 \
+ LIBZMQ_VERSION=4.3.2 \
+ SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
@@ -61,27 +61,27 @@ ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
-RUN buildDeps="autoconf \
- automake \
- cmake \
- curl \
- bzip2 \
- libexpat1-dev \
- g++ \
- gcc \
- git \
- gperf \
- libtool \
- make \
- nasm \
- perl \
- pkg-config \
- python \
- libssl-dev \
- yasm \
- linux-headers-raspi2 \
- libomxil-bellagio-dev \
- zlib1g-dev" && \
+RUN buildDeps="autoconf \
+ automake \
+ cmake \
+ curl \
+ bzip2 \
+ libexpat1-dev \
+ g++ \
+ gcc \
+ git \
+ gperf \
+ libtool \
+ make \
+ nasm \
+ perl \
+ pkg-config \
+ python \
+ libssl-dev \
+ yasm \
+ linux-headers-raspi2 \
+ libomxil-bellagio-dev \
+ zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
@@ -459,7 +459,7 @@ RUN \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
- sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
+ sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
diff --git a/docker/Dockerfile.ffmpeg.amd64 b/docker/Dockerfile.ffmpeg.amd64
index 21e0e6b9f..5dc005505 100644
--- a/docker/Dockerfile.ffmpeg.amd64
+++ b/docker/Dockerfile.ffmpeg.amd64
@@ -14,33 +14,33 @@ RUN apt-get -yqq update && \
FROM base as build
-ENV FFMPEG_VERSION=4.3.1 \
- AOM_VERSION=v1.0.0 \
- FDKAAC_VERSION=0.1.5 \
- FREETYPE_VERSION=2.5.5 \
- FRIBIDI_VERSION=0.19.7 \
- KVAZAAR_VERSION=1.2.0 \
- LAME_VERSION=3.100 \
- LIBPTHREAD_STUBS_VERSION=0.4 \
- LIBVIDSTAB_VERSION=1.1.0 \
- LIBXCB_VERSION=1.13.1 \
- XCBPROTO_VERSION=1.13 \
- OGG_VERSION=1.3.2 \
- OPENCOREAMR_VERSION=0.1.5 \
- OPUS_VERSION=1.2 \
- OPENJPEG_VERSION=2.1.2 \
- THEORA_VERSION=1.1.1 \
- VORBIS_VERSION=1.3.5 \
- VPX_VERSION=1.8.0 \
- WEBP_VERSION=1.0.2 \
- X264_VERSION=20170226-2245-stable \
- X265_VERSION=3.1.1 \
- XAU_VERSION=1.0.9 \
- XORG_MACROS_VERSION=1.19.2 \
- XPROTO_VERSION=7.0.31 \
- XVID_VERSION=1.3.4 \
- LIBZMQ_VERSION=4.3.2 \
- SRC=/usr/local
+ENV FFMPEG_VERSION=4.3.2 \
+ AOM_VERSION=v1.0.0 \
+ FDKAAC_VERSION=0.1.5 \
+ FREETYPE_VERSION=2.5.5 \
+ FRIBIDI_VERSION=0.19.7 \
+ KVAZAAR_VERSION=1.2.0 \
+ LAME_VERSION=3.100 \
+ LIBPTHREAD_STUBS_VERSION=0.4 \
+ LIBVIDSTAB_VERSION=1.1.0 \
+ LIBXCB_VERSION=1.13.1 \
+ XCBPROTO_VERSION=1.13 \
+ OGG_VERSION=1.3.2 \
+ OPENCOREAMR_VERSION=0.1.5 \
+ OPUS_VERSION=1.2 \
+ OPENJPEG_VERSION=2.1.2 \
+ THEORA_VERSION=1.1.1 \
+ VORBIS_VERSION=1.3.5 \
+ VPX_VERSION=1.8.0 \
+ WEBP_VERSION=1.0.2 \
+ X264_VERSION=20170226-2245-stable \
+ X265_VERSION=3.1.1 \
+ XAU_VERSION=1.0.9 \
+ XORG_MACROS_VERSION=1.19.2 \
+ XPROTO_VERSION=7.0.31 \
+ XVID_VERSION=1.3.4 \
+ LIBZMQ_VERSION=4.3.2 \
+ SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
@@ -60,27 +60,27 @@ ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib"
-RUN buildDeps="autoconf \
- automake \
- cmake \
- curl \
- bzip2 \
- libexpat1-dev \
- g++ \
- gcc \
- git \
- gperf \
- libtool \
- make \
- nasm \
- perl \
- pkg-config \
- python \
- libssl-dev \
- yasm \
- libva-dev \
- libmfx-dev \
- zlib1g-dev" && \
+RUN buildDeps="autoconf \
+ automake \
+ cmake \
+ curl \
+ bzip2 \
+ libexpat1-dev \
+ g++ \
+ gcc \
+ git \
+ gperf \
+ libtool \
+ make \
+ nasm \
+ perl \
+ pkg-config \
+ python \
+ libssl-dev \
+ yasm \
+ libva-dev \
+ libmfx-dev \
+ zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
@@ -450,7 +450,7 @@ RUN \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
- sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
+ sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
@@ -463,6 +463,6 @@ ENTRYPOINT ["ffmpeg"]
COPY --from=build /usr/local /usr/local/
RUN \
- apt-get update -y && \
- apt-get install -y --no-install-recommends libva-drm2 libva2 i965-va-driver mesa-va-drivers && \
- rm -rf /var/lib/apt/lists/*
+ apt-get update -y && \
+ apt-get install -y --no-install-recommends libva-drm2 libva2 i965-va-driver mesa-va-drivers && \
+ rm -rf /var/lib/apt/lists/*
diff --git a/docker/Dockerfile.ffmpeg.amd64nvidia b/docker/Dockerfile.ffmpeg.amd64nvidia
index 402e8973d..71f91fe4f 100644
--- a/docker/Dockerfile.ffmpeg.amd64nvidia
+++ b/docker/Dockerfile.ffmpeg.amd64nvidia
@@ -37,36 +37,36 @@ FROM devel-base as build
ENV NVIDIA_HEADERS_VERSION=9.1.23.1
-ENV FFMPEG_VERSION=4.3.1 \
- AOM_VERSION=v1.0.0 \
- FDKAAC_VERSION=0.1.5 \
- FREETYPE_VERSION=2.5.5 \
- FRIBIDI_VERSION=0.19.7 \
- KVAZAAR_VERSION=1.2.0 \
- LAME_VERSION=3.100 \
- LIBPTHREAD_STUBS_VERSION=0.4 \
- LIBVIDSTAB_VERSION=1.1.0 \
- LIBXCB_VERSION=1.13.1 \
- XCBPROTO_VERSION=1.13 \
- OGG_VERSION=1.3.2 \
- OPENCOREAMR_VERSION=0.1.5 \
- OPUS_VERSION=1.2 \
- OPENJPEG_VERSION=2.1.2 \
- THEORA_VERSION=1.1.1 \
- VORBIS_VERSION=1.3.5 \
- VPX_VERSION=1.8.0 \
- WEBP_VERSION=1.0.2 \
- X264_VERSION=20170226-2245-stable \
- X265_VERSION=3.1.1 \
- XAU_VERSION=1.0.9 \
- XORG_MACROS_VERSION=1.19.2 \
- XPROTO_VERSION=7.0.31 \
- XVID_VERSION=1.3.4 \
- LIBZMQ_VERSION=4.3.2 \
- LIBSRT_VERSION=1.4.1 \
- LIBARIBB24_VERSION=1.0.3 \
- LIBPNG_VERSION=1.6.9 \
- SRC=/usr/local
+ENV FFMPEG_VERSION=4.3.2 \
+ AOM_VERSION=v1.0.0 \
+ FDKAAC_VERSION=0.1.5 \
+ FREETYPE_VERSION=2.5.5 \
+ FRIBIDI_VERSION=0.19.7 \
+ KVAZAAR_VERSION=1.2.0 \
+ LAME_VERSION=3.100 \
+ LIBPTHREAD_STUBS_VERSION=0.4 \
+ LIBVIDSTAB_VERSION=1.1.0 \
+ LIBXCB_VERSION=1.13.1 \
+ XCBPROTO_VERSION=1.13 \
+ OGG_VERSION=1.3.2 \
+ OPENCOREAMR_VERSION=0.1.5 \
+ OPUS_VERSION=1.2 \
+ OPENJPEG_VERSION=2.1.2 \
+ THEORA_VERSION=1.1.1 \
+ VORBIS_VERSION=1.3.5 \
+ VPX_VERSION=1.8.0 \
+ WEBP_VERSION=1.0.2 \
+ X264_VERSION=20170226-2245-stable \
+ X265_VERSION=3.1.1 \
+ XAU_VERSION=1.0.9 \
+ XORG_MACROS_VERSION=1.19.2 \
+ XPROTO_VERSION=7.0.31 \
+ XVID_VERSION=1.3.4 \
+ LIBZMQ_VERSION=4.3.2 \
+ LIBSRT_VERSION=1.4.1 \
+ LIBARIBB24_VERSION=1.0.3 \
+ LIBPNG_VERSION=1.6.9 \
+ SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
@@ -87,35 +87,35 @@ ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64"
-RUN buildDeps="autoconf \
- automake \
- cmake \
- curl \
- bzip2 \
- libexpat1-dev \
- g++ \
- gcc \
- git \
- gperf \
- libtool \
- make \
- nasm \
- perl \
- pkg-config \
- python \
- libssl-dev \
- yasm \
- zlib1g-dev" && \
+RUN buildDeps="autoconf \
+ automake \
+ cmake \
+ curl \
+ bzip2 \
+ libexpat1-dev \
+ g++ \
+ gcc \
+ git \
+ gperf \
+ libtool \
+ make \
+ nasm \
+ perl \
+ pkg-config \
+ python \
+ libssl-dev \
+ yasm \
+ zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
RUN \
- DIR=/tmp/nv-codec-headers && \
- git clone https://github.com/FFmpeg/nv-codec-headers ${DIR} && \
- cd ${DIR} && \
- git checkout n${NVIDIA_HEADERS_VERSION} && \
- make PREFIX="${PREFIX}" && \
- make install PREFIX="${PREFIX}" && \
+ DIR=/tmp/nv-codec-headers && \
+ git clone https://github.com/FFmpeg/nv-codec-headers ${DIR} && \
+ cd ${DIR} && \
+ git checkout n${NVIDIA_HEADERS_VERSION} && \
+ make PREFIX="${PREFIX}" && \
+ make install PREFIX="${PREFIX}" && \
rm -rf ${DIR}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
@@ -527,7 +527,7 @@ RUN \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
- sed "s:${PREFIX}:/usr/local:g; s:/lib64:/lib:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
+ sed "s:${PREFIX}:/usr/local:g; s:/lib64:/lib:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
@@ -539,7 +539,7 @@ ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib64
CMD ["--help"]
ENTRYPOINT ["ffmpeg"]
-# copy only needed files, without copying nvidia dev files
+# copy only needed files, without copying nvidia dev files
COPY --from=build /usr/local/bin /usr/local/bin/
COPY --from=build /usr/local/share /usr/local/share/
COPY --from=build /usr/local/lib /usr/local/lib/
diff --git a/docker/Dockerfile.ffmpeg.armv7 b/docker/Dockerfile.ffmpeg.armv7
index b56f8d909..0c40cef85 100644
--- a/docker/Dockerfile.ffmpeg.armv7
+++ b/docker/Dockerfile.ffmpeg.armv7
@@ -15,33 +15,33 @@ RUN apt-get -yqq update && \
FROM base as build
-ENV FFMPEG_VERSION=4.3.1 \
- AOM_VERSION=v1.0.0 \
- FDKAAC_VERSION=0.1.5 \
- FREETYPE_VERSION=2.5.5 \
- FRIBIDI_VERSION=0.19.7 \
- KVAZAAR_VERSION=1.2.0 \
- LAME_VERSION=3.100 \
- LIBPTHREAD_STUBS_VERSION=0.4 \
- LIBVIDSTAB_VERSION=1.1.0 \
- LIBXCB_VERSION=1.13.1 \
- XCBPROTO_VERSION=1.13 \
- OGG_VERSION=1.3.2 \
- OPENCOREAMR_VERSION=0.1.5 \
- OPUS_VERSION=1.2 \
- OPENJPEG_VERSION=2.1.2 \
- THEORA_VERSION=1.1.1 \
- VORBIS_VERSION=1.3.5 \
- VPX_VERSION=1.8.0 \
- WEBP_VERSION=1.0.2 \
- X264_VERSION=20170226-2245-stable \
- X265_VERSION=3.1.1 \
- XAU_VERSION=1.0.9 \
- XORG_MACROS_VERSION=1.19.2 \
- XPROTO_VERSION=7.0.31 \
- XVID_VERSION=1.3.4 \
- LIBZMQ_VERSION=4.3.3 \
- SRC=/usr/local
+ENV FFMPEG_VERSION=4.3.2 \
+ AOM_VERSION=v1.0.0 \
+ FDKAAC_VERSION=0.1.5 \
+ FREETYPE_VERSION=2.5.5 \
+ FRIBIDI_VERSION=0.19.7 \
+ KVAZAAR_VERSION=1.2.0 \
+ LAME_VERSION=3.100 \
+ LIBPTHREAD_STUBS_VERSION=0.4 \
+ LIBVIDSTAB_VERSION=1.1.0 \
+ LIBXCB_VERSION=1.13.1 \
+ XCBPROTO_VERSION=1.13 \
+ OGG_VERSION=1.3.2 \
+ OPENCOREAMR_VERSION=0.1.5 \
+ OPUS_VERSION=1.2 \
+ OPENJPEG_VERSION=2.1.2 \
+ THEORA_VERSION=1.1.1 \
+ VORBIS_VERSION=1.3.5 \
+ VPX_VERSION=1.8.0 \
+ WEBP_VERSION=1.0.2 \
+ X264_VERSION=20170226-2245-stable \
+ X265_VERSION=3.1.1 \
+ XAU_VERSION=1.0.9 \
+ XORG_MACROS_VERSION=1.19.2 \
+ XPROTO_VERSION=7.0.31 \
+ XVID_VERSION=1.3.4 \
+ LIBZMQ_VERSION=4.3.3 \
+ SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
@@ -60,30 +60,30 @@ ARG PREFIX=/opt/ffmpeg
ARG LD_LIBRARY_PATH="/opt/ffmpeg/lib:/opt/ffmpeg/lib64:/usr/lib64:/usr/lib:/lib64:/lib:/opt/vc/lib"
-RUN buildDeps="autoconf \
- automake \
- cmake \
- curl \
- bzip2 \
- libexpat1-dev \
- g++ \
- gcc \
- git \
- gperf \
- libtool \
- make \
- nasm \
- perl \
- pkg-config \
- python \
- sudo \
- libssl-dev \
- yasm \
- linux-headers-raspi2 \
- libomxil-bellagio-dev \
- libx265-dev \
- libaom-dev \
- zlib1g-dev" && \
+RUN buildDeps="autoconf \
+ automake \
+ cmake \
+ curl \
+ bzip2 \
+ libexpat1-dev \
+ g++ \
+ gcc \
+ git \
+ gperf \
+ libtool \
+ make \
+ nasm \
+ perl \
+ pkg-config \
+ python \
+ sudo \
+ libssl-dev \
+ yasm \
+ linux-headers-raspi2 \
+ libomxil-bellagio-dev \
+ libx265-dev \
+ libaom-dev \
+ zlib1g-dev" && \
apt-get -yqq update && \
apt-get install -yq --no-install-recommends ${buildDeps}
## opencore-amr https://sourceforge.net/projects/opencore-amr/
@@ -471,7 +471,7 @@ RUN \
cp -r ${PREFIX}/include/libav* ${PREFIX}/include/libpostproc ${PREFIX}/include/libsw* /usr/local/include && \
mkdir -p /usr/local/lib/pkgconfig && \
for pc in ${PREFIX}/lib/pkgconfig/libav*.pc ${PREFIX}/lib/pkgconfig/libpostproc.pc ${PREFIX}/lib/pkgconfig/libsw*.pc; do \
- sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
+ sed "s:${PREFIX}:/usr/local:g" <"$pc" >/usr/local/lib/pkgconfig/"${pc##*/}"; \
done
FROM base AS release
diff --git a/docker/Dockerfile.nginx b/docker/Dockerfile.nginx
new file mode 100644
index 000000000..72e15f8e0
--- /dev/null
+++ b/docker/Dockerfile.nginx
@@ -0,0 +1,52 @@
+FROM ubuntu:20.04 AS base
+
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get -yqq update && \
+ apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \
+ apt-get autoremove -y && \
+ apt-get clean -y
+
+FROM base as build
+
+ARG NGINX_VERSION=1.18.0
+ARG VOD_MODULE_VERSION=1.28
+ARG SECURE_TOKEN_MODULE_VERSION=1.4
+ARG RTMP_MODULE_VERSION=1.2.1
+
+RUN cp /etc/apt/sources.list /etc/apt/sources.list~ \
+ && sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list \
+ && apt-get update
+
+RUN apt-get -yqq build-dep nginx
+
+RUN apt-get -yqq install --no-install-recommends curl \
+ && mkdir /tmp/nginx \
+ && curl -sL https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz | tar -C /tmp/nginx -zx --strip-components=1 \
+ && mkdir /tmp/nginx-vod-module \
+ && curl -sL https://github.com/kaltura/nginx-vod-module/archive/refs/tags/${VOD_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-vod-module -zx --strip-components=1 \
+ # Patch MAX_CLIPS to allow more clips to be added than the default 128
+ && sed -i 's/MAX_CLIPS (128)/MAX_CLIPS (1080)/g' /tmp/nginx-vod-module/vod/media_set.h \
+ && mkdir /tmp/nginx-secure-token-module \
+ && curl -sL https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-secure-token-module -zx --strip-components=1 \
+ && mkdir /tmp/nginx-rtmp-module \
+ && curl -sL https://github.com/arut/nginx-rtmp-module/archive/refs/tags/v${RTMP_MODULE_VERSION}.tar.gz | tar -C /tmp/nginx-rtmp-module -zx --strip-components=1
+
+WORKDIR /tmp/nginx
+
+RUN ./configure --prefix=/usr/local/nginx \
+ --with-file-aio \
+ --with-http_sub_module \
+ --with-http_ssl_module \
+ --with-threads \
+ --add-module=../nginx-vod-module \
+ --add-module=../nginx-secure-token-module \
+ --add-module=../nginx-rtmp-module \
+ --with-cc-opt="-O3 -Wno-error=implicit-fallthrough"
+
+RUN make && make install
+RUN rm -rf /usr/local/nginx/html /usr/local/nginx/conf/*.default
+
+FROM base
+COPY --from=build /usr/local/nginx /usr/local/nginx
+ENTRYPOINT ["/usr/local/nginx/sbin/nginx"]
+CMD ["-g", "daemon off;"]
\ No newline at end of file
diff --git a/docker/Dockerfile.wheels b/docker/Dockerfile.wheels
index 278d989e0..a6fa222ec 100644
--- a/docker/Dockerfile.wheels
+++ b/docker/Dockerfile.wheels
@@ -34,8 +34,7 @@ RUN pip3 wheel --wheel-dir=/wheels \
matplotlib \
click \
setproctitle \
- peewee \
- gevent
+ peewee
FROM scratch
diff --git a/docker/rootfs/etc/services.d/nginx/finish b/docker/rootfs/etc/services.d/nginx/finish
new file mode 100644
index 000000000..24482e77f
--- /dev/null
+++ b/docker/rootfs/etc/services.d/nginx/finish
@@ -0,0 +1,5 @@
+#!/usr/bin/execlineb -S1
+if { s6-test ${1} -ne 0 }
+if { s6-test ${1} -ne 256 }
+
+s6-svscanctl -t /var/run/s6/services
\ No newline at end of file
diff --git a/docker/rootfs/etc/services.d/nginx/run b/docker/rootfs/etc/services.d/nginx/run
new file mode 100644
index 000000000..5aba51c91
--- /dev/null
+++ b/docker/rootfs/etc/services.d/nginx/run
@@ -0,0 +1,2 @@
+#!/usr/bin/execlineb -P
+/usr/local/nginx/sbin/nginx
\ No newline at end of file
diff --git a/nginx/nginx.conf b/docker/rootfs/usr/local/nginx/conf/nginx.conf
similarity index 71%
rename from nginx/nginx.conf
rename to docker/rootfs/usr/local/nginx/conf/nginx.conf
index 51842cb70..259d2668a 100644
--- a/nginx/nginx.conf
+++ b/docker/rootfs/usr/local/nginx/conf/nginx.conf
@@ -1,23 +1,22 @@
+daemon off;
worker_processes 1;
-error_log /var/log/nginx/error.log warn;
+error_log /usr/local/nginx/logs/error.log warn;
pid /var/run/nginx.pid;
-load_module "modules/ngx_rtmp_module.so";
-
events {
worker_connections 1024;
}
http {
- include /etc/nginx/mime.types;
+ include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
- access_log /var/log/nginx/access.log main;
+ access_log /usr/local/nginx/logs/access.log main;
sendfile on;
@@ -34,9 +33,54 @@ http {
keepalive 1024;
}
+ upstream mqtt_ws {
+ server localhost:5002;
+ keepalive 1024;
+ }
+
+ upstream jsmpeg {
+ server localhost:8082;
+ keepalive 1024;
+ }
+
server {
listen 5000;
+ # vod settings
+ vod_base_url '';
+ vod_segments_base_url '';
+ vod_mode mapped;
+ vod_max_mapping_response_size 1m;
+ vod_upstream_location /api;
+
+ # vod caches
+ vod_metadata_cache metadata_cache 512m;
+ vod_mapping_cache mapping_cache 5m;
+
+ # gzip manifests
+ gzip on;
+ gzip_types application/vnd.apple.mpegurl;
+
+ # file handle caching / aio
+ open_file_cache max=1000 inactive=5m;
+ open_file_cache_valid 2m;
+ open_file_cache_min_uses 1;
+ open_file_cache_errors on;
+ aio on;
+
+ location /vod/ {
+ vod hls;
+
+ secure_token $args;
+ secure_token_types application/vnd.apple.mpegurl;
+
+ add_header Access-Control-Allow-Headers '*';
+ add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
+ add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
+ add_header Access-Control-Allow-Origin '*';
+ expires -1;
+ }
+
location /stream/ {
add_header 'Cache-Control' 'no-cache';
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
@@ -81,6 +125,11 @@ http {
root /media/frigate;
}
+ location /cache/ {
+ internal; # This tells nginx it's not accessible from the outside
+ alias /tmp/cache/;
+ }
+
location /recordings/ {
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
add_header 'Access-Control-Allow-Credentials' 'true';
@@ -103,7 +152,15 @@ http {
}
location /ws {
- proxy_pass http://frigate_api/ws;
+ proxy_pass http://mqtt_ws/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ proxy_set_header Host $host;
+ }
+
+ location /live/ {
+ proxy_pass http://jsmpeg/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
@@ -112,6 +169,7 @@ http {
location /api/ {
add_header 'Access-Control-Allow-Origin' '*';
+ add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header Cache-Control "no-store";
proxy_pass http://frigate_api/;
proxy_pass_request_headers on;
diff --git a/docs/docs/configuration/advanced.md b/docs/docs/configuration/advanced.md
index 7efcfb680..eafc91d99 100644
--- a/docs/docs/configuration/advanced.md
+++ b/docs/docs/configuration/advanced.md
@@ -16,7 +16,7 @@ motion:
# Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive.
# The value should be between 1 and 255.
threshold: 25
- # Optional: Minimum size in pixels in the resized motion image that counts as motion
+ # Optional: Minimum size in pixels in the resized motion image that counts as motion (default: ~0.17% of the motion frame area)
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will make motion detection more sensitive to smaller
# moving objects.
contour_area: 100
@@ -29,7 +29,7 @@ motion:
# Low values will cause things like moving shadows to be detected as motion for longer.
# https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/
frame_alpha: 0.2
- # Optional: Height of the resized motion frame (default: 1/6th of the original frame height)
+ # Optional: Height of the resized motion frame (default: 1/6th of the original frame height, but no less than 180)
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense of higher CPU usage.
# Lower values result in less CPU, but small changes may not register as motion.
frame_height: 180
@@ -81,15 +81,15 @@ environment_vars:
### `database`
-Event and clip information is managed in a sqlite database at `/media/frigate/clips/frigate.db`. If that database is deleted, clips will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within Home Assistant.
+Event and recording information is managed in a sqlite database at `/media/frigate/frigate.db`. If that database is deleted, recordings will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within Home Assistant.
-If you are storing your clips on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database in the config if necessary.
+If you are storing your database on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database in the config if necessary.
-This may need to be in a custom location if network storage is used for clips.
+This may need to be in a custom location if network storage is used for the media folder.
```yaml
database:
- path: /media/frigate/clips/frigate.db
+ path: /media/frigate/frigate.db
```
### `detectors`
@@ -110,10 +110,17 @@ detectors:
### `model`
+If using a custom model, the width and height will need to be specified.
+
+The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model.
+
```yaml
model:
# Required: height of the trained model
height: 320
# Required: width of the trained model
width: 320
+ # Optional: labelmap overrides
+ labelmap:
+ 7: car
```
diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md
index 5184e516c..7592a5560 100644
--- a/docs/docs/configuration/cameras.md
+++ b/docs/docs/configuration/cameras.md
@@ -5,16 +5,15 @@ title: Cameras
## Setting Up Camera Inputs
-Up to 4 inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create clips from a higher resolution stream, or vice versa.
+Up to 4 inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create recordings from a higher resolution stream, or vice versa.
Each role can only be assigned to one input per camera. The options for roles are as follows:
-| Role | Description |
-| -------- | ------------------------------------------------------------------------------------ |
-| `detect` | Main feed for object detection |
-| `clips` | Clips of events from objects detected in the `detect` feed. [docs](#recording-clips) |
-| `record` | Saves 60 second segments of the video feed. [docs](#247-recordings) |
-| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](#rtmp-streams) |
+| Role | Description |
+| -------- | ------------------------------------------------------------------------------------- |
+| `detect` | Main feed for object detection |
+| `record` | Saves segments of the video feed based on configuration settings. [docs](#recordings) |
+| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](#rtmp-streams) |
### Example
@@ -31,11 +30,11 @@ cameras:
- rtmp
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live
roles:
- - clips
- record
- width: 1280
- height: 720
- fps: 5
+ detect:
+ width: 1280
+ height: 720
+ fps: 5
```
`width`, `height`, and `fps` are only used for the `detect` role. Other streams are passed through, so there is no need to specify the resolution.
@@ -95,6 +94,9 @@ zones:
# Required: List of x,y coordinates to define the polygon of the zone.
# NOTE: Coordinates can be generated at https://www.image-map.net/
coordinates: 545,1077,747,939,788,805
+ # Optional: List of objects that can trigger this zone (default: all tracked objects)
+ objects:
+ - person
# Optional: Zone level object filters.
# NOTE: The global and camera filters are applied upstream.
filters:
@@ -129,37 +131,48 @@ objects:
mask: 0,0,1000,0,1000,200,0,200
```
-## Clips
+## Recordings
-Frigate can save video clips without any CPU overhead for encoding by simply copying the stream directly with FFmpeg. It leverages FFmpeg's segment functionality to maintain a cache of video for each camera. The cache files are written to disk at `/tmp/cache` and do not introduce memory overhead. When an object is being tracked, it will extend the cache to ensure it can assemble a clip when the event ends. Once the event ends, it again uses FFmpeg to assemble a clip by combining the video clips without any encoding by the CPU. Assembled clips are are saved to `/media/frigate/clips`. Clips are retained according to the retention settings defined on the config for each object type.
+24/7 recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH//MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding and are available in Home Assistant's media browser. Each camera supports a configurable retention policy in the config.
-These clips will not be playable in the web UI or in Home Assistant's media browser unless your camera sends video as h264.
+Exported clips are also created off of these recordings. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
+
+These recordings will not be playable in the web UI or in Home Assistant's media browser unless your camera sends video as h264.
:::caution
Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
:::
```yaml
-clips:
- # Required: enables clips for the camera (default: shown below)
- # This value can be set via MQTT and will be updated in startup based on retained value
+record:
+ # Optional: Enable recording (default: shown below)
enabled: False
- # Optional: Number of seconds before the event to include in the clips (default: shown below)
- pre_capture: 5
- # Optional: Number of seconds after the event to include in the clips (default: shown below)
- post_capture: 5
- # Optional: Objects to save clips for. (default: all tracked objects)
- objects:
- - person
- # Optional: Restrict clips to objects that entered any of the listed zones (default: no required zones)
- required_zones: []
- # Optional: Camera override for retention settings (default: global values)
- retain:
- # Required: Default retention days (default: shown below)
- default: 10
- # Optional: Per object retention days
+ # Optional: Number of days to retain (default: shown below)
+ retain_days: 0
+ # Optional: Event recording settings
+ events:
+ # Optional: Enable event recording retention settings (default: shown below)
+ enabled: False
+ # Optional: Maximum length of time to retain video during long events. (default: shown below)
+ # NOTE: 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 unless retain_days under record is > 0.
+ max_seconds: 300
+ # Optional: Number of seconds before the event to include in the event (default: shown below)
+ pre_capture: 5
+ # Optional: Number of seconds after the event to include in the event (default: shown below)
+ post_capture: 5
+ # Optional: Objects to save event for. (default: all tracked objects)
objects:
- person: 15
+ - person
+ # Optional: Restrict event to objects that entered any of the listed zones (default: no required zones)
+ required_zones: []
+ # Optional: Retention settings for event
+ retain:
+ # Required: Default retention days (default: shown below)
+ default: 10
+ # Optional: Per object retention days
+ objects:
+ person: 15
```
## Snapshots
@@ -172,6 +185,9 @@ snapshots:
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
# This value can be set via MQTT and will be updated in startup based on retained value
enabled: False
+ # Optional: Enable writing a clean copy png snapshot to /media/frigate/clips (default: shown below)
+ # Only works if snapshots are enabled. This image is intended to be used for training purposes.
+ clean_copy: True
# Optional: print a timestamp on the snapshots (default: shown below)
timestamp: False
# Optional: draw bounding box on the snapshots (default: shown below)
@@ -180,6 +196,8 @@ snapshots:
crop: False
# Optional: height to resize the snapshot to (default: original size)
height: 175
+ # Optional: jpeg encode quality (default: shown below)
+ quality: 70
# Optional: Restrict snapshots to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Camera override for retention settings (default: global values)
@@ -191,29 +209,43 @@ snapshots:
person: 15
```
-## 24/7 Recordings
-
-24/7 recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH//MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding and are available in Home Assistant's media browser. Each camera supports a configurable retention policy in the config.
-
-:::caution
-Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
-:::
-
-```yaml
-# Optional: 24/7 recording configuration
-record:
- # Optional: Enable recording (default: global setting)
- enabled: False
- # Optional: Number of days to retain (default: global setting)
- retain_days: 30
-```
-
## RTMP streams
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp:///live/`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization.
+## Timestamp style configuration
+
+For the debug view and snapshots it is possible to embed a timestamp in the feed. In some instances the default position obstructs important space, visibility or contrast is too low because of color or the datetime format does not match ones desire.
+
+```yaml
+# Optional: in-feed timestamp style configuration
+timestamp_style:
+ # Optional: Position of the timestamp (default: shown below)
+ # "tl" (top left), "tr" (top right), "bl" (bottom left), "br" (bottom right)
+ position: "tl"
+ # Optional: Format specifier conform to the Python package "datetime" (default: shown below)
+ # Additional Examples:
+ # german: "%d.%m.%Y %H:%M:%S"
+ format: "%m/%d/%Y %H:%M:%S"
+ # Optional: Color of font
+ color:
+ # All Required when color is specified (default: shown below)
+ red: 255
+ green: 255
+ blue: 255
+ # Optional: Scale factor for font (default: shown below)
+ scale: 1.0
+ # Optional: Line thickness of font (default: shown below)
+ thickness: 2
+ # Optional: Effect of lettering (default: shown below)
+ # None (No effect),
+ # "solid" (solid background in inverse color of font)
+ # "shadow" (shadow for font)
+ effect: None
+```
+
## Full example
The following is a full example of all of the options together for a camera configuration
@@ -229,8 +261,8 @@ cameras:
# Required: the path to the stream
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- # Required: list of roles for this stream. valid values are: detect,record,clips,rtmp
- # NOTICE: In addition to assigning the record, clips, and rtmp roles,
+ # Required: list of roles for this stream. valid values are: detect,record,rtmp
+ # NOTICE: In addition to assigning the record, and rtmp roles,
# they must also be enabled in the camera config.
roles:
- detect
@@ -250,14 +282,20 @@ cameras:
# Optional: camera specific output args (default: inherit)
output_args:
- # Required: width of the frame for the input with the detect role
- width: 1280
- # Required: height of the frame for the input with the detect role
- height: 720
- # Optional: desired fps for your camera for the input with the detect role
- # NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
- # Frigate will attempt to autodetect if not specified.
- fps: 5
+ # Required: Camera level detect settings
+ detect:
+ # Required: width of the frame for the input with the detect role
+ width: 1280
+ # Required: height of the frame for the input with the detect role
+ height: 720
+ # Required: desired fps for your camera for the input with the detect role
+ # NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
+ fps: 5
+ # Optional: enables detection for the camera (default: True)
+ # This value can be set via MQTT and will be updated in startup based on retained value
+ enabled: True
+ # Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
+ max_disappeared: 25
# Optional: camera level motion config
motion:
@@ -278,6 +316,9 @@ cameras:
# Required: List of x,y coordinates to define the polygon of the zone.
# NOTE: Coordinates can be generated at https://www.image-map.net/
coordinates: 545,1077,747,939,788,805
+ # Optional: List of objects that can trigger this zone (default: all tracked objects)
+ objects:
+ - person
# Optional: Zone level object filters.
# NOTE: The global and camera filters are applied upstream.
filters:
@@ -286,48 +327,49 @@ cameras:
max_area: 100000
threshold: 0.7
- # Optional: Camera level detect settings
- detect:
- # Optional: enables detection for the camera (default: True)
- # This value can be set via MQTT and will be updated in startup based on retained value
- enabled: True
- # Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
- max_disappeared: 25
-
- # Optional: save clips configuration
- clips:
- # Required: enables clips for the camera (default: shown below)
- # This value can be set via MQTT and will be updated in startup based on retained value
- enabled: False
- # Optional: Number of seconds before the event to include in the clips (default: shown below)
- pre_capture: 5
- # Optional: Number of seconds after the event to include in the clips (default: shown below)
- post_capture: 5
- # Optional: Objects to save clips for. (default: all tracked objects)
- objects:
- - person
- # Optional: Restrict clips to objects that entered any of the listed zones (default: no required zones)
- required_zones: []
- # Optional: Camera override for retention settings (default: global values)
- retain:
- # Required: Default retention days (default: shown below)
- default: 10
- # Optional: Per object retention days
- objects:
- person: 15
-
# Optional: 24/7 recording configuration
record:
# Optional: Enable recording (default: global setting)
enabled: False
# Optional: Number of days to retain (default: global setting)
retain_days: 30
+ # Optional: Event recording settings
+ events:
+ # Required: enables event recordings for the camera (default: shown below)
+ # This value can be set via MQTT and will be updated in startup based on retained value
+ enabled: False
+ # Optional: Number of seconds before the event to include (default: shown below)
+ pre_capture: 5
+ # Optional: Number of seconds after the event to include (default: shown below)
+ post_capture: 5
+ # Optional: Objects to save events for. (default: all tracked objects)
+ objects:
+ - person
+ # Optional: Restrict events to objects that entered any of the listed zones (default: no required zones)
+ required_zones: []
+ # Optional: Camera override for retention settings (default: global values)
+ retain:
+ # Required: Default retention days (default: shown below)
+ default: 10
+ # Optional: Per object retention days
+ objects:
+ person: 15
# Optional: RTMP re-stream configuration
rtmp:
- # Required: Enable the live stream (default: True)
+ # Required: Enable the RTMP stream (default: True)
enabled: True
+ # Optional: Live stream configuration for WebUI
+ live:
+ # Optional: Set the height of the live stream. (default: 720)
+ # This must be less than or equal to the height of the detect stream. Lower resolutions
+ # reduce bandwidth required for viewing the live stream. Width is computed to match known aspect ratio.
+ height: 720
+ # Optional: Set the encode quality of the live stream (default: shown below)
+ # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources.
+ quality: 8
+
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
snapshots:
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
@@ -365,6 +407,8 @@ cameras:
crop: True
# Optional: height to resize the snapshot to (default: shown below)
height: 270
+ # Optional: jpeg encode quality (default: shown below)
+ quality: 70
# Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones)
required_zones: []
@@ -386,6 +430,31 @@ cameras:
# Optional: mask to prevent this object type from being detected in certain areas (default: no mask)
# Checks based on the bottom center of the bounding box of the object
mask: 0,0,1000,0,1000,200,0,200
+
+ # Optional: In-feed timestamp style configuration
+ timestamp_style:
+ # Optional: Position of the timestamp (default: shown below)
+ # "tl" (top left), "tr" (top right), "bl" (bottom left), "br" (bottom right)
+ position: "tl"
+ # Optional: Format specifier conform to the Python package "datetime" (default: shown below)
+ # Additional Examples:
+ # german: "%d.%m.%Y %H:%M:%S"
+ format: "%m/%d/%Y %H:%M:%S"
+ # Optional: Color of font
+ color:
+ # All Required when color is specified (default: shown below)
+ red: 255
+ green: 255
+ blue: 255
+ # Optional: Scale factor for font (default: shown below)
+ scale: 1.0
+ # Optional: Line thickness of font (default: shown below)
+ thickness: 2
+ # Optional: Effect of lettering (default: shown below)
+ # None (No effect),
+ # "solid" (solid background in inverse color of font)
+ # "shadow" (shadow for font)
+ effect: None
```
## Camera specific configuration
@@ -412,12 +481,11 @@ input_args:
- "1"
```
-Note that mjpeg cameras require encoding the video into h264 for clips, recording, and rtmp roles. This will use significantly more CPU than if the cameras supported h264 feeds directly.
+Note that mjpeg cameras require encoding the video into h264 for recording, and rtmp roles. This will use significantly more CPU than if the cameras supported h264 feeds directly.
```yaml
output_args:
record: -f segment -segment_time 60 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v libx264 -an
- clips: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v libx264 -an
rtmp: -c:v libx264 -an -f flv
```
diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md
index bdffb16ef..9128fead5 100644
--- a/docs/docs/configuration/index.md
+++ b/docs/docs/configuration/index.md
@@ -20,9 +20,10 @@ cameras:
roles:
- detect
- rtmp
- width: 1280
- height: 720
- fps: 5
+ detect:
+ width: 1280
+ height: 720
+ fps: 5
```
## Required
@@ -47,6 +48,17 @@ mqtt:
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}.
# eg. password: '{FRIGATE_MQTT_PASSWORD}'
password: password
+ # Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
+ tls_ca_certs: /path/to/ca.crt
+ # Optional: tls_client_cert and tls_client key in order to use self-signed client
+ # certificates (default: None)
+ # NOTE: certificate must not be password-protected
+ # do not set user and password when using a client certificate
+ tls_client_cert: /path/to/client.crt
+ tls_client_key: /path/to/client.key
+ # Optional: tls_insecure (true/false) for enabling TLS verification of
+ # the server hostname in the server certificate (default: None)
+ tls_insecure: false
# Optional: interval in seconds for publishing stats (default: shown below)
stats_interval: 60
```
@@ -65,27 +77,103 @@ cameras:
roles:
- detect
- rtmp
- width: 1280
- height: 720
- fps: 5
+ detect:
+ width: 1280
+ height: 720
+ fps: 5
```
## Optional
-### `clips`
+### `database`
```yaml
-clips:
- # Optional: Maximum length of time to retain video during long events. (default: shown below)
- # NOTE: 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.
- max_seconds: 300
- # Optional: size of tmpfs mount to create for cache files (default: not set)
- # mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache
- # NOTICE: Addon users must have Protection mode disabled for the addon when using this setting.
- # Also, if you have mounted a tmpfs volume through docker, this value should not be set in your config.
- tmpfs_cache_size: 256m
- # Optional: Retention settings for clips (default: shown below)
+database:
+ # The path to store the SQLite DB (default: shown below)
+ path: /media/frigate/frigate.db
+```
+
+### `model`
+
+```yaml
+# Optional: model modifications
+model:
+ # Required: Object detection model input width (default: shown below)
+ width: 320
+ # Required: Object detection model input height (default: shown below)
+ height: 320
+ # Optional: Label name modifications
+ labelmap:
+ 2: vehicle # previously "car"
+```
+
+### `detectors`
+
+Check the [detectors configuration page](detectors.md) for a complete list of options.
+
+### `logger`
+
+```yaml
+# Optional: logger verbosity settings
+logger:
+ # Optional: Default log verbosity (default: shown below)
+ default: info
+ # Optional: Component specific logger overrides
+ logs:
+ frigate.event: debug
+```
+
+### `record`
+
+Can be overridden at the camera level. 24/7 recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH//MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding and are available in Home Assistant's media browser. Each camera supports a configurable retention policy in the config.
+
+Exported clips are also created off of these recordings. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
+
+These recordings will not be playable in the web UI or in Home Assistant's media browser unless your camera sends video as h264.
+
+:::caution
+Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
+:::
+
+```yaml
+record:
+ # Optional: Enable recording (default: shown below)
+ enabled: False
+ # Optional: Number of days to retain (default: shown below)
+ retain_days: 0
+ # Optional: Event recording settings
+ events:
+ # Optional: Enable event recording retention settings (default: shown below)
+ enabled: False
+ # Optional: Maximum length of time to retain video during long events. (default: shown below)
+ # NOTE: 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 unless retain_days under record is > 0.
+ max_seconds: 300
+ # Optional: Number of seconds before the event to include (default: shown below)
+ pre_capture: 5
+ # Optional: Number of seconds after the event to include (default: shown below)
+ post_capture: 5
+ # Optional: Objects to save recordings for. (default: all tracked objects)
+ objects:
+ - person
+ # Optional: Restrict recordings to objects that entered any of the listed zones (default: no required zones)
+ required_zones: []
+ # Optional: Retention settings for events
+ retain:
+ # Required: Default retention days (default: shown below)
+ default: 10
+ # Optional: Per object retention days
+ objects:
+ person: 15
+```
+
+## `snapshots`
+
+Can be overridden at the camera level. Global snapshot retention settings.
+
+```yaml
+# Optional: Configuration for the jpg snapshots written to the clips directory for each event
+snapshots:
retain:
# Required: Default retention days (default: shown below)
default: 10
@@ -96,6 +184,8 @@ clips:
### `ffmpeg`
+Can be overridden at the camera level.
+
```yaml
ffmpeg:
# Optional: global ffmpeg args (default: shown below)
@@ -111,8 +201,6 @@ ffmpeg:
detect: -f rawvideo -pix_fmt yuv420p
# Optional: output args for record streams (default: shown below)
record: -f segment -segment_time 60 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
- # Optional: output args for clips streams (default: shown below)
- clips: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
# Optional: output args for rtmp streams (default: shown below)
rtmp: -c copy -f flv
```
@@ -139,18 +227,24 @@ objects:
threshold: 0.7
```
-### `record`
+### `birdseye`
-Can be overridden at the camera level. 24/7 recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH//MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding and are available in Home Assistant's media browser. Each camera supports a configurable retention policy in the config.
-
-:::caution
-Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
-:::
+A dynamic combined camera view of all tracked cameras. This is optimized for minimal bandwidth and server resource utilization. Encoding is only performed when actively viewing the video feed, and only active (defined by the mode) cameras are included in the view.
```yaml
-record:
- # Optional: Enable recording
- enabled: False
- # Optional: Number of days to retain
- retain_days: 30
+birdseye:
+ # Optional: Enable birdseye view (default: shown below)
+ enabled: True
+ # Optional: Width of the output resolution (default: shown below)
+ width: 1280
+ # Optional: Height of the output resolution (default: shown below)
+ height: 720
+ # Optional: Encoding quality of the mpeg1 feed (default: shown below)
+ # 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources.
+ quality: 8
+ # Optional: Mode of the view. Available options are: objects, motion, and continuous
+ # objects - cameras are included if they have had a tracked object within the last 30 seconds
+ # motion - cameras are included if motion was detected in the last 30 seconds
+ # continuous - all cameras are included always
+ mode: objects
```
diff --git a/docs/docs/configuration/objects.mdx b/docs/docs/configuration/objects.mdx
index 3e95f9e83..a8608c286 100644
--- a/docs/docs/configuration/objects.mdx
+++ b/docs/docs/configuration/objects.mdx
@@ -4,13 +4,13 @@ title: Default available objects
sidebar_label: Available objects
---
-import labels from '../../../labelmap.txt';
+import labels from "../../../labelmap.txt";
By default, Frigate includes the following object models from the Google Coral test data.
- {labels.split('\n').map((label) => (
-
{label.replace(/^\d+\s+/, '')}
+ {labels.split("\n").map((label) => (
+
{label.replace(/^\d+\s+/, "")}
))}
@@ -23,14 +23,3 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
- Labels: `/labelmap.txt`
You also need to update the model width/height in the config if they differ from the defaults.
-
-### Customizing the Labelmap
-
-The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. You must retain the same number of labels, but you can change the names. To change:
-
-- Download the [COCO labelmap](https://dl.google.com/coral/canned_models/coco_labels.txt)
-- Modify the label names as desired. For example, change `7 truck` to `7 car`
-- Mount the new file at `/labelmap.txt` in the container with an additional volume
- ```
- -v ./config/labelmap.txt:/labelmap.txt
- ```
diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md
index 6d03d8708..a818000b4 100644
--- a/docs/docs/contributing.md
+++ b/docs/docs/contributing.md
@@ -36,6 +36,59 @@ Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshe
- [Frigate source code](#frigate-core-web-and-docs)
- GNU make
- Docker
+- Extra Coral device (optional, but very helpful to simulate real world performance)
+
+### Setup
+
+#### 1. Build the docker container locally with the appropriate make command
+
+For x86 machines, use `make amd64_frigate`
+
+#### 2. Create a local config file for testing
+
+Place the file at `config/config.yml` in the root of the repo.
+
+Here is an example, but modify for your needs:
+
+```yaml
+mqtt:
+ host: mqtt
+
+cameras:
+ test:
+ ffmpeg:
+ inputs:
+ - path: /media/frigate/car-stopping.mp4
+ input_args: -re -stream_loop -1 -fflags +genpts
+ roles:
+ - detect
+ - rtmp
+ detect:
+ height: 1080
+ width: 1920
+ fps: 5
+```
+
+These input args tell ffmpeg to read the mp4 file in an infinite loop. You can use any valid ffmpeg input here.
+
+#### 3. Gather some mp4 files for testing
+
+Create and place these files in a `debug` folder in the root of the repo. This is also where recordings will be created if you enable them in your test config. Update your config from step 2 above to point at the right file. You can check the `docker-compose.yml` file in the repo to see how the volumes are mapped.
+
+#### 4. Open the repo with Visual Studio Code
+
+Upon opening, you should be prompted to open the project in a remote container. This will build a container on top of the base frigate container with all the development dependencies installed. This ensures everyone uses a consistent development environment without the need to install any dependencies on your host machine.
+
+#### 5. Run frigate from the command line
+
+VSCode will start the docker compose file for you and open a terminal window connected to `frigate-dev`.
+
+- Run `python3 -m frigate` to start the backend.
+- In a separate terminal window inside VS Code, change into the `web` directory and run `npm install && npm start` to start the frontend.
+
+#### 6. Teardown
+
+After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
## Web Interface
@@ -109,7 +162,7 @@ npm run test
#### 1. Installation
```console
-npm run install
+npm install
```
#### 2. Local Development
diff --git a/docs/docs/hardware.md b/docs/docs/hardware.md
index 5cf20df87..a68a5e120 100644
--- a/docs/docs/hardware.md
+++ b/docs/docs/hardware.md
@@ -5,7 +5,7 @@ title: Recommended hardware
## Cameras
-Cameras that output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, clips, and recordings without re-encoding.
+Cameras that output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, and recordings without re-encoding.
## Computer
diff --git a/docs/docs/installation.md b/docs/docs/installation.md
index c3f916ad1..67e8252ae 100644
--- a/docs/docs/installation.md
+++ b/docs/docs/installation.md
@@ -5,7 +5,7 @@ title: Installation
Frigate is a Docker container that can be run on any Docker host including as a [HassOS Addon](https://www.home-assistant.io/addons/). See instructions below for installing the HassOS addon.
-For Home Assistant users, there is also a [custom component (aka integration)](https://github.com/blakeblackshear/frigate-hass-integration). This custom component adds tighter integration with Home Assistant by automatically setting up camera entities, sensors, media browser for clips and recordings, and a public API to simplify notifications.
+For Home Assistant users, there is also a [custom component (aka integration)](https://github.com/blakeblackshear/frigate-hass-integration). This custom component adds tighter integration with Home Assistant by automatically setting up camera entities, sensors, media browser for recordings, and a public API to simplify notifications.
Note that HassOS Addons and custom components are different things. If you are already running Frigate with Docker directly, you do not need the Addon since the Addon would run another instance of Frigate.
diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md
index d7d12afa2..15ec9332b 100644
--- a/docs/docs/troubleshooting.md
+++ b/docs/docs/troubleshooting.md
@@ -3,25 +3,27 @@ id: troubleshooting
title: Troubleshooting and FAQ
---
-### How can I get sound or audio in my clips and recordings?
-By default, Frigate removes audio from clips and recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/frigate/configuration/index#ffmpeg).
+### I am seeing a solid green image for my camera.
+
+A solid green image means that frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly.
+
+### How can I get sound or audio in my recordings?
+
+By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/frigate/configuration/index#ffmpeg).
### My mjpeg stream or snapshots look green and crazy
+
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.

-### I have clips and snapshots in my clips folder, but I can't view them in the Web UI.
-This is usually caused one of two things:
-
-- The permissions on the parent folder don't have execute and nginx returns a 403 error you can see in the browser logs
- - In this case, try mounting a volume to `/media/frigate` inside the container instead of `/media/frigate/clips`.
-- Your cameras do not send h264 encoded video and the mp4 files are not playable in the browser
+### I can't view events or recordings in the Web UI.
+Ensure your cameras send h264 encoded video
### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
-These messages in the logs are expected in certain situations. Frigate checks the integrity of the video cache before assembling clips. Occasionally these cached files will be invalid and cleaned up automatically.
+These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically.
### "On connect called"
diff --git a/docs/docs/usage/api.md b/docs/docs/usage/api.md
index 83ffbdf00..b5d0eaa94 100644
--- a/docs/docs/usage/api.md
+++ b/docs/docs/usage/api.md
@@ -5,7 +5,7 @@ title: HTTP API
A web server is available on port 5000 with the following endpoints.
-### `/api/`
+### `GET /api/`
An mjpeg stream for debugging. Keep in mind the mjpeg endpoint is for debugging only and will put additional load on the system when in use.
@@ -24,7 +24,7 @@ Accepts the following query string parameters:
You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/api/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/api/back?fps=10` or both with `?fps=10&h=1000`.
-### `/api///best.jpg[?h=300&crop=1]`
+### `GET /api///best.jpg[?h=300&crop=1&quality=70]`
The best snapshot for any object type. It is a full resolution image by default.
@@ -32,8 +32,9 @@ Example parameters:
- `h=300`: resizes the image to 300 pixes tall
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
+- `quality=70`: sets the jpeg encoding quality (0-100)
-### `/api//latest.jpg[?h=300]`
+### `GET /api//latest.jpg[?h=300]`
The most recent frame that frigate has finished processing. It is a full resolution image by default.
@@ -48,12 +49,13 @@ Accepts the following query string parameters:
| `mask` | int | Overlay the mask on the image (0 or 1) |
| `motion` | int | Draw blue boxes for areas with detected motion (0 or 1) |
| `regions` | int | Draw green boxes for areas where object detection was run (0 or 1) |
+| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
Example parameters:
- `h=300`: resizes the image to 300 pixes tall
-### `/api/stats`
+### `GET /api/stats`
Contains some granular debug info that can be used for sensors in Home Assistant.
@@ -150,15 +152,15 @@ Sample response:
}
```
-### `/api/config`
+### `GET /api/config`
A json representation of your configuration
-### `/api/version`
+### `GET /api/version`
Version info
-### `/api/events`
+### `GET /api/events`
Events from the database. Accepts the following query string parameters:
@@ -174,19 +176,23 @@ Events from the database. Accepts the following query string parameters:
| `has_clip` | int | Filter to events that have clips (0 or 1) |
| `include_thumbnails` | int | Include thumbnails in the response (0 or 1) |
-### `/api/events/summary`
+### `GET /api/events/summary`
Returns summary data for events in the database. Used by the Home Assistant integration.
-### `/api/events/`
+### `GET /api/events/`
Returns data for a single event.
-### `/api/events//thumbnail.jpg`
+### `DELETE /api/events/`
+
+Permanently deletes the event along with any clips/snapshots.
+
+### `GET /api/events//thumbnail.jpg`
Returns a thumbnail for the event id optimized for notifications. Works while the event is in progress and after completion. Passing `?format=android` will convert the thumbnail to 2:1 aspect ratio.
-### `/api/events//snapshot.jpg`
+### `GET /api/events//snapshot.jpg`
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
@@ -198,11 +204,12 @@ Accepts the following query string parameters, but they are only applied when an
| `bbox` | int | Show bounding boxes for detected objects (0 or 1) |
| `timestamp` | int | Print the timestamp in the upper left (0 or 1) |
| `crop` | int | Crop the snapshot to the (0 or 1) |
-
-### `/clips/-.mp4`
-
-Video clip for the given camera and event id.
+| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
### `/clips/-.jpg`
JPG snapshot for the given camera and event id.
+
+### `/vod/-////master.m3u8`
+
+HTTP Live Streaming Video on Demand URL for the specified hour and camera. Can be viewed in an application like VLC.
diff --git a/docs/docs/usage/home-assistant.md b/docs/docs/usage/home-assistant.md
index bf0b577b1..5342ec98a 100644
--- a/docs/docs/usage/home-assistant.md
+++ b/docs/docs/usage/home-assistant.md
@@ -64,14 +64,14 @@ Home Assistant > Configuration > Integrations > Frigate > Options
| --------------- | --------------------------------------------------------------------------------- |
| `camera` | Live camera stream (requires RTMP), camera for image of the last detected object. |
| `sensor` | States to monitor Frigate performance, object counts for all zones and cameras. |
-| `switch` | Switch entities to toggle detection, clips and snapshots. |
+| `switch` | Switch entities to toggle detection, recordings and snapshots. |
| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. |
## Media Browser Support
The integration provides:
-- Rich UI with thumbnails for browsing event clips
+- Rich UI with thumbnails for browsing event recordings
- Rich UI for browsing 24/7 recordings by month, day, camera, time
This is accessible via "Media Browser" on the left menu panel in Home Assistant.
diff --git a/docs/docs/usage/mqtt.md b/docs/docs/usage/mqtt.md
index 83d92bbc2..73ab23bb8 100644
--- a/docs/docs/usage/mqtt.md
+++ b/docs/docs/usage/mqtt.md
@@ -11,6 +11,10 @@ Designed to be used as an availability topic with Home Assistant. Possible messa
"online": published when frigate is running (on startup)
"offline": published right before frigate stops
+### `frigate/restart`
+
+Causes frigate to exit. Docker should be configured to automatically restart the container on exit.
+
### `frigate//`
Publishes the count of objects for the camera for use as a sensor in Home Assistant.
@@ -32,11 +36,12 @@ Message published for each changed event. The first message is published when th
```json
{
- "type": "update", // new, update, or end
+ "type": "update", // new, update, end or clip_ready
"before": {
"id": "1607123955.475377-mxklsc",
"camera": "front_door",
"frame_time": 1607123961.837752,
+ "snapshot_time": 1607123961.837752,
"label": "person",
"top_score": 0.958984375,
"false_positive": false,
@@ -54,6 +59,7 @@ Message published for each changed event. The first message is published when th
"id": "1607123955.475377-mxklsc",
"camera": "front_door",
"frame_time": 1607123962.082975,
+ "snapshot_time": 1607123961.837752,
"label": "person",
"top_score": 0.958984375,
"false_positive": false,
@@ -82,13 +88,13 @@ Topic to turn detection for a camera on and off. Expected values are `ON` and `O
Topic with current state of detection for a camera. Published values are `ON` and `OFF`.
-### `frigate//clips/set`
+### `frigate//recordings/set`
-Topic to turn clips for a camera on and off. Expected values are `ON` and `OFF`.
+Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`.
-### `frigate//clips/state`
+### `frigate//recordings/state`
-Topic with current state of clips for a camera. Published values are `ON` and `OFF`.
+Topic with current state of recordings for a camera. Published values are `ON` and `OFF`.
### `frigate//snapshots/set`
diff --git a/frigate/__main__.py b/frigate/__main__.py
index 2524c621a..0d9a3dbe2 100644
--- a/frigate/__main__.py
+++ b/frigate/__main__.py
@@ -1,4 +1,6 @@
-import faulthandler; faulthandler.enable()
+import faulthandler
+
+faulthandler.enable()
import sys
import threading
@@ -6,10 +8,10 @@ threading.current_thread().name = "frigate"
from frigate.app import FrigateApp
-cli = sys.modules['flask.cli']
+cli = sys.modules["flask.cli"]
cli.show_server_banner = lambda *x: None
-if __name__ == '__main__':
+if __name__ == "__main__":
frigate_app = FrigateApp()
frigate_app.start()
diff --git a/frigate/app.py b/frigate/app.py
index 8e5178c79..bf3f12989 100644
--- a/frigate/app.py
+++ b/frigate/app.py
@@ -2,28 +2,28 @@ import json
import logging
import multiprocessing as mp
import os
+import signal
+import sys
+import threading
from logging.handlers import QueueHandler
from typing import Dict, List
-import sys
-import signal
import yaml
-from gevent import pywsgi
-from geventwebsocket.handler import WebSocketHandler
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
-from frigate.config import FrigateConfig
-from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
+from frigate.config import DetectorTypeEnum, FrigateConfig
+from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
from frigate.edgetpu import EdgeTPUProcess
-from frigate.events import EventProcessor, EventCleanup
+from frigate.events import EventCleanup, EventProcessor
from frigate.http import create_app
from frigate.log import log_process, root_configurer
-from frigate.models import Event
-from frigate.mqtt import create_mqtt_client
+from frigate.models import Event, Recordings
+from frigate.mqtt import create_mqtt_client, MqttSocketRelay
from frigate.object_processing import TrackedObjectProcessor
-from frigate.record import RecordingMaintainer
+from frigate.output import output_frames
+from frigate.record import RecordingCleanup, RecordingMaintainer
from frigate.stats import StatsEmitter, stats_init
from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog
@@ -31,9 +31,11 @@ from frigate.zeroconf import broadcast_zeroconf
logger = logging.getLogger(__name__)
-class FrigateApp():
+
+class FrigateApp:
def __init__(self):
self.stop_event = mp.Event()
+ self.base_config: FrigateConfig = None
self.config: FrigateConfig = None
self.detection_queue = mp.Queue()
self.detectors: Dict[str, EdgeTPUProcess] = {}
@@ -54,148 +56,254 @@ class FrigateApp():
else:
logger.debug(f"Skipping directory: {d}")
- tmpfs_size = self.config.clips.tmpfs_cache_size
- if tmpfs_size:
- logger.info(f"Creating tmpfs of size {tmpfs_size}")
- rc = os.system(f"mount -t tmpfs -o size={tmpfs_size} tmpfs {CACHE_DIR}")
- if rc != 0:
- logger.error(f"Failed to create tmpfs, error code: {rc}")
-
def init_logger(self):
- self.log_process = mp.Process(target=log_process, args=(self.log_queue,), name='log_process')
+ self.log_process = mp.Process(
+ target=log_process, args=(self.log_queue,), name="log_process"
+ )
self.log_process.daemon = True
self.log_process.start()
root_configurer(self.log_queue)
-
+
def init_config(self):
- config_file = os.environ.get('CONFIG_FILE', '/config/config.yml')
- self.config = FrigateConfig(config_file=config_file)
+ config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
+ user_config = FrigateConfig.parse_file(config_file)
+ self.config = user_config.runtime_config
for camera_name in self.config.cameras.keys():
# create camera_metrics
self.camera_metrics[camera_name] = {
- 'camera_fps': mp.Value('d', 0.0),
- 'skipped_fps': mp.Value('d', 0.0),
- 'process_fps': mp.Value('d', 0.0),
- 'detection_enabled': mp.Value('i', self.config.cameras[camera_name].detect.enabled),
- 'detection_fps': mp.Value('d', 0.0),
- 'detection_frame': mp.Value('d', 0.0),
- 'read_start': mp.Value('d', 0.0),
- 'ffmpeg_pid': mp.Value('i', 0),
- 'frame_queue': mp.Queue(maxsize=2),
+ "camera_fps": mp.Value("d", 0.0),
+ "skipped_fps": mp.Value("d", 0.0),
+ "process_fps": mp.Value("d", 0.0),
+ "detection_enabled": mp.Value(
+ "i", self.config.cameras[camera_name].detect.enabled
+ ),
+ "detection_fps": mp.Value("d", 0.0),
+ "detection_frame": mp.Value("d", 0.0),
+ "read_start": mp.Value("d", 0.0),
+ "ffmpeg_pid": mp.Value("i", 0),
+ "frame_queue": mp.Queue(maxsize=2),
}
-
+
def check_config(self):
for name, camera in self.config.cameras.items():
- assigned_roles = list(set([r for i in camera.ffmpeg.inputs for r in i.roles]))
- if not camera.clips.enabled and 'clips' in assigned_roles:
- logger.warning(f"Camera {name} has clips assigned to an input, but clips is not enabled.")
- elif camera.clips.enabled and not 'clips' in assigned_roles:
- logger.warning(f"Camera {name} has clips enabled, but clips is not assigned to an input.")
+ assigned_roles = list(
+ set([r for i in camera.ffmpeg.inputs for r in i.roles])
+ )
+ if not camera.record.enabled and "record" in assigned_roles:
+ logger.warning(
+ f"Camera {name} has record assigned to an input, but record is not enabled."
+ )
+ elif camera.record.enabled and not "record" in assigned_roles:
+ logger.warning(
+ f"Camera {name} has record enabled, but record is not assigned to an input."
+ )
- if not camera.record.enabled and 'record' in assigned_roles:
- logger.warning(f"Camera {name} has record assigned to an input, but record is not enabled.")
- elif camera.record.enabled and not 'record' in assigned_roles:
- logger.warning(f"Camera {name} has record enabled, but record is not assigned to an input.")
+ if not camera.rtmp.enabled and "rtmp" in assigned_roles:
+ logger.warning(
+ f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled."
+ )
+ elif camera.rtmp.enabled and not "rtmp" in assigned_roles:
+ logger.warning(
+ f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
+ )
- if not camera.rtmp.enabled and 'rtmp' in assigned_roles:
- logger.warning(f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled.")
- elif camera.rtmp.enabled and not 'rtmp' in assigned_roles:
- logger.warning(f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input.")
-
def set_log_levels(self):
- logging.getLogger().setLevel(self.config.logger.default)
+ logging.getLogger().setLevel(self.config.logger.default.value.upper())
for log, level in self.config.logger.logs.items():
- logging.getLogger(log).setLevel(level)
-
- if not 'geventwebsocket.handler' in self.config.logger.logs:
- logging.getLogger('geventwebsocket.handler').setLevel('ERROR')
+ logging.getLogger(log).setLevel(level.value.upper())
+
+ if not "werkzeug" in self.config.logger.logs:
+ logging.getLogger("werkzeug").setLevel("ERROR")
def init_queues(self):
# Queues for clip processing
self.event_queue = mp.Queue()
self.event_processed_queue = mp.Queue()
+ self.video_output_queue = mp.Queue(maxsize=len(self.config.cameras.keys()) * 2)
# Queue for cameras to push tracked objects to
- self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
+ self.detected_frames_queue = mp.Queue(
+ maxsize=len(self.config.cameras.keys()) * 2
+ )
def init_database(self):
+ # Migrate DB location
+ old_db_path = os.path.join(CLIPS_DIR, "frigate.db")
+ if not os.path.isfile(self.config.database.path) and os.path.isfile(
+ old_db_path
+ ):
+ os.rename(old_db_path, self.config.database.path)
+
+ # Migrate DB schema
migrate_db = SqliteExtDatabase(self.config.database.path)
# Run migrations
- del(logging.getLogger('peewee_migrate').handlers[:])
+ del logging.getLogger("peewee_migrate").handlers[:]
router = Router(migrate_db)
router.run()
migrate_db.close()
self.db = SqliteQueueDatabase(self.config.database.path)
- models = [Event]
+ models = [Event, Recordings]
self.db.bind(models)
def init_stats(self):
self.stats_tracking = stats_init(self.camera_metrics, self.detectors)
def init_web_server(self):
- self.flask_app = create_app(self.config, self.db, self.stats_tracking, self.detected_frames_processor, self.mqtt_client)
+ self.flask_app = create_app(
+ self.config,
+ self.db,
+ self.stats_tracking,
+ self.detected_frames_processor,
+ )
def init_mqtt(self):
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
+ def start_mqtt_relay(self):
+ self.mqtt_relay = MqttSocketRelay(
+ self.mqtt_client, self.config.mqtt.topic_prefix
+ )
+ self.mqtt_relay.start()
+
def start_detectors(self):
model_shape = (self.config.model.height, self.config.model.width)
for name in self.config.cameras.keys():
self.detection_out_events[name] = mp.Event()
- shm_in = mp.shared_memory.SharedMemory(name=name, create=True, size=self.config.model.height*self.config.model.width*3)
- shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}", create=True, size=20*6*4)
+
+ try:
+ shm_in = mp.shared_memory.SharedMemory(
+ name=name,
+ create=True,
+ size=self.config.model.height * self.config.model.width * 3,
+ )
+ except FileExistsError:
+ shm_in = mp.shared_memory.SharedMemory(name=name)
+
+ try:
+ shm_out = mp.shared_memory.SharedMemory(
+ name=f"out-{name}", create=True, size=20 * 6 * 4
+ )
+ except FileExistsError:
+ shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}")
+
self.detection_shms.append(shm_in)
self.detection_shms.append(shm_out)
for name, detector in self.config.detectors.items():
- if detector.type == 'cpu':
- self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, 'cpu', detector.num_threads)
- if detector.type == 'edgetpu':
- self.detectors[name] = EdgeTPUProcess(name, self.detection_queue, self.detection_out_events, model_shape, detector.device, detector.num_threads)
+ if detector.type == DetectorTypeEnum.cpu:
+ self.detectors[name] = EdgeTPUProcess(
+ name,
+ self.detection_queue,
+ self.detection_out_events,
+ model_shape,
+ "cpu",
+ detector.num_threads,
+ )
+ if detector.type == DetectorTypeEnum.edgetpu:
+ self.detectors[name] = EdgeTPUProcess(
+ name,
+ self.detection_queue,
+ self.detection_out_events,
+ model_shape,
+ detector.device,
+ detector.num_threads,
+ )
def start_detected_frames_processor(self):
- self.detected_frames_processor = TrackedObjectProcessor(self.config, self.mqtt_client, self.config.mqtt.topic_prefix,
- self.detected_frames_queue, self.event_queue, self.event_processed_queue, self.stop_event)
+ self.detected_frames_processor = TrackedObjectProcessor(
+ self.config,
+ self.mqtt_client,
+ self.config.mqtt.topic_prefix,
+ self.detected_frames_queue,
+ self.event_queue,
+ self.event_processed_queue,
+ self.video_output_queue,
+ self.stop_event,
+ )
self.detected_frames_processor.start()
+ def start_video_output_processor(self):
+ output_processor = mp.Process(
+ target=output_frames,
+ name=f"output_processor",
+ args=(
+ self.config,
+ self.video_output_queue,
+ ),
+ )
+ output_processor.daemon = True
+ self.output_processor = output_processor
+ output_processor.start()
+ logger.info(f"Output process started: {output_processor.pid}")
+
def start_camera_processors(self):
model_shape = (self.config.model.height, self.config.model.width)
for name, config in self.config.cameras.items():
- camera_process = mp.Process(target=track_camera, name=f"camera_processor:{name}", args=(name, config, model_shape,
- self.detection_queue, self.detection_out_events[name], self.detected_frames_queue,
- self.camera_metrics[name]))
+ camera_process = mp.Process(
+ target=track_camera,
+ name=f"camera_processor:{name}",
+ args=(
+ name,
+ config,
+ model_shape,
+ self.config.model.merged_labelmap,
+ self.detection_queue,
+ self.detection_out_events[name],
+ self.detected_frames_queue,
+ self.camera_metrics[name],
+ ),
+ )
camera_process.daemon = True
- self.camera_metrics[name]['process'] = camera_process
+ self.camera_metrics[name]["process"] = camera_process
camera_process.start()
logger.info(f"Camera processor started for {name}: {camera_process.pid}")
def start_camera_capture_processes(self):
for name, config in self.config.cameras.items():
- capture_process = mp.Process(target=capture_camera, name=f"camera_capture:{name}", args=(name, config,
- self.camera_metrics[name]))
+ capture_process = mp.Process(
+ target=capture_camera,
+ name=f"camera_capture:{name}",
+ args=(name, config, self.camera_metrics[name]),
+ )
capture_process.daemon = True
- self.camera_metrics[name]['capture_process'] = capture_process
+ self.camera_metrics[name]["capture_process"] = capture_process
capture_process.start()
logger.info(f"Capture process started for {name}: {capture_process.pid}")
-
+
def start_event_processor(self):
- self.event_processor = EventProcessor(self.config, self.camera_metrics, self.event_queue, self.event_processed_queue, self.stop_event)
+ self.event_processor = EventProcessor(
+ self.config,
+ self.camera_metrics,
+ self.event_queue,
+ self.event_processed_queue,
+ self.stop_event,
+ )
self.event_processor.start()
-
+
def start_event_cleanup(self):
self.event_cleanup = EventCleanup(self.config, self.stop_event)
self.event_cleanup.start()
-
+
def start_recording_maintainer(self):
self.recording_maintainer = RecordingMaintainer(self.config, self.stop_event)
self.recording_maintainer.start()
+ def start_recording_cleanup(self):
+ self.recording_cleanup = RecordingCleanup(self.config, self.stop_event)
+ self.recording_cleanup.start()
+
def start_stats_emitter(self):
- self.stats_emitter = StatsEmitter(self.config, self.stats_tracking, self.mqtt_client, self.config.mqtt.topic_prefix, self.stop_event)
+ self.stats_emitter = StatsEmitter(
+ self.config,
+ self.stats_tracking,
+ self.mqtt_client,
+ self.config.mqtt.topic_prefix,
+ self.stop_event,
+ )
self.stats_emitter.start()
def start_watchdog(self):
@@ -223,14 +331,17 @@ class FrigateApp():
self.log_process.terminate()
sys.exit(1)
self.start_detectors()
+ self.start_video_output_processor()
self.start_detected_frames_processor()
self.start_camera_processors()
self.start_camera_capture_processes()
self.init_stats()
self.init_web_server()
+ self.start_mqtt_relay()
self.start_event_processor()
self.start_event_cleanup()
self.start_recording_maintainer()
+ self.start_recording_cleanup()
self.start_stats_emitter()
self.start_watchdog()
# self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id)
@@ -238,22 +349,26 @@ class FrigateApp():
def receiveSignal(signalNumber, frame):
self.stop()
sys.exit()
-
+
signal.signal(signal.SIGTERM, receiveSignal)
- server = pywsgi.WSGIServer(('127.0.0.1', 5001), self.flask_app, handler_class=WebSocketHandler)
- server.serve_forever()
+ try:
+ self.flask_app.run(host="127.0.0.1", port=5001, debug=False)
+ except KeyboardInterrupt:
+ pass
self.stop()
-
+
def stop(self):
logger.info(f"Stopping...")
self.stop_event.set()
+ self.mqtt_relay.stop()
self.detected_frames_processor.join()
self.event_processor.join()
self.event_cleanup.join()
self.recording_maintainer.join()
+ self.recording_cleanup.join()
self.stats_emitter.join()
self.frigate_watchdog.join()
self.db.stop()
diff --git a/frigate/config.py b/frigate/config.py
index 0878f3c1f..ea6ea3280 100644
--- a/frigate/config.py
+++ b/frigate/config.py
@@ -1,1057 +1,770 @@
-import base64
+from __future__ import annotations
+
import json
import logging
import os
-from typing import Dict
+from enum import Enum
+from typing import Dict, List, Optional, Tuple, Union
-import cv2
import matplotlib.pyplot as plt
import numpy as np
-import voluptuous as vol
import yaml
+from pydantic import BaseModel, Field, validator
+from pydantic.fields import PrivateAttr
-from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
-from frigate.util import create_mask
+from frigate.const import BASE_DIR, CACHE_DIR, RECORD_DIR
+from frigate.edgetpu import load_labels
+from frigate.util import create_mask, deep_merge
logger = logging.getLogger(__name__)
-DEFAULT_TRACKED_OBJECTS = ['person']
+# TODO: Identify what the default format to display timestamps is
+DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
+# German Style:
+# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
+
+FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
+
+DEFAULT_TRACKED_OBJECTS = ["person"]
+DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
+
+
+class DetectorTypeEnum(str, Enum):
+ edgetpu = "edgetpu"
+ cpu = "cpu"
+
+
+class DetectorConfig(BaseModel):
+ type: DetectorTypeEnum = Field(default=DetectorTypeEnum.cpu, title="Detector Type")
+ device: str = Field(default="usb", title="Device Type")
+ num_threads: int = Field(default=3, title="Number of detection threads")
+
+
+class MqttConfig(BaseModel):
+ host: str = Field(title="MQTT Host")
+ port: int = Field(default=1883, title="MQTT Port")
+ topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix")
+ client_id: str = Field(default="frigate", title="MQTT Client ID")
+ stats_interval: int = Field(default=60, title="MQTT Camera Stats Interval")
+ user: Optional[str] = Field(title="MQTT Username")
+ password: Optional[str] = Field(title="MQTT Password")
+ tls_ca_certs: Optional[str] = Field(title="MQTT TLS CA Certificates")
+ tls_client_cert: Optional[str] = Field(title="MQTT TLS Client Certificate")
+ tls_client_key: Optional[str] = Field(title="MQTT TLS Client Key")
+ tls_insecure: Optional[bool] = Field(title="MQTT TLS Insecure")
+
+ @validator("password", pre=True, always=True)
+ def validate_password(cls, v, values):
+ if (v is None) != (values["user"] is None):
+ raise ValueError("Password must be provided with username.")
+ return v
+
+
+class RetainConfig(BaseModel):
+ default: int = Field(default=10, title="Default retention period.")
+ objects: Dict[str, int] = Field(
+ default_factory=dict, title="Object retention period."
+ )
+
+
+# DEPRECATED: Will eventually be removed
+class ClipsConfig(BaseModel):
+ enabled: bool = Field(default=False, title="Save clips.")
+ max_seconds: int = Field(default=300, title="Maximum clip duration.")
+ pre_capture: int = Field(default=5, title="Seconds to capture before event starts.")
+ post_capture: int = Field(default=5, title="Seconds to capture after event ends.")
+ required_zones: List[str] = Field(
+ default_factory=list,
+ title="List of required zones to be entered in order to save the clip.",
+ )
+ objects: Optional[List[str]] = Field(
+ title="List of objects to be detected in order to save the clip.",
+ )
+ retain: RetainConfig = Field(
+ default_factory=RetainConfig, title="Clip retention settings."
+ )
+
+
+class RecordConfig(BaseModel):
+ enabled: bool = Field(default=False, title="Enable record on all cameras.")
+ retain_days: int = Field(default=0, title="Recording retention period in days.")
+ events: ClipsConfig = Field(
+ default_factory=ClipsConfig, title="Event specific settings."
+ )
+
+
+class MotionConfig(BaseModel):
+ threshold: int = Field(
+ default=25,
+ title="Motion detection threshold (1-255).",
+ ge=1,
+ le=255,
+ )
+ contour_area: Optional[int] = Field(title="Contour Area")
+ delta_alpha: float = Field(default=0.2, title="Delta Alpha")
+ frame_alpha: float = Field(default=0.2, title="Frame Alpha")
+ frame_height: Optional[int] = Field(title="Frame Height")
+ mask: Union[str, List[str]] = Field(
+ default="", title="Coordinates polygon for the motion mask."
+ )
+
+
+class RuntimeMotionConfig(MotionConfig):
+ raw_mask: Union[str, List[str]] = ""
+ mask: np.ndarray = None
+
+ def __init__(self, **config):
+ frame_shape = config.get("frame_shape", (1, 1))
+
+ if "frame_height" not in config:
+ config["frame_height"] = max(frame_shape[0] // 6, 180)
+
+ if "contour_area" not in config:
+ frame_width = frame_shape[1] * config["frame_height"] / frame_shape[0]
+ config["contour_area"] = (
+ config["frame_height"] * frame_width * 0.00173611111
+ )
+
+ mask = config.get("mask", "")
+ config["raw_mask"] = mask
-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','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']
-
- @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, camera_config, global_config, ffmpeg_input):
- self._path = ffmpeg_input['path']
- self._roles = ffmpeg_input['roles']
- self._global_args = ffmpeg_input.get('global_args', camera_config.get('global_args', global_config['global_args']))
- self._hwaccel_args = ffmpeg_input.get('hwaccel_args', camera_config.get('hwaccel_args', global_config['hwaccel_args']))
- self._input_args = ffmpeg_input.get('input_args', camera_config.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(config, 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, global_mask=None, 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 = []
- if global_mask:
- if isinstance(global_mask, list):
- self._raw_mask += global_mask
- elif isinstance(global_mask, str):
- self._raw_mask += [global_mask]
-
- mask = config.get('mask')
if mask:
- if isinstance(mask, list):
- self._raw_mask += mask
- elif isinstance(mask, str):
- self._raw_mask += [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._raw_mask = config.get('mask')
- self._filters = { name: FilterConfig(global_config['filters'].get(name, {}), config['filters'].get(name, {}), self._raw_mask, 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,
- 'mask': self._raw_mask,
- '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'])
- self._required_zones = config['required_zones']
-
- @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
-
- @property
- def required_zones(self):
- return self._required_zones
-
- 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(),
- 'required_zones': self.required_zones
- }
-
-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')
- self._required_zones = config['required_zones']
-
- @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 required_zones(self):
- return self._required_zones
-
- def to_dict(self):
- return {
- 'enabled': self.enabled,
- 'timestamp': self.timestamp,
- 'bounding_box': self.bounding_box,
- 'crop': self.crop,
- 'height': self.height,
- 'required_zones': self.required_zones
- }
-
-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'])
- self._required_zones = config['required_zones']
-
- @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
-
- @property
- def required_zones(self):
- return self._required_zones
-
- 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(),
- 'required_zones': self.required_zones
- }
-
-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)
+ config["mask"] = create_mask(frame_shape, 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))
+ empty_mask = np.zeros(frame_shape, np.uint8)
+ empty_mask[:] = 255
+ config["mask"] = empty_mask
+
+ super().__init__(**config)
+
+ def dict(self, **kwargs):
+ ret = super().dict(**kwargs)
+ if "mask" in ret:
+ ret["mask"] = ret["raw_mask"]
+ ret.pop("raw_mask")
+ return ret
+
+ class Config:
+ arbitrary_types_allowed = True
+
+
+class DetectConfig(BaseModel):
+ height: int = Field(title="Height of the stream for the detect role.")
+ width: int = Field(title="Width of the stream for the detect role.")
+ fps: int = Field(title="Number of frames per second to process through detection.")
+ enabled: bool = Field(default=True, title="Detection Enabled.")
+ max_disappeared: Optional[int] = Field(
+ title="Maximum number of frames the object can dissapear before detection ends."
+ )
+
+
+class FilterConfig(BaseModel):
+ min_area: int = Field(
+ default=0, title="Minimum area of bounding box for object to be counted."
+ )
+ max_area: int = Field(
+ default=24000000, title="Maximum area of bounding box for object to be counted."
+ )
+ threshold: float = Field(
+ default=0.7,
+ title="Average detection confidence threshold for object to be counted.",
+ )
+ min_score: float = Field(
+ default=0.5, title="Minimum detection confidence for object to be counted."
+ )
+ mask: Optional[Union[str, List[str]]] = Field(
+ title="Detection area polygon mask for this filter configuration.",
+ )
+
+
+class RuntimeFilterConfig(FilterConfig):
+ mask: Optional[np.ndarray]
+ raw_mask: Optional[Union[str, List[str]]]
+
+ def __init__(self, **config):
+ mask = config.get("mask")
+ config["raw_mask"] = mask
+
+ if mask is not None:
+ config["mask"] = create_mask(config.get("frame_shape", (1, 1)), mask)
+
+ super().__init__(**config)
+
+ def dict(self, **kwargs):
+ ret = super().dict(**kwargs)
+ if "mask" in ret:
+ ret["mask"] = ret["raw_mask"]
+ ret.pop("raw_mask")
+ return ret
+
+ class Config:
+ arbitrary_types_allowed = True
+
+
+class ZoneConfig(BaseModel):
+ filters: Dict[str, FilterConfig] = Field(
+ default_factory=dict, title="Zone filters."
+ )
+ coordinates: Union[str, List[str]] = Field(
+ title="Coordinates polygon for the defined zone."
+ )
+ objects: List[str] = Field(
+ default_factory=list,
+ title="List of objects that can trigger the zone.",
+ )
+ _color: Optional[Tuple[int, int, int]] = PrivateAttr()
+ _contour: np.ndarray = PrivateAttr()
@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*5))
-
- @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):
+ def color(self) -> Tuple[int, int, int]:
return self._color
- @color.setter
- def color(self, val):
- self._color = val
+ @property
+ def contour(self) -> np.ndarray:
+ return self._contour
+
+ def __init__(self, **config):
+ super().__init__(**config)
+
+ self._color = config.get("color", (0, 0, 0))
+ coordinates = config["coordinates"]
+
+ if isinstance(coordinates, list):
+ self._contour = np.array(
+ [[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates]
+ )
+ elif isinstance(coordinates, str):
+ points = coordinates.split(",")
+ self._contour = np.array(
+ [[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
+ )
+ else:
+ self._contour = np.array([])
+
+
+class ObjectConfig(BaseModel):
+ track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
+ filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.")
+ mask: Union[str, List[str]] = Field(default="", title="Object mask.")
+
+
+class BirdseyeModeEnum(str, Enum):
+ objects = "objects"
+ motion = "motion"
+ continuous = "continuous"
+
+
+class BirdseyeConfig(BaseModel):
+ enabled: bool = Field(default=True, title="Enable birdseye view.")
+ width: int = Field(default=1280, title="Birdseye width.")
+ height: int = Field(default=720, title="Birdseye height.")
+ quality: int = Field(
+ default=8,
+ title="Encoding quality.",
+ ge=1,
+ le=31,
+ )
+ mode: BirdseyeModeEnum = Field(
+ default=BirdseyeModeEnum.objects, title="Tracking mode."
+ )
+
+
+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"]
+RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
+ "-f",
+ "segment",
+ "-segment_time",
+ "10",
+ "-segment_format",
+ "mp4",
+ "-reset_timestamps",
+ "1",
+ "-strftime",
+ "1",
+ "-c",
+ "copy",
+ "-an",
+]
+
+
+class FfmpegOutputArgsConfig(BaseModel):
+ detect: Union[str, List[str]] = Field(
+ default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
+ title="Detect role FFmpeg output arguments.",
+ )
+ record: Union[str, List[str]] = Field(
+ default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
+ title="Record role FFmpeg output arguments.",
+ )
+ rtmp: Union[str, List[str]] = Field(
+ default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT,
+ title="RTMP role FFmpeg output arguments.",
+ )
+
+
+class FfmpegConfig(BaseModel):
+ global_args: Union[str, List[str]] = Field(
+ default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments."
+ )
+ hwaccel_args: Union[str, List[str]] = Field(
+ default_factory=list, title="FFmpeg hardware acceleration arguments."
+ )
+ input_args: Union[str, List[str]] = Field(
+ default=FFMPEG_INPUT_ARGS_DEFAULT, title="FFmpeg input arguments."
+ )
+ output_args: FfmpegOutputArgsConfig = Field(
+ default_factory=FfmpegOutputArgsConfig,
+ title="FFmpeg output arguments per role.",
+ )
+
+
+class CameraInput(BaseModel):
+ path: str = Field(title="Camera input path.")
+ roles: List[str] = Field(title="Roles assigned to this input.")
+ global_args: Union[str, List[str]] = Field(
+ default_factory=list, title="FFmpeg global arguments."
+ )
+ hwaccel_args: Union[str, List[str]] = Field(
+ default_factory=list, title="FFmpeg hardware acceleration arguments."
+ )
+ input_args: Union[str, List[str]] = Field(
+ default_factory=list, title="FFmpeg input arguments."
+ )
+
+
+class CameraFfmpegConfig(FfmpegConfig):
+ inputs: List[CameraInput] = Field(title="Camera inputs.")
+
+ @validator("inputs")
+ def validate_roles(cls, v):
+ roles = [role for i in v for role in i.roles]
+ roles_set = set(roles)
+
+ if len(roles) > len(roles_set):
+ raise ValueError("Each input role may only be used once.")
+
+ if not "detect" in roles:
+ raise ValueError("The detect role is required.")
+
+ return v
+
+
+class CameraSnapshotsConfig(BaseModel):
+ enabled: bool = Field(default=False, title="Snapshots enabled.")
+ clean_copy: bool = Field(
+ default=True, title="Create a clean copy of the snapshot image."
+ )
+ timestamp: bool = Field(
+ default=False, title="Add a timestamp overlay on the snapshot."
+ )
+ bounding_box: bool = Field(
+ default=True, title="Add a bounding box overlay on the snapshot."
+ )
+ crop: bool = Field(default=False, title="Crop the snapshot to the detected object.")
+ required_zones: List[str] = Field(
+ default_factory=list,
+ title="List of required zones to be entered in order to save a snapshot.",
+ )
+ height: Optional[int] = Field(title="Snapshot image height.")
+ retain: RetainConfig = Field(
+ default_factory=RetainConfig, title="Snapshot retention."
+ )
+ quality: int = Field(
+ default=70,
+ title="Quality of the encoded jpeg (0-100).",
+ ge=0,
+ le=100,
+ )
+
+
+class ColorConfig(BaseModel):
+ red: int = Field(default=255, le=0, ge=255, title="Red")
+ green: int = Field(default=255, le=0, ge=255, title="Green")
+ blue: int = Field(default=255, le=0, ge=255, title="Blue")
+
+
+class TimestampStyleConfig(BaseModel):
+ position: str = Field(default="tl", title="Timestamp position.")
+ format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.")
+ color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.")
+ scale: float = Field(default=1.0, title="Timestamp scale.")
+ thickness: int = Field(default=2, title="Timestamp thickness.")
+ effect: Optional[str] = Field(title="Timestamp effect.")
+
+
+class CameraMqttConfig(BaseModel):
+ enabled: bool = Field(default=True, title="Send image over MQTT.")
+ timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.")
+ bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.")
+ crop: bool = Field(default=True, title="Crop MQTT image to detected object.")
+ height: int = Field(default=270, title="MQTT image height.")
+ required_zones: List[str] = Field(
+ default_factory=list,
+ title="List of required zones to be entered in order to send the image.",
+ )
+ quality: int = Field(
+ default=70,
+ title="Quality of the encoded jpeg (0-100).",
+ ge=0,
+ le=100,
+ )
+
+
+class CameraRtmpConfig(BaseModel):
+ enabled: bool = Field(default=True, title="RTMP restreaming enabled.")
+
+
+class CameraLiveConfig(BaseModel):
+ height: int = Field(default=720, title="Live camera view height")
+ quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
+
+
+class CameraConfig(BaseModel):
+ name: Optional[str] = Field(title="Camera name.")
+ ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
+ best_image_timeout: int = Field(
+ default=60,
+ title="How long to wait for the image with the highest confidence score.",
+ )
+ zones: Dict[str, ZoneConfig] = Field(
+ default_factory=dict, title="Zone configuration."
+ )
+ record: RecordConfig = Field(
+ default_factory=RecordConfig, title="Record configuration."
+ )
+ rtmp: CameraRtmpConfig = Field(
+ default_factory=CameraRtmpConfig, title="RTMP restreaming configuration."
+ )
+ live: Optional[CameraLiveConfig] = Field(title="Live playback settings.")
+ snapshots: CameraSnapshotsConfig = Field(
+ default_factory=CameraSnapshotsConfig, title="Snapshot configuration."
+ )
+ mqtt: CameraMqttConfig = Field(
+ default_factory=CameraMqttConfig, title="MQTT configuration."
+ )
+ objects: ObjectConfig = Field(
+ default_factory=ObjectConfig, title="Object configuration."
+ )
+ motion: Optional[MotionConfig] = Field(title="Motion detection configuration.")
+ detect: DetectConfig = Field(title="Object detection configuration.")
+ timestamp_style: TimestampStyleConfig = Field(
+ default_factory=TimestampStyleConfig, title="Timestamp style configuration."
+ )
+
+ def __init__(self, **config):
+ # Set zone colors
+ if "zones" in config:
+ colors = plt.cm.get_cmap("tab10", len(config["zones"]))
+ config["zones"] = {
+ name: {**z, "color": tuple(round(255 * c) for c in colors(idx)[:3])}
+ for idx, (name, z) in enumerate(config["zones"].items())
+ }
+
+ super().__init__(**config)
@property
- def filters(self):
- return self._filters
+ def frame_shape(self) -> Tuple[int, int]:
+ return self.detect.height, self.detect.width
- def to_dict(self):
- return {
- 'filters': {k: f.to_dict() for k, f in self.filters.items()},
- 'coordinates': self._coordinates
- }
+ @property
+ def frame_shape_yuv(self) -> Tuple[int, int]:
+ return self.detect.height * 3 // 2, self.detect.width
-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:
+ @property
+ def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
+ 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
- })
+ ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
+ return ffmpeg_cmds
-
- self._set_zone_colors(self._zones)
-
- def _get_ffmpeg_cmd(self, ffmpeg_input):
+ def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
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 "detect" in ffmpeg_input.roles:
+ detect_args = (
+ self.ffmpeg.output_args.detect
+ if isinstance(self.ffmpeg.output_args.detect, list)
+ else self.ffmpeg.output_args.detect.split(" ")
+ )
+ ffmpeg_output_args = (
+ [
+ "-r",
+ str(self.detect.fps),
+ "-s",
+ f"{self.detect.width}x{self.detect.height}",
+ ]
+ + detect_args
+ + ffmpeg_output_args
+ + ["pipe:"]
+ )
+ if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
+ rtmp_args = (
+ self.ffmpeg.output_args.rtmp
+ if isinstance(self.ffmpeg.output_args.rtmp, list)
+ else self.ffmpeg.output_args.rtmp.split(" ")
+ )
+ ffmpeg_output_args = (
+ rtmp_args + [f"rtmp://127.0.0.1/live/{self.name}"] + ffmpeg_output_args
+ )
+ if "record" in ffmpeg_input.roles and self.record.enabled:
+ record_args = (
+ self.ffmpeg.output_args.record
+ if isinstance(self.ffmpeg.output_args.record, list)
+ else self.ffmpeg.output_args.record.split(" ")
+ )
+ ffmpeg_output_args = (
+ record_args
+ + [f"{os.path.join(CACHE_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)
+ global_args = ffmpeg_input.global_args or self.ffmpeg.global_args
+ hwaccel_args = ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args
+ input_args = ffmpeg_input.input_args or self.ffmpeg.input_args
- return [part for part in cmd if part != '']
+ global_args = (
+ global_args if isinstance(global_args, list) else global_args.split(" ")
+ )
+ hwaccel_args = (
+ hwaccel_args if isinstance(hwaccel_args, list) else hwaccel_args.split(" ")
+ )
+ input_args = (
+ input_args if isinstance(input_args, list) else input_args.split(" ")
+ )
- 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])
+ cmd = (
+ ["ffmpeg"]
+ + global_args
+ + hwaccel_args
+ + input_args
+ + ["-i", ffmpeg_input.path]
+ + ffmpeg_output_args
+ )
- for name, zone in zones.items():
- zone.color = zone_colors[name]
+ return [part for part in cmd if part != ""]
+
+
+class DatabaseConfig(BaseModel):
+ path: str = Field(
+ default=os.path.join(BASE_DIR, "frigate.db"), title="Database path."
+ )
+
+
+class ModelConfig(BaseModel):
+ width: int = Field(default=320, title="Object detection model input width.")
+ height: int = Field(default=320, title="Object detection model input height.")
+ labelmap: Dict[int, str] = Field(
+ default_factory=dict, title="Labelmap customization."
+ )
+ _merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
+ _colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()
@property
- def name(self):
- return self._name
+ def merged_labelmap(self) -> Dict[int, str]:
+ return self._merged_labelmap
@property
- def ffmpeg(self):
- return self._ffmpeg
+ def colormap(self) -> Dict[int, tuple[int, int, int]]:
+ return self._colormap
- @property
- def height(self):
- return self._height
+ def __init__(self, **config):
+ super().__init__(**config)
- @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],
+ self._merged_labelmap = {
+ **load_labels("/labelmap.txt"),
+ **config.get("labelmap", {}),
}
+ cmap = plt.cm.get_cmap("tab10", len(self._merged_labelmap.keys()))
-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)
+ self._colormap = {}
+ for key, val in self._merged_labelmap.items():
+ self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
- config = FRIGATE_CONFIG_SCHEMA(config)
- config = self._sub_env_vars(config)
+class LogLevelEnum(str, Enum):
+ debug = "debug"
+ info = "info"
+ warning = "warning"
+ error = "error"
+ critical = "critical"
- 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_')}
+class LoggerConfig(BaseModel):
+ default: LogLevelEnum = Field(
+ default=LogLevelEnum.info, title="Default logging level."
+ )
+ logs: Dict[str, LogLevelEnum] = Field(
+ default_factory=dict, title="Log level for specified processes."
+ )
- 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)
+class SnapshotsConfig(BaseModel):
+ retain: RetainConfig = Field(
+ default_factory=RetainConfig, title="Global snapshot retention configuration."
+ )
+
+
+class FrigateConfig(BaseModel):
+ mqtt: MqttConfig = Field(title="MQTT Configuration.")
+ database: DatabaseConfig = Field(
+ default_factory=DatabaseConfig, title="Database configuration."
+ )
+ environment_vars: Dict[str, str] = Field(
+ default_factory=dict, title="Frigate environment variables."
+ )
+ model: ModelConfig = Field(
+ default_factory=ModelConfig, title="Detection model configuration."
+ )
+ detectors: Dict[str, DetectorConfig] = Field(
+ default={name: DetectorConfig(**d) for name, d in DEFAULT_DETECTORS.items()},
+ title="Detector hardware configuration.",
+ )
+ logger: LoggerConfig = Field(
+ default_factory=LoggerConfig, title="Logging configuration."
+ )
+ record: RecordConfig = Field(
+ default_factory=RecordConfig, title="Global record configuration."
+ )
+ snapshots: SnapshotsConfig = Field(
+ default_factory=SnapshotsConfig, title="Global snapshots configuration."
+ )
+ birdseye: BirdseyeConfig = Field(
+ default_factory=BirdseyeConfig, title="Birdseye configuration."
+ )
+ ffmpeg: FfmpegConfig = Field(
+ default_factory=FfmpegConfig, title="Global FFmpeg configuration."
+ )
+ objects: ObjectConfig = Field(
+ default_factory=ObjectConfig, title="Global object configuration."
+ )
+ motion: Optional[MotionConfig] = Field(
+ title="Global motion detection configuration."
+ )
+ detect: Optional[DetectConfig] = Field(
+ title="Global object tracking configuration."
+ )
+ cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
+
+ @property
+ def runtime_config(self) -> FrigateConfig:
+ """Merge camera config with globals."""
+ config = self.copy(deep=True)
+
+ # MQTT password substitution
+ if config.mqtt.password:
+ config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS)
+
+ # Global config to propegate down to camera level
+ global_config = config.dict(
+ include={
+ "record": ...,
+ "snapshots": ...,
+ "objects": ...,
+ "motion": ...,
+ "detect": ...,
+ "ffmpeg": ...,
+ },
+ exclude_unset=True,
+ )
+
+ for name, camera in config.cameras.items():
+ merged_config = deep_merge(camera.dict(exclude_unset=True), global_config)
+ camera_config: CameraConfig = CameraConfig.parse_obj(
+ {"name": name, **merged_config}
+ )
+
+ # FFMPEG input substitution
+ for input in camera_config.ffmpeg.inputs:
+ input.path = input.path.format(**FRIGATE_ENV_VARS)
+
+ # Add default filters
+ object_keys = camera_config.objects.track
+ if camera_config.objects.filters is None:
+ camera_config.objects.filters = {}
+ object_keys = object_keys - camera_config.objects.filters.keys()
+ for key in object_keys:
+ camera_config.objects.filters[key] = FilterConfig()
+
+ # Apply global object masks and convert masks to numpy array
+ for object, filter in camera_config.objects.filters.items():
+ if camera_config.objects.mask:
+ filter_mask = []
+ if filter.mask is not None:
+ filter_mask = (
+ filter.mask
+ if isinstance(filter.mask, list)
+ else [filter.mask]
+ )
+ object_mask = (
+ camera_config.objects.mask
+ if isinstance(camera_config.objects.mask, list)
+ else [camera_config.objects.mask]
+ )
+ filter.mask = filter_mask + object_mask
+
+ # Set runtime filter to create masks
+ camera_config.objects.filters[object] = RuntimeFilterConfig(
+ frame_shape=camera_config.frame_shape,
+ **filter.dict(exclude_unset=True),
+ )
+
+ # Convert motion configuration
+ if camera_config.motion is None:
+ camera_config.motion = RuntimeMotionConfig(
+ frame_shape=camera_config.frame_shape
+ )
+ else:
+ camera_config.motion = RuntimeMotionConfig(
+ frame_shape=camera_config.frame_shape,
+ raw_mask=camera_config.motion.mask,
+ **camera_config.motion.dict(exclude_unset=True),
+ )
+
+ # Default detect configuration
+ max_disappeared = camera_config.detect.fps * 5
+ if camera_config.detect.max_disappeared is None:
+ camera_config.detect.max_disappeared = max_disappeared
+
+ # Default live configuration
+ if camera_config.live is None:
+ camera_config.live = CameraLiveConfig()
+
+ config.cameras[name] = camera_config
return config
- def _load_file(self, config_file):
+ @validator("cameras")
+ def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
+ zones = [zone for camera in v.values() for zone in camera.zones.keys()]
+ for zone in zones:
+ if zone in v.keys():
+ raise ValueError("Zones cannot share names with cameras")
+ return v
+
+ @classmethod
+ def parse_file(cls, config_file):
with open(config_file) as f:
raw_config = f.read()
@@ -1060,53 +773,4 @@ class FrigateConfig():
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
+ return cls.parse_obj(config)
diff --git a/frigate/const.py b/frigate/const.py
index 2ea9f9f68..c2b0f8e9d 100644
--- a/frigate/const.py
+++ b/frigate/const.py
@@ -1,3 +1,4 @@
-CLIPS_DIR = '/media/frigate/clips'
-RECORD_DIR = '/media/frigate/recordings'
-CACHE_DIR = '/tmp/cache'
\ No newline at end of file
+BASE_DIR = "/media/frigate"
+CLIPS_DIR = f"{BASE_DIR}/clips"
+RECORD_DIR = f"{BASE_DIR}/recordings"
+CACHE_DIR = "/tmp/cache"
diff --git a/frigate/edgetpu.py b/frigate/edgetpu.py
index d65ce523b..62c35eaf5 100644
--- a/frigate/edgetpu.py
+++ b/frigate/edgetpu.py
@@ -1,48 +1,49 @@
import datetime
-import hashlib
import logging
import multiprocessing as mp
import os
import queue
-import threading
import signal
+import threading
from abc import ABC, abstractmethod
-from multiprocessing.connection import Connection
-from setproctitle import setproctitle
from typing import Dict
import numpy as np
import tflite_runtime.interpreter as tflite
+from setproctitle import setproctitle
from tflite_runtime.interpreter import load_delegate
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen
logger = logging.getLogger(__name__)
-def load_labels(path, encoding='utf-8'):
- """Loads labels from file (with or without index numbers).
- Args:
- path: path to label file.
- encoding: label file encoding.
- Returns:
- Dictionary mapping indices to labels.
- """
- with open(path, 'r', encoding=encoding) as f:
- lines = f.readlines()
- if not lines:
- return {}
- if lines[0].split(' ', maxsplit=1)[0].isdigit():
- pairs = [line.split(' ', maxsplit=1) for line in lines]
- return {int(index): label.strip() for index, label in pairs}
- else:
- return {index: line.strip() for index, line in enumerate(lines)}
+def load_labels(path, encoding="utf-8"):
+ """Loads labels from file (with or without index numbers).
+ Args:
+ path: path to label file.
+ encoding: label file encoding.
+ Returns:
+ Dictionary mapping indices to labels.
+ """
+ with open(path, "r", encoding=encoding) as f:
+ lines = f.readlines()
+ if not lines:
+ return {}
+
+ if lines[0].split(" ", maxsplit=1)[0].isdigit():
+ pairs = [line.split(" ", maxsplit=1) for line in lines]
+ return {int(index): label.strip() for index, label in pairs}
+ else:
+ return {index: line.strip() for index, line in enumerate(lines)}
+
class ObjectDetector(ABC):
@abstractmethod
- def detect(self, tensor_input, threshold = .4):
+ def detect(self, tensor_input, threshold=0.4):
pass
+
class LocalObjectDetector(ObjectDetector):
def __init__(self, tf_device=None, num_threads=3, labels=None):
self.fps = EventsPerSecond()
@@ -57,27 +58,34 @@ class LocalObjectDetector(ObjectDetector):
edge_tpu_delegate = None
- if tf_device != 'cpu':
+ if tf_device != "cpu":
try:
logger.info(f"Attempting to load TPU as {device_config['device']}")
- edge_tpu_delegate = load_delegate('libedgetpu.so.1.0', device_config)
+ edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config)
logger.info("TPU found")
self.interpreter = tflite.Interpreter(
- model_path='/edgetpu_model.tflite',
- experimental_delegates=[edge_tpu_delegate])
+ model_path="/edgetpu_model.tflite",
+ experimental_delegates=[edge_tpu_delegate],
+ )
except ValueError:
- logger.info("No EdgeTPU detected.")
+ logger.error(
+ "No EdgeTPU was detected. If you do not have a Coral device yet, you must configure CPU detectors."
+ )
raise
else:
+ logger.warning(
+ "CPU detectors are not recommended and should only be used for testing or for trial purposes."
+ )
self.interpreter = tflite.Interpreter(
- model_path='/cpu_model.tflite', num_threads=num_threads)
-
+ model_path="/cpu_model.tflite", num_threads=num_threads
+ )
+
self.interpreter.allocate_tensors()
self.tensor_input_details = self.interpreter.get_input_details()
self.tensor_output_details = self.interpreter.get_output_details()
-
- def detect(self, tensor_input, threshold=.4):
+
+ def detect(self, tensor_input, threshold=0.4):
detections = []
raw_detections = self.detect_raw(tensor_input)
@@ -85,28 +93,50 @@ class LocalObjectDetector(ObjectDetector):
for d in raw_detections:
if d[1] < threshold:
break
- detections.append((
- self.labels[int(d[0])],
- float(d[1]),
- (d[2], d[3], d[4], d[5])
- ))
+ detections.append(
+ (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5]))
+ )
self.fps.update()
return detections
def detect_raw(self, tensor_input):
- self.interpreter.set_tensor(self.tensor_input_details[0]['index'], tensor_input)
+ self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input)
self.interpreter.invoke()
- boxes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[0]['index']))
- label_codes = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[1]['index']))
- scores = np.squeeze(self.interpreter.get_tensor(self.tensor_output_details[2]['index']))
- detections = np.zeros((20,6), np.float32)
- for i, score in enumerate(scores):
- detections[i] = [label_codes[i], score, boxes[i][0], boxes[i][1], boxes[i][2], boxes[i][3]]
-
+ boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0]
+ class_ids = self.interpreter.tensor(self.tensor_output_details[1]["index"])()[0]
+ scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[0]
+ count = int(
+ self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0]
+ )
+
+ detections = np.zeros((20, 6), np.float32)
+
+ for i in range(count):
+ if scores[i] < 0.4 or i == 20:
+ break
+ detections[i] = [
+ class_ids[i],
+ float(scores[i]),
+ boxes[i][0],
+ boxes[i][1],
+ boxes[i][2],
+ boxes[i][3],
+ ]
+
return detections
-def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.Event], avg_speed, start, model_shape, tf_device, num_threads):
+
+def run_detector(
+ name: str,
+ detection_queue: mp.Queue,
+ out_events: Dict[str, mp.Event],
+ avg_speed,
+ start,
+ model_shape,
+ tf_device,
+ num_threads,
+):
threading.current_thread().name = f"detector:{name}"
logger = logging.getLogger(f"detector.{name}")
logger.info(f"Starting detection process: {os.getpid()}")
@@ -114,9 +144,10 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
listen()
stop_event = mp.Event()
+
def receiveSignal(signalNumber, frame):
stop_event.set()
-
+
signal.signal(signal.SIGTERM, receiveSignal)
signal.signal(signal.SIGINT, receiveSignal)
@@ -126,21 +157,17 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
outputs = {}
for name in out_events.keys():
out_shm = mp.shared_memory.SharedMemory(name=f"out-{name}", create=False)
- out_np = np.ndarray((20,6), dtype=np.float32, buffer=out_shm.buf)
- outputs[name] = {
- 'shm': out_shm,
- 'np': out_np
- }
-
- while True:
- if stop_event.is_set():
- break
+ out_np = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf)
+ outputs[name] = {"shm": out_shm, "np": out_np}
+ while not stop_event.is_set():
try:
connection_id = detection_queue.get(timeout=5)
except queue.Empty:
continue
- input_frame = frame_manager.get(connection_id, (1,model_shape[0],model_shape[1],3))
+ input_frame = frame_manager.get(
+ connection_id, (1, model_shape[0], model_shape[1], 3)
+ )
if input_frame is None:
continue
@@ -148,26 +175,35 @@ def run_detector(name: str, detection_queue: mp.Queue, out_events: Dict[str, mp.
# detect and send the output
start.value = datetime.datetime.now().timestamp()
detections = object_detector.detect_raw(input_frame)
- duration = datetime.datetime.now().timestamp()-start.value
- outputs[connection_id]['np'][:] = detections[:]
+ duration = datetime.datetime.now().timestamp() - start.value
+ outputs[connection_id]["np"][:] = detections[:]
out_events[connection_id].set()
start.value = 0.0
- avg_speed.value = (avg_speed.value*9 + duration)/10
-
-class EdgeTPUProcess():
- def __init__(self, name, detection_queue, out_events, model_shape, tf_device=None, num_threads=3):
+ avg_speed.value = (avg_speed.value * 9 + duration) / 10
+
+
+class EdgeTPUProcess:
+ def __init__(
+ self,
+ name,
+ detection_queue,
+ out_events,
+ model_shape,
+ tf_device=None,
+ num_threads=3,
+ ):
self.name = name
self.out_events = out_events
self.detection_queue = detection_queue
- self.avg_inference_speed = mp.Value('d', 0.01)
- self.detection_start = mp.Value('d', 0.0)
+ self.avg_inference_speed = mp.Value("d", 0.01)
+ self.detection_start = mp.Value("d", 0.0)
self.detect_process = None
self.model_shape = model_shape
self.tf_device = tf_device
self.num_threads = num_threads
self.start_or_restart()
-
+
def stop(self):
self.detect_process.terminate()
logging.info("Waiting for detection process to exit gracefully...")
@@ -181,23 +217,41 @@ class EdgeTPUProcess():
self.detection_start.value = 0.0
if (not self.detect_process is None) and self.detect_process.is_alive():
self.stop()
- self.detect_process = mp.Process(target=run_detector, name=f"detector:{self.name}", args=(self.name, self.detection_queue, self.out_events, self.avg_inference_speed, self.detection_start, self.model_shape, self.tf_device, self.num_threads))
+ self.detect_process = mp.Process(
+ target=run_detector,
+ name=f"detector:{self.name}",
+ args=(
+ self.name,
+ self.detection_queue,
+ self.out_events,
+ self.avg_inference_speed,
+ self.detection_start,
+ self.model_shape,
+ self.tf_device,
+ self.num_threads,
+ ),
+ )
self.detect_process.daemon = True
self.detect_process.start()
-class RemoteObjectDetector():
+
+class RemoteObjectDetector:
def __init__(self, name, labels, detection_queue, event, model_shape):
- self.labels = load_labels(labels)
+ self.labels = labels
self.name = name
self.fps = EventsPerSecond()
self.detection_queue = detection_queue
self.event = event
self.shm = mp.shared_memory.SharedMemory(name=self.name, create=False)
- self.np_shm = np.ndarray((1,model_shape[0],model_shape[1],3), dtype=np.uint8, buffer=self.shm.buf)
- self.out_shm = mp.shared_memory.SharedMemory(name=f"out-{self.name}", create=False)
- self.out_np_shm = np.ndarray((20,6), dtype=np.float32, buffer=self.out_shm.buf)
-
- def detect(self, tensor_input, threshold=.4):
+ self.np_shm = np.ndarray(
+ (1, model_shape[0], model_shape[1], 3), dtype=np.uint8, buffer=self.shm.buf
+ )
+ self.out_shm = mp.shared_memory.SharedMemory(
+ name=f"out-{self.name}", create=False
+ )
+ self.out_np_shm = np.ndarray((20, 6), dtype=np.float32, buffer=self.out_shm.buf)
+
+ def detect(self, tensor_input, threshold=0.4):
detections = []
# copy input to shared memory
@@ -213,14 +267,12 @@ class RemoteObjectDetector():
for d in self.out_np_shm:
if d[1] < threshold:
break
- detections.append((
- self.labels[int(d[0])],
- float(d[1]),
- (d[2], d[3], d[4], d[5])
- ))
+ detections.append(
+ (self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5]))
+ )
self.fps.update()
return detections
-
+
def cleanup(self):
self.shm.unlink()
self.out_shm.unlink()
diff --git a/frigate/events.py b/frigate/events.py
index 2430d9db3..3293d19bb 100644
--- a/frigate/events.py
+++ b/frigate/events.py
@@ -1,29 +1,26 @@
import datetime
-import json
import logging
import os
import queue
-import subprocess as sp
import threading
import time
-from collections import defaultdict
from pathlib import Path
-import psutil
-import shutil
-
-from frigate.config import FrigateConfig
-from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
-from frigate.models import Event
+from frigate.config import FrigateConfig, RecordConfig
+from frigate.const import CLIPS_DIR
+from frigate.models import Event, Recordings
from peewee import fn
logger = logging.getLogger(__name__)
+
class EventProcessor(threading.Thread):
- def __init__(self, config, camera_processes, event_queue, event_processed_queue, stop_event):
+ def __init__(
+ self, config, camera_processes, event_queue, event_processed_queue, stop_event
+ ):
threading.Thread.__init__(self)
- self.name = 'event_processor'
+ self.name = "event_processor"
self.config = config
self.camera_processes = camera_processes
self.cached_clips = {}
@@ -33,291 +30,185 @@ class EventProcessor(threading.Thread):
self.stop_event = stop_event
def should_create_clip(self, camera, event_data):
- if event_data['false_positive']:
- return False
-
- # if there are required zones and there is no overlap
- required_zones = self.config.cameras[camera].clips.required_zones
- if len(required_zones) > 0 and not set(event_data['entered_zones']) & set(required_zones):
- logger.debug(f"Not creating clip for {event_data['id']} because it did not enter required zones")
+ if event_data["false_positive"]:
return False
- return True
-
- def refresh_cache(self):
- cached_files = os.listdir(CACHE_DIR)
+ record_config: RecordConfig = self.config.cameras[camera].record
- files_in_use = []
- for process in psutil.process_iter():
- try:
- if process.name() != 'ffmpeg':
- continue
-
- flist = process.open_files()
- if flist:
- for nt in flist:
- if nt.path.startswith(CACHE_DIR):
- files_in_use.append(nt.path.split('/')[-1])
- except:
- continue
-
- for f in cached_files:
- if f in files_in_use or f in self.cached_clips:
- continue
-
- camera = '-'.join(f.split('-')[:-1])
- start_time = datetime.datetime.strptime(f.split('-')[-1].split('.')[0], '%Y%m%d%H%M%S')
-
- ffprobe_cmd = " ".join([
- 'ffprobe',
- '-v',
- 'error',
- '-show_entries',
- 'format=duration',
- '-of',
- 'default=noprint_wrappers=1:nokey=1',
- f"{os.path.join(CACHE_DIR,f)}"
- ])
- p = sp.Popen(ffprobe_cmd, stdout=sp.PIPE, shell=True)
- (output, err) = p.communicate()
- p_status = p.wait()
- if p_status == 0:
- duration = float(output.decode('utf-8').strip())
- else:
- logger.info(f"bad file: {f}")
- os.remove(os.path.join(CACHE_DIR,f))
- continue
-
- self.cached_clips[f] = {
- 'path': f,
- 'camera': camera,
- 'start_time': start_time.timestamp(),
- 'duration': duration
- }
-
- if len(self.events_in_process) > 0:
- earliest_event = min(self.events_in_process.values(), key=lambda x:x['start_time'])['start_time']
- else:
- earliest_event = datetime.datetime.now().timestamp()
-
- # if the earliest event exceeds the max seconds, cap it
- max_seconds = self.config.clips.max_seconds
- if datetime.datetime.now().timestamp()-earliest_event > max_seconds:
- earliest_event = datetime.datetime.now().timestamp()-max_seconds
-
- for f, data in list(self.cached_clips.items()):
- if earliest_event-90 > data['start_time']+data['duration']:
- del self.cached_clips[f]
- logger.debug(f"Cleaning up cached file {f}")
- os.remove(os.path.join(CACHE_DIR,f))
-
- # if we are still using more than 90% of the cache, proactively cleanup
- cache_usage = shutil.disk_usage("/tmp/cache")
- if cache_usage.used/cache_usage.total > .9 and cache_usage.free < 200000000 and len(self.cached_clips) > 0:
- logger.warning("More than 90% of the cache is used.")
- logger.warning("Consider increasing space available at /tmp/cache or reducing max_seconds in your clips config.")
- logger.warning("Proactively cleaning up the cache...")
- while cache_usage.used/cache_usage.total > .9:
- oldest_clip = min(self.cached_clips.values(), key=lambda x:x['start_time'])
- del self.cached_clips[oldest_clip['path']]
- os.remove(os.path.join(CACHE_DIR,oldest_clip['path']))
- cache_usage = shutil.disk_usage("/tmp/cache")
-
- def create_clip(self, camera, event_data, pre_capture, post_capture):
- # get all clips from the camera with the event sorted
- sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
-
- # if there are no clips in the cache or we are still waiting on a needed file check every 5 seconds
- wait_count = 0
- while len(sorted_clips) == 0 or sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']+post_capture:
- if wait_count > 4:
- logger.warning(f"Unable to create clip for {camera} and event {event_data['id']}. There were no cache files for this event.")
- return False
- logger.debug(f"No cache clips for {camera}. Waiting...")
- time.sleep(5)
- self.refresh_cache()
- # get all clips from the camera with the event sorted
- sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
- wait_count += 1
-
- playlist_start = event_data['start_time']-pre_capture
- playlist_end = event_data['end_time']+post_capture
- playlist_lines = []
- for clip in sorted_clips:
- # clip ends before playlist start time, skip
- if clip['start_time']+clip['duration'] < playlist_start:
- continue
- # clip starts after playlist ends, finish
- if clip['start_time'] > playlist_end:
- break
- playlist_lines.append(f"file '{os.path.join(CACHE_DIR,clip['path'])}'")
- # if this is the starting clip, add an inpoint
- if clip['start_time'] < playlist_start:
- playlist_lines.append(f"inpoint {int(playlist_start-clip['start_time'])}")
- # if this is the ending clip, add an outpoint
- if clip['start_time']+clip['duration'] > playlist_end:
- playlist_lines.append(f"outpoint {int(playlist_end-clip['start_time'])}")
-
- clip_name = f"{camera}-{event_data['id']}"
- ffmpeg_cmd = [
- 'ffmpeg',
- '-y',
- '-protocol_whitelist',
- 'pipe,file',
- '-f',
- 'concat',
- '-safe',
- '0',
- '-i',
- '-',
- '-c',
- 'copy',
- '-movflags',
- '+faststart',
- f"{os.path.join(CLIPS_DIR, clip_name)}.mp4"
- ]
-
- p = sp.run(ffmpeg_cmd, input="\n".join(playlist_lines), encoding='ascii', capture_output=True)
- if p.returncode != 0:
- logger.error(p.stderr)
+ # Recording clips is disabled
+ if not record_config.enabled or (
+ record_config.retain_days == 0 and not record_config.events.enabled
+ ):
return False
+
+ # If there are required zones and there is no overlap
+ required_zones = record_config.events.required_zones
+ if len(required_zones) > 0 and not set(event_data["entered_zones"]) & set(
+ required_zones
+ ):
+ logger.debug(
+ f"Not creating clip for {event_data['id']} because it did not enter required zones"
+ )
+ return False
+
+ # If the required objects are not present
+ if (
+ record_config.events.objects is not None
+ and event_data["label"] not in record_config.events.objects
+ ):
+ logger.debug(
+ f"Not creating clip for {event_data['id']} because it did not contain required objects"
+ )
+ return False
+
return True
def run(self):
- while True:
- if self.stop_event.is_set():
- logger.info(f"Exiting event processor...")
- break
-
+ while not self.stop_event.is_set():
try:
event_type, camera, event_data = self.event_queue.get(timeout=10)
except queue.Empty:
- if not self.stop_event.is_set():
- self.refresh_cache()
continue
logger.debug(f"Event received: {event_type} {camera} {event_data['id']}")
- self.refresh_cache()
- if event_type == 'start':
- self.events_in_process[event_data['id']] = event_data
+ if event_type == "start":
+ self.events_in_process[event_data["id"]] = event_data
- if event_type == 'end':
- clips_config = self.config.cameras[camera].clips
+ if event_type == "end":
+ record_config: RecordConfig = self.config.cameras[camera].record
- clip_created = False
- if self.should_create_clip(camera, event_data):
- if clips_config.enabled and (clips_config.objects is None or event_data['label'] in clips_config.objects):
- clip_created = self.create_clip(camera, event_data, clips_config.pre_capture, clips_config.post_capture)
-
- if clip_created or event_data['has_snapshot']:
+ has_clip = self.should_create_clip(camera, event_data)
+
+ if has_clip or event_data["has_snapshot"]:
Event.create(
- id=event_data['id'],
- label=event_data['label'],
+ id=event_data["id"],
+ label=event_data["label"],
camera=camera,
- start_time=event_data['start_time'],
- end_time=event_data['end_time'],
- top_score=event_data['top_score'],
- false_positive=event_data['false_positive'],
- zones=list(event_data['entered_zones']),
- thumbnail=event_data['thumbnail'],
- has_clip=clip_created,
- has_snapshot=event_data['has_snapshot'],
+ start_time=event_data["start_time"],
+ end_time=event_data["end_time"],
+ top_score=event_data["top_score"],
+ false_positive=event_data["false_positive"],
+ zones=list(event_data["entered_zones"]),
+ thumbnail=event_data["thumbnail"],
+ has_clip=has_clip,
+ has_snapshot=event_data["has_snapshot"],
)
- del self.events_in_process[event_data['id']]
- self.event_processed_queue.put((event_data['id'], camera))
+
+ del self.events_in_process[event_data["id"]]
+ self.event_processed_queue.put((event_data["id"], camera, has_clip))
+
+ logger.info(f"Exiting event processor...")
+
class EventCleanup(threading.Thread):
def __init__(self, config: FrigateConfig, stop_event):
threading.Thread.__init__(self)
- self.name = 'event_cleanup'
+ self.name = "event_cleanup"
self.config = config
self.stop_event = stop_event
self.camera_keys = list(self.config.cameras.keys())
- def expire(self, media):
+ def expire(self, media_type):
## Expire events from unlisted cameras based on the global config
- if media == 'clips':
- retain_config = self.config.clips.retain
- file_extension = 'mp4'
- update_params = {'has_clip': False}
+ if media_type == "clips":
+ retain_config = self.config.record.events.retain
+ file_extension = "mp4"
+ update_params = {"has_clip": False}
else:
retain_config = self.config.snapshots.retain
- file_extension = 'jpg'
- update_params = {'has_snapshot': False}
-
- distinct_labels = (Event.select(Event.label)
- .where(Event.camera.not_in(self.camera_keys))
- .distinct())
-
+ file_extension = "jpg"
+ update_params = {"has_snapshot": False}
+
+ distinct_labels = (
+ Event.select(Event.label)
+ .where(Event.camera.not_in(self.camera_keys))
+ .distinct()
+ )
+
# loop over object types in db
for l in distinct_labels:
# get expiration time for this label
expire_days = retain_config.objects.get(l.label, retain_config.default)
- expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
+ expire_after = (
+ datetime.datetime.now() - datetime.timedelta(days=expire_days)
+ ).timestamp()
# grab all events after specific time
- expired_events = (
- Event.select()
- .where(Event.camera.not_in(self.camera_keys),
- Event.start_time < expire_after,
- Event.label == l.label)
+ expired_events = Event.select().where(
+ Event.camera.not_in(self.camera_keys),
+ Event.start_time < expire_after,
+ Event.label == l.label,
)
# delete the media from disk
for event in expired_events:
media_name = f"{event.camera}-{event.id}"
- media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}")
- media.unlink(missing_ok=True)
+ media_path = Path(
+ f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
+ )
+ media_path.unlink(missing_ok=True)
+ if file_extension == "jpg":
+ media_path = Path(
+ f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
+ )
+ media_path.unlink(missing_ok=True)
+
# update the clips attribute for the db entry
- update_query = (
- Event.update(update_params)
- .where(Event.camera.not_in(self.camera_keys),
- Event.start_time < expire_after,
- Event.label == l.label)
+ update_query = Event.update(update_params).where(
+ Event.camera.not_in(self.camera_keys),
+ Event.start_time < expire_after,
+ Event.label == l.label,
)
update_query.execute()
## Expire events from cameras based on the camera config
for name, camera in self.config.cameras.items():
- if media == 'clips':
- retain_config = camera.clips.retain
+ if media_type == "clips":
+ retain_config = camera.record.events.retain
else:
retain_config = camera.snapshots.retain
# get distinct objects in database for this camera
- distinct_labels = (Event.select(Event.label)
- .where(Event.camera == name)
- .distinct())
+ distinct_labels = (
+ Event.select(Event.label).where(Event.camera == name).distinct()
+ )
# loop over object types in db
for l in distinct_labels:
# get expiration time for this label
expire_days = retain_config.objects.get(l.label, retain_config.default)
- expire_after = (datetime.datetime.now() - datetime.timedelta(days=expire_days)).timestamp()
+ expire_after = (
+ datetime.datetime.now() - datetime.timedelta(days=expire_days)
+ ).timestamp()
# grab all events after specific time
- expired_events = (
- Event.select()
- .where(Event.camera == name,
- Event.start_time < expire_after,
- Event.label == l.label)
+ expired_events = Event.select().where(
+ Event.camera == name,
+ Event.start_time < expire_after,
+ Event.label == l.label,
)
# delete the grabbed clips from disk
for event in expired_events:
media_name = f"{event.camera}-{event.id}"
- media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}")
- media.unlink(missing_ok=True)
+ media_path = Path(
+ f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
+ )
+ media_path.unlink(missing_ok=True)
+ if file_extension == "jpg":
+ media_path = Path(
+ f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
+ )
+ media_path.unlink(missing_ok=True)
# update the clips attribute for the db entry
- update_query = (
- Event.update(update_params)
- .where( Event.camera == name,
- Event.start_time < expire_after,
- Event.label == l.label)
+ update_query = Event.update(update_params).where(
+ Event.camera == name,
+ Event.start_time < expire_after,
+ Event.label == l.label,
)
update_query.execute()
def purge_duplicates(self):
duplicate_query = """with grouped_events as (
select id,
- label,
- camera,
+ label,
+ camera,
has_snapshot,
has_clip,
row_number() over (
@@ -327,7 +218,7 @@ class EventCleanup(threading.Thread):
from event
)
- select distinct id, camera, has_snapshot, has_clip from grouped_events
+ select distinct id, camera, has_snapshot, has_clip from grouped_events
where copy_number > 1;"""
duplicate_events = Event.raw(duplicate_query)
@@ -335,38 +226,29 @@ class EventCleanup(threading.Thread):
logger.debug(f"Removing duplicate: {event.id}")
media_name = f"{event.camera}-{event.id}"
if event.has_snapshot:
- media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
- media.unlink(missing_ok=True)
+ media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
+ media_path.unlink(missing_ok=True)
if event.has_clip:
- media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
- media.unlink(missing_ok=True)
+ media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
+ media_path.unlink(missing_ok=True)
+
+ (
+ Event.delete()
+ .where(Event.id << [event.id for event in duplicate_events])
+ .execute()
+ )
- (Event.delete()
- .where( Event.id << [event.id for event in duplicate_events] )
- .execute())
-
def run(self):
- counter = 0
- while(True):
- if self.stop_event.is_set():
- logger.info(f"Exiting event cleanup...")
- break
-
- # only expire events every 5 minutes, but check for stop events every 10 seconds
- time.sleep(10)
- counter = counter + 1
- if counter < 30:
- continue
- counter = 0
-
- self.expire('clips')
- self.expire('snapshots')
+ # only expire events every 5 minutes
+ while not self.stop_event.wait(300):
+ self.expire("clips")
+ self.expire("snapshots")
self.purge_duplicates()
# drop events from db where has_clip and has_snapshot are false
- delete_query = (
- Event.delete()
- .where( Event.has_clip == False,
- Event.has_snapshot == False)
+ delete_query = Event.delete().where(
+ Event.has_clip == False, Event.has_snapshot == False
)
delete_query.execute()
+
+ logger.info(f"Exiting event cleanup...")
diff --git a/frigate/http.py b/frigate/http.py
index 4d818d37c..cef448beb 100644
--- a/frigate/http.py
+++ b/frigate/http.py
@@ -1,92 +1,56 @@
import base64
-import datetime
+from collections import OrderedDict
+from datetime import datetime, timedelta
import json
+import glob
import logging
import os
+import re
+import subprocess as sp
import time
from functools import reduce
+from pathlib import Path
import cv2
-import gevent
+from flask.helpers import send_file
+
import numpy as np
-from flask import (Blueprint, Flask, Response, current_app, jsonify,
- make_response, request)
-from flask_sockets import Sockets
-from peewee import SqliteDatabase, operator, fn, DoesNotExist
+from flask import (
+ Blueprint,
+ Flask,
+ Response,
+ current_app,
+ jsonify,
+ make_response,
+ request,
+)
+
+from peewee import SqliteDatabase, operator, fn, DoesNotExist, Value
from playhouse.shortcuts import model_to_dict
-from frigate.const import CLIPS_DIR
-from frigate.models import Event
+from frigate.const import CLIPS_DIR, RECORD_DIR
+from frigate.models import Event, Recordings
from frigate.stats import stats_snapshot
from frigate.util import calculate_region
from frigate.version import VERSION
logger = logging.getLogger(__name__)
-bp = Blueprint('frigate', __name__)
-ws = Blueprint('ws', __name__)
+bp = Blueprint("frigate", __name__)
-class MqttBackend():
- """Interface for registering and updating WebSocket clients."""
- def __init__(self, mqtt_client, topic_prefix):
- self.clients = list()
- self.mqtt_client = mqtt_client
- self.topic_prefix = topic_prefix
-
- def register(self, client):
- """Register a WebSocket connection for Mqtt updates."""
- self.clients.append(client)
-
- def publish(self, message):
- try:
- json_message = json.loads(message)
- json_message = {
- 'topic': f"{self.topic_prefix}/{json_message['topic']}",
- 'payload': json_message['payload'],
- 'retain': json_message.get('retain', False)
- }
- except:
- logger.warning("Unable to parse websocket message as valid json.")
- return
-
- logger.debug(f"Publishing mqtt message from websockets at {json_message['topic']}.")
- self.mqtt_client.publish(json_message['topic'], json_message['payload'], retain=json_message['retain'])
-
- def run(self):
- def send(client, userdata, message):
- """Sends mqtt messages to clients."""
- try:
- logger.debug(f"Received mqtt message on {message.topic}.")
- ws_message = json.dumps({
- 'topic': message.topic.replace(f"{self.topic_prefix}/",""),
- 'payload': message.payload.decode()
- })
- except:
- # if the payload can't be decoded don't relay to clients
- logger.debug(f"MQTT payload for {message.topic} wasn't text. Skipping...")
- return
-
- for client in self.clients:
- try:
- client.send(ws_message)
- except:
- logger.debug("Removing websocket client due to a closed connection.")
- self.clients.remove(client)
-
- self.mqtt_client.message_callback_add(f"{self.topic_prefix}/#", send)
-
- def start(self):
- """Maintains mqtt subscription in the background."""
- gevent.spawn(self.run)
-
-def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detected_frames_processor, mqtt_client):
+def create_app(
+ frigate_config,
+ database: SqliteDatabase,
+ stats_tracking,
+ detected_frames_processor,
+):
app = Flask(__name__)
- sockets = Sockets(app)
@app.before_request
def _db_connect():
- database.connect()
+ if database.is_closed():
+ database.connect()
@app.teardown_request
def _db_close(exc):
@@ -98,21 +62,19 @@ def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detecte
app.detected_frames_processor = detected_frames_processor
app.register_blueprint(bp)
- sockets.register_blueprint(ws)
-
- app.mqtt_backend = MqttBackend(mqtt_client, frigate_config.mqtt.topic_prefix)
- app.mqtt_backend.start()
return app
-@bp.route('/')
+
+@bp.route("/")
def is_healthy():
return "Frigate is running. Alive and healthy!"
-@bp.route('/events/summary')
+
+@bp.route("/events/summary")
def events_summary():
- has_clip = request.args.get('has_clip', type=int)
- has_snapshot = request.args.get('has_snapshot', type=int)
+ has_clip = request.args.get("has_clip", type=int)
+ has_snapshot = request.args.get("has_snapshot", type=int)
clauses = []
@@ -123,38 +85,66 @@ def events_summary():
clauses.append((Event.has_snapshot == has_snapshot))
if len(clauses) == 0:
- clauses.append((1 == 1))
+ clauses.append((True))
groups = (
- Event
- .select(
- Event.camera,
- Event.label,
- fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')).alias('day'),
- Event.zones,
- fn.COUNT(Event.id).alias('count')
- )
- .where(reduce(operator.and_, clauses))
- .group_by(
- Event.camera,
- Event.label,
- fn.strftime('%Y-%m-%d', fn.datetime(Event.start_time, 'unixepoch', 'localtime')),
- Event.zones
- )
+ Event.select(
+ Event.camera,
+ Event.label,
+ fn.strftime(
+ "%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", "localtime")
+ ).alias("day"),
+ Event.zones,
+ fn.COUNT(Event.id).alias("count"),
)
+ .where(reduce(operator.and_, clauses))
+ .group_by(
+ Event.camera,
+ Event.label,
+ fn.strftime(
+ "%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", "localtime")
+ ),
+ Event.zones,
+ )
+ )
return jsonify([e for e in groups.dicts()])
-@bp.route('/events/')
+
+@bp.route("/events/", methods=("GET",))
def event(id):
try:
return model_to_dict(Event.get(Event.id == id))
except DoesNotExist:
return "Event not found", 404
-@bp.route('/events//thumbnail.jpg')
+
+@bp.route("/events/", methods=("DELETE",))
+def delete_event(id):
+ try:
+ event = Event.get(Event.id == id)
+ except DoesNotExist:
+ return make_response(
+ jsonify({"success": False, "message": "Event" + id + " not found"}), 404
+ )
+
+ media_name = f"{event.camera}-{event.id}"
+ if event.has_snapshot:
+ media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
+ media.unlink(missing_ok=True)
+ if event.has_clip:
+ media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
+ media.unlink(missing_ok=True)
+
+ event.delete_instance()
+ return make_response(
+ jsonify({"success": True, "message": "Event" + id + " deleted"}), 200
+ )
+
+
+@bp.route("/events//thumbnail.jpg")
def event_thumbnail(id):
- format = request.args.get('format', 'ios')
+ format = request.args.get("format", "ios")
thumbnail_bytes = None
try:
event = Event.get(Event.id == id)
@@ -162,7 +152,8 @@ def event_thumbnail(id):
except DoesNotExist:
# see if the object is currently being tracked
try:
- for camera_state in current_app.detected_frames_processor.camera_states.values():
+ camera_states = current_app.detected_frames_processor.camera_states.values()
+ for camera_state in camera_states:
if id in camera_state.tracked_objects:
tracked_obj = camera_state.tracked_objects.get(id)
if not tracked_obj is None:
@@ -174,60 +165,117 @@ def event_thumbnail(id):
return "Event not found", 404
# android notifications prefer a 2:1 ratio
- if format == 'android':
+ if format == "android":
jpg_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
img = cv2.imdecode(jpg_as_np, flags=1)
- thumbnail = cv2.copyMakeBorder(img, 0, 0, int(img.shape[1]*0.5), int(img.shape[1]*0.5), cv2.BORDER_CONSTANT, (0,0,0))
- ret, jpg = cv2.imencode('.jpg', thumbnail, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
+ thumbnail = cv2.copyMakeBorder(
+ img,
+ 0,
+ 0,
+ int(img.shape[1] * 0.5),
+ int(img.shape[1] * 0.5),
+ cv2.BORDER_CONSTANT,
+ (0, 0, 0),
+ )
+ ret, jpg = cv2.imencode(".jpg", thumbnail, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
thumbnail_bytes = jpg.tobytes()
response = make_response(thumbnail_bytes)
- response.headers['Content-Type'] = 'image/jpg'
+ response.headers["Content-Type"] = "image/jpg"
return response
-@bp.route('/events//snapshot.jpg')
+
+@bp.route("/events//snapshot.jpg")
def event_snapshot(id):
+ download = request.args.get("download", type=bool)
jpg_bytes = None
try:
event = Event.get(Event.id == id)
if not event.has_snapshot:
return "Snapshot not available", 404
# read snapshot from disk
- with open(os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), 'rb') as image_file:
+ with open(
+ os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), "rb"
+ ) as image_file:
jpg_bytes = image_file.read()
except DoesNotExist:
# see if the object is currently being tracked
try:
- for camera_state in current_app.detected_frames_processor.camera_states.values():
+ camera_states = current_app.detected_frames_processor.camera_states.values()
+ for camera_state in camera_states:
if id in camera_state.tracked_objects:
tracked_obj = camera_state.tracked_objects.get(id)
if not tracked_obj is None:
jpg_bytes = tracked_obj.get_jpg_bytes(
- timestamp=request.args.get('timestamp', type=int),
- bounding_box=request.args.get('bbox', type=int),
- crop=request.args.get('crop', type=int),
- height=request.args.get('h', type=int)
+ timestamp=request.args.get("timestamp", type=int),
+ bounding_box=request.args.get("bbox", type=int),
+ crop=request.args.get("crop", type=int),
+ height=request.args.get("h", type=int),
+ quality=request.args.get("quality", default=70, type=int),
)
except:
return "Event not found", 404
except:
return "Event not found", 404
+ if jpg_bytes is None:
+ return "Event not found", 404
+
response = make_response(jpg_bytes)
- response.headers['Content-Type'] = 'image/jpg'
+ response.headers["Content-Type"] = "image/jpg"
+ if download:
+ response.headers[
+ "Content-Disposition"
+ ] = f"attachment; filename=snapshot-{id}.jpg"
return response
-@bp.route('/events')
+
+@bp.route("/events//clip.mp4")
+def event_clip(id):
+ download = request.args.get("download", type=bool)
+
+ try:
+ event: Event = Event.get(Event.id == id)
+ except DoesNotExist:
+ return "Event not found.", 404
+
+ if not event.has_clip:
+ return "Clip not available", 404
+
+ event_config = current_app.frigate_config.cameras[event.camera].record.events
+ start_ts = event.start_time - event_config.pre_capture
+ end_ts = event.end_time + event_config.post_capture
+ file_name = f"{event.camera}-{id}.mp4"
+ clip_path = os.path.join(CLIPS_DIR, file_name)
+
+ if not os.path.isfile(clip_path):
+ return recording_clip(event.camera, start_ts, end_ts)
+
+ response = make_response()
+ response.headers["Content-Description"] = "File Transfer"
+ response.headers["Cache-Control"] = "no-cache"
+ response.headers["Content-Type"] = "video/mp4"
+ if download:
+ response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
+ response.headers["Content-Length"] = os.path.getsize(clip_path)
+ response.headers[
+ "X-Accel-Redirect"
+ ] = f"/clips/{file_name}" # nginx: http://wiki.nginx.org/NginxXSendfile
+
+ return response
+
+
+@bp.route("/events")
def events():
- limit = request.args.get('limit', 100)
- camera = request.args.get('camera')
- label = request.args.get('label')
- zone = request.args.get('zone')
- after = request.args.get('after', type=float)
- before = request.args.get('before', type=float)
- has_clip = request.args.get('has_clip', type=int)
- has_snapshot = request.args.get('has_snapshot', type=int)
- include_thumbnails = request.args.get('include_thumbnails', default=1, type=int)
+ limit = request.args.get("limit", 100)
+ camera = request.args.get("camera")
+ label = request.args.get("label")
+ zone = request.args.get("zone")
+ after = request.args.get("after", type=float)
+ before = request.args.get("before", type=float)
+ has_clip = request.args.get("has_clip", type=int)
+ has_snapshot = request.args.get("has_snapshot", type=int)
+ include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
clauses = []
excluded_fields = []
@@ -239,7 +287,7 @@ def events():
clauses.append((Event.label == label))
if zone:
- clauses.append((Event.zones.cast('text') % f"*\"{zone}\"*"))
+ clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
if after:
clauses.append((Event.start_time >= after))
@@ -257,125 +305,430 @@ def events():
excluded_fields.append(Event.thumbnail)
if len(clauses) == 0:
- clauses.append((1 == 1))
+ clauses.append((True))
- events = (Event.select()
- .where(reduce(operator.and_, clauses))
- .order_by(Event.start_time.desc())
- .limit(limit))
+ events = (
+ Event.select()
+ .where(reduce(operator.and_, clauses))
+ .order_by(Event.start_time.desc())
+ .limit(limit)
+ )
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
-@bp.route('/config')
-def config():
- return jsonify(current_app.frigate_config.to_dict())
-@bp.route('/version')
+@bp.route("/config")
+def config():
+ config = current_app.frigate_config.dict()
+
+ # add in the ffmpeg_cmds
+ for camera_name, camera in current_app.frigate_config.cameras.items():
+ camera_dict = config["cameras"][camera_name]
+ camera_dict["ffmpeg_cmds"] = camera.ffmpeg_cmds
+ for cmd in camera_dict["ffmpeg_cmds"]:
+ cmd["cmd"] = " ".join(cmd["cmd"])
+
+ return jsonify(config)
+
+
+@bp.route("/config/schema")
+def config_schema():
+ return current_app.response_class(
+ current_app.frigate_config.schema_json(), mimetype="application/json"
+ )
+
+
+@bp.route("/version")
def version():
return VERSION
-@bp.route('/stats')
+
+@bp.route("/stats")
def stats():
stats = stats_snapshot(current_app.stats_tracking)
return jsonify(stats)
-@bp.route('//
) : null;
+ let player;
+ if (viewMode === 'live') {
+ player = (
+
+
+
+
+
+ );
+ }
+ else if (viewMode === 'debug') {
+ player = (
+
+