Compare commits

..

27 Commits

Author SHA1 Message Date
Blake Blackshear
37325c70ba fix plus enabled for addons 2022-07-22 08:12:58 -05:00
Blake Blackshear
3c46a33992 revert false warning messages 2022-07-20 06:55:06 -05:00
deviant77
ed1897db71 Add log message when discarding recording segments in cache (#3439)
* Add log message when discarding recording segments in cache

Currently Frigate silently discards recording segments in cache if there's more than "keep_count" for a camera, which can happen on high load/busy/slow systems.
This results in recording segments being lost with no apparent cause in the logs (even when set to debug).
This PR adds a warning log entry when discarding segments, in the same way as discarding corrupted segments

* Add explanatory warning and properly format cache_path warning

* lint fixes

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2022-07-19 07:24:44 -05:00
Caros2017
dfbebb63ff Update hardware_acceleration.md
Added the solution for the problem in https://github.com/blakeblackshear/frigate/issues/3227
2022-07-19 07:06:40 -05:00
JohnMark Sill
a67a768e89 improvement: better play/pause 2022-07-19 07:04:33 -05:00
JohnMark Sill
43f05c18d6 chore: remove unused import 2022-07-19 07:04:33 -05:00
JohnMark Sill
3b076c28c2 chore: removed unused properties interface 2022-07-19 07:04:33 -05:00
JohnMark Sill
cbf12e3f90 fix: removed unused state 2022-07-19 07:04:33 -05:00
JohnMark Sill
17b745434c improvement: migrated to videojs 2022-07-19 07:04:33 -05:00
JohnMark Sill
37011c2fda improvement: use useCallback instead of setting ref auto-magically 2022-07-19 07:04:33 -05:00
JohnMark Sill
fa95a041dd fix: height of video is now constant in history viewer 2022-07-19 07:04:33 -05:00
JohnMark Sill
0879d7a2d1 fix: marker time image 2022-07-19 07:04:33 -05:00
Nick Mowen
061fb15a80 Reset motion to false on startup 2022-07-19 06:59:20 -05:00
Blake Blackshear
3246fcce22 document buildx setup 2022-07-19 06:56:23 -05:00
Blake Blackshear
f2a3797b46 add additional render group 2022-07-19 06:44:11 -05:00
Blake Blackshear
b80080ac52 don't refetch data on refocus 2022-07-07 07:05:05 -05:00
Blake Blackshear
b36b63599b update stimeout to timeout 2022-07-05 11:55:55 -05:00
Blake Blackshear
5d8c0e43c2 try ffmpeg5 again 2022-07-05 11:54:21 -05:00
Josh Hawkins
7845995dfd Adjust threshold and contour_area with mqtt 2022-07-05 08:46:10 -05:00
Blake Blackshear
afe88d6e3a switch back to upgraded numpy 2022-07-04 16:51:48 -05:00
Blake Blackshear
560ee0104d arm32 compat 2022-07-04 09:06:26 -05:00
Blake Blackshear
dc8b625d55 sync numpy version 2022-07-03 10:12:14 -05:00
Blake Blackshear
162c0147d2 use jellyfin for all arch 2022-07-03 10:12:00 -05:00
Blake Blackshear
ef54cd6fb3 add plus endpoint to docs 2022-07-01 06:57:03 -05:00
Nicolas Mowen
c2465a46a8 Http tests (#3350)
* Set up for http tests

* Setup basics for testing and first test

* Add testing consts

* Cleanup db creation

* Add one more check to test

* Get event that does not exist

* Get events working with cleaner db

* Test retain / un-retain

* Test setting and deleting sub label

* Test getting list of sub labels

* Fix bug caught in tests

* Test deleting event

* Test geting list of events

* Expand test

* Test more event filters

* Write version module so tests don't fail on version import

* Test config

* Test recordings endpoint

* Formatting

* Remove unused imports

* Test stats

* Add cleanup files in const

* Add name to match other checks
2022-06-30 07:53:46 -05:00
Blake Blackshear
24d3a9cdd5 read plus api key from addon options 2022-06-30 07:19:40 -05:00
Blake Blackshear
5e82eaed88 switch back to ffmpeg 4.4.1 2022-06-28 06:50:19 -05:00
25 changed files with 606 additions and 115 deletions

View File

@@ -54,6 +54,7 @@ jobs:
python_tests: python_tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Python Tests
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -69,6 +70,8 @@ jobs:
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Create Version Module
run: make version
- name: Build - name: Build
run: make run: make
- name: Run mypy - name: Run mypy

View File

@@ -6,6 +6,7 @@ services:
# add groups from host for render, plugdev, video # add groups from host for render, plugdev, video
group_add: group_add:
- "109" # render - "109" # render
- "110" # render
- "44" # video - "44" # video
- "46" # plugdev - "46" # plugdev
shm_size: "256mb" shm_size: "256mb"

View File

@@ -11,8 +11,8 @@ RUN apt-get -qq update \
apt-transport-https \ apt-transport-https \
gnupg \ gnupg \
wget \ wget \
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \ && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \ && echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
&& apt-get -qq update \ && apt-get -qq update \
&& apt-get -qq install -y \ && apt-get -qq install -y \
python3 \ python3 \
@@ -46,7 +46,7 @@ RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
FROM debian:11-slim FROM debian:11-slim
ARG TARGETARCH ARG TARGETARCH
ARG JELLYFIN_FFMPEG_VERSION=4.3.2-1 ARG JELLYFIN_FFMPEG_VERSION=5.0.1-7
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
# http://stackoverflow.com/questions/48162574/ddg#49462622 # http://stackoverflow.com/questions/48162574/ddg#49462622
@@ -67,8 +67,8 @@ RUN apt-get -qq update \
unzip tzdata libxml2 xz-utils \ unzip tzdata libxml2 xz-utils \
python3-pip \ python3-pip \
# add raspberry pi repo # add raspberry pi repo
&& wget -O - http://archive.raspberrypi.org/debian/raspberrypi.gpg.key | apt-key add - \ && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
&& echo "deb http://archive.raspberrypi.org/debian/ bullseye main" | tee /etc/apt/sources.list.d/raspi.list \ && echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
# add coral repo # add coral repo
&& apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \ && 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 "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
@@ -80,15 +80,24 @@ RUN apt-get -qq update \
# coral drivers # coral drivers
libedgetpu1-max python3-tflite-runtime python3-pycoral \ libedgetpu1-max python3-tflite-runtime python3-pycoral \
&& pip3 install -U /wheels/*.whl \ && pip3 install -U /wheels/*.whl \
# jellyfin-ffmpeg
&& wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg5_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y ./jellyfin.deb \
&& rm jellyfin.deb \
# arch specific packages # arch specific packages
&& if [ "${TARGETARCH}" = "amd64" ]; then \ && if [ "${TARGETARCH}" = "amd64" ]; then \
# jellyfin-ffmpeg
wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers intel-media-va-driver-non-free ./jellyfin.deb \
&& rm jellyfin.deb; else \
apt-get -qq install --no-install-recommends --no-install-suggests -y \ apt-get -qq install --no-install-recommends --no-install-suggests -y \
ffmpeg; \ mesa-va-drivers intel-media-va-driver-non-free; \
fi \
# not sure why 32bit arm requires all these
&& if [ "${TARGETARCH}" = "arm" ]; then \
apt-get -qq install --no-install-recommends --no-install-suggests -y \
libgtk-3-dev \
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
gfortran openexr libatlas-base-dev libssl-dev\
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev; \
fi \ fi \
&& rm -rf /wheels \ && rm -rf /wheels \
&& apt-get remove gnupg apt-transport-https -y \ && apt-get remove gnupg apt-transport-https -y \

View File

@@ -102,7 +102,7 @@ You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
```yaml ```yaml
ffmpeg: ffmpeg:
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
``` ```
### UDP Only Cameras ### UDP Only Cameras
@@ -111,5 +111,5 @@ If your cameras do not support TCP connections for RTSP, you can use UDP.
```yaml ```yaml
ffmpeg: ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -stimeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1
``` ```

View File

@@ -21,6 +21,7 @@ ffmpeg:
ffmpeg: ffmpeg:
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
``` ```
**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME_JELLYFIN=i965` to your docker-compose file.
### Intel-based CPUs (>=10th Generation) via Quicksync ### Intel-based CPUs (>=10th Generation) via Quicksync

View File

@@ -135,7 +135,7 @@ ffmpeg:
# NOTE: See hardware acceleration docs for your specific device # NOTE: See hardware acceleration docs for your specific device
hwaccel_args: [] hwaccel_args: []
# Optional: global input args (default: shown below) # Optional: global input args (default: shown below)
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1 input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
# Optional: global output args # Optional: global output args
output_args: output_args:
# Optional: output args for detect streams (default: shown below) # Optional: output args for detect streams (default: shown below)

View File

@@ -208,3 +208,16 @@ npm run build
``` ```
This command generates static content into the `build` directory and can be served using any static contents hosting service. This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Official builds
Setup buildx for multiarch
```
docker buildx stop builder && docker buildx rm builder # <---- if existing
docker run --privileged --rm tonistiigi/binfmt --install all
docker buildx create --name builder --driver docker-container --driver-opt network=host --use
docker buildx inspect builder --bootstrap
make build_web
make push
```

View File

@@ -183,6 +183,10 @@ Permanently deletes the event along with any clips/snapshots.
Sets retain to true for the event id. Sets retain to true for the event id.
### `POST /api/events/<id>/plus`
Submits the snapshot of the event to Frigate+ for labeling.
### `DELETE /api/events/<id>/retain` ### `DELETE /api/events/<id>/retain`
Sets retain to false for the event id (event may be deleted quickly after removing). Sets retain to false for the event id (event may be deleted quickly after removing).

View File

@@ -140,3 +140,19 @@ Topic to turn improve_contrast for a camera on and off. Expected values are `ON`
### `frigate/<camera_name>/improve_contrast/state` ### `frigate/<camera_name>/improve_contrast/state`
Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`. Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/motion_threshold/set`
Topic to adjust motion threshold for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_threshold/state`
Topic with current motion threshold for a camera. Published value is an integer.
### `frigate/<camera_name>/motion_contour_area/set`
Topic to adjust motion contour area for a camera. Expected value is an integer.
### `frigate/<camera_name>/motion_contour_area/state`
Topic with current motion contour area for a camera. Published value is an integer.

View File

@@ -95,6 +95,12 @@ class FrigateApp:
"improve_contrast_enabled": mp.Value( "improve_contrast_enabled": mp.Value(
"i", self.config.cameras[camera_name].motion.improve_contrast "i", self.config.cameras[camera_name].motion.improve_contrast
), ),
"motion_threshold": mp.Value(
"i", self.config.cameras[camera_name].motion.threshold
),
"motion_contour_area": mp.Value(
"i", self.config.cameras[camera_name].motion.contour_area
),
"detection_fps": mp.Value("d", 0.0), "detection_fps": mp.Value("d", 0.0),
"detection_frame": mp.Value("d", 0.0), "detection_frame": mp.Value("d", 0.0),
"read_start": mp.Value("d", 0.0), "read_start": mp.Value("d", 0.0),

View File

@@ -346,7 +346,7 @@ FFMPEG_INPUT_ARGS_DEFAULT = [
"+genpts+discardcorrupt", "+genpts+discardcorrupt",
"-rtsp_transport", "-rtsp_transport",
"tcp", "tcp",
"-stimeout", "-timeout",
"5000000", "5000000",
"-use_wallclock_as_timestamps", "-use_wallclock_as_timestamps",
"1", "1",

View File

@@ -25,7 +25,7 @@ from flask import (
from peewee import SqliteDatabase, operator, fn, DoesNotExist from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
from frigate.const import CLIPS_DIR, PLUS_ENV_VAR from frigate.const import CLIPS_DIR
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.stats import stats_snapshot from frigate.stats import stats_snapshot
from frigate.version import VERSION from frigate.version import VERSION
@@ -256,7 +256,10 @@ def get_sub_labels():
) )
sub_labels = [e.sub_label for e in events] sub_labels = [e.sub_label for e in events]
if None in sub_labels:
sub_labels.remove(None) sub_labels.remove(None)
return jsonify(sub_labels) return jsonify(sub_labels)
@@ -568,7 +571,7 @@ def config():
for cmd in camera_dict["ffmpeg_cmds"]: for cmd in camera_dict["ffmpeg_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"]) cmd["cmd"] = " ".join(cmd["cmd"])
config["plus"] = {"enabled": PLUS_ENV_VAR in os.environ} config["plus"] = {"enabled": current_app.plus_api.is_active()}
return jsonify(config) return jsonify(config)

View File

@@ -5,7 +5,14 @@ from frigate.config import MotionConfig
class MotionDetector: class MotionDetector:
def __init__(self, frame_shape, config: MotionConfig, improve_contrast_enabled): def __init__(
self,
frame_shape,
config: MotionConfig,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
):
self.config = config self.config = config
self.frame_shape = frame_shape self.frame_shape = frame_shape
self.resize_factor = frame_shape[0] / config.frame_height self.resize_factor = frame_shape[0] / config.frame_height
@@ -25,6 +32,8 @@ class MotionDetector:
self.mask = np.where(resized_mask == [0]) self.mask = np.where(resized_mask == [0])
self.save_images = False self.save_images = False
self.improve_contrast = improve_contrast_enabled self.improve_contrast = improve_contrast_enabled
self.threshold = motion_threshold
self.contour_area = motion_contour_area
def detect(self, frame): def detect(self, frame):
motion_boxes = [] motion_boxes = []
@@ -69,7 +78,7 @@ class MotionDetector:
# compute the threshold image for the current frame # compute the threshold image for the current frame
current_thresh = cv2.threshold( current_thresh = cv2.threshold(
frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY
)[1] )[1]
# black out everything in the avg_delta where there isnt motion in the current frame # black out everything in the avg_delta where there isnt motion in the current frame
@@ -79,7 +88,7 @@ class MotionDetector:
# then look for deltas above the threshold, but only in areas where there is a delta # then look for deltas above the threshold, but only in areas where there is a delta
# in the current frame. this prevents deltas from previous frames from being included # in the current frame. this prevents deltas from previous frames from being included
thresh = cv2.threshold( thresh = cv2.threshold(
avg_delta_image, self.config.threshold, 255, cv2.THRESH_BINARY avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY
)[1] )[1]
# dilate the thresholded image to fill in holes, then find contours # dilate the thresholded image to fill in holes, then find contours
@@ -94,7 +103,7 @@ class MotionDetector:
for c in cnts: for c in cnts:
# if the contour is big enough, count it as motion # if the contour is big enough, count it as motion
contour_area = cv2.contourArea(c) contour_area = cv2.contourArea(c)
if contour_area > self.config.contour_area: if contour_area > self.contour_area.value:
x, y, w, h = cv2.boundingRect(c) x, y, w, h = cv2.boundingRect(c)
motion_boxes.append( motion_boxes.append(
( (
@@ -111,8 +120,7 @@ class MotionDetector:
# print(self.frame_counter) # print(self.frame_counter)
for c in cnts: for c in cnts:
contour_area = cv2.contourArea(c) contour_area = cv2.contourArea(c)
# print(contour_area) if contour_area > self.contour_area.value:
if contour_area > self.config.contour_area:
x, y, w, h = cv2.boundingRect(c) x, y, w, h = cv2.boundingRect(c)
cv2.rectangle( cv2.rectangle(
thresh_dilated, thresh_dilated,

View File

@@ -145,6 +145,52 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
state_topic = f"{message.topic[:-4]}/state" state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True) client.publish(state_topic, payload, retain=True)
def on_motion_threshold_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_threshold_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(f"Setting motion threshold for {camera_name} via mqtt: {payload}")
camera_metrics[camera_name]["motion_threshold"].value = payload
motion_settings.threshold = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_motion_contour_area_command(client, userdata, message):
try:
payload = int(message.payload.decode())
except ValueError:
logger.warning(
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
)
return
logger.debug(f"on_motion_contour_area_toggle: {message.topic} {payload}")
camera_name = message.topic.split("/")[-3]
motion_settings = config.cameras[camera_name].motion
logger.info(
f"Setting motion contour area for {camera_name} via mqtt: {payload}"
)
camera_metrics[camera_name]["motion_contour_area"].value = payload
motion_settings.contour_area = payload
state_topic = f"{message.topic[:-4]}/state"
client.publish(state_topic, payload, retain=True)
def on_restart_command(client, userdata, message): def on_restart_command(client, userdata, message):
restart_frigate() restart_frigate()
@@ -195,6 +241,14 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/set", f"{mqtt_config.topic_prefix}/{name}/improve_contrast/set",
on_improve_contrast_command, on_improve_contrast_command,
) )
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/set",
on_motion_threshold_command,
)
client.message_callback_add(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/set",
on_motion_contour_area_command,
)
client.message_callback_add( client.message_callback_add(
f"{mqtt_config.topic_prefix}/restart", on_restart_command f"{mqtt_config.topic_prefix}/restart", on_restart_command
@@ -250,6 +304,21 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
"ON" if config.cameras[name].motion.improve_contrast else "OFF", "ON" if config.cameras[name].motion.improve_contrast else "OFF",
retain=True, retain=True,
) )
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/state",
config.cameras[name].motion.threshold,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/state",
config.cameras[name].motion.contour_area,
retain=True,
)
client.publish(
f"{mqtt_config.topic_prefix}/{name}/motion",
"OFF",
retain=False,
)
return client return client

View File

@@ -1,6 +1,8 @@
import datetime import datetime
import json
import logging import logging
import os import os
import re
import requests import requests
from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST from frigate.const import PLUS_ENV_VAR, PLUS_API_HOST
from requests.models import Response from requests.models import Response
@@ -28,10 +30,23 @@ def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes:
class PlusApi: class PlusApi:
def __init__(self) -> None: def __init__(self) -> None:
self.host = PLUS_API_HOST self.host = PLUS_API_HOST
self.key = None
if PLUS_ENV_VAR in os.environ: if PLUS_ENV_VAR in os.environ:
self.key = os.environ.get(PLUS_ENV_VAR) self.key = os.environ.get(PLUS_ENV_VAR)
else: # check for the addon options file
elif os.path.isfile("/data/options.json"):
with open("/data/options.json") as f:
raw_options = f.read()
options = json.loads(raw_options)
self.key = options.get("plus_api_key")
if self.key is not None and not re.match(
r"[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}:[a-z0-9]{40}",
self.key,
):
logger.error("Plus API Key is not formatted correctly.")
self.key = None self.key = None
self._is_active: bool = self.key is not None self._is_active: bool = self.key is not None
self._token_data: dict = {} self._token_data: dict = {}

View File

@@ -99,11 +99,23 @@ class RecordingMaintainer(threading.Thread):
# delete all cached files past the most recent 5 # delete all cached files past the most recent 5
keep_count = 5 keep_count = 5
for camera in grouped_recordings.keys(): for camera in grouped_recordings.keys():
if len(grouped_recordings[camera]) > keep_count: segment_count = len(grouped_recordings[camera])
if segment_count > keep_count:
####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(
# f"Too many recording segments in cache for {camera}. Keeping the {keep_count} most recent segments out of {segment_count}, discarding the rest..."
# )
to_remove = grouped_recordings[camera][:-keep_count] to_remove = grouped_recordings[camera][:-keep_count]
for f in to_remove: for f in to_remove:
Path(f["cache_path"]).unlink(missing_ok=True) cache_path = f["cache_path"]
self.end_time_cache.pop(f["cache_path"], None) ####
# Need to find a way to tell if these are aging out based on retention settings or if the system is overloaded.
####
# logger.warning(f"Discarding a recording segment: {cache_path}")
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
grouped_recordings[camera] = grouped_recordings[camera][-keep_count:] grouped_recordings[camera] = grouped_recordings[camera][-keep_count:]
for camera, recordings in grouped_recordings.items(): for camera, recordings in grouped_recordings.items():

4
frigate/test/const.py Normal file
View File

@@ -0,0 +1,4 @@
"""Consts for testing."""
TEST_DB = "test.db"
TEST_DB_CLEANUPS = ["test.db", "test.db-shm", "test.db-wal"]

328
frigate/test/test_http.py Normal file
View File

@@ -0,0 +1,328 @@
import datetime
import json
import logging
import os
import unittest
from unittest.mock import patch
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from playhouse.shortcuts import model_to_dict
from frigate.config import FrigateConfig
from frigate.http import create_app
from frigate.models import Event, Recordings
from frigate.plus import PlusApi
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
class TestHttp(unittest.TestCase):
def setUp(self):
# setup clean database for each test run
migrate_db = SqliteExtDatabase("test.db")
del logging.getLogger("peewee_migrate").handlers[:]
router = Router(migrate_db)
router.run()
migrate_db.close()
self.db = SqliteQueueDatabase(TEST_DB)
models = [Event, Recordings]
self.db.bind(models)
self.minimal_config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
}
},
}
self.test_stats = {
"detection_fps": 13.7,
"detectors": {
"cpu1": {
"detection_start": 0.0,
"inference_speed": 91.43,
"pid": 42,
},
"cpu2": {
"detection_start": 0.0,
"inference_speed": 84.99,
"pid": 44,
},
},
"front_door": {
"camera_fps": 0.0,
"capture_pid": 53,
"detection_fps": 0.0,
"pid": 52,
"process_fps": 0.0,
"skipped_fps": 0.0,
},
"service": {
"storage": {
"/dev/shm": {
"free": 50.5,
"mount_type": "tmpfs",
"total": 67.1,
"used": 16.6,
},
"/media/frigate/clips": {
"free": 42429.9,
"mount_type": "ext4",
"total": 244529.7,
"used": 189607.0,
},
"/media/frigate/recordings": {
"free": 0.2,
"mount_type": "ext4",
"total": 8.0,
"used": 7.8,
},
"/tmp/cache": {
"free": 976.8,
"mount_type": "tmpfs",
"total": 1000.0,
"used": 23.2,
},
},
"uptime": 101113,
"version": "0.10.1",
"latest_version": "0.11",
},
}
def tearDown(self):
if not self.db.is_closed():
self.db.close()
try:
for file in TEST_DB_CLEANUPS:
os.remove(file)
except OSError:
pass
def test_get_event_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
id2 = "7890.random"
with app.test_client() as client:
_insert_mock_event(id)
events = client.get(f"/events").json
assert events
assert len(events) == 1
assert events[0]["id"] == id
_insert_mock_event(id2)
events = client.get(f"/events").json
assert events
assert len(events) == 2
events = client.get(
f"/events",
query_string={"limit": 1},
).json
assert events
assert len(events) == 1
events = client.get(
f"/events",
query_string={"has_clip": 0},
).json
assert not events
def test_get_good_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event == model_to_dict(Event.get(Event.id == id))
def test_get_bad_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
bad_id = "654321.other"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{bad_id}").json
assert not event
def test_delete_event(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
client.delete(f"/events/{id}")
event = client.get(f"/events/{id}").json
assert not event
def test_event_retention(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_event(id)
client.post(f"/events/{id}/retain")
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["retain_indefinitely"] == True
client.delete(f"/events/{id}/retain")
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["retain_indefinitely"] == False
def test_set_delete_sub_label(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
with app.test_client() as client:
_insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": sub_label}),
content_type="application/json",
)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["sub_label"] == sub_label
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": ""}),
content_type="application/json",
)
event = client.get(f"/events/{id}").json
assert event
assert event["id"] == id
assert event["sub_label"] == ""
def test_sub_label_list(self):
app = create_app(
FrigateConfig(**self.minimal_config), self.db, None, None, PlusApi()
)
id = "123456.random"
sub_label = "sub"
with app.test_client() as client:
_insert_mock_event(id)
client.post(
f"/events/{id}/sub_label",
data=json.dumps({"subLabel": sub_label}),
content_type="application/json",
)
sub_labels = client.get("/sub_labels").json
assert sub_labels
assert sub_labels == [sub_label]
def test_config(self):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
with app.test_client() as client:
config = client.get("/config").json
assert config
assert config["cameras"]["front_door"]
def test_recordings(self):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
id = "123456.random"
with app.test_client() as client:
_insert_mock_recording(id)
recording = client.get("/front_door/recordings").json
assert recording
assert recording[0]["id"] == id
@patch("frigate.http.stats_snapshot")
def test_stats(self, mock_stats):
app = create_app(
FrigateConfig(**self.minimal_config).runtime_config,
self.db,
None,
None,
PlusApi(),
)
mock_stats.return_value = self.test_stats
with app.test_client() as client:
stats = client.get("/stats").json
assert stats == self.test_stats
def _insert_mock_event(id: str) -> Event:
"""Inserts a basic event model with a given id."""
return Event.insert(
id=id,
label="Mock",
camera="front_door",
start_time=datetime.datetime.now().timestamp(),
end_time=datetime.datetime.now().timestamp() + 20,
top_score=100,
false_positive=False,
zones=list(),
thumbnail="",
region=[],
box=[],
area=0,
has_clip=True,
has_snapshot=True,
).execute()
def _insert_mock_recording(id: str) -> Event:
"""Inserts a basic recording model with a given id."""
return Recordings.insert(
id=id,
camera="front_door",
path=f"/recordings/{id}",
start_time=datetime.datetime.now().timestamp() - 50,
end_time=datetime.datetime.now().timestamp() - 60,
duration=10,
motion=True,
objects=True,
).execute()

View File

@@ -16,6 +16,8 @@ class CameraMetricsTypes(TypedDict):
frame_queue: Queue frame_queue: Queue
motion_enabled: Synchronized motion_enabled: Synchronized
improve_contrast_enabled: Synchronized improve_contrast_enabled: Synchronized
motion_threshold: Synchronized
motion_contour_area: Synchronized
process: Optional[Process] process: Optional[Process]
process_fps: Synchronized process_fps: Synchronized
read_start: Synchronized read_start: Synchronized

View File

@@ -363,13 +363,19 @@ def track_camera(
detection_enabled = process_info["detection_enabled"] detection_enabled = process_info["detection_enabled"]
motion_enabled = process_info["motion_enabled"] motion_enabled = process_info["motion_enabled"]
improve_contrast_enabled = process_info["improve_contrast_enabled"] improve_contrast_enabled = process_info["improve_contrast_enabled"]
motion_threshold = process_info["motion_threshold"]
motion_contour_area = process_info["motion_contour_area"]
frame_shape = config.frame_shape frame_shape = config.frame_shape
objects_to_track = config.objects.track objects_to_track = config.objects.track
object_filters = config.objects.filters object_filters = config.objects.filters
motion_detector = MotionDetector( motion_detector = MotionDetector(
frame_shape, config.motion, improve_contrast_enabled frame_shape,
config.motion,
improve_contrast_enabled,
motion_threshold,
motion_contour_area,
) )
object_detector = RemoteObjectDetector( object_detector = RemoteObjectDetector(
name, labelmap, detection_queue, result_connection, model_shape name, labelmap, detection_queue, result_connection, model_shape

13
web/package-lock.json generated
View File

@@ -30,6 +30,7 @@
"@testing-library/preact": "^2.0.1", "@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0", "@testing-library/preact-hooks": "^1.1.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/video.js": "^7.3.42",
"@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0", "@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
@@ -3234,6 +3235,12 @@
"@types/jest": "*" "@types/jest": "*"
} }
}, },
"node_modules/@types/video.js": {
"version": "7.3.42",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.42.tgz",
"integrity": "sha512-AD6AQNMgLTqrgoayC6SshKh8EDkDd9x5pmEuiY9YsniHlhn5jPXdkVqrzKLwviapaRhQF15TQYxo1JWpqXCUBg==",
"dev": true
},
"node_modules/@types/yargs": { "node_modules/@types/yargs": {
"version": "16.0.4", "version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@@ -14504,6 +14511,12 @@
"@types/jest": "*" "@types/jest": "*"
} }
}, },
"@types/video.js": {
"version": "7.3.42",
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.42.tgz",
"integrity": "sha512-AD6AQNMgLTqrgoayC6SshKh8EDkDd9x5pmEuiY9YsniHlhn5jPXdkVqrzKLwviapaRhQF15TQYxo1JWpqXCUBg==",
"dev": true
},
"@types/yargs": { "@types/yargs": {
"version": "16.0.4", "version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",

View File

@@ -32,6 +32,7 @@
"@testing-library/preact": "^2.0.1", "@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0", "@testing-library/preact-hooks": "^1.1.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/video.js": "^7.3.42",
"@typescript-eslint/eslint-plugin": "^5.18.0", "@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0", "@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",

View File

@@ -3,17 +3,17 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
import { useApiHost } from '../../api'; import { useApiHost } from '../../api';
import { isNullOrUndefined } from '../../utils/objectUtils'; import { isNullOrUndefined } from '../../utils/objectUtils';
import 'videojs-seek-buttons';
import 'video.js/dist/video-js.css';
import 'videojs-seek-buttons/dist/videojs-seek-buttons.css';
import videojs, { VideoJsPlayer } from 'video.js';
interface OnTimeUpdateEvent { interface OnTimeUpdateEvent {
timestamp: number; timestamp: number;
isPlaying: boolean; isPlaying: boolean;
} }
interface VideoProperties {
posterUrl: string;
videoUrl: string;
height: number;
}
interface HistoryVideoProps { interface HistoryVideoProps {
id?: string; id?: string;
isPlaying: boolean; isPlaying: boolean;
@@ -32,68 +32,39 @@ export const HistoryVideo = ({
onPlay, onPlay,
}: HistoryVideoProps) => { }: HistoryVideoProps) => {
const apiHost = useApiHost(); const apiHost = useApiHost();
const videoRef = useRef<HTMLVideoElement|null>(null); const videoRef = useRef<HTMLVideoElement>(null);
const [videoHeight, setVideoHeight] = useState<number>(0);
const [videoProperties, setVideoProperties] = useState<VideoProperties>({
posterUrl: '',
videoUrl: '',
height: 0,
});
const currentVideo = videoRef.current; const [video, setVideo] = useState<VideoJsPlayer>();
if (currentVideo && !videoHeight) {
const currentVideoHeight = currentVideo.offsetHeight;
if (currentVideoHeight > 0) {
setVideoHeight(currentVideoHeight);
}
}
useEffect(() => { useEffect(() => {
const idExists = !isNullOrUndefined(id); let video: VideoJsPlayer
if (idExists) { if (videoRef.current) {
if (videoRef.current && !videoRef.current.paused) { video = videojs(videoRef.current, {})
videoRef.current = null; setVideo(video)
} }
() => video?.dispose()
setVideoProperties({ }, [videoRef]);
posterUrl: `${apiHost}/api/events/${id}/snapshot.jpg`,
videoUrl: `${apiHost}/vod/event/${id}/index.m3u8`,
height: videoHeight,
});
} else {
setVideoProperties({
posterUrl: '',
videoUrl: '',
height: 0,
});
}
}, [id, videoHeight, videoRef, apiHost]);
useEffect(() => { useEffect(() => {
const playVideo = (video: HTMLMediaElement) => video.play(); if (!video) {
return
const attemptPlayVideo = (video: HTMLMediaElement) => {
const videoHasNotLoaded = video.readyState <= 1;
if (videoHasNotLoaded) {
video.oncanplay = () => {
playVideo(video);
};
video.load();
} else {
playVideo(video);
} }
};
const video = videoRef.current;
const videoExists = !isNullOrUndefined(video); if (!id) {
if (video && videoExists) { video.pause()
return
}
video.src({
src: `${apiHost}/vod/event/${id}/index.m3u8`,
type: 'application/vnd.apple.mpegurl',
});
video.poster(`${apiHost}/api/events/${id}/snapshot.jpg`);
if (videoIsPlaying) { if (videoIsPlaying) {
attemptPlayVideo(video); video.play();
} else {
video.pause();
} }
} }, [video, id]);
}, [videoIsPlaying, videoRef]);
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
@@ -111,32 +82,38 @@ export const HistoryVideo = ({
isPlaying: videoIsPlaying, isPlaying: videoIsPlaying,
timestamp: target.currentTime, timestamp: target.currentTime,
}; };
onTimeUpdate && onTimeUpdate(timeUpdateEvent); onTimeUpdate && onTimeUpdate(timeUpdateEvent);
}, },
[videoIsPlaying, onTimeUpdate] [videoIsPlaying, onTimeUpdate]
); );
const videoPropertiesIsUndefined = isNullOrUndefined(videoProperties); useEffect(() => {
if (videoPropertiesIsUndefined) { if (video && video.readyState() >= 1) {
return <div style={{ height: `${videoHeight}px`, width: '100%' }} />; if (videoIsPlaying) {
video.play()
} else {
video.pause()
} }
}
}, [video, videoIsPlaying])
const onLoad = useCallback(() => {
if (video && video.readyState() >= 1 && videoIsPlaying) {
video.play()
}
}, [video, videoIsPlaying])
const { posterUrl, videoUrl, height } = videoProperties;
return ( return (
<div data-vjs-player>
<video <video
ref={videoRef} ref={videoRef}
key={posterUrl}
onTimeUpdate={onTimeUpdateHandler} onTimeUpdate={onTimeUpdateHandler}
onLoadedMetadata={onLoad}
onPause={onPause} onPause={onPause}
onPlay={onPlay} onPlay={onPlay}
poster={posterUrl} className="video-js vjs-fluid"
preload='metadata' data-setup="{}"
controls />
style={height ? { minHeight: `${height}px` } : {}} </div>
playsInline
>
<source type='application/vnd.apple.mpegurl' src={videoUrl} />
</video>
); );
}; };

View File

@@ -20,11 +20,11 @@ export const TimelineBlocks = ({ timeline, firstBlockOffset, onEventClick }: Tim
const timelineBlockOffset = (timelineContainerHeight - largestYOffsetInBlocks) / 2; const timelineBlockOffset = (timelineContainerHeight - largestYOffsetInBlocks) / 2;
return ( return (
<div <div
className='relative' className="relative"
style={{ style={{
height: `${timelineContainerHeight}px`, height: `${timelineContainerHeight}px`,
width: `${timelineContainerWidth}px`, width: `${timelineContainerWidth}px`,
background: "url('/marker.png')", background: "url('/images/marker.png')",
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundSize: '30px', backgroundSize: '30px',
backgroundRepeat: 'repeat', backgroundRepeat: 'repeat',
@@ -41,7 +41,7 @@ export const TimelineBlocks = ({ timeline, firstBlockOffset, onEventClick }: Tim
</div> </div>
); );
} }
return <div /> return <div />;
}, [timeline, onEventClick, firstBlockOffset]); }, [timeline, onEventClick, firstBlockOffset]);
return timelineEventBlocks; return timelineEventBlocks;

View File

@@ -15,13 +15,13 @@ export default function Recording({ camera, date, hour = '00', minute = '00', se
); );
const apiHost = useApiHost(); const apiHost = useApiHost();
const { data: recordingsSummary } = useSWR(`${camera}/recordings/summary`); const { data: recordingsSummary } = useSWR(`${camera}/recordings/summary`, { revalidateOnFocus: false });
const recordingParams = { const recordingParams = {
before: getUnixTime(endOfHour(currentDate)), before: getUnixTime(endOfHour(currentDate)),
after: getUnixTime(startOfHour(currentDate)), after: getUnixTime(startOfHour(currentDate)),
}; };
const { data: recordings } = useSWR([`${camera}/recordings`, recordingParams]); const { data: recordings } = useSWR([`${camera}/recordings`, recordingParams], { revalidateOnFocus: false });
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time // calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
const seekSeconds = useMemo(() => { const seekSeconds = useMemo(() => {