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
- name: Build
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
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
docker buildx build --push --platform linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:0.11.0-$(COMMIT_HASH) --file docker/Dockerfile .
run_tests:
# PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7
# ARCH: aarch64 amd64 or armv7
@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
run_tests: frigate
docker run --rm --entrypoint=python3 frigate:latest -u -m unittest
docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
.PHONY: run_tests

View File

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

View File

@ -42,7 +42,7 @@ COPY requirements.txt /requirements.txt
RUN pip3 install -r requirements.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
FROM debian:11-slim

View File

@ -110,6 +110,7 @@ environment_vars:
EXAMPLE_VAR: value
# Optional: birdseye configuration
# NOTE: Can (enabled, mode) be overridden at the camera level
birdseye:
# Optional: Enable birdseye view (default: shown below)
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_INPUT_ARGS_DEFAULT = [
"-avoid_negative_ts",
@ -547,6 +554,9 @@ class CameraConfig(FrigateBaseModel):
ui: CameraUiConfig = Field(
default_factory=CameraUiConfig, title="Camera UI Modifications."
)
birdseye: BirdseyeCameraConfig = Field(
default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration."
)
timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, title="Timestamp style configuration."
)
@ -783,6 +793,7 @@ class FrigateConfig(FrigateBaseModel):
# Global config to propegate down to camera level
global_config = config.dict(
include={
"birdseye": ...,
"record": ...,
"snapshots": ...,
"live": ...,

View File

@ -4,13 +4,14 @@ import threading
import os
import signal
import queue
import multiprocessing as mp
from multiprocessing.queues import Queue
from logging import handlers
from setproctitle import setproctitle
from typing import Deque
from collections import deque
def listener_configurer():
def listener_configurer() -> None:
root = logging.getLogger()
console_handler = logging.StreamHandler()
formatter = logging.Formatter(
@ -21,14 +22,14 @@ def listener_configurer():
root.setLevel(logging.INFO)
def root_configurer(queue):
def root_configurer(queue: Queue) -> None:
h = handlers.QueueHandler(queue)
root = logging.getLogger()
root.addHandler(h)
root.setLevel(logging.INFO)
def log_process(log_queue):
def log_process(log_queue: Queue) -> None:
threading.current_thread().name = f"logger"
setproctitle("frigate.logger")
listener_configurer()
@ -43,34 +44,32 @@ def log_process(log_queue):
# based on https://codereview.stackexchange.com/a/17959
class LogPipe(threading.Thread):
def __init__(self, log_name, level):
"""Setup the object with a logger and a loglevel
and start the thread
"""
def __init__(self, log_name: str):
"""Setup the object with a logger and start the thread"""
threading.Thread.__init__(self)
self.daemon = False
self.logger = logging.getLogger(log_name)
self.level = level
self.deque = deque(maxlen=100)
self.level = logging.ERROR
self.deque: Deque[str] = deque(maxlen=100)
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.start()
def fileno(self):
def fileno(self) -> int:
"""Return the write file descriptor of the pipe"""
return self.fdWrite
def run(self):
def run(self) -> None:
"""Run the thread, logging everything."""
for line in iter(self.pipeReader.readline, ""):
self.deque.append(line.strip("\n"))
self.pipeReader.close()
def dump(self):
def dump(self) -> None:
while len(self.deque) > 0:
self.logger.log(self.level, self.deque.popleft())
def close(self):
def close(self) -> None:
"""Close the write end of the pipe."""
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,
)
def camera_active(self, object_box_count, motion_box_count):
if self.mode == BirdseyeModeEnum.continuous:
def camera_active(self, mode, object_box_count, motion_box_count):
if mode == BirdseyeModeEnum.continuous:
return True
if self.mode == BirdseyeModeEnum.motion and motion_box_count > 0:
if mode == BirdseyeModeEnum.motion and motion_box_count > 0:
return True
if self.mode == BirdseyeModeEnum.objects and object_box_count > 0:
if mode == BirdseyeModeEnum.objects and object_box_count > 0:
return True
def update_frame(self):
@ -311,10 +311,14 @@ class BirdsEyeFrameManager:
return True
def update(self, camera, object_count, motion_count, frame_time, frame) -> bool:
# update the last active frame for the camera
# don't process if birdseye is disabled for this 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
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
now = datetime.datetime.now().timestamp()

View File

@ -2,6 +2,7 @@ import unittest
import numpy as np
from pydantic import ValidationError
from frigate.config import (
BirdseyeModeEnum,
FrigateConfig,
DetectorTypeEnum,
)
@ -80,6 +81,62 @@ class TestConfig(unittest.TestCase):
runtime_config = frigate_config.runtime_config
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):
config = {
"mqtt": {"host": "mqtt"},

View File

@ -1,4 +1,3 @@
import collections
import copy
import datetime
import hashlib
@ -11,6 +10,7 @@ import threading
import time
import traceback
from abc import ABC, abstractmethod
from collections.abc import Mapping
from multiprocessing import shared_memory
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():
if k in merged:
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)
elif isinstance(v1, list) and isinstance(v2, list):
if merge_lists:

View File

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

View File

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

View File

@ -14,38 +14,41 @@ logger = logging.getLogger(__name__)
ZEROCONF_TYPE = "_frigate._tcp.local."
# 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."""
host_ip_str = ""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(("8.8.8.8", 80))
return sock.getsockname()[0] # type: ignore
host_ip_str = sock.getsockname()[0]
except OSError:
try:
return socket.gethostbyname(socket.gethostname())
host_ip_str = socket.gethostbyname(socket.gethostname())
except socket.gaierror:
return "127.0.0.1"
host_ip_str = "127.0.0.1"
finally:
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)
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(
ZEROCONF_TYPE,
name=f"{frigate_id}.{ZEROCONF_TYPE}",
addresses=[host_ip_pton],
addresses=[host_ip],
port=5000,
)
@ -56,4 +59,4 @@ def broadcast_zeroconf(frigate_id):
logger.error(
"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.*
imutils == 0.5.*
matplotlib == 3.5.*
mypy == 0.942
numpy == 1.22.*
opencv-python-headless == 4.5.5.*
paho-mqtt == 1.6.*