Merge branch 'release-0.11.0' of https://github.com/blakeblackshear/frigate into reorder-hide-cameras

This commit is contained in:
Nick Mowen 2022-04-15 06:16:31 -06:00
commit 56e4b0ee4b
15 changed files with 173 additions and 60 deletions

View File

@ -56,5 +56,7 @@ jobs:
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Build - name: Build
run: make run: make
- name: Run mypy
run: docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
- name: Run tests - name: Run tests
run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest

View File

@ -14,16 +14,8 @@ frigate: version
frigate_push: version frigate_push: version
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:0.11.0-$(COMMIT_HASH) --file docker/Dockerfile . docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:0.11.0-$(COMMIT_HASH) --file docker/Dockerfile .
run_tests: run_tests: frigate
# PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7 docker run --rm --entrypoint=python3 frigate:latest -u -m unittest
# ARCH: aarch64 amd64 or armv7 docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
@cat docker/Dockerfile.base docker/Dockerfile.$(ARCH) > docker/Dockerfile.test
@sed -i "s/FROM frigate-web as web/#/g" docker/Dockerfile.test
@sed -i "s/COPY --from=web \/opt\/frigate\/build web\//#/g" docker/Dockerfile.test
@sed -i "s/FROM frigate-base/#/g" docker/Dockerfile.test
@echo "" >> docker/Dockerfile.test
@echo "RUN python3 -m unittest" >> docker/Dockerfile.test
@docker buildx build --platform=$(PLATFORM) --tag frigate-base --build-arg NGINX_VERSION=1.0.2 --build-arg FFMPEG_VERSION=1.0.0 --build-arg ARCH=$(ARCH) --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.test .
@rm docker/Dockerfile.test
.PHONY: run_tests .PHONY: run_tests

View File

@ -12,9 +12,9 @@ services:
build: build:
context: . context: .
dockerfile: docker/Dockerfile.dev dockerfile: docker/Dockerfile.dev
devices: #devices:
- /dev/bus/usb:/dev/bus/usb # - /dev/bus/usb:/dev/bus/usb
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware # - /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- .:/lab/frigate:cached - .:/lab/frigate:cached

View File

@ -42,7 +42,7 @@ COPY requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt RUN pip3 install -r requirements.txt
COPY requirements-wheels.txt /requirements-wheels.txt COPY requirements-wheels.txt /requirements-wheels.txt
RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
# Frigate Container # Frigate Container
FROM debian:11-slim FROM debian:11-slim

View File

@ -110,6 +110,7 @@ environment_vars:
EXAMPLE_VAR: value EXAMPLE_VAR: value
# Optional: birdseye configuration # Optional: birdseye configuration
# NOTE: Can (enabled, mode) be overridden at the camera level
birdseye: birdseye:
# Optional: Enable birdseye view (default: shown below) # Optional: Enable birdseye view (default: shown below)
enabled: True enabled: True

View File

@ -324,6 +324,13 @@ class BirdseyeConfig(FrigateBaseModel):
) )
class BirdseyeCameraConfig(FrigateBaseModel):
enabled: bool = Field(default=True, title="Enable birdseye view for camera.")
mode: BirdseyeModeEnum = Field(
default=BirdseyeModeEnum.objects, title="Tracking mode for camera."
)
FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"] FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
FFMPEG_INPUT_ARGS_DEFAULT = [ FFMPEG_INPUT_ARGS_DEFAULT = [
"-avoid_negative_ts", "-avoid_negative_ts",
@ -547,6 +554,9 @@ class CameraConfig(FrigateBaseModel):
ui: CameraUiConfig = Field( ui: CameraUiConfig = Field(
default_factory=CameraUiConfig, title="Camera UI Modifications." default_factory=CameraUiConfig, title="Camera UI Modifications."
) )
birdseye: BirdseyeCameraConfig = Field(
default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration."
)
timestamp_style: TimestampStyleConfig = Field( timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, title="Timestamp style configuration." default_factory=TimestampStyleConfig, title="Timestamp style configuration."
) )
@ -783,6 +793,7 @@ class FrigateConfig(FrigateBaseModel):
# Global config to propegate down to camera level # Global config to propegate down to camera level
global_config = config.dict( global_config = config.dict(
include={ include={
"birdseye": ...,
"record": ..., "record": ...,
"snapshots": ..., "snapshots": ...,
"live": ..., "live": ...,

View File

@ -4,13 +4,14 @@ import threading
import os import os
import signal import signal
import queue import queue
import multiprocessing as mp from multiprocessing.queues import Queue
from logging import handlers from logging import handlers
from setproctitle import setproctitle from setproctitle import setproctitle
from typing import Deque
from collections import deque from collections import deque
def listener_configurer(): def listener_configurer() -> None:
root = logging.getLogger() root = logging.getLogger()
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
formatter = logging.Formatter( formatter = logging.Formatter(
@ -21,14 +22,14 @@ def listener_configurer():
root.setLevel(logging.INFO) root.setLevel(logging.INFO)
def root_configurer(queue): def root_configurer(queue: Queue) -> None:
h = handlers.QueueHandler(queue) h = handlers.QueueHandler(queue)
root = logging.getLogger() root = logging.getLogger()
root.addHandler(h) root.addHandler(h)
root.setLevel(logging.INFO) root.setLevel(logging.INFO)
def log_process(log_queue): def log_process(log_queue: Queue) -> None:
threading.current_thread().name = f"logger" threading.current_thread().name = f"logger"
setproctitle("frigate.logger") setproctitle("frigate.logger")
listener_configurer() listener_configurer()
@ -43,34 +44,32 @@ def log_process(log_queue):
# based on https://codereview.stackexchange.com/a/17959 # based on https://codereview.stackexchange.com/a/17959
class LogPipe(threading.Thread): class LogPipe(threading.Thread):
def __init__(self, log_name, level): def __init__(self, log_name: str):
"""Setup the object with a logger and a loglevel """Setup the object with a logger and start the thread"""
and start the thread
"""
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.daemon = False self.daemon = False
self.logger = logging.getLogger(log_name) self.logger = logging.getLogger(log_name)
self.level = level self.level = logging.ERROR
self.deque = deque(maxlen=100) self.deque: Deque[str] = deque(maxlen=100)
self.fdRead, self.fdWrite = os.pipe() self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead) self.pipeReader = os.fdopen(self.fdRead)
self.start() self.start()
def fileno(self): def fileno(self) -> int:
"""Return the write file descriptor of the pipe""" """Return the write file descriptor of the pipe"""
return self.fdWrite return self.fdWrite
def run(self): def run(self) -> None:
"""Run the thread, logging everything.""" """Run the thread, logging everything."""
for line in iter(self.pipeReader.readline, ""): for line in iter(self.pipeReader.readline, ""):
self.deque.append(line.strip("\n")) self.deque.append(line.strip("\n"))
self.pipeReader.close() self.pipeReader.close()
def dump(self): def dump(self) -> None:
while len(self.deque) > 0: while len(self.deque) > 0:
self.logger.log(self.level, self.deque.popleft()) self.logger.log(self.level, self.deque.popleft())
def close(self): def close(self) -> None:
"""Close the write end of the pipe.""" """Close the write end of the pipe."""
os.close(self.fdWrite) os.close(self.fdWrite)

40
frigate/mypy.ini Normal file
View File

@ -0,0 +1,40 @@
[mypy]
python_version = 3.9
show_error_codes = true
follow_imports = silent
ignore_missing_imports = true
strict_equality = true
warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
warn_unused_ignores = true
enable_error_code = ignore-without-code
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
no_implicit_reexport = true
[mypy-frigate.*]
ignore_errors = true
[mypy-frigate.const]
ignore_errors = false
[mypy-frigate.log]
ignore_errors = false
[mypy-frigate.version]
ignore_errors = false
[mypy-frigate.watchdog]
ignore_errors = false
disallow_untyped_calls = false
[mypy-frigate.zeroconf]
ignore_errors = false

View File

@ -190,14 +190,14 @@ class BirdsEyeFrameManager:
channel_dims, channel_dims,
) )
def camera_active(self, object_box_count, motion_box_count): def camera_active(self, mode, object_box_count, motion_box_count):
if self.mode == BirdseyeModeEnum.continuous: if mode == BirdseyeModeEnum.continuous:
return True return True
if self.mode == BirdseyeModeEnum.motion and motion_box_count > 0: if mode == BirdseyeModeEnum.motion and motion_box_count > 0:
return True return True
if self.mode == BirdseyeModeEnum.objects and object_box_count > 0: if mode == BirdseyeModeEnum.objects and object_box_count > 0:
return True return True
def update_frame(self): def update_frame(self):
@ -311,10 +311,14 @@ class BirdsEyeFrameManager:
return True return True
def update(self, camera, object_count, motion_count, frame_time, frame) -> bool: def update(self, camera, object_count, motion_count, frame_time, frame) -> bool:
# don't process if birdseye is disabled for this camera
# update the last active frame for the camera camera_config = self.config.cameras[camera].birdseye
if not camera_config.enabled:
return False
# update the last active frame for the camera
self.cameras[camera]["current_frame"] = frame_time self.cameras[camera]["current_frame"] = frame_time
if self.camera_active(object_count, motion_count): if self.camera_active(camera_config.mode, object_count, motion_count):
self.cameras[camera]["last_active_frame"] = frame_time self.cameras[camera]["last_active_frame"] = frame_time
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()

View File

@ -2,6 +2,7 @@ import unittest
import numpy as np import numpy as np
from pydantic import ValidationError from pydantic import ValidationError
from frigate.config import ( from frigate.config import (
BirdseyeModeEnum,
FrigateConfig, FrigateConfig,
DetectorTypeEnum, DetectorTypeEnum,
) )
@ -80,6 +81,62 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert "dog" in runtime_config.cameras["back"].objects.track assert "dog" in runtime_config.cameras["back"].objects.track
def test_override_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": { "enabled": True, "mode": "continuous" },
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
"birdseye": {
"enabled": False,
"mode": "motion"
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert not runtime_config.cameras["back"].birdseye.enabled
assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.motion
def test_inherit_birdseye(self):
config = {
"mqtt": {"host": "mqtt"},
"birdseye": { "enabled": True, "mode": "continuous" },
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].birdseye.enabled
assert runtime_config.cameras["back"].birdseye.mode is BirdseyeModeEnum.continuous
def test_override_tracked_objects(self): def test_override_tracked_objects(self):
config = { config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},

View File

@ -1,4 +1,3 @@
import collections
import copy import copy
import datetime import datetime
import hashlib import hashlib
@ -11,6 +10,7 @@ import threading
import time import time
import traceback import traceback
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Mapping
from multiprocessing import shared_memory from multiprocessing import shared_memory
from typing import AnyStr from typing import AnyStr
@ -34,7 +34,7 @@ def deep_merge(dct1: dict, dct2: dict, override=False, merge_lists=False) -> dic
for k, v2 in dct2.items(): for k, v2 in dct2.items():
if k in merged: if k in merged:
v1 = merged[k] v1 = merged[k]
if isinstance(v1, dict) and isinstance(v2, collections.Mapping): if isinstance(v1, dict) and isinstance(v2, Mapping):
merged[k] = deep_merge(v1, v2, override) merged[k] = deep_merge(v1, v2, override)
elif isinstance(v1, list) and isinstance(v2, list): elif isinstance(v1, list) and isinstance(v2, list):
if merge_lists: if merge_lists:

View File

@ -203,7 +203,7 @@ class CameraWatchdog(threading.Thread):
self.config = config self.config = config
self.capture_thread = None self.capture_thread = None
self.ffmpeg_detect_process = None self.ffmpeg_detect_process = None
self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect", logging.ERROR) self.logpipe = LogPipe(f"ffmpeg.{self.camera_name}.detect")
self.ffmpeg_other_processes = [] self.ffmpeg_other_processes = []
self.camera_fps = camera_fps self.camera_fps = camera_fps
self.ffmpeg_pid = ffmpeg_pid self.ffmpeg_pid = ffmpeg_pid
@ -219,8 +219,7 @@ class CameraWatchdog(threading.Thread):
if "detect" in c["roles"]: if "detect" in c["roles"]:
continue continue
logpipe = LogPipe( logpipe = LogPipe(
f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}", f"ffmpeg.{self.camera_name}.{'_'.join(sorted(c['roles']))}"
logging.ERROR,
) )
self.ffmpeg_other_processes.append( self.ffmpeg_other_processes.append(
{ {

View File

@ -5,21 +5,22 @@ import time
import os import os
import signal import signal
from frigate.util import ( from frigate.edgetpu import EdgeTPUProcess
restart_frigate, from frigate.util import restart_frigate
) from multiprocessing.synchronize import Event
from typing import Dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FrigateWatchdog(threading.Thread): class FrigateWatchdog(threading.Thread):
def __init__(self, detectors, stop_event): def __init__(self, detectors: Dict[str, EdgeTPUProcess], stop_event: Event):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "frigate_watchdog" self.name = "frigate_watchdog"
self.detectors = detectors self.detectors = detectors
self.stop_event = stop_event self.stop_event = stop_event
def run(self): def run(self) -> None:
time.sleep(10) time.sleep(10)
while not self.stop_event.wait(10): while not self.stop_event.wait(10):
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
@ -32,7 +33,10 @@ class FrigateWatchdog(threading.Thread):
"Detection appears to be stuck. Restarting detection process..." "Detection appears to be stuck. Restarting detection process..."
) )
detector.start_or_restart() detector.start_or_restart()
elif not detector.detect_process.is_alive(): elif (
detector.detect_process is not None
and not detector.detect_process.is_alive()
):
logger.info("Detection appears to have stopped. Exiting frigate...") logger.info("Detection appears to have stopped. Exiting frigate...")
restart_frigate() restart_frigate()

View File

@ -14,38 +14,41 @@ logger = logging.getLogger(__name__)
ZEROCONF_TYPE = "_frigate._tcp.local." ZEROCONF_TYPE = "_frigate._tcp.local."
# Taken from: http://stackoverflow.com/a/11735897 # Taken from: http://stackoverflow.com/a/11735897
def get_local_ip() -> str: def get_local_ip() -> bytes:
"""Try to determine the local IP address of the machine.""" """Try to determine the local IP address of the machine."""
host_ip_str = ""
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP # Use Google Public DNS server to determine own IP
sock.connect(("8.8.8.8", 80)) sock.connect(("8.8.8.8", 80))
return sock.getsockname()[0] # type: ignore host_ip_str = sock.getsockname()[0]
except OSError: except OSError:
try: try:
return socket.gethostbyname(socket.gethostname()) host_ip_str = socket.gethostbyname(socket.gethostname())
except socket.gaierror: except socket.gaierror:
return "127.0.0.1" host_ip_str = "127.0.0.1"
finally: finally:
sock.close() sock.close()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip_str)
except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip_str)
def broadcast_zeroconf(frigate_id): return host_ip_pton
def broadcast_zeroconf(frigate_id: str) -> Zeroconf:
zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only) zeroconf = Zeroconf(interfaces=InterfaceChoice.Default, ip_version=IPVersion.V4Only)
host_ip = get_local_ip() host_ip = get_local_ip()
try:
host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
except OSError:
host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
info = ServiceInfo( info = ServiceInfo(
ZEROCONF_TYPE, ZEROCONF_TYPE,
name=f"{frigate_id}.{ZEROCONF_TYPE}", name=f"{frigate_id}.{ZEROCONF_TYPE}",
addresses=[host_ip_pton], addresses=[host_ip],
port=5000, port=5000,
) )
@ -56,4 +59,4 @@ def broadcast_zeroconf(frigate_id):
logger.error( logger.error(
"Frigate instance with identical name present in the local network" "Frigate instance with identical name present in the local network"
) )
return zeroconf return zeroconf

View File

@ -2,6 +2,7 @@ click == 8.1.*
Flask == 2.1.* Flask == 2.1.*
imutils == 0.5.* imutils == 0.5.*
matplotlib == 3.5.* matplotlib == 3.5.*
mypy == 0.942
numpy == 1.22.* numpy == 1.22.*
opencv-python-headless == 4.5.5.* opencv-python-headless == 4.5.5.*
paho-mqtt == 1.6.* paho-mqtt == 1.6.*