forked from Github/frigate
Compare commits
65 Commits
v0.9.0-bet
...
v0.9.0-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a943ac1308 | ||
|
|
96319e795c | ||
|
|
5a8016de87 | ||
|
|
bc350644bd | ||
|
|
c793500ad2 | ||
|
|
1b2134c49e | ||
|
|
86a5b46c68 | ||
|
|
f83d4a58dd | ||
|
|
a5f241d5bd | ||
|
|
661f7baa21 | ||
|
|
7b063a19dc | ||
|
|
0320d94ea6 | ||
|
|
a7b7a45b23 | ||
|
|
89e317a6bb | ||
|
|
5a209caed3 | ||
|
|
288b1a0562 | ||
|
|
d35b09b18f | ||
|
|
e8eb3125a5 | ||
|
|
8109445fdd | ||
|
|
f63a7cb6c0 | ||
|
|
7fc5297f60 | ||
|
|
00ff76a0b9 | ||
|
|
b8df419bad | ||
|
|
faf103152a | ||
|
|
65855e23d9 | ||
|
|
6c28613def | ||
|
|
56480dc1ef | ||
|
|
8e1c15291d | ||
|
|
a1e52c51b1 | ||
|
|
8cc834633e | ||
|
|
7d65c05994 | ||
|
|
d74021af47 | ||
|
|
46fe06e779 | ||
|
|
fbea51372f | ||
|
|
fa5ec8d019 | ||
|
|
11c425a7eb | ||
|
|
0d352f3d8a | ||
|
|
6ccff71408 | ||
|
|
41fea2a531 | ||
|
|
3d6dad7e7e | ||
|
|
4efc584816 | ||
|
|
10ab70080a | ||
|
|
bddde74c06 | ||
|
|
29de723267 | ||
|
|
354a9240f0 | ||
|
|
5ae4f47e96 | ||
|
|
26424488a5 | ||
|
|
334095252c | ||
|
|
1c85f774eb | ||
|
|
bbf0fc8324 | ||
|
|
b143e11e0e | ||
|
|
927f56ab9f | ||
|
|
2181379475 | ||
|
|
45798d6d14 | ||
|
|
f6d5e96dbf | ||
|
|
e18aa56427 | ||
|
|
f3a1c1de0a | ||
|
|
0ccf543ec1 | ||
|
|
1f1a708388 | ||
|
|
58c0d97b5f | ||
|
|
abef002af8 | ||
|
|
adf2bc078c | ||
|
|
3bc75ae931 | ||
|
|
03e756dd27 | ||
|
|
5d0984998d |
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe what you are trying to accomplish and why in non technical terms**
|
||||
I want to be able to ... so that I can ...
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
2
Makefile
2
Makefile
@@ -45,7 +45,7 @@ aarch64_frigate: version web
|
||||
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
|
||||
aarch64_all: aarch64_wheels aarch64_ffmpeg aarch64_frigate
|
||||
|
||||
armv7_wheels:
|
||||
docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels .
|
||||
|
||||
@@ -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
|
||||
|
||||
<div>
|
||||
<a href="docs/static/img/media_browser.png"><img src="docs/static/img/media_browser.png" height=400></a>
|
||||
<a href="docs/static/img/notification.png"><img src="docs/static/img/notification.png" height=400></a>
|
||||
</div>
|
||||
|
||||
Also comes with a builtin UI:
|
||||
|
||||
<div>
|
||||
<a href="docs/static/img/home-ui.png"><img src="docs/static/img/home-ui.png" height=400></a>
|
||||
<a href="docs/static/img/camera-ui.png"><img src="docs/static/img/camera-ui.png" height=400></a>
|
||||
|
||||
@@ -40,8 +40,8 @@ COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
|
||||
|
||||
# get model and labels
|
||||
COPY labelmap.txt /labelmap.txt
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/master/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
|
||||
|
||||
WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
|
||||
@@ -52,6 +52,8 @@ http {
|
||||
vod_mode mapped;
|
||||
vod_max_mapping_response_size 1m;
|
||||
vod_upstream_location /api;
|
||||
vod_align_segments_to_key_frames on;
|
||||
vod_manifest_segment_durations_mode accurate;
|
||||
|
||||
# vod caches
|
||||
vod_metadata_cache metadata_cache 512m;
|
||||
|
||||
@@ -1,50 +1,11 @@
|
||||
---
|
||||
id: advanced
|
||||
title: Advanced
|
||||
sidebar_label: Advanced
|
||||
title: Advanced Options
|
||||
sidebar_label: Advanced Options
|
||||
---
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
### `motion`
|
||||
|
||||
Global motion detection config. These may also be defined at the camera level.
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
# Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below)
|
||||
# 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 (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
|
||||
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below)
|
||||
# Higher values mean the current frame impacts the delta a lot, and a single raindrop may register as motion.
|
||||
# Too low and a fast moving person wont be detected as motion.
|
||||
delta_alpha: 0.2
|
||||
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
|
||||
# Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
|
||||
# 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, 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
|
||||
```
|
||||
|
||||
### `detect`
|
||||
|
||||
Global object detection settings. These may also be defined at the camera level.
|
||||
|
||||
```yaml
|
||||
detect:
|
||||
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
|
||||
max_disappeared: 25
|
||||
```
|
||||
|
||||
### `logger`
|
||||
|
||||
Change the default log level for troubleshooting purposes.
|
||||
@@ -72,55 +33,18 @@ Examples of available modules are:
|
||||
|
||||
### `environment_vars`
|
||||
|
||||
This section can be used to set environment variables for those unable to modify the environment of the container (ie. within Hass.io)
|
||||
|
||||
```yaml
|
||||
environment_vars:
|
||||
EXAMPLE_VAR: value
|
||||
```
|
||||
This section can be used to set environment variables for those unable to modify the environment of the container (ie. within HassOS)
|
||||
|
||||
### `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.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
path: /media/frigate/clips/frigate.db
|
||||
```
|
||||
|
||||
### `detectors`
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
# Required: name of the detector
|
||||
coral:
|
||||
# Required: type of the detector
|
||||
# Valid values are 'edgetpu' (requires device property below) and 'cpu'.
|
||||
type: edgetpu
|
||||
# Optional: device name as defined here: https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
|
||||
device: usb
|
||||
# Optional: num_threads value passed to the tflite.Interpreter (default: shown below)
|
||||
# This value is only used for CPU types
|
||||
num_threads: 3
|
||||
```
|
||||
This may need to be in a custom location if network storage is used for the media folder.
|
||||
|
||||
### `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
|
||||
```
|
||||
|
||||
47
docs/docs/configuration/camera_specific.md
Normal file
47
docs/docs/configuration/camera_specific.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: camera_specific
|
||||
title: Camera Specific Configurations
|
||||
---
|
||||
|
||||
### MJPEG Cameras
|
||||
|
||||
The input and output parameters need to be adjusted for MJPEG cameras
|
||||
|
||||
```yaml
|
||||
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -use_wallclock_as_timestamps 1
|
||||
```
|
||||
|
||||
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 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v libx264 -an
|
||||
rtmp: -c:v libx264 -an -f flv
|
||||
```
|
||||
|
||||
### RTMP Cameras (Reolink 410/520 and possibly others)
|
||||
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv
|
||||
```
|
||||
|
||||
### Blue Iris RTSP Cameras
|
||||
|
||||
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
|
||||
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
|
||||
### UDP Only Cameras
|
||||
|
||||
If your cameras do not support TCP connections for RTSP, you can use UDP.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -stimeout 5000000 -use_wallclock_as_timestamps 1
|
||||
```
|
||||
@@ -5,17 +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.
|
||||
Several 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 |
|
||||
| `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
|
||||
| Role | Description |
|
||||
| -------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `detect` | Main feed for object detection |
|
||||
| `record` | Saves segments of the video feed based on configuration settings. [docs](/configuration/record) |
|
||||
| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](/configuration/rtmp) |
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
@@ -30,530 +28,18 @@ cameras:
|
||||
- rtmp
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live
|
||||
roles:
|
||||
- clips
|
||||
- record
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
```
|
||||
|
||||
## Masks & Zones
|
||||
|
||||
### Masks
|
||||
|
||||
Masks are used to ignore initial detection in areas of your camera's field of view.
|
||||
|
||||
There are two types of masks available:
|
||||
|
||||
- **Motion masks**: Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the video feed with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. Over masking will make it more difficult for objects to be tracked. To see this effect, create a mask, and then watch the video feed with `Motion Boxes` enabled again.
|
||||
- **Object filter masks**: Object filter masks are used to filter out false positives for a given object type. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell frigate that anything in your yard is a false positive.
|
||||
|
||||
To create a poly mask:
|
||||
|
||||
1. Visit the [web UI](/usage/web)
|
||||
1. Click the camera you wish to create a mask for
|
||||
1. Click "Mask & Zone creator"
|
||||
1. Click "Add" on the type of mask or zone you would like to create
|
||||
1. Click on the camera's latest image to create a masked area. The yaml representation will be updated in real-time
|
||||
1. When you've finished creating your mask, click "Copy" and paste the contents into your `config.yaml` file and restart Frigate
|
||||
|
||||
Example of a finished row corresponding to the below example image:
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
mask: "0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432"
|
||||
```
|
||||
|
||||

|
||||
|
||||
```yaml
|
||||
# Optional: camera level motion config
|
||||
motion:
|
||||
# Optional: motion mask
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
mask: 0,900,1080,900,1080,1920,0,1920
|
||||
```
|
||||
|
||||
### Zones
|
||||
|
||||
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
|
||||
|
||||
During testing, `draw_zones` should be set in the config to draw the zone on the frames so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
|
||||
|
||||
To create a zone, follow the same steps above for a "Motion mask", but use the section of the web UI for creating a zone instead.
|
||||
|
||||
```yaml
|
||||
# Optional: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# 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:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.7
|
||||
```
|
||||
|
||||
## Objects
|
||||
|
||||
For a list of available objects, see the [objects documentation](./objects.mdx).
|
||||
|
||||
```yaml
|
||||
# Optional: Camera level object filters config.
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
# Optional: mask to prevent all object types from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object.
|
||||
# NOTE: This mask is COMBINED with the object type specific mask below
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
min_score: 0.5
|
||||
threshold: 0.7
|
||||
# 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
|
||||
```
|
||||
|
||||
## 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/<camera_name>/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.
|
||||
|
||||
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 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: Retention settings for clips
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
```
|
||||
|
||||
## Snapshots
|
||||
|
||||
Frigate can save a snapshot image to `/media/frigate/clips` for each event named as `<camera>-<id>.jpg`.
|
||||
|
||||
```yaml
|
||||
# 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)
|
||||
# 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)
|
||||
bounding_box: False
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
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)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
```
|
||||
|
||||
## 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://<frigate_host>/live/<camera_name>`. 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
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
# Required: name of the camera
|
||||
back:
|
||||
# Required: ffmpeg settings for the camera
|
||||
ffmpeg:
|
||||
# Required: A list of input streams for the camera. See documentation for more information.
|
||||
inputs:
|
||||
# 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,
|
||||
# they must also be enabled in the camera config.
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
# Optional: stream specific global args (default: inherit)
|
||||
global_args:
|
||||
# Optional: stream specific hwaccel args (default: inherit)
|
||||
hwaccel_args:
|
||||
# Optional: stream specific input args (default: inherit)
|
||||
input_args:
|
||||
# Optional: camera specific global args (default: inherit)
|
||||
global_args:
|
||||
# Optional: camera specific hwaccel args (default: inherit)
|
||||
hwaccel_args:
|
||||
# Optional: camera specific input args (default: inherit)
|
||||
input_args:
|
||||
# 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
|
||||
|
||||
# Optional: camera level motion config
|
||||
motion:
|
||||
# Optional: motion mask
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
mask: 0,900,1080,900,1080,1920,0,1920
|
||||
|
||||
# Optional: timeout for highest scoring image before allowing it
|
||||
# to be replaced by a newer image. (default: shown below)
|
||||
best_image_timeout: 60
|
||||
|
||||
# Optional: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# 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:
|
||||
person:
|
||||
min_area: 5000
|
||||
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: RTMP re-stream configuration
|
||||
rtmp:
|
||||
# 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.
|
||||
width: 1280
|
||||
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)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: False
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: False
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: False
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: False
|
||||
# Optional: height to resize the snapshot to (default: original size)
|
||||
height: 175
|
||||
# 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)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: Configuration for the jpg snapshots published via MQTT
|
||||
mqtt:
|
||||
# Optional: Enable publishing snapshot via mqtt for camera (default: shown below)
|
||||
# NOTE: Only applies to publishing image data to MQTT via 'frigate/<camera_name>/<object_name>/snapshot'.
|
||||
# All other messages will still be published.
|
||||
enabled: True
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: True
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: True
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
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: []
|
||||
|
||||
# Optional: Camera level object filters config.
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
# Optional: mask to prevent all object types from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object.
|
||||
# NOTE: This mask is COMBINED with the object type specific mask below
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
min_score: 0.5
|
||||
threshold: 0.7
|
||||
# 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
|
||||
|
||||
### MJPEG Cameras
|
||||
|
||||
The input and output parameters need to be adjusted for MJPEG cameras
|
||||
Additional cameras are simply added to the config under the `cameras` entry.
|
||||
|
||||
```yaml
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -r
|
||||
- "3" # <---- adjust depending on your desired frame rate from the mjpeg image
|
||||
- -use_wallclock_as_timestamps
|
||||
- "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.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### RTMP Cameras
|
||||
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- "1"
|
||||
```
|
||||
|
||||
### Reolink 410/520 (possibly others)
|
||||
|
||||
Several users have reported success with the rtmp video from Reolink cameras.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -rw_timeout
|
||||
- "5000000"
|
||||
- -use_wallclock_as_timestamps
|
||||
- "1"
|
||||
```
|
||||
|
||||
### Blue Iris RTSP Cameras
|
||||
|
||||
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
|
||||
|
||||
```yaml
|
||||
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"
|
||||
mqtt: ...
|
||||
cameras:
|
||||
back: ...
|
||||
front: ...
|
||||
side: ...
|
||||
```
|
||||
|
||||
@@ -3,13 +3,13 @@ id: detectors
|
||||
title: Detectors
|
||||
---
|
||||
|
||||
The default config will look for a USB Coral device. If you do not have a Coral, you will need to configure a CPU detector. If you have PCI or multiple Coral devices, you need to configure your detector devices in the config file. When using multiple detectors, they run in dedicated processes, but pull from a common queue of requested detections across all cameras.
|
||||
By default, Frigate will use a single CPU detector. If you have a Coral, you will need to configure your detector devices in the config file. When using multiple detectors, they run in dedicated processes, but pull from a common queue of requested detections across all cameras.
|
||||
|
||||
Frigate supports `edgetpu` and `cpu` as detector types. The device value should be specified according to the [Documentation for the TensorFlow Lite Python API](https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api).
|
||||
|
||||
**Note**: There is no support for Nvidia GPUs to perform object detection with tensorflow. It can be used for ffmpeg decoding, but not object detection.
|
||||
|
||||
Single USB Coral:
|
||||
### Single USB Coral
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
@@ -18,7 +18,7 @@ detectors:
|
||||
device: usb
|
||||
```
|
||||
|
||||
Multiple USB Corals:
|
||||
### Multiple USB Corals
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
@@ -30,7 +30,16 @@ detectors:
|
||||
device: usb:1
|
||||
```
|
||||
|
||||
Multiple PCIE/M.2 Corals:
|
||||
### Native Coral (Dev Board)
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: ""
|
||||
```
|
||||
|
||||
### Multiple PCIE/M.2 Corals
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
@@ -42,7 +51,7 @@ detectors:
|
||||
device: pci:1
|
||||
```
|
||||
|
||||
Mixing Corals:
|
||||
### Mixing Corals
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
@@ -54,12 +63,16 @@ detectors:
|
||||
device: pci
|
||||
```
|
||||
|
||||
CPU Detectors (not recommended):
|
||||
### CPU Detectors (not recommended)
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
cpu1:
|
||||
type: cpu
|
||||
num_threads: 3
|
||||
cpu2:
|
||||
type: cpu
|
||||
num_threads: 3
|
||||
```
|
||||
|
||||
When using CPU detectors, you can add a CPU detector per camera. Adding more detectors than the number of cameras should not improve performance.
|
||||
|
||||
70
docs/docs/configuration/hardware_acceleration.md
Normal file
70
docs/docs/configuration/hardware_acceleration.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
id: hardware_acceleration
|
||||
title: Hardware Acceleration
|
||||
---
|
||||
|
||||
It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
|
||||
|
||||
### Raspberry Pi 3/4 (32-bit OS)
|
||||
|
||||
Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config > Performance Options > GPU Memory).
|
||||
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_mmal
|
||||
```
|
||||
|
||||
### Raspberry Pi 3/4 (64-bit OS)
|
||||
|
||||
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_v4l2m2m
|
||||
```
|
||||
|
||||
### Intel-based CPUs (<10th Generation) via Quicksync
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
- -hwaccel_output_format
|
||||
- yuv420p
|
||||
```
|
||||
|
||||
### Intel-based CPUs (>=10th Generation) via Quicksync
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- qsv
|
||||
- -qsv_device
|
||||
- /dev/dri/renderD128
|
||||
```
|
||||
|
||||
### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver
|
||||
|
||||
**Note:** You also need to set `LIBVA_DRIVER_NAME=radeonsi` as an environment variable on the container.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
```
|
||||
|
||||
### NVIDIA GPU
|
||||
|
||||
NVIDIA GPU based decoding via NVDEC is supported, but requires special configuration. See the [NVIDIA NVDEC documentation](/configuration/nvdec) for more details.
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
id: index
|
||||
title: Configuration
|
||||
title: Configuration File
|
||||
---
|
||||
|
||||
For HassOS installations, the default location for the config file is `/config/frigate.yml`.
|
||||
For Home Assistant Addon installations, the config file needs to be in the root of your Home Assistant config directory (same location as `configuration.yaml`) and named `frigate.yml`.
|
||||
|
||||
For all other installations, the default location for the config file is '/config/config.yml'. This can be overridden with the `CONFIG_FILE` environment variable. Camera specific ffmpeg parameters are documented [here](cameras.md).
|
||||
For all other installation types, the config file should be mapped to `/config/config.yml` inside the container.
|
||||
|
||||
It is recommended to start with a minimal configuration and add to it:
|
||||
It is recommended to start with a minimal configuration and add to it as described in [this guide](/guides/getting_started):
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
@@ -20,14 +20,18 @@ cameras:
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
detect:
|
||||
width: 1280
|
||||
height: 720
|
||||
```
|
||||
|
||||
## Required
|
||||
### Full configuration reference:
|
||||
|
||||
## `mqtt`
|
||||
:::caution
|
||||
|
||||
It is not recommended to copy this full configuration file. Only specify values that are different from the defaults. Configuration options and default values may change in future versions.
|
||||
|
||||
:::
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
@@ -36,10 +40,10 @@ mqtt:
|
||||
# Optional: port (default: shown below)
|
||||
port: 1883
|
||||
# Optional: topic prefix (default: shown below)
|
||||
# WARNING: must be unique if you are running multiple instances
|
||||
# NOTE: must be unique if you are running multiple instances
|
||||
topic_prefix: frigate
|
||||
# Optional: client id (default: shown below)
|
||||
# WARNING: must be unique if you are running multiple instances
|
||||
# NOTE: must be unique if you are running multiple instances
|
||||
client_id: frigate
|
||||
# Optional: user
|
||||
user: mqtt_user
|
||||
@@ -60,58 +64,39 @@ mqtt:
|
||||
tls_insecure: false
|
||||
# Optional: interval in seconds for publishing stats (default: shown below)
|
||||
stats_interval: 60
|
||||
```
|
||||
|
||||
## `cameras`
|
||||
# Optional: Detectors configuration. Defaults to a single CPU detector
|
||||
detectors:
|
||||
# Required: name of the detector
|
||||
coral:
|
||||
# Required: type of the detector
|
||||
# Valid values are 'edgetpu' (requires device property below) and 'cpu'.
|
||||
type: edgetpu
|
||||
# Optional: device name as defined here: https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
|
||||
device: usb
|
||||
# Optional: num_threads value passed to the tflite.Interpreter (default: shown below)
|
||||
# This value is only used for CPU types
|
||||
num_threads: 3
|
||||
|
||||
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras.md) for a complete list of options.
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
# Name of your camera
|
||||
front_door:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
```
|
||||
|
||||
## Optional
|
||||
|
||||
### `database`
|
||||
|
||||
```yaml
|
||||
# Optional: Database configuration
|
||||
database:
|
||||
# The path to store the SQLite DB (default: shown below)
|
||||
path: /media/frigate/frigate.db
|
||||
```
|
||||
|
||||
### `model`
|
||||
|
||||
```yaml
|
||||
# Optional: model modifications
|
||||
model:
|
||||
# Optional: path to the model (default: automatic based on detector)
|
||||
path: /edgetpu_model.tflite
|
||||
# Optional: path to the labelmap (default: shown below)
|
||||
labelmap_path: /labelmap.txt
|
||||
# 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
|
||||
# Optional: Label name modifications. These are merged into the standard labelmap.
|
||||
labelmap:
|
||||
2: vehicle # previously "car"
|
||||
```
|
||||
2: vehicle
|
||||
|
||||
### `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)
|
||||
@@ -119,119 +104,12 @@ logger:
|
||||
# Optional: Component specific logger overrides
|
||||
logs:
|
||||
frigate.event: debug
|
||||
```
|
||||
|
||||
### `record`
|
||||
# Optional: set environment variables
|
||||
environment_vars:
|
||||
EXAMPLE_VAR: value
|
||||
|
||||
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/<camera_name>/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.
|
||||
|
||||
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 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: Retention settings for clips
|
||||
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
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
```
|
||||
|
||||
### `ffmpeg`
|
||||
|
||||
Can be overridden at the camera level.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
# Optional: global ffmpeg args (default: shown below)
|
||||
global_args: -hide_banner -loglevel warning
|
||||
# Optional: global hwaccel args (default: shown below)
|
||||
# NOTE: See hardware acceleration docs for your specific device
|
||||
hwaccel_args: []
|
||||
# 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
|
||||
# Optional: global output args
|
||||
output_args:
|
||||
# Optional: output args for detect streams (default: shown below)
|
||||
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
|
||||
```
|
||||
|
||||
### `objects`
|
||||
|
||||
Can be overridden at the camera level. For a list of available objects, see the [objects documentation](./objects.mdx).
|
||||
|
||||
```yaml
|
||||
objects:
|
||||
# Optional: list of objects to track from labelmap.txt (default: shown below)
|
||||
track:
|
||||
- person
|
||||
# Optional: filters to reduce false positives for specific object types
|
||||
filters:
|
||||
person:
|
||||
# Optional: minimum width*height of the bounding box for the detected object (default: 0)
|
||||
min_area: 5000
|
||||
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
|
||||
max_area: 100000
|
||||
# Optional: minimum score for the object to initiate tracking (default: shown below)
|
||||
min_score: 0.5
|
||||
# Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
|
||||
threshold: 0.7
|
||||
```
|
||||
|
||||
### `birdseye`
|
||||
|
||||
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
|
||||
# Optional: birdseye configuration
|
||||
birdseye:
|
||||
# Optional: Enable birdseye view (default: shown below)
|
||||
enabled: True
|
||||
@@ -247,4 +125,263 @@ birdseye:
|
||||
# motion - cameras are included if motion was detected in the last 30 seconds
|
||||
# continuous - all cameras are included always
|
||||
mode: objects
|
||||
|
||||
# Optional: ffmpeg configuration
|
||||
ffmpeg:
|
||||
# Optional: global ffmpeg args (default: shown below)
|
||||
global_args: -hide_banner -loglevel warning
|
||||
# Optional: global hwaccel args (default: shown below)
|
||||
# NOTE: See hardware acceleration docs for your specific device
|
||||
hwaccel_args: []
|
||||
# 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
|
||||
# Optional: global output args
|
||||
output_args:
|
||||
# Optional: output args for detect streams (default: shown below)
|
||||
detect: -f rawvideo -pix_fmt yuv420p
|
||||
# Optional: output args for record streams (default: shown below)
|
||||
record: -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
|
||||
|
||||
# Optional: Detect configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
detect:
|
||||
# Optional: width of the frame for the input with the detect role (default: shown below)
|
||||
width: 1280
|
||||
# Optional: height of the frame for the input with the detect role (default: shown below)
|
||||
height: 720
|
||||
# Optional: desired fps for your camera for the input with the detect role (default: shown below)
|
||||
# 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: Object configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
objects:
|
||||
# Optional: list of objects to track from labelmap.txt (default: shown below)
|
||||
track:
|
||||
- person
|
||||
# Optional: mask to prevent all object types from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object.
|
||||
# NOTE: This mask is COMBINED with the object type specific mask below
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
# Optional: filters to reduce false positives for specific object types
|
||||
filters:
|
||||
person:
|
||||
# Optional: minimum width*height of the bounding box for the detected object (default: 0)
|
||||
min_area: 5000
|
||||
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
|
||||
max_area: 100000
|
||||
# Optional: minimum score for the object to initiate tracking (default: shown below)
|
||||
min_score: 0.5
|
||||
# Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
|
||||
threshold: 0.7
|
||||
# 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: Motion configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
motion:
|
||||
# Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below)
|
||||
# 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 (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
|
||||
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below)
|
||||
# Higher values mean the current frame impacts the delta a lot, and a single raindrop may register as motion.
|
||||
# Too low and a fast moving person wont be detected as motion.
|
||||
delta_alpha: 0.2
|
||||
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
|
||||
# Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
|
||||
# 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, 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
|
||||
# Optional: motion mask
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
mask: 0,900,1080,900,1080,1920,0,1920
|
||||
|
||||
# Optional: Record configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
record:
|
||||
# Optional: Enable recording (default: shown below)
|
||||
enabled: False
|
||||
# Optional: Number of days to retain recordings regardless of events (default: shown below)
|
||||
# NOTE: This should be set to 0 and retention should be defined in events section below
|
||||
# if you only want to retain recordings of events.
|
||||
retain_days: 0
|
||||
# Optional: Event recording settings
|
||||
events:
|
||||
# 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 retained recordings
|
||||
# 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 recordings of events
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
|
||||
# NOTE: Can be overridden at the camera level
|
||||
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: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: False
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: False
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: False
|
||||
# Optional: height to resize the snapshot to (default: original size)
|
||||
height: 175
|
||||
# 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)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: RTMP configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
rtmp:
|
||||
# Optional: Enable the RTMP stream (default: True)
|
||||
enabled: True
|
||||
|
||||
# Optional: Live stream configuration for WebUI
|
||||
# NOTE: Can be overridden at the camera level
|
||||
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: in-feed timestamp style configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
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: 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
|
||||
|
||||
# Required
|
||||
cameras:
|
||||
# Required: name of the camera
|
||||
back:
|
||||
# Required: ffmpeg settings for the camera
|
||||
ffmpeg:
|
||||
# Required: A list of input streams for the camera. See documentation for more information.
|
||||
inputs:
|
||||
# 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,rtmp
|
||||
# NOTICE: In addition to assigning the record, and rtmp roles,
|
||||
# they must also be enabled in the camera config.
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
# Optional: stream specific global args (default: inherit)
|
||||
# global_args:
|
||||
# Optional: stream specific hwaccel args (default: inherit)
|
||||
# hwaccel_args:
|
||||
# Optional: stream specific input args (default: inherit)
|
||||
# input_args:
|
||||
# Optional: camera specific global args (default: inherit)
|
||||
# global_args:
|
||||
# Optional: camera specific hwaccel args (default: inherit)
|
||||
# hwaccel_args:
|
||||
# Optional: camera specific input args (default: inherit)
|
||||
# input_args:
|
||||
# Optional: camera specific output args (default: inherit)
|
||||
# output_args:
|
||||
|
||||
# Optional: timeout for highest scoring image before allowing it
|
||||
# to be replaced by a newer image. (default: shown below)
|
||||
best_image_timeout: 60
|
||||
|
||||
# Optional: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# 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:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.7
|
||||
|
||||
# Optional: Configuration for the jpg snapshots published via MQTT
|
||||
mqtt:
|
||||
# Optional: Enable publishing snapshot via mqtt for camera (default: shown below)
|
||||
# NOTE: Only applies to publishing image data to MQTT via 'frigate/<camera_name>/<object_name>/snapshot'.
|
||||
# All other messages will still be published.
|
||||
enabled: True
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: True
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: True
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
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: []
|
||||
```
|
||||
|
||||
77
docs/docs/configuration/masks.md
Normal file
77
docs/docs/configuration/masks.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: masks
|
||||
title: Masks
|
||||
---
|
||||
|
||||
There are two types of masks available:
|
||||
|
||||
**Motion masks**: Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the debug feed with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. Over masking will make it more difficult for objects to be tracked. To see this effect, create a mask, and then watch the video feed with `Motion Boxes` enabled again.
|
||||
|
||||
**Object filter masks**: Object filter masks are used to filter out false positives for a given object type based on location. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell frigate that anything in your yard is a false positive.
|
||||
|
||||
To create a poly mask:
|
||||
|
||||
1. Visit the Web UI
|
||||
1. Click the camera you wish to create a mask for
|
||||
1. Select "Debug" at the top
|
||||
1. Expand the "Options" below the video feed
|
||||
1. Click "Mask & Zone creator"
|
||||
1. Click "Add" on the type of mask or zone you would like to create
|
||||
1. Click on the camera's latest image to create a masked area. The yaml representation will be updated in real-time
|
||||
1. When you've finished creating your mask, click "Copy" and paste the contents into your config file and restart Frigate
|
||||
|
||||
Example of a finished row corresponding to the below example image:
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
mask: "0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432"
|
||||
```
|
||||
|
||||
Multiple masks can be listed.
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
mask:
|
||||
- 458,1346,336,973,317,869,375,866,432
|
||||
- 0,461,3,0,1919,0,1919,843,1699,492,1344
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Further Clarification
|
||||
|
||||
This is a response to a [question posed on reddit](https://www.reddit.com/r/homeautomation/comments/ppxdve/replacing_my_doorbell_with_a_security_camera_a_6/hd876w4?utm_source=share&utm_medium=web2x&context=3):
|
||||
|
||||
It is helpful to understand a bit about how Frigate uses motion detection and object detection together.
|
||||
|
||||
First, Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.
|
||||
|
||||
Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer.
|
||||
|
||||
After the area with motion is identified, Frigate creates a "region" (the green boxes in the debug viewer) to run object detection on. The models are trained on square images, so these regions are always squares. It adds a margin around the motion area in hopes of capturing a cropped view of the object moving that fills most of the image passed to object detection, but doesn't cut anything off. It also takes into consideration the location of the bounding box from the previous frame if it is tracking an object.
|
||||
|
||||
After object detection runs, if there are detected objects that seem to be cut off, Frigate reframes the region and runs object detection again on the same frame to get a better look.
|
||||
|
||||
All of this happens for each area of motion and tracked object.
|
||||
|
||||
> Are you simply saying that INITIAL triggering of any kind of detection will only happen in un-masked areas, but that once this triggering happens, the masks become irrelevant and object detection takes precedence?
|
||||
|
||||
Essentially, yes. I wouldn't describe it as object detection taking precedence though. The motion masks just prevent those areas from being counted as motion. Those masks do not modify the regions passed to object detection in any way, so you can absolutely detect objects in areas masked for motion.
|
||||
|
||||
> If so, this is completely expected and intuitive behavior for me. Because obviously if a "foot" starts motion detection the camera should be able to check if it's an entire person before it fully crosses into the zone. The docs imply this is the behavior, so I also don't understand why this would be detrimental to object detection on the whole.
|
||||
|
||||
When just a foot is triggering motion, Frigate will zoom in and look only at the foot. If that even qualifies as a person, it will determine the object is being cut off and look again and again until it zooms back out enough to find the whole person.
|
||||
|
||||
It is also detrimental to how Frigate tracks a moving object. Motion nearby the bounding box from the previous frame is used to intelligently determine where the region should be in the next frame. With too much masking, tracking is hampered and if an object walks from an unmasked area into a fully masked area, they essentially disappear and will be picked up as a "new" object if they leave the masked area. This is important because Frigate uses the history of scores while tracking an object to determine if it is a false positive or not. It takes a minimum of 3 frames for Frigate to determine is the object type it thinks it is, and the median score must be greater than the threshold. If a person meets this threshold while on the sidewalk before they walk into your stoop, you will get an alert the instant they step a single foot into a zone.
|
||||
|
||||
> I thought the main point of this feature was to cut down on CPU use when motion is happening in unnecessary areas.
|
||||
|
||||
It is, but the definition of "unnecessary" varies. I want to ignore areas of motion that I know are definitely not being triggered by objects of interest. Timestamps, trees, sky, rooftops. I don't want to ignore motion from objects that I want to track and know where they go.
|
||||
|
||||
> For me, giving my masks ANY padding results in a lot of people detection I'm not interested in. I live in the city and catch a lot of the sidewalk on my camera. People walk by my front door all the time and the margin between the sidewalk and actually walking onto my stoop is very thin, so I basically have everything but the exact contours of my stoop masked out. This results in very tidy detections but this info keeps throwing me off. Am I just overthinking it?
|
||||
|
||||
This is what `required_zones` are for. You should define a zone (remember this is evaluated based on the bottom center of the bounding box) and make it required to save snapshots and clips (now events in 0.9.0). You can also use this in your conditions for a notification.
|
||||
|
||||
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
|
||||
|
||||
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: nvdec
|
||||
title: nVidia hardware decoder
|
||||
title: NVIDIA hardware decoder
|
||||
---
|
||||
|
||||
Certain nvidia cards include a hardware decoder, which can greatly improve the
|
||||
@@ -23,7 +23,7 @@ In order to pass NVDEC, the docker engine must be set to `nvidia` and the enviro
|
||||
|
||||
In a docker compose file, these lines need to be set:
|
||||
|
||||
```
|
||||
```yaml
|
||||
services:
|
||||
frigate:
|
||||
...
|
||||
@@ -41,7 +41,7 @@ The decoder you choose will depend on the input video.
|
||||
|
||||
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
|
||||
|
||||
```
|
||||
```shell
|
||||
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
|
||||
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
|
||||
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
|
||||
@@ -57,10 +57,9 @@ A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the c
|
||||
For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
|
||||
`-c:v hevc_cuvid` to your ffmpeg input arguments:
|
||||
|
||||
```
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args:
|
||||
...
|
||||
input_args: ...
|
||||
- -c:v
|
||||
- hevc_cuvid
|
||||
```
|
||||
@@ -100,10 +99,10 @@ processes:
|
||||
To further improve performance, you can set ffmpeg to skip frames in the output,
|
||||
using the fps filter:
|
||||
|
||||
```
|
||||
output_args:
|
||||
- -filter:v
|
||||
- fps=fps=5
|
||||
```yaml
|
||||
output_args:
|
||||
- -filter:v
|
||||
- fps=fps=5
|
||||
```
|
||||
|
||||
This setting, for example, allows Frigate to consume my 10-15fps camera streams on
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
id: objects
|
||||
title: Default available objects
|
||||
sidebar_label: Available objects
|
||||
title: Objects
|
||||
---
|
||||
|
||||
import labels from "../../../labelmap.txt";
|
||||
|
||||
By default, Frigate includes the following object models from the Google Coral test data.
|
||||
By default, Frigate includes the following object models from the Google Coral test data. Note that `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
|
||||
|
||||
<ul>
|
||||
{labels.split("\n").map((label) => (
|
||||
@@ -22,4 +21,4 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
|
||||
- EdgeTPU Model: `/edgetpu_model.tflite`
|
||||
- Labels: `/labelmap.txt`
|
||||
|
||||
You also need to update the model width/height in the config if they differ from the defaults.
|
||||
You also need to update the [model config](/configuration/advanced#model) if they differ from the defaults.
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
id: optimizing
|
||||
title: Optimizing performance
|
||||
---
|
||||
|
||||
- **Google Coral**: It is strongly recommended to use a Google Coral, Frigate will no longer fall back to CPU in the event one is not found. Offloading TensorFlow to the Google Coral is an order of magnitude faster and will reduce your CPU load dramatically. A $60 device will outperform $2000 CPU. Frigate should work with any supported Coral device from https://coral.ai
|
||||
- **Resolution**: For the `detect` input, choose a camera resolution where the smallest object you want to detect barely fits inside a 300x300px square. The model used by Frigate is trained on 300x300px images, so you will get worse performance and no improvement in accuracy by using a larger resolution since Frigate resizes the area where it is looking for objects to 300x300 anyway.
|
||||
- **FPS**: 5 frames per second should be adequate. Higher frame rates will require more CPU usage without improving detections or accuracy. Reducing the frame rate on your camera will have the greatest improvement on system resources.
|
||||
- **Hardware Acceleration**: Make sure you configure the `hwaccel_args` for your hardware. They provide a significant reduction in CPU usage if they are available.
|
||||
- **Masks**: Masks can be used to ignore motion and reduce your idle CPU load. If you have areas with regular motion such as timestamps or trees blowing in the wind, frigate will constantly try to determine if that motion is from a person or other object you are tracking. Those detections not only increase your average CPU usage, but also clog the pipeline for detecting objects elsewhere. If you are experiencing high values for `detection_fps` when no objects of interest are in the cameras, you should use masks to tell frigate to ignore movement from trees, bushes, timestamps, or any part of the image where detections should not be wasted looking for objects.
|
||||
|
||||
### FFmpeg Hardware Acceleration
|
||||
|
||||
Frigate works on Raspberry Pi 3b/4 and x86 machines. It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible.
|
||||
|
||||
Raspberry Pi 3/4 (32-bit OS)
|
||||
**NOTICE**: If you are using the addon, ensure you turn off `Protection mode` for hardware acceleration.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_mmal
|
||||
```
|
||||
|
||||
Raspberry Pi 3/4 (64-bit OS)
|
||||
**NOTICE**: If you are using the addon, ensure you turn off `Protection mode` for hardware acceleration.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -c:v
|
||||
- h264_v4l2m2m
|
||||
```
|
||||
|
||||
Intel-based CPUs (<10th Generation) via Quicksync (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
- -hwaccel_output_format
|
||||
- yuv420p
|
||||
```
|
||||
|
||||
Intel-based CPUs (>=10th Generation) via Quicksync (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- qsv
|
||||
- -qsv_device
|
||||
- /dev/dri/renderD128
|
||||
```
|
||||
|
||||
AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver (https://trac.ffmpeg.org/wiki/Hardware/QuickSync)
|
||||
**Note:** You also need to set `LIBVA_DRIVER_NAME=radeonsi` as an environment variable on the container.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
- -hwaccel
|
||||
- vaapi
|
||||
- -hwaccel_device
|
||||
- /dev/dri/renderD128
|
||||
```
|
||||
|
||||
Nvidia GPU based decoding via NVDEC is supported, but requires special configuration. See the [nvidia NVDEC documentation](/configuration/nvdec) for more details.
|
||||
25
docs/docs/configuration/record.md
Normal file
25
docs/docs/configuration/record.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
id: record
|
||||
title: Recording
|
||||
---
|
||||
|
||||
Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy in the config. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
|
||||
|
||||
H265 recordings can be viewed in Edge and Safari only. All other browsers require recordings to be encoded with H264.
|
||||
|
||||
## What if I don't want 24/7 recordings?
|
||||
|
||||
If you only used clips in previous versions with recordings disabled, you can use the following config to get the same behavior. This is also the default behavior when recordings are enabled.
|
||||
|
||||
```yaml
|
||||
record:
|
||||
enabled: True
|
||||
retain_days: 0
|
||||
events:
|
||||
retain:
|
||||
default: 10
|
||||
```
|
||||
|
||||
This configuration will retain recording segments that overlap with events for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs.
|
||||
|
||||
When `retain_days` is set to `0`, events will have up to `max_seconds` (defaults to 5 minutes) of recordings retained. Increasing `retain_days` to `1` will allow events to exceed the `max_seconds` limitation of up to 1 day.
|
||||
8
docs/docs/configuration/rtmp.md
Normal file
8
docs/docs/configuration/rtmp.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
id: rtmp
|
||||
title: RTMP
|
||||
---
|
||||
|
||||
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. 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.
|
||||
6
docs/docs/configuration/snapshots.md
Normal file
6
docs/docs/configuration/snapshots.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
id: snapshots
|
||||
title: Snapshots
|
||||
---
|
||||
|
||||
Frigate can save a snapshot image to `/media/frigate/clips` for each event named as `<camera>-<id>.jpg`.
|
||||
34
docs/docs/configuration/zones.md
Normal file
34
docs/docs/configuration/zones.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: zones
|
||||
title: Zones
|
||||
---
|
||||
|
||||
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
|
||||
|
||||
During testing, enable the Zones option for the debug feed so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
|
||||
|
||||
To create a zone, follow [the steps for a "Motion mask"](/configuration/masks), but use the section of the web UI for creating a zone instead.
|
||||
|
||||
### Restricting zones to specific objects
|
||||
|
||||
Sometimes you want to limit a zone to specific object types to have more granular control of when events/snapshots are saved. The following example will limit one zone to person objects and the other to cars.
|
||||
|
||||
```yaml
|
||||
camera:
|
||||
record:
|
||||
events:
|
||||
required_zones:
|
||||
- entire_yard
|
||||
- front_yard_street
|
||||
zones:
|
||||
entire_yard:
|
||||
coordinates: ... (everywhere you want a person)
|
||||
objects:
|
||||
- person
|
||||
front_yard_street:
|
||||
coordinates: ... (just the street)
|
||||
objects:
|
||||
- car
|
||||
```
|
||||
|
||||
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get clips for person objects that enter anywhere in the yard, and clips for cars only if they enter the street.
|
||||
@@ -63,17 +63,17 @@ cameras:
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
- clips
|
||||
height: 1080
|
||||
width: 1920
|
||||
fps: 5
|
||||
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 clips and 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.
|
||||
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
|
||||
|
||||
|
||||
34
docs/docs/faqs.md
Normal file
34
docs/docs/faqs.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: faqs
|
||||
title: Frequently Asked Questions
|
||||
---
|
||||
|
||||
### Fatal Python error: Bus error
|
||||
|
||||
This error message is due to a shm-size that is too small. Try updating your shm-size according to [this guide](/installation#calculating-required-shm-size).
|
||||
|
||||
### 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 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 recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically.
|
||||
|
||||
### "On connect called"
|
||||
|
||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||
47
docs/docs/guides/camera_setup.md
Normal file
47
docs/docs/guides/camera_setup.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: camera_setup
|
||||
title: Camera setup
|
||||
---
|
||||
|
||||
Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but far less compatibility. Safari and Edge are the only browsers able to play H.265. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around.
|
||||
|
||||
- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The recommended frame rate is 5fps, but may need to be higher for very fast moving objects. Higher resolutions and frame rates will drive higher CPU usage on your server.
|
||||
|
||||
- **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed to 15 fps.
|
||||
|
||||
- **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections.
|
||||
|
||||
### Choosing a detect resolution
|
||||
|
||||
The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions.
|
||||
|
||||
Larger resolutions **do** improve performance if the objects are very small in the frame.
|
||||
|
||||

|
||||
|
||||
### Example Camera Configuration
|
||||
|
||||
For the Dahua/Loryta 5442 camera, I use the following settings:
|
||||
|
||||
**Main Stream (Recording)**
|
||||
|
||||
- Encode Mode: H.264
|
||||
- Resolution: 2688\*1520
|
||||
- Frame Rate(FPS): 15
|
||||
- I Frame Interval: 30
|
||||
|
||||
**Sub Stream 1 (RTMP)**
|
||||
|
||||
- Enable: Sub Stream 1
|
||||
- Encode Mode: H.264
|
||||
- Resolution: 720\*576
|
||||
- Frame Rate: 10
|
||||
- I Frame Interval: 10
|
||||
|
||||
**Sub Stream 2 (Detection)**
|
||||
|
||||
- Enable: Sub Stream 2
|
||||
- Encode Mode: H.264
|
||||
- Resolution: 1280\*720
|
||||
- Frame Rate: 5
|
||||
- I Frame Interval: 5
|
||||
193
docs/docs/guides/getting_started.md
Normal file
193
docs/docs/guides/getting_started.md
Normal file
@@ -0,0 +1,193 @@
|
||||
---
|
||||
id: getting_started
|
||||
title: Creating a config file
|
||||
---
|
||||
|
||||
This guide walks through the steps to build a configuration file for Frigate. It assumes that you already have an environment setup as described in [Installation](/installation). You should also configure your cameras according to the [camera setup guide](/guides/camera_setup)
|
||||
|
||||
### Step 1: Configure the MQTT server
|
||||
|
||||
Frigate requires a functioning MQTT server. Start by adding the mqtt section at the top level in your config:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: <ip of your mqtt server>
|
||||
```
|
||||
|
||||
If using the Mosquitto Addon in Home Assistant, a username and password is required. For example:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: <ip of your mqtt server>
|
||||
user: <username>
|
||||
password: <password>
|
||||
```
|
||||
|
||||
Frigate supports many configuration options for mqtt. See the [configuration reference](/configuration/index#full-configuration-reference) for more info.
|
||||
|
||||
### Step 2: Configure detectors
|
||||
|
||||
By default, Frigate will use a single CPU detector. If you have a USB Coral, you will need to add a detectors section to your config.
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: <ip of your mqtt server>
|
||||
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
```
|
||||
|
||||
More details on available detectors can be found [here](/configuration/detectors).
|
||||
|
||||
### Step 3: Add a minimal camera configuration
|
||||
|
||||
Now let's add the first camera:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: <ip of your mqtt server>
|
||||
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
|
||||
cameras:
|
||||
camera_1: # <------ Name the camera
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://10.0.10.10:554/rtsp # <----- Update for your camera
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
detect:
|
||||
width: 1280 # <---- update for your camera's resolution
|
||||
height: 720 # <---- update for your camera's resolution
|
||||
```
|
||||
|
||||
### Step 4: Start Frigate
|
||||
|
||||
At this point you should be able to start Frigate and see the the video feed in the UI.
|
||||
|
||||
If you get a green image from the camera, this means ffmpeg was not able to get the video feed from your camera. Check the logs for error messages from ffmpeg. The default ffmpeg arguments are designed to work with RTSP cameras that support TCP connections. FFmpeg arguments for other types of cameras can be found [here](/configuration/camera_specific).
|
||||
|
||||
### Step 5: Configure hardware acceleration (optional)
|
||||
|
||||
Now that you have a working camera configuration, you want to setup hardware acceleration to minimize the CPU required to decode your video streams. See the [hardware acceleration](/configuration/hardware_acceleration) config reference for examples applicable to your hardware.
|
||||
|
||||
In order to best evaluate the performance impact of hardware acceleration, it is recommended to temporarily disable detection.
|
||||
|
||||
```yaml
|
||||
mqtt: ...
|
||||
|
||||
detectors: ...
|
||||
|
||||
cameras:
|
||||
camera_1:
|
||||
ffmpeg: ...
|
||||
detect:
|
||||
enabled: False
|
||||
...
|
||||
```
|
||||
|
||||
Here is an example configuration with hardware acceleration configured:
|
||||
|
||||
```yaml
|
||||
mqtt: ...
|
||||
|
||||
detectors: ...
|
||||
|
||||
cameras:
|
||||
camera_1:
|
||||
ffmpeg:
|
||||
inputs: ...
|
||||
hwaccel_args: -c:v h264_v4l2m2m
|
||||
detect: ...
|
||||
```
|
||||
|
||||
### Step 6: Setup motion masks
|
||||
|
||||
Now that you have optimized your configuration for decoding the video stream, you will want to check to see where to implement motion masks. To do this, navigate to the camera in the UI, select "Debug" at the top, and enable "Motion boxes" in the options below the video feed. Watch for areas that continuously trigger unwanted motion to be detected. Common areas to mask include camera timestamps and trees that frequently blow in the wind. The goal is to avoid wasting object detection cycles looking at these areas.
|
||||
|
||||
Now that you know where you need to mask, use the "Mask & Zone creator" in the options pane to generate the coordinates needed for your config file. More information about masks can be found [here](/configuration/masks).
|
||||
|
||||
:::caution
|
||||
|
||||
Note that motion masks should not be used to mark out areas where you do not want objects to be detected or to reduce false positives. They do not alter the image sent to object detection, so you can still get events and detections in areas with motion masks. These only prevent motion in these areas from initiating object detection.
|
||||
|
||||
:::
|
||||
|
||||
Your configuration should look similar to this now.
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: mqtt.local
|
||||
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
|
||||
cameras:
|
||||
camera_1:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://10.0.10.10:554/rtsp
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
detect:
|
||||
width: 1280
|
||||
height: 720
|
||||
motion:
|
||||
mask:
|
||||
- 0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432
|
||||
```
|
||||
|
||||
### Step 7: Enable recording (optional)
|
||||
|
||||
To enable recording video, add the `record` role to a stream and enable it in the config.
|
||||
|
||||
```yaml
|
||||
mqtt: ...
|
||||
|
||||
detectors: ...
|
||||
|
||||
cameras:
|
||||
camera_1:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://10.0.10.10:554/rtsp
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
- record # <----- Add role
|
||||
detect: ...
|
||||
record: # <----- Enable recording
|
||||
enabled: True
|
||||
motion: ...
|
||||
```
|
||||
|
||||
By default, Frigate will retain video of all events for 10 days. The full set of options for recording can be found [here](/configuration/index#full-configuration-reference).
|
||||
|
||||
### Step 8: Enable snapshots (optional)
|
||||
|
||||
To enable snapshots of your events, just enable it in the config.
|
||||
|
||||
```yaml
|
||||
mqtt: ...
|
||||
|
||||
detectors: ...
|
||||
|
||||
cameras:
|
||||
camera_1: ...
|
||||
detect: ...
|
||||
record: ...
|
||||
snapshots: # <----- Enable snapshots
|
||||
enabled: True
|
||||
motion: ...
|
||||
```
|
||||
|
||||
By default, Frigate will retain snapshots of all events for 10 days. The full set of options for snapshots can be found [here](/configuration/index#full-configuration-reference).
|
||||
56
docs/docs/guides/ha_notifications.md
Normal file
56
docs/docs/guides/ha_notifications.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: ha_notifications
|
||||
title: Home Assistant notifications
|
||||
---
|
||||
|
||||
The best way to get started with notifications for Frigate is to use the [Blueprint](https://community.home-assistant.io/t/frigate-mobile-app-notifications/311091). You can use the yaml generated from the Blueprint as a starting point and customize from there.
|
||||
|
||||
It is generally recommended to trigger notifications based on the `frigate/events` mqtt topic. This provides the event_id needed to fetch [thumbnails/snapshots/clips](/integrations/home-assistant#notification-api) and other useful information to customize when and where you want to receive alerts. The data is published in the form of a change feed, which means you can reference the "previous state" of the object in the `before` section and the "current state" of the object in the `after` section. You can see an example [here](/integrations/mqtt#frigateevents).
|
||||
|
||||
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: Notify of events
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
|
||||
data:
|
||||
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android'
|
||||
tag: '{{trigger.payload_json["after"]["id"]}}'
|
||||
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
|
||||
```
|
||||
|
||||
## Conditions
|
||||
|
||||
Conditions with the `before` and `after` values allow a high degree of customization for automations.
|
||||
|
||||
When a person enters a zone named yard
|
||||
|
||||
```yaml
|
||||
condition:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
|
||||
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
|
||||
```
|
||||
|
||||
When a person leaves a zone named yard
|
||||
|
||||
```yaml
|
||||
condition:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
|
||||
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
|
||||
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
|
||||
```
|
||||
|
||||
Notify for dogs in the front with a high top score
|
||||
|
||||
```yaml
|
||||
condition:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
|
||||
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
|
||||
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
|
||||
```
|
||||
37
docs/docs/guides/stationary_objects.md
Normal file
37
docs/docs/guides/stationary_objects.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: stationary_objects
|
||||
title: Avoiding stationary objects
|
||||
---
|
||||
|
||||
Many people use Frigate to detect cars entering their driveway, and they often run into an issue with repeated events of a parked car being repeatedly detected. This is because object tracking stops when motion ends and the event ends. Motion detection works by determining if a sufficient number of pixels have changed between frames. Shadows or other lighting changes will be detected as motion. This will often cause a new event for a parked car.
|
||||
|
||||
You can use zones to restrict events and notifications to objects that have entered specific areas.
|
||||
|
||||
:::caution
|
||||
|
||||
It is not recommended to use masks to try and eliminate parked cars in your driveway. Masks are designed to prevent motion from triggering object detection and/or to indicate areas that are guaranteed false positives.
|
||||
|
||||
Frigate is designed to track objects as they move and over-masking can prevent it from knowing that an object in the current frame is the same as the previous frame. You want Frigate to detect objects everywhere and configure your events and alerts to be based on the location of the object with zones.
|
||||
|
||||
:::
|
||||
|
||||
For example, you could create multiple zones that cover your driveway. For cars, you would only notify if entered_zones has more than 1 zone. For person, you would notify regardless of the number of entered_zones.
|
||||
|
||||
See [this example](/configuration/zones#restricting-zones-to-specific-objects) from the Zones documentation.
|
||||
|
||||
You can also create a zone for the entrance of your driveway and only save an event if that zone is in the list of entered_zones when the object is a car.
|
||||
|
||||

|
||||
|
||||
```yaml
|
||||
camera:
|
||||
record:
|
||||
events:
|
||||
required_zones:
|
||||
- zone_2
|
||||
zones:
|
||||
zone_1:
|
||||
coordinates: ... (parking area)
|
||||
zone_2:
|
||||
coordinates: ... (entrance to driveway)
|
||||
```
|
||||
@@ -5,25 +5,36 @@ 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
|
||||
I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options.
|
||||
|
||||
Here are some of the camera's I recommend:
|
||||
|
||||
- [Loryta(Dahua) T5442TM-AS-LED](https://www.amazon.com/Loryta-IPC-T5442TM-AS-LED-Starlight-Eyeball-Network/dp/B07S5QZJDH/)
|
||||
- [Loryta(Dahua) IPC-T5442TM-AS](https://www.amazon.com/Loryta-IPC-T5442TM-AS-Starlight-Eyeball-Network/dp/B07S21FVC7/)
|
||||
- [Amcrest IP5M-T1179EW-28MM](https://www.amazon.com/Amcrest-5-Megapixel-NightVision-Weatherproof-IP5M-T1179EW-28MM/dp/B083G9KT4C/)
|
||||
|
||||
## Server
|
||||
|
||||
My current favorite is the Minisforum GK41 because the dual NICs allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet.
|
||||
|
||||
| Name | Inference Speed | Notes |
|
||||
| ----------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Atomic Pi | 16ms | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
|
||||
| Minisforum GK41 | 9-10ms | Great alternative to a NUC with dual Gigabit NICs. Easily handles several 1080p cameras. |
|
||||
| Intel NUC NUC7i3BNK | 8-10ms | Great performance. Can handle many cameras at 5fps depending on typical amounts of motion. |
|
||||
| BMAX B2 Plus | 10-12ms | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
|
||||
| Minisforum GK41 | 9-10ms | Great alternative to a NUC with dual Gigabit NICs. Easily handles several 1080p cameras. |
|
||||
| Atomic Pi | 16ms | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
|
||||
| Raspberry Pi 3B (32bit) | 60ms | Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0. |
|
||||
| Raspberry Pi 4 (32bit) | 15-20ms | Can handle a small number of cameras. The 2GB version runs fine. |
|
||||
| Raspberry Pi 4 (64bit) | 10-15ms | Can handle a small number of cameras. The 2GB version runs fine. |
|
||||
|
||||
## Unraid
|
||||
## Google Coral TPU
|
||||
|
||||
Many people have powerful enough NAS devices or home servers to also run docker. There is a Unraid Community App.
|
||||
To install make sure you have the [community app plugin here](https://forums.unraid.net/topic/38582-plug-in-community-applications/). Then search for "Frigate" in the apps section within Unraid - you can see the online store [here](https://unraid.net/community/apps?q=frigate#r)
|
||||
It is strongly recommended to use a Google Coral. Frigate is designed around the expectation that a Coral is used to achieve very low inference speeds. Offloading TensorFlow to the Google Coral is an order of magnitude faster and will reduce your CPU load dramatically. A $60 device will outperform $2000 CPU. Frigate should work with any supported Coral device from https://coral.ai
|
||||
|
||||
| Name | Inference Speed | Notes |
|
||||
| ------------------------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [M2 Coral Edge TPU](http://coral.ai) | 6.2ms | Install the Coral plugin from Unraid Community App Center [info here](https://forums.unraid.net/topic/98064-support-blakeblackshear-frigate/?do=findComment&comment=949789) |
|
||||
The USB version is compatible with the widest variety of hardware and does not require a driver on the host machine. However, it does lack the automatic throttling features of the other versions.
|
||||
|
||||
The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai
|
||||
|
||||
A single Coral can handle many cameras and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
id: how-it-works
|
||||
title: How Frigate Works
|
||||
sidebar_label: How it works
|
||||
---
|
||||
|
||||
Frigate is designed to minimize resource and maximize performance by only looking for objects when and where it is necessary
|
||||
|
||||

|
||||
|
||||
1. Look for Motion
|
||||
2. Calculate Detection Regions
|
||||
3. Run Object Detection
|
||||
@@ -1,13 +1,12 @@
|
||||
---
|
||||
id: index
|
||||
title: Frigate
|
||||
sidebar_label: Features
|
||||
title: Introduction
|
||||
slug: /
|
||||
---
|
||||
|
||||
A complete and local NVR designed for Home Assistant with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras.
|
||||
|
||||
Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but highly recommended. The Coral will outperform even the best CPUs and can process 100+ FPS with very little overhead.
|
||||
Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but strongly recommended. CPU detection should only be used for testing purposes. The Coral will outperform even the best CPUs and can process 100+ FPS with very little overhead.
|
||||
|
||||
- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration)
|
||||
- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
|
||||
@@ -15,8 +14,9 @@ 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
|
||||
- 24/7 recording
|
||||
- Recording with retention based on detected objects
|
||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||
- A dynamic combined camera view of all tracked cameras.
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
||||
@@ -3,25 +3,42 @@ id: installation
|
||||
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.
|
||||
Frigate is a Docker container that can be run on any Docker host including as a [HassOS Addon](https://www.home-assistant.io/addons/).
|
||||
|
||||
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.
|
||||
Frigate requires an MQTT broker. If using the Home Assistant integration, Frigate and Home Assistant must be connected to the same MQTT server to function properly.
|
||||
|
||||
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.
|
||||
## Preparing your hardware
|
||||
|
||||
## HassOS Addon
|
||||
### Operating System
|
||||
|
||||
HassOS users can install via the addon repository. Frigate requires an MQTT server.
|
||||
Frigate runs best with docker installed on bare metal debian-based distributions. For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer often introduces a sizable amount of overhead for communication with Coral devices.
|
||||
|
||||
1. Navigate to Supervisor > Add-on Store > Repositories
|
||||
2. Add https://github.com/blakeblackshear/frigate-hass-addons
|
||||
3. Setup your network configuration in the `Configuration` tab if deisred
|
||||
4. Create the file `frigate.yml` in your `config` directory with your detailed Frigate configuration
|
||||
5. Start the addon container
|
||||
6. If you are using hardware acceleration for ffmpeg, you will need to disable "Protection mode"
|
||||
Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help.
|
||||
|
||||
### Calculating required shm-size
|
||||
|
||||
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m.
|
||||
|
||||
The default shm-size of 64m is fine for setups with 2 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula:
|
||||
|
||||
```
|
||||
(width * height * 1.5 * 9 + 270480)/1048576 = <shm size in mb>
|
||||
```
|
||||
|
||||
The shm size cannot be set per container for Home Assistant Addons. You must set `default-shm-size` in `/etc/docker/daemon.json` to increase the default shm size. This will increase the shm size for all of your docker containers. This may or may not cause issues with your setup. https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
|
||||
|
||||
### Raspberry Pi 3/4
|
||||
|
||||
By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options).
|
||||
|
||||
Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with [this](https://www.amazon.com/-/en/RSHTECH-Active-Splitter-Lightweight-Portable/dp/B091F7C5K4).
|
||||
|
||||
## Docker
|
||||
|
||||
Running in Docker directly is the recommended install method.
|
||||
|
||||
Make sure you choose the right image for your architecture:
|
||||
|
||||
| Arch | Image Name |
|
||||
@@ -41,13 +58,14 @@ services:
|
||||
privileged: true # this may not be necessary for all setups
|
||||
restart: unless-stopped
|
||||
image: blakeblackshear/frigate:<specify_version_tag>
|
||||
shm_size: "64mb" # update for your cameras based on calculation above
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
|
||||
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- <path_to_config_file>:/config/config.yml:ro
|
||||
- <path_to_directory_for_media>:/media/frigate
|
||||
- /path/to/your/config.yml:/config/config.yml:ro
|
||||
- /path/to/your/storage:/media/frigate
|
||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
@@ -68,8 +86,9 @@ docker run -d \
|
||||
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \
|
||||
--device /dev/bus/usb:/dev/bus/usb \
|
||||
--device /dev/dri/renderD128 \
|
||||
-v <path_to_directory_for_media>:/media/frigate \
|
||||
-v <path_to_config_file>:/config/config.yml:ro \
|
||||
--shm-size=64m \
|
||||
-v /path/to/your/storage:/media/frigate \
|
||||
-v /path/to/your/config.yml:/config/config.yml:ro \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-e FRIGATE_RTSP_PASSWORD='password' \
|
||||
-p 5000:5000 \
|
||||
@@ -77,48 +96,62 @@ docker run -d \
|
||||
blakeblackshear/frigate:<specify_version_tag>
|
||||
```
|
||||
|
||||
### Calculating shm-size
|
||||
## Home Assistant Operating System (HassOS)
|
||||
|
||||
The default shm-size of 64m is fine for setups with 3 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it could be because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
:::caution
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula:
|
||||
Due to limitations in Home Assistant Operating System, Frigate cannot utilize external storage for recordings or snapshots.
|
||||
|
||||
```
|
||||
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
|
||||
:::
|
||||
|
||||
:::tip
|
||||
|
||||
If possible, it is recommended to run Frigate standalone in Docker and use [Frigate's Proxy Addon](https://github.com/blakeblackshear/frigate-hass-addons/blob/main/frigate_proxy/README.md).
|
||||
|
||||
:::
|
||||
|
||||
HassOS users can install via the addon repository.
|
||||
|
||||
1. Navigate to Supervisor > Add-on Store > Repositories
|
||||
2. Add https://github.com/blakeblackshear/frigate-hass-addons
|
||||
3. Install your desired Frigate NVR Addon and navigate to it's page
|
||||
4. Setup your network configuration in the `Configuration` tab
|
||||
5. (not for proxy addon) Create the file `frigate.yml` in your `config` directory with your detailed Frigate configuration
|
||||
6. Start the addon container
|
||||
7. (not for proxy addon) If you are using hardware acceleration for ffmpeg, you may need to disable "Protection mode"
|
||||
|
||||
## Home Assistant Supervised
|
||||
|
||||
:::tip
|
||||
|
||||
If possible, it is recommended to run Frigate standalone in Docker and use [Frigate's Proxy Addon](https://github.com/blakeblackshear/frigate-hass-addons/blob/main/frigate_proxy/README.md).
|
||||
|
||||
:::
|
||||
|
||||
When running Home Assistant with the [Supervised install method](https://github.com/home-assistant/supervised-installer), you can get the benefit of running the Addon along with the ability to customize the storage used by Frigate.
|
||||
|
||||
In order to customize the storage location for Frigate, simply use `fstab` to mount the drive you want at `/usr/share/hassio/media`. Here is an example fstab entry:
|
||||
|
||||
```shell
|
||||
UUID=1a65fec6-c25f-404a-b3d2-1f2fcf6095c8 /media/data ext4 defaults 0 0
|
||||
/media/data/homeassistant/media /usr/share/hassio/media none bind 0 0
|
||||
```
|
||||
|
||||
The shm size cannot be set per container for Home Assistant Addons. You must set `default-shm-size` in `/etc/docker/daemon.json` to increase the default shm size. This will increase the shm size for all of your docker containers. This may or may not cause issues with your setup. https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
|
||||
Then follow the instructions listed for [Home Assistant Operating System](#home-assistant-operating-system-hassos).
|
||||
|
||||
## Kubernetes
|
||||
|
||||
Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate).
|
||||
|
||||
## Virtualization
|
||||
## Unraid
|
||||
|
||||
For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices for ffmpeg decoding. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer typically introduces a sizable amount of overhead for communication with Coral devices.
|
||||
Many people have powerful enough NAS devices or home servers to also run docker. There is a Unraid Community App.
|
||||
To install make sure you have the [community app plugin here](https://forums.unraid.net/topic/38582-plug-in-community-applications/). Then search for "Frigate" in the apps section within Unraid - you can see the online store [here](https://unraid.net/community/apps?q=frigate#r)
|
||||
|
||||
### Proxmox
|
||||
## Proxmox
|
||||
|
||||
Some people have had success running Frigate in LXC directly with the following config:
|
||||
It is recommended to run Frigate in LXC for maximum performance. See [this discussion](https://github.com/blakeblackshear/frigate/discussions/1111) for more information.
|
||||
|
||||
```
|
||||
arch: amd64
|
||||
cores: 2
|
||||
features: nesting=1
|
||||
hostname: FrigateLXC
|
||||
memory: 4096
|
||||
net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=2E:76:AE:5A:58:48,ip=dhcp,ip6=auto,type=veth
|
||||
ostype: debian
|
||||
rootfs: local-lvm:vm-115-disk-0,size=12G
|
||||
swap: 512
|
||||
lxc.cgroup.devices.allow: c 189:385 rwm
|
||||
lxc.mount.entry: /dev/dri/renderD128 dev/dri/renderD128 none bind,optional,create=file
|
||||
lxc.mount.entry: /dev/bus/usb/004/002 dev/bus/usb/004/002 none bind,optional,create=file
|
||||
lxc.apparmor.profile: unconfined
|
||||
lxc.cgroup.devices.allow: a
|
||||
lxc.cap.drop:
|
||||
```
|
||||
|
||||
### ESX
|
||||
## ESX
|
||||
|
||||
For details on running Frigate under ESX, see details [here](https://github.com/blakeblackshear/frigate/issues/305).
|
||||
|
||||
@@ -192,6 +192,10 @@ Permanently deletes the event along with any clips/snapshots.
|
||||
|
||||
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.
|
||||
|
||||
### `GET /api/events/<id>/clip.mp4`
|
||||
|
||||
Returns the clip for the event id. Works after the event has ended.
|
||||
|
||||
### `GET /api/events/<id>/snapshot.jpg`
|
||||
|
||||
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
|
||||
@@ -206,14 +210,22 @@ Accepts the following query string parameters, but they are only applied when an
|
||||
| `crop` | int | Crop the snapshot to the (0 or 1) |
|
||||
| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
|
||||
|
||||
### `/clips/<camera>-<id>.mp4`
|
||||
|
||||
Video clip for the given camera and event id.
|
||||
|
||||
### `/clips/<camera>-<id>.jpg`
|
||||
### `GET /clips/<camera>-<id>.jpg`
|
||||
|
||||
JPG snapshot for the given camera and event id.
|
||||
|
||||
### `/vod/<year>-<month>/<day>/<hour>/<camera>/master.m3u8`
|
||||
### `GET /vod/<year>-<month>/<day>/<hour>/<camera>/master.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the specified hour and camera. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /vod/event/<event-id>/index.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /vod/event/<event-id>/index.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC.
|
||||
189
docs/docs/integrations/home-assistant.md
Normal file
189
docs/docs/integrations/home-assistant.md
Normal file
@@ -0,0 +1,189 @@
|
||||
---
|
||||
id: home-assistant
|
||||
title: Home Assistant Integration
|
||||
---
|
||||
|
||||
The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration).
|
||||
|
||||
## Installation
|
||||
|
||||
### Preparation
|
||||
|
||||
The Frigate integration requires the `mqtt` integration to be installed and
|
||||
manually configured first.
|
||||
|
||||
See the [MQTT integration
|
||||
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
|
||||
details.
|
||||
|
||||
### Integration installation
|
||||
|
||||
Available via HACS as a default repository. To install:
|
||||
|
||||
- Use [HACS](https://hacs.xyz/) to install the integration:
|
||||
|
||||
```
|
||||
Home Assistant > HACS > Integrations > "Explore & Add Integrations" > Frigate
|
||||
```
|
||||
|
||||
- Restart Home Assistant.
|
||||
- Then add/configure the integration:
|
||||
|
||||
```
|
||||
Home Assistant > Configuration > Integrations > Add Integration > Frigate
|
||||
```
|
||||
|
||||
Note: You will also need
|
||||
[media_source](https://www.home-assistant.io/integrations/media_source/) enabled
|
||||
in your Home Assistant configuration for the Media Browser to appear.
|
||||
|
||||
### (Optional) Lovelace Card Installation
|
||||
|
||||
To install the optional companion Lovelace card, please see the [separate
|
||||
installation instructions](https://github.com/dermotduffy/frigate-hass-card) for
|
||||
that card.
|
||||
|
||||
## Configuration
|
||||
|
||||
When configuring the integration, you will be asked for the following parameters:
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| URL | The `URL` of your frigate instance, the URL you use to access Frigate in the browser. This may look like `http://<host>:5000/`. If you are using HassOS with the addon, the URL should be `http://ccab4aaf-frigate:5000` (or `http://ccab4aaf-frigate-beta:5000` if your are using the beta version of the addon). Live streams required port 1935, see [RTMP streams](#streams) |
|
||||
|
||||
<a name="options"></a>
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
Home Assistant > Configuration > Integrations > Frigate > Options
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| RTMP URL Template | A [jinja2](https://jinja.palletsprojects.com/) template that is used to override the standard RTMP stream URL (e.g. for use with reverse proxies). This option is only shown to users who have [advanced mode](https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode) enabled. See [RTMP streams](#streams) below. |
|
||||
|
||||
## Entities Provided
|
||||
|
||||
| Platform | Description |
|
||||
| --------------- | --------------------------------------------------------------------------------- |
|
||||
| `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, recordings and snapshots. |
|
||||
| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. |
|
||||
|
||||
## Media Browser Support
|
||||
|
||||
The integration provides:
|
||||
|
||||
- Browsing event recordings with thumbnails
|
||||
- Browsing snapshots
|
||||
- Browsing recordings by month, day, camera, time
|
||||
|
||||
This is accessible via "Media Browser" on the left menu panel in Home Assistant.
|
||||
|
||||
<a name="api"></a>
|
||||
|
||||
## Notification API
|
||||
|
||||
Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications.
|
||||
|
||||
To load a thumbnail for an event:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/thumbnail.jpg
|
||||
```
|
||||
|
||||
To load a snapshot for an event:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/snapshot.jpg
|
||||
```
|
||||
|
||||
To load a video clip of an event:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/clip.mp4
|
||||
```
|
||||
|
||||
<a name="streams"></a>
|
||||
|
||||
## RTMP stream
|
||||
|
||||
In order for the live streams to function they need to be accessible on the RTMP
|
||||
port (default: `1935`) at `<frigatehost>:1935`. Home Assistant will directly
|
||||
connect to that streaming port when the live camera is viewed.
|
||||
|
||||
#### RTMP URL Template
|
||||
|
||||
For advanced usecases, this behavior can be changed with the [RTMP URL
|
||||
template](#options) option. When set, this string will override the default stream
|
||||
address that is derived from the default behavior described above. This option supports
|
||||
[jinja2 templates](https://jinja.palletsprojects.com/) and has the `camera` dict
|
||||
variables from [Frigate API](https://blakeblackshear.github.io/frigate/usage/api#apiconfig)
|
||||
available for the template. Note that no Home Assistant state is available to the
|
||||
template, only the camera dict from Frigate.
|
||||
|
||||
This is potentially useful when Frigate is behind a reverse proxy, and/or when
|
||||
the default stream port is otherwise not accessible to Home Assistant (e.g.
|
||||
firewall rules).
|
||||
|
||||
###### RTMP URL Template Examples
|
||||
|
||||
Use a different port number:
|
||||
|
||||
```
|
||||
rtmp://<frigate_host>:2000/live/front_door
|
||||
```
|
||||
|
||||
Use the camera name in the stream URL:
|
||||
|
||||
```
|
||||
rtmp://<frigate_host>:2000/live/{{ name }}
|
||||
```
|
||||
|
||||
Use the camera name in the stream URL, converting it to lowercase first:
|
||||
|
||||
```
|
||||
rtmp://<frigate_host>:2000/live/{{ name|lower }}
|
||||
```
|
||||
|
||||
## Multiple Instance Support
|
||||
|
||||
The Frigate integration seamlessly supports the use of multiple Frigate servers.
|
||||
|
||||
### Requirements for Multiple Instances
|
||||
|
||||
In order for multiple Frigate instances to function correctly, the
|
||||
`topic_prefix` and `client_id` parameters must be set differently per server.
|
||||
See [MQTT
|
||||
configuration](https://blakeblackshear.github.io/frigate/configuration/index#mqtt)
|
||||
for how to set these.
|
||||
|
||||
#### API URLs
|
||||
|
||||
When multiple Frigate instances are configured, [API](#api) URLs should include an
|
||||
identifier to tell Home Assistant which Frigate instance to refer to. The
|
||||
identifier used is the MQTT `client_id` paremeter included in the configuration,
|
||||
and is used like so:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/<client-id>/notifications/<event-id>/thumbnail.jpg
|
||||
```
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/<client-id>/clips/front_door-1624599978.427826-976jaa.mp4
|
||||
```
|
||||
|
||||
#### Default Treatment
|
||||
|
||||
When a single Frigate instance is configured, the `client-id` parameter need not
|
||||
be specified in URLs/identifiers -- that single instance is assumed. When
|
||||
multiple Frigate instances are configured, the user **must** explicitly specify
|
||||
which server they are referring to.
|
||||
|
||||
## FAQ
|
||||
|
||||
#### If I am detecting multiple objects, how do I assign the correct `binary_sensor` to the camera in HomeKit?
|
||||
|
||||
The [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) randomly links one of the binary sensors (motion sensor entities) grouped with the camera device in Home Assistant. You can specify a `linked_motion_sensor` in the Home Assistant [HomeKit configuration](https://www.home-assistant.io/integrations/homekit/#linked_motion_sensor) for each camera.
|
||||
@@ -36,7 +36,7 @@ Message published for each changed event. The first message is published when th
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "update", // new, update, end or clip_ready
|
||||
"type": "update", // new, update, end
|
||||
"before": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
@@ -53,7 +53,9 @@ Message published for each changed event. The first message is published when th
|
||||
"region": [264, 450, 667, 853],
|
||||
"current_zones": ["driveway"],
|
||||
"entered_zones": ["yard", "driveway"],
|
||||
"thumbnail": null
|
||||
"thumbnail": null,
|
||||
"has_snapshot": false,
|
||||
"has_clip": false
|
||||
},
|
||||
"after": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
@@ -71,7 +73,9 @@ Message published for each changed event. The first message is published when th
|
||||
"region": [218, 440, 693, 915],
|
||||
"current_zones": ["yard", "driveway"],
|
||||
"entered_zones": ["yard", "driveway"],
|
||||
"thumbnail": null
|
||||
"thumbnail": null,
|
||||
"has_snapshot": false,
|
||||
"has_clip": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
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).
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
### "[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.
|
||||
|
||||
### "On connect called"
|
||||
|
||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||
@@ -1,133 +0,0 @@
|
||||
---
|
||||
id: home-assistant
|
||||
title: Integration with Home Assistant
|
||||
sidebar_label: Home Assistant
|
||||
---
|
||||
|
||||
The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration). When configuring the integration, you will be asked for the `Host` of your frigate instance. This value should be the url you use to access Frigate in the browser and will look like `http://<host>:5000/`. If you are using HassOS with the addon, the host should be `http://ccab4aaf-frigate:5000` (or `http://ccab4aaf-frigate-beta:5000` if your are using the beta version of the addon). Home Assistant needs access to port 5000 (api) and 1935 (rtmp) for all features. The integration will setup the following entities within Home Assistant:
|
||||
|
||||
## Sensors:
|
||||
|
||||
- Stats to monitor frigate performance
|
||||
- Object counts for all zones and cameras
|
||||
|
||||
## Cameras:
|
||||
|
||||
- Cameras for image of the last detected object for each camera
|
||||
- Camera entities with stream support (requires RTMP)
|
||||
|
||||
## Media Browser:
|
||||
|
||||
- Rich UI with thumbnails for browsing event clips
|
||||
- Rich UI for browsing 24/7 recordings by month, day, camera, time
|
||||
|
||||
## API:
|
||||
|
||||
- Notification API with public facing endpoints for images in notifications
|
||||
|
||||
### Notifications
|
||||
|
||||
Frigate publishes event information in the form of a change feed via MQTT. This allows lots of customization for notifications to meet your needs. Event changes are published with `before` and `after` information as shown [here](#frigateevents).
|
||||
Note that some people may not want to expose frigate to the web, so you can leverage the HA API that frigate custom_integration ties into (which is exposed to the web, and thus can be used for mobile notifications etc):
|
||||
|
||||
To load an image taken by frigate from Home Assistants API see below:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/thumbnail.jpg
|
||||
```
|
||||
|
||||
To load a video clip taken by frigate from Home Assistants API :
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/<camera>/clip.mp4
|
||||
```
|
||||
|
||||
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: Notify of events
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
|
||||
data:
|
||||
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android'
|
||||
tag: '{{trigger.payload_json["after"]["id"]}}'
|
||||
```
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: When a person enters a zone named yard
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
condition:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
|
||||
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
- alias: When a person leaves a zone named yard
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
condition:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
|
||||
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
|
||||
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: "A {{trigger.payload_json['after']['label']}} has left the yard."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
```
|
||||
|
||||
```yaml
|
||||
- alias: Notify for dogs in the front with a high top score
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
condition:
|
||||
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
|
||||
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
|
||||
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
|
||||
action:
|
||||
- service: notify.mobile_app_pixel_3
|
||||
data_template:
|
||||
message: "High confidence dog detection."
|
||||
data:
|
||||
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
|
||||
tag: "{{trigger.payload_json['after']['id']}}"
|
||||
```
|
||||
|
||||
If you are using telegram, you can fetch the image directly from Frigate:
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: Notify of events
|
||||
trigger:
|
||||
platform: mqtt
|
||||
topic: frigate/events
|
||||
action:
|
||||
- service: notify.telegram_full
|
||||
data_template:
|
||||
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
|
||||
data:
|
||||
photo:
|
||||
# this url should work for addon users
|
||||
- url: 'http://ccab4aaf-frigate:5000/api/events/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg'
|
||||
caption: 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
|
||||
```
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
id: howtos
|
||||
title: Community Guides
|
||||
sidebar_label: Community Guides
|
||||
---
|
||||
|
||||
## Communitiy Guides/How-To's
|
||||
|
||||
- Best Camera AI Person & Object Detection - How to Setup Frigate w/ Home Assistant - digiblurDIY [YouTube](https://youtu.be/V8vGdoYO6-Y) - [Article](https://www.digiblur.com/2021/05/how-to-setup-frigate-home-assistant.html)
|
||||
- Even More Free Local Object Detection with Home Assistant - Frigate Install - Everything Smart Home [YouTube](https://youtu.be/pqDCEZSVeRk)
|
||||
- Home Assistant Frigate integration for local image recognition - KPeyanski [YouTube](https://youtu.be/Q2UT78lFQpo) - [Article](https://peyanski.com/home-assistant-frigate-integration/)
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
id: web
|
||||
title: Web Interface
|
||||
---
|
||||
|
||||
Frigate comes bundled with a simple web ui that supports the following:
|
||||
|
||||
- Show cameras
|
||||
- Browse events
|
||||
- Mask helper
|
||||
9129
docs/package-lock.json
generated
9129
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,8 @@
|
||||
"clear": "docusaurus clear"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-alpha.70",
|
||||
"@docusaurus/preset-classic": "2.0.0-alpha.70",
|
||||
"@docusaurus/core": "^2.0.0-beta.ff31de0ff",
|
||||
"@docusaurus/preset-classic": "^2.0.0-beta.ff31de0ff",
|
||||
"@mdx-js/react": "^1.6.21",
|
||||
"clsx": "^1.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
module.exports = {
|
||||
docs: {
|
||||
Frigate: ['index', 'how-it-works', 'hardware', 'installation', 'troubleshooting'],
|
||||
Frigate: [
|
||||
'index',
|
||||
'hardware',
|
||||
'installation',
|
||||
],
|
||||
Guides: [
|
||||
'guides/camera_setup',
|
||||
'guides/getting_started',
|
||||
'guides/false_positives',
|
||||
'guides/ha_notifications',
|
||||
'guides/stationary_objects',
|
||||
],
|
||||
Configuration: [
|
||||
'configuration/index',
|
||||
'configuration/cameras',
|
||||
'configuration/optimizing',
|
||||
'configuration/detectors',
|
||||
'configuration/false_positives',
|
||||
'configuration/cameras',
|
||||
'configuration/masks',
|
||||
'configuration/record',
|
||||
'configuration/snapshots',
|
||||
'configuration/objects',
|
||||
'configuration/rtmp',
|
||||
'configuration/zones',
|
||||
'configuration/advanced',
|
||||
'configuration/hardware_acceleration',
|
||||
'configuration/nvdec',
|
||||
'configuration/camera_specific',
|
||||
],
|
||||
Usage: ['usage/home-assistant', 'usage/web', 'usage/api', 'usage/mqtt'],
|
||||
Integrations: ['integrations/home-assistant', 'integrations/api', 'integrations/mqtt'],
|
||||
Troubleshooting: ['faqs'],
|
||||
Development: ['contributing'],
|
||||
},
|
||||
};
|
||||
|
||||
BIN
docs/static/img/driveway_zones.png
vendored
Normal file
BIN
docs/static/img/driveway_zones.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/static/img/resolutions.png
vendored
Normal file
BIN
docs/static/img/resolutions.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
@@ -20,14 +20,14 @@ 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, Recordings
|
||||
from frigate.mqtt import create_mqtt_client, MqttSocketRelay
|
||||
from frigate.mqtt import MqttSocketRelay, create_mqtt_client
|
||||
from frigate.object_processing import TrackedObjectProcessor
|
||||
from frigate.output import output_frames
|
||||
from frigate.record import RecordingCleanup, RecordingMaintainer
|
||||
from frigate.stats import StatsEmitter, stats_init
|
||||
from frigate.version import VERSION
|
||||
from frigate.video import capture_camera, track_camera
|
||||
from frigate.watchdog import FrigateWatchdog
|
||||
from frigate.zeroconf import broadcast_zeroconf
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -90,15 +90,6 @@ class FrigateApp:
|
||||
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."
|
||||
)
|
||||
|
||||
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."
|
||||
@@ -179,6 +170,7 @@ class FrigateApp:
|
||||
self.mqtt_relay.start()
|
||||
|
||||
def start_detectors(self):
|
||||
model_path = self.config.model.path
|
||||
model_shape = (self.config.model.height, self.config.model.width)
|
||||
for name in self.config.cameras.keys():
|
||||
self.detection_out_events[name] = mp.Event()
|
||||
@@ -208,6 +200,7 @@ class FrigateApp:
|
||||
name,
|
||||
self.detection_queue,
|
||||
self.detection_out_events,
|
||||
model_path,
|
||||
model_shape,
|
||||
"cpu",
|
||||
detector.num_threads,
|
||||
@@ -217,6 +210,7 @@ class FrigateApp:
|
||||
name,
|
||||
self.detection_queue,
|
||||
self.detection_out_events,
|
||||
model_path,
|
||||
model_shape,
|
||||
detector.device,
|
||||
detector.num_threads,
|
||||
@@ -321,6 +315,7 @@ class FrigateApp:
|
||||
|
||||
def start(self):
|
||||
self.init_logger()
|
||||
logger.info(f"Starting Frigate ({VERSION})")
|
||||
try:
|
||||
try:
|
||||
self.init_config()
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic.fields import PrivateAttr
|
||||
import yaml
|
||||
from pydantic import BaseModel, Extra, Field, validator
|
||||
from pydantic.fields import PrivateAttr
|
||||
|
||||
from frigate.const import BASE_DIR, RECORD_DIR, CACHE_DIR
|
||||
from frigate.const import BASE_DIR, CACHE_DIR, RECORD_DIR
|
||||
from frigate.edgetpu import load_labels
|
||||
from frigate.util import create_mask, deep_merge
|
||||
|
||||
@@ -26,7 +26,12 @@ DEFAULT_TIME_FORMAT = "%m/%d/%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 = {"coral": {"type": "edgetpu", "device": "usb"}}
|
||||
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
|
||||
|
||||
|
||||
class FrigateBaseModel(BaseModel):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
class DetectorTypeEnum(str, Enum):
|
||||
@@ -34,15 +39,13 @@ class DetectorTypeEnum(str, Enum):
|
||||
cpu = "cpu"
|
||||
|
||||
|
||||
class DetectorConfig(BaseModel):
|
||||
type: DetectorTypeEnum = Field(
|
||||
default=DetectorTypeEnum.edgetpu, title="Detector Type"
|
||||
)
|
||||
class DetectorConfig(FrigateBaseModel):
|
||||
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):
|
||||
class MqttConfig(FrigateBaseModel):
|
||||
host: str = Field(title="MQTT Host")
|
||||
port: int = Field(default=1883, title="MQTT Port")
|
||||
topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix")
|
||||
@@ -62,40 +65,38 @@ class MqttConfig(BaseModel):
|
||||
return v
|
||||
|
||||
|
||||
class RetainConfig(BaseModel):
|
||||
class RetainConfig(FrigateBaseModel):
|
||||
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.")
|
||||
class EventsConfig(FrigateBaseModel):
|
||||
max_seconds: int = Field(default=300, title="Maximum event duration.")
|
||||
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
|
||||
post_capture: int = Field(default=5, title="Seconds to retain 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.",
|
||||
title="List of required zones to be entered in order to save the event.",
|
||||
)
|
||||
objects: Optional[List[str]] = Field(
|
||||
title="List of objects to be detected in order to save the clip.",
|
||||
title="List of objects to be detected in order to save the event.",
|
||||
)
|
||||
retain: RetainConfig = Field(
|
||||
default_factory=RetainConfig, title="Clip retention settings."
|
||||
default_factory=RetainConfig, title="Event retention settings."
|
||||
)
|
||||
|
||||
|
||||
class RecordConfig(BaseModel):
|
||||
class RecordConfig(FrigateBaseModel):
|
||||
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."
|
||||
events: EventsConfig = Field(
|
||||
default_factory=EventsConfig, title="Event specific settings."
|
||||
)
|
||||
|
||||
|
||||
class MotionConfig(BaseModel):
|
||||
class MotionConfig(FrigateBaseModel):
|
||||
threshold: int = Field(
|
||||
default=25,
|
||||
title="Motion detection threshold (1-255).",
|
||||
@@ -148,16 +149,22 @@ class RuntimeMotionConfig(MotionConfig):
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
extra = Extra.ignore
|
||||
|
||||
|
||||
class DetectConfig(BaseModel):
|
||||
class DetectConfig(FrigateBaseModel):
|
||||
height: int = Field(default=720, title="Height of the stream for the detect role.")
|
||||
width: int = Field(default=1280, title="Width of the stream for the detect role.")
|
||||
fps: int = Field(
|
||||
default=5, 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):
|
||||
class FilterConfig(FrigateBaseModel):
|
||||
min_area: int = Field(
|
||||
default=0, title="Minimum area of bounding box for object to be counted."
|
||||
)
|
||||
@@ -198,8 +205,10 @@ class RuntimeFilterConfig(FilterConfig):
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
extra = Extra.ignore
|
||||
|
||||
|
||||
# this uses the base model because the color is an extra attribute
|
||||
class ZoneConfig(BaseModel):
|
||||
filters: Dict[str, FilterConfig] = Field(
|
||||
default_factory=dict, title="Zone filters."
|
||||
@@ -241,7 +250,7 @@ class ZoneConfig(BaseModel):
|
||||
self._contour = np.array([])
|
||||
|
||||
|
||||
class ObjectConfig(BaseModel):
|
||||
class ObjectConfig(FrigateBaseModel):
|
||||
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.")
|
||||
@@ -253,7 +262,7 @@ class BirdseyeModeEnum(str, Enum):
|
||||
continuous = "continuous"
|
||||
|
||||
|
||||
class BirdseyeConfig(BaseModel):
|
||||
class BirdseyeConfig(FrigateBaseModel):
|
||||
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.")
|
||||
@@ -300,7 +309,7 @@ RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
|
||||
]
|
||||
|
||||
|
||||
class FfmpegOutputArgsConfig(BaseModel):
|
||||
class FfmpegOutputArgsConfig(FrigateBaseModel):
|
||||
detect: Union[str, List[str]] = Field(
|
||||
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
||||
title="Detect role FFmpeg output arguments.",
|
||||
@@ -315,7 +324,7 @@ class FfmpegOutputArgsConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class FfmpegConfig(BaseModel):
|
||||
class FfmpegConfig(FrigateBaseModel):
|
||||
global_args: Union[str, List[str]] = Field(
|
||||
default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments."
|
||||
)
|
||||
@@ -331,9 +340,15 @@ class FfmpegConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class CameraInput(BaseModel):
|
||||
class CameraRoleEnum(str, Enum):
|
||||
record = "record"
|
||||
rtmp = "rtmp"
|
||||
detect = "detect"
|
||||
|
||||
|
||||
class CameraInput(FrigateBaseModel):
|
||||
path: str = Field(title="Camera input path.")
|
||||
roles: List[str] = Field(title="Roles assigned to this input.")
|
||||
roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.")
|
||||
global_args: Union[str, List[str]] = Field(
|
||||
default_factory=list, title="FFmpeg global arguments."
|
||||
)
|
||||
@@ -362,7 +377,7 @@ class CameraFfmpegConfig(FfmpegConfig):
|
||||
return v
|
||||
|
||||
|
||||
class CameraSnapshotsConfig(BaseModel):
|
||||
class SnapshotsConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=False, title="Snapshots enabled.")
|
||||
clean_copy: bool = Field(
|
||||
default=True, title="Create a clean copy of the snapshot image."
|
||||
@@ -390,22 +405,35 @@ class CameraSnapshotsConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
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 ColorConfig(FrigateBaseModel):
|
||||
red: int = Field(default=255, ge=0, le=255, title="Red")
|
||||
green: int = Field(default=255, ge=0, le=255, title="Green")
|
||||
blue: int = Field(default=255, ge=0, le=255, title="Blue")
|
||||
|
||||
|
||||
class TimestampStyleConfig(BaseModel):
|
||||
position: str = Field(default="tl", title="Timestamp position.")
|
||||
class TimestampPositionEnum(str, Enum):
|
||||
tl = "tl"
|
||||
tr = "tr"
|
||||
bl = "bl"
|
||||
br = "br"
|
||||
|
||||
|
||||
class TimestampEffectEnum(str, Enum):
|
||||
solid = "solid"
|
||||
shadow = "shadow"
|
||||
|
||||
|
||||
class TimestampStyleConfig(FrigateBaseModel):
|
||||
position: TimestampPositionEnum = Field(
|
||||
default=TimestampPositionEnum.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.")
|
||||
effect: Optional[TimestampEffectEnum] = Field(title="Timestamp effect.")
|
||||
|
||||
|
||||
class CameraMqttConfig(BaseModel):
|
||||
class CameraMqttConfig(FrigateBaseModel):
|
||||
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.")
|
||||
@@ -423,23 +451,18 @@ class CameraMqttConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class CameraRtmpConfig(BaseModel):
|
||||
class RtmpConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="RTMP restreaming enabled.")
|
||||
|
||||
|
||||
class CameraLiveConfig(BaseModel):
|
||||
class CameraLiveConfig(FrigateBaseModel):
|
||||
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):
|
||||
class CameraConfig(FrigateBaseModel):
|
||||
name: Optional[str] = Field(title="Camera name.")
|
||||
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
|
||||
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: Optional[int] = Field(
|
||||
title="Number of frames per second to process through Frigate."
|
||||
)
|
||||
best_image_timeout: int = Field(
|
||||
default=60,
|
||||
title="How long to wait for the image with the highest confidence score.",
|
||||
@@ -447,16 +470,17 @@ class CameraConfig(BaseModel):
|
||||
zones: Dict[str, ZoneConfig] = Field(
|
||||
default_factory=dict, title="Zone configuration."
|
||||
)
|
||||
clips: ClipsConfig = Field(default_factory=ClipsConfig, title="Clip configuration.")
|
||||
record: RecordConfig = Field(
|
||||
default_factory=RecordConfig, title="Record configuration."
|
||||
)
|
||||
rtmp: CameraRtmpConfig = Field(
|
||||
default_factory=CameraRtmpConfig, title="RTMP restreaming configuration."
|
||||
rtmp: RtmpConfig = Field(
|
||||
default_factory=RtmpConfig, title="RTMP restreaming configuration."
|
||||
)
|
||||
live: Optional[CameraLiveConfig] = Field(title="Live playback settings.")
|
||||
snapshots: CameraSnapshotsConfig = Field(
|
||||
default_factory=CameraSnapshotsConfig, title="Snapshot configuration."
|
||||
live: CameraLiveConfig = Field(
|
||||
default_factory=CameraLiveConfig, title="Live playback settings."
|
||||
)
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig, title="Snapshot configuration."
|
||||
)
|
||||
mqtt: CameraMqttConfig = Field(
|
||||
default_factory=CameraMqttConfig, title="MQTT configuration."
|
||||
@@ -465,7 +489,9 @@ class CameraConfig(BaseModel):
|
||||
default_factory=ObjectConfig, title="Object configuration."
|
||||
)
|
||||
motion: Optional[MotionConfig] = Field(title="Motion detection configuration.")
|
||||
detect: Optional[DetectConfig] = Field(title="Object detection configuration.")
|
||||
detect: DetectConfig = Field(
|
||||
default_factory=DetectConfig, title="Object detection configuration."
|
||||
)
|
||||
timestamp_style: TimestampStyleConfig = Field(
|
||||
default_factory=TimestampStyleConfig, title="Timestamp style configuration."
|
||||
)
|
||||
@@ -483,11 +509,11 @@ class CameraConfig(BaseModel):
|
||||
|
||||
@property
|
||||
def frame_shape(self) -> Tuple[int, int]:
|
||||
return self.height, self.width
|
||||
return self.detect.height, self.detect.width
|
||||
|
||||
@property
|
||||
def frame_shape_yuv(self) -> Tuple[int, int]:
|
||||
return self.height * 3 // 2, self.width
|
||||
return self.detect.height * 3 // 2, self.detect.width
|
||||
|
||||
@property
|
||||
def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
|
||||
@@ -508,9 +534,17 @@ class CameraConfig(BaseModel):
|
||||
if isinstance(self.ffmpeg.output_args.detect, list)
|
||||
else self.ffmpeg.output_args.detect.split(" ")
|
||||
)
|
||||
ffmpeg_output_args = detect_args + ffmpeg_output_args + ["pipe:"]
|
||||
if self.fps:
|
||||
ffmpeg_output_args = ["-r", str(self.fps)] + ffmpeg_output_args
|
||||
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
|
||||
@@ -520,9 +554,7 @@ class CameraConfig(BaseModel):
|
||||
ffmpeg_output_args = (
|
||||
rtmp_args + [f"rtmp://127.0.0.1/live/{self.name}"] + ffmpeg_output_args
|
||||
)
|
||||
if any(role in ["clips", "record"] for role in ffmpeg_input.roles) and (
|
||||
self.record.enabled or self.clips.enabled
|
||||
):
|
||||
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)
|
||||
@@ -564,32 +596,45 @@ class CameraConfig(BaseModel):
|
||||
return [part for part in cmd if part != ""]
|
||||
|
||||
|
||||
class DatabaseConfig(BaseModel):
|
||||
class DatabaseConfig(FrigateBaseModel):
|
||||
path: str = Field(
|
||||
default=os.path.join(BASE_DIR, "frigate.db"), title="Database path."
|
||||
)
|
||||
|
||||
|
||||
class ModelConfig(BaseModel):
|
||||
class ModelConfig(FrigateBaseModel):
|
||||
path: Optional[str] = Field(title="Custom Object detection model path.")
|
||||
labelmap_path: Optional[str] = Field(title="Label map for custom object detector.")
|
||||
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 merged_labelmap(self) -> Dict[int, str]:
|
||||
return self._merged_labelmap
|
||||
|
||||
@property
|
||||
def colormap(self) -> Dict[int, tuple[int, int, int]]:
|
||||
return self._colormap
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self._merged_labelmap = {
|
||||
**load_labels("/labelmap.txt"),
|
||||
**load_labels(config.get("labelmap_path", "/labelmap.txt")),
|
||||
**config.get("labelmap", {}),
|
||||
}
|
||||
|
||||
cmap = plt.cm.get_cmap("tab10", len(self._merged_labelmap.keys()))
|
||||
|
||||
self._colormap = {}
|
||||
for key, val in self._merged_labelmap.items():
|
||||
self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
|
||||
|
||||
|
||||
class LogLevelEnum(str, Enum):
|
||||
debug = "debug"
|
||||
@@ -599,7 +644,7 @@ class LogLevelEnum(str, Enum):
|
||||
critical = "critical"
|
||||
|
||||
|
||||
class LoggerConfig(BaseModel):
|
||||
class LoggerConfig(FrigateBaseModel):
|
||||
default: LogLevelEnum = Field(
|
||||
default=LogLevelEnum.info, title="Default logging level."
|
||||
)
|
||||
@@ -608,13 +653,7 @@ class LoggerConfig(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class SnapshotsConfig(BaseModel):
|
||||
retain: RetainConfig = Field(
|
||||
default_factory=RetainConfig, title="Global snapshot retention configuration."
|
||||
)
|
||||
|
||||
|
||||
class FrigateConfig(BaseModel):
|
||||
class FrigateConfig(FrigateBaseModel):
|
||||
mqtt: MqttConfig = Field(title="MQTT Configuration.")
|
||||
database: DatabaseConfig = Field(
|
||||
default_factory=DatabaseConfig, title="Database configuration."
|
||||
@@ -632,15 +671,18 @@ class FrigateConfig(BaseModel):
|
||||
logger: LoggerConfig = Field(
|
||||
default_factory=LoggerConfig, title="Logging configuration."
|
||||
)
|
||||
clips: ClipsConfig = Field(
|
||||
default_factory=ClipsConfig, title="Global clips configuration."
|
||||
)
|
||||
record: RecordConfig = Field(
|
||||
default_factory=RecordConfig, title="Global record configuration."
|
||||
)
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig, title="Global snapshots configuration."
|
||||
)
|
||||
live: CameraLiveConfig = Field(
|
||||
default_factory=CameraLiveConfig, title="Global live configuration."
|
||||
)
|
||||
rtmp: RtmpConfig = Field(
|
||||
default_factory=RtmpConfig, title="Global RTMP restreaming configuration."
|
||||
)
|
||||
birdseye: BirdseyeConfig = Field(
|
||||
default_factory=BirdseyeConfig, title="Birdseye configuration."
|
||||
)
|
||||
@@ -653,10 +695,14 @@ class FrigateConfig(BaseModel):
|
||||
motion: Optional[MotionConfig] = Field(
|
||||
title="Global motion detection configuration."
|
||||
)
|
||||
detect: Optional[DetectConfig] = Field(
|
||||
title="Global object tracking configuration."
|
||||
detect: DetectConfig = Field(
|
||||
default_factory=DetectConfig, title="Global object tracking configuration."
|
||||
)
|
||||
cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
|
||||
timestamp_style: TimestampStyleConfig = Field(
|
||||
default_factory=TimestampStyleConfig,
|
||||
title="Global timestamp style configuration.",
|
||||
)
|
||||
|
||||
@property
|
||||
def runtime_config(self) -> FrigateConfig:
|
||||
@@ -670,13 +716,15 @@ class FrigateConfig(BaseModel):
|
||||
# Global config to propegate down to camera level
|
||||
global_config = config.dict(
|
||||
include={
|
||||
"clips": ...,
|
||||
"record": ...,
|
||||
"snapshots": ...,
|
||||
"live": ...,
|
||||
"rtmp": ...,
|
||||
"objects": ...,
|
||||
"motion": ...,
|
||||
"detect": ...,
|
||||
"ffmpeg": ...,
|
||||
"timestamp_style": ...,
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
@@ -687,6 +735,11 @@ class FrigateConfig(BaseModel):
|
||||
{"name": name, **merged_config}
|
||||
)
|
||||
|
||||
# Default max_disappeared configuration
|
||||
max_disappeared = camera_config.detect.fps * 5
|
||||
if camera_config.detect.max_disappeared is None:
|
||||
camera_config.detect.max_disappeared = max_disappeared
|
||||
|
||||
# FFMPEG input substitution
|
||||
for input in camera_config.ffmpeg.inputs:
|
||||
input.path = input.path.format(**FRIGATE_ENV_VARS)
|
||||
@@ -734,35 +787,8 @@ class FrigateConfig(BaseModel):
|
||||
**camera_config.motion.dict(exclude_unset=True),
|
||||
)
|
||||
|
||||
# Default detect configuration
|
||||
max_disappeared = (camera_config.fps or 5) * 5
|
||||
if camera_config.detect:
|
||||
if camera_config.detect.max_disappeared is None:
|
||||
camera_config.detect.max_disappeared = max_disappeared
|
||||
else:
|
||||
camera_config.detect = DetectConfig(max_disappeared=max_disappeared)
|
||||
|
||||
# Default live configuration
|
||||
if camera_config.live is None:
|
||||
camera_config.live = CameraLiveConfig()
|
||||
|
||||
config.cameras[name] = camera_config
|
||||
|
||||
# Merge Clips configuration for backward compatibility
|
||||
if camera_config.clips.enabled:
|
||||
logger.warn(
|
||||
"Clips configuration is deprecated. Configure clip settings under record -> events."
|
||||
)
|
||||
if not camera_config.record.enabled:
|
||||
camera_config.record.enabled = True
|
||||
camera_config.record.retain_days = 0
|
||||
camera_config.record.events = ClipsConfig.parse_obj(
|
||||
deep_merge(
|
||||
camera_config.clips.dict(exclude_unset=True),
|
||||
camera_config.record.events.dict(exclude_unset=True),
|
||||
)
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
@validator("cameras")
|
||||
|
||||
@@ -9,7 +9,6 @@ from abc import ABC, abstractmethod
|
||||
from typing import Dict
|
||||
|
||||
import numpy as np
|
||||
from pycoral.adapters import detect
|
||||
import tflite_runtime.interpreter as tflite
|
||||
from setproctitle import setproctitle
|
||||
from tflite_runtime.interpreter import load_delegate
|
||||
@@ -46,7 +45,7 @@ class ObjectDetector(ABC):
|
||||
|
||||
|
||||
class LocalObjectDetector(ObjectDetector):
|
||||
def __init__(self, tf_device=None, num_threads=3, labels=None):
|
||||
def __init__(self, tf_device=None, model_path=None, num_threads=3, labels=None):
|
||||
self.fps = EventsPerSecond()
|
||||
if labels is None:
|
||||
self.labels = {}
|
||||
@@ -65,15 +64,20 @@ class LocalObjectDetector(ObjectDetector):
|
||||
edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config)
|
||||
logger.info("TPU found")
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path="/edgetpu_model.tflite",
|
||||
model_path=model_path or "/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=model_path or "/cpu_model.tflite", num_threads=num_threads
|
||||
)
|
||||
|
||||
self.interpreter.allocate_tensors()
|
||||
@@ -99,19 +103,25 @@ class LocalObjectDetector(ObjectDetector):
|
||||
self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input)
|
||||
self.interpreter.invoke()
|
||||
|
||||
objects = detect.get_objects(self.interpreter, 0.4)
|
||||
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, obj in enumerate(objects):
|
||||
if i == 20:
|
||||
|
||||
for i in range(count):
|
||||
if scores[i] < 0.4 or i == 20:
|
||||
break
|
||||
detections[i] = [
|
||||
obj.id,
|
||||
obj.score,
|
||||
obj.bbox.ymin,
|
||||
obj.bbox.xmin,
|
||||
obj.bbox.ymax,
|
||||
obj.bbox.xmax,
|
||||
class_ids[i],
|
||||
float(scores[i]),
|
||||
boxes[i][0],
|
||||
boxes[i][1],
|
||||
boxes[i][2],
|
||||
boxes[i][3],
|
||||
]
|
||||
|
||||
return detections
|
||||
@@ -123,6 +133,7 @@ def run_detector(
|
||||
out_events: Dict[str, mp.Event],
|
||||
avg_speed,
|
||||
start,
|
||||
model_path,
|
||||
model_shape,
|
||||
tf_device,
|
||||
num_threads,
|
||||
@@ -142,7 +153,9 @@ def run_detector(
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
frame_manager = SharedMemoryFrameManager()
|
||||
object_detector = LocalObjectDetector(tf_device=tf_device, num_threads=num_threads)
|
||||
object_detector = LocalObjectDetector(
|
||||
tf_device=tf_device, model_path=model_path, num_threads=num_threads
|
||||
)
|
||||
|
||||
outputs = {}
|
||||
for name in out_events.keys():
|
||||
@@ -179,6 +192,7 @@ class EdgeTPUProcess:
|
||||
name,
|
||||
detection_queue,
|
||||
out_events,
|
||||
model_path,
|
||||
model_shape,
|
||||
tf_device=None,
|
||||
num_threads=3,
|
||||
@@ -189,6 +203,7 @@ class EdgeTPUProcess:
|
||||
self.avg_inference_speed = mp.Value("d", 0.01)
|
||||
self.detection_start = mp.Value("d", 0.0)
|
||||
self.detect_process = None
|
||||
self.model_path = model_path
|
||||
self.model_shape = model_shape
|
||||
self.tf_device = tf_device
|
||||
self.num_threads = num_threads
|
||||
@@ -216,6 +231,7 @@ class EdgeTPUProcess:
|
||||
self.out_events,
|
||||
self.avg_inference_speed,
|
||||
self.detection_start,
|
||||
self.model_path,
|
||||
self.model_shape,
|
||||
self.tf_device,
|
||||
self.num_threads,
|
||||
|
||||
@@ -6,12 +6,12 @@ import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from frigate.config import FrigateConfig, RecordConfig
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event, Recordings
|
||||
|
||||
from peewee import fn
|
||||
|
||||
from frigate.config import EventsConfig, FrigateConfig, RecordConfig
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -29,40 +29,6 @@ class EventProcessor(threading.Thread):
|
||||
self.events_in_process = {}
|
||||
self.stop_event = stop_event
|
||||
|
||||
def should_create_clip(self, camera, event_data):
|
||||
if event_data["false_positive"]:
|
||||
return False
|
||||
|
||||
record_config: RecordConfig = self.config.cameras[camera].record
|
||||
|
||||
# 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 not self.stop_event.is_set():
|
||||
try:
|
||||
@@ -76,27 +42,25 @@ class EventProcessor(threading.Thread):
|
||||
self.events_in_process[event_data["id"]] = event_data
|
||||
|
||||
if event_type == "end":
|
||||
record_config: RecordConfig = self.config.cameras[camera].record
|
||||
event_config: EventsConfig = self.config.cameras[camera].record.events
|
||||
|
||||
has_clip = self.should_create_clip(camera, event_data)
|
||||
|
||||
if has_clip or event_data["has_snapshot"]:
|
||||
if event_data["has_clip"] or event_data["has_snapshot"]:
|
||||
Event.create(
|
||||
id=event_data["id"],
|
||||
label=event_data["label"],
|
||||
camera=camera,
|
||||
start_time=event_data["start_time"],
|
||||
end_time=event_data["end_time"],
|
||||
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||
end_time=event_data["end_time"] + event_config.post_capture,
|
||||
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_clip=event_data["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, has_clip))
|
||||
self.event_processed_queue.put((event_data["id"], camera))
|
||||
|
||||
logger.info(f"Exiting event processor...")
|
||||
|
||||
@@ -112,7 +76,7 @@ class EventCleanup(threading.Thread):
|
||||
def expire(self, media_type):
|
||||
## Expire events from unlisted cameras based on the global config
|
||||
if media_type == "clips":
|
||||
retain_config = self.config.clips.retain
|
||||
retain_config = self.config.record.events.retain
|
||||
file_extension = "mp4"
|
||||
update_params = {"has_clip": False}
|
||||
else:
|
||||
@@ -163,7 +127,7 @@ class EventCleanup(threading.Thread):
|
||||
## Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
if media_type == "clips":
|
||||
retain_config = camera.clips.retain
|
||||
retain_config = camera.record.events.retain
|
||||
else:
|
||||
retain_config = camera.snapshots.retain
|
||||
# get distinct objects in database for this camera
|
||||
|
||||
@@ -242,14 +242,11 @@ def event_clip(id):
|
||||
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)
|
||||
return recording_clip(event.camera, event.start_time, event.end_time)
|
||||
|
||||
response = make_response()
|
||||
response.headers["Content-Description"] = "File Transfer"
|
||||
@@ -697,15 +694,12 @@ def vod_event(id):
|
||||
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
|
||||
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
|
||||
|
||||
if not os.path.isfile(clip_path):
|
||||
return vod_ts(event.camera, start_ts, end_ts)
|
||||
return vod_ts(event.camera, event.start_time, event.end_time)
|
||||
|
||||
duration = int((end_ts - start_ts) * 1000)
|
||||
duration = int((event.end_time - event.start_time) * 1000)
|
||||
return jsonify(
|
||||
{
|
||||
"cache": True,
|
||||
|
||||
@@ -96,14 +96,14 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
||||
threading.current_thread().name = "mqtt"
|
||||
if rc != 0:
|
||||
if rc == 3:
|
||||
logger.error("MQTT Server unavailable")
|
||||
logger.error("Unable to connect to MQTT server: MQTT Server unavailable")
|
||||
elif rc == 4:
|
||||
logger.error("MQTT Bad username or password")
|
||||
logger.error("Unable to connect to MQTT server: MQTT Bad username or password")
|
||||
elif rc == 5:
|
||||
logger.error("MQTT Not authorized")
|
||||
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
|
||||
else:
|
||||
logger.error(
|
||||
"Unable to connect to MQTT: Connection refused. Error code: "
|
||||
"Unable to connect to MQTT server: Connection refused. Error code: "
|
||||
+ str(rc)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import copy
|
||||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
import hashlib
|
||||
import itertools
|
||||
@@ -14,30 +14,20 @@ from statistics import mean, median
|
||||
from typing import Callable, Dict
|
||||
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from frigate.config import FrigateConfig, CameraConfig
|
||||
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
|
||||
from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig
|
||||
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
|
||||
from frigate.edgetpu import load_labels
|
||||
from frigate.util import (
|
||||
SharedMemoryFrameManager,
|
||||
calculate_region,
|
||||
draw_box_with_label,
|
||||
draw_timestamp,
|
||||
calculate_region,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PATH_TO_LABELS = "/labelmap.txt"
|
||||
|
||||
LABELS = load_labels(PATH_TO_LABELS)
|
||||
cmap = plt.cm.get_cmap("tab10", len(LABELS.keys()))
|
||||
|
||||
COLOR_MAP = {}
|
||||
for key, val in LABELS.items():
|
||||
COLOR_MAP[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
|
||||
|
||||
|
||||
def on_edge(box, frame_shape):
|
||||
if (
|
||||
@@ -72,14 +62,19 @@ def is_better_thumbnail(current_thumb, new_obj, frame_shape) -> bool:
|
||||
|
||||
|
||||
class TrackedObject:
|
||||
def __init__(self, camera, camera_config: CameraConfig, frame_cache, obj_data):
|
||||
def __init__(
|
||||
self, camera, colormap, camera_config: CameraConfig, frame_cache, obj_data
|
||||
):
|
||||
self.obj_data = obj_data
|
||||
self.camera = camera
|
||||
self.colormap = colormap
|
||||
self.camera_config = camera_config
|
||||
self.frame_cache = frame_cache
|
||||
self.current_zones = []
|
||||
self.entered_zones = set()
|
||||
self.false_positive = True
|
||||
self.has_clip = False
|
||||
self.has_snapshot = False
|
||||
self.top_score = self.computed_score = 0.0
|
||||
self.thumbnail_data = None
|
||||
self.last_updated = 0
|
||||
@@ -183,6 +178,8 @@ class TrackedObject:
|
||||
"region": self.obj_data["region"],
|
||||
"current_zones": self.current_zones.copy(),
|
||||
"entered_zones": list(self.entered_zones).copy(),
|
||||
"has_clip": self.has_clip,
|
||||
"has_snapshot": self.has_snapshot,
|
||||
}
|
||||
|
||||
if include_thumbnail:
|
||||
@@ -247,7 +244,7 @@ class TrackedObject:
|
||||
|
||||
if bounding_box:
|
||||
thickness = 2
|
||||
color = COLOR_MAP[self.obj_data["label"]]
|
||||
color = self.colormap[self.obj_data["label"]]
|
||||
|
||||
# draw the bounding boxes on the frame
|
||||
box = self.thumbnail_data["box"]
|
||||
@@ -282,9 +279,8 @@ class TrackedObject:
|
||||
self.thumbnail_data["frame_time"],
|
||||
self.camera_config.timestamp_style.format,
|
||||
font_effect=self.camera_config.timestamp_style.effect,
|
||||
font_scale=self.camera_config.timestamp_style.scale,
|
||||
font_thickness=self.camera_config.timestamp_style.thickness,
|
||||
font_color=(color.red, color.green, color.blue),
|
||||
font_color=(color.blue, color.green, color.red),
|
||||
position=self.camera_config.timestamp_style.position,
|
||||
)
|
||||
|
||||
@@ -357,7 +353,7 @@ class CameraState:
|
||||
for obj in tracked_objects.values():
|
||||
if obj["frame_time"] == frame_time:
|
||||
thickness = 2
|
||||
color = COLOR_MAP[obj["label"]]
|
||||
color = self.config.model.colormap[obj["label"]]
|
||||
else:
|
||||
thickness = 1
|
||||
color = (255, 0, 0)
|
||||
@@ -418,9 +414,8 @@ class CameraState:
|
||||
frame_time,
|
||||
self.camera_config.timestamp_style.format,
|
||||
font_effect=self.camera_config.timestamp_style.effect,
|
||||
font_scale=self.camera_config.timestamp_style.scale,
|
||||
font_thickness=self.camera_config.timestamp_style.thickness,
|
||||
font_color=(color.red, color.green, color.blue),
|
||||
font_color=(color.blue, color.green, color.red),
|
||||
position=self.camera_config.timestamp_style.position,
|
||||
)
|
||||
|
||||
@@ -448,7 +443,11 @@ class CameraState:
|
||||
|
||||
for id in new_ids:
|
||||
new_obj = tracked_objects[id] = TrackedObject(
|
||||
self.name, self.camera_config, self.frame_cache, current_detections[id]
|
||||
self.name,
|
||||
self.config.model.colormap,
|
||||
self.camera_config,
|
||||
self.frame_cache,
|
||||
current_detections[id],
|
||||
)
|
||||
|
||||
# call event handlers
|
||||
@@ -616,9 +615,46 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
obj.previous = after
|
||||
|
||||
def end(camera, obj: TrackedObject, current_frame_time):
|
||||
snapshot_config = self.config.cameras[camera].snapshots
|
||||
event_data = obj.to_dict(include_thumbnail=True)
|
||||
event_data["has_snapshot"] = False
|
||||
# populate has_snapshot
|
||||
obj.has_snapshot = self.should_save_snapshot(camera, obj)
|
||||
obj.has_clip = self.should_retain_recording(camera, obj)
|
||||
|
||||
# write the snapshot to disk
|
||||
if obj.has_snapshot:
|
||||
snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots
|
||||
jpg_bytes = obj.get_jpg_bytes(
|
||||
timestamp=snapshot_config.timestamp,
|
||||
bounding_box=snapshot_config.bounding_box,
|
||||
crop=snapshot_config.crop,
|
||||
height=snapshot_config.height,
|
||||
quality=snapshot_config.quality,
|
||||
)
|
||||
if jpg_bytes is None:
|
||||
logger.warning(f"Unable to save snapshot for {obj.obj_data['id']}.")
|
||||
else:
|
||||
with open(
|
||||
os.path.join(CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"),
|
||||
"wb",
|
||||
) as j:
|
||||
j.write(jpg_bytes)
|
||||
|
||||
# write clean snapshot if enabled
|
||||
if snapshot_config.clean_copy:
|
||||
png_bytes = obj.get_clean_png()
|
||||
if png_bytes is None:
|
||||
logger.warning(
|
||||
f"Unable to save clean snapshot for {obj.obj_data['id']}."
|
||||
)
|
||||
else:
|
||||
with open(
|
||||
os.path.join(
|
||||
CLIPS_DIR,
|
||||
f"{camera}-{obj.obj_data['id']}-clean.png",
|
||||
),
|
||||
"wb",
|
||||
) as p:
|
||||
p.write(png_bytes)
|
||||
|
||||
if not obj.false_positive:
|
||||
message = {
|
||||
"before": obj.previous,
|
||||
@@ -628,46 +664,8 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
self.client.publish(
|
||||
f"{self.topic_prefix}/events", json.dumps(message), retain=False
|
||||
)
|
||||
# write snapshot to disk if enabled
|
||||
if snapshot_config.enabled and self.should_save_snapshot(camera, obj):
|
||||
jpg_bytes = obj.get_jpg_bytes(
|
||||
timestamp=snapshot_config.timestamp,
|
||||
bounding_box=snapshot_config.bounding_box,
|
||||
crop=snapshot_config.crop,
|
||||
height=snapshot_config.height,
|
||||
quality=snapshot_config.quality,
|
||||
)
|
||||
if jpg_bytes is None:
|
||||
logger.warning(
|
||||
f"Unable to save snapshot for {obj.obj_data['id']}."
|
||||
)
|
||||
else:
|
||||
with open(
|
||||
os.path.join(
|
||||
CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"
|
||||
),
|
||||
"wb",
|
||||
) as j:
|
||||
j.write(jpg_bytes)
|
||||
event_data["has_snapshot"] = True
|
||||
|
||||
# write clean snapshot if enabled
|
||||
if snapshot_config.clean_copy:
|
||||
png_bytes = obj.get_clean_png()
|
||||
if png_bytes is None:
|
||||
logger.warning(
|
||||
f"Unable to save clean snapshot for {obj.obj_data['id']}."
|
||||
)
|
||||
else:
|
||||
with open(
|
||||
os.path.join(
|
||||
CLIPS_DIR,
|
||||
f"{camera}-{obj.obj_data['id']}-clean.png",
|
||||
),
|
||||
"wb",
|
||||
) as p:
|
||||
p.write(png_bytes)
|
||||
self.event_queue.put(("end", camera, event_data))
|
||||
self.event_queue.put(("end", camera, obj.to_dict(include_thumbnail=True)))
|
||||
|
||||
def snapshot(camera, obj: TrackedObject, current_frame_time):
|
||||
mqtt_config = self.config.cameras[camera].mqtt
|
||||
@@ -716,8 +714,16 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
self.zone_data = defaultdict(lambda: defaultdict(dict))
|
||||
|
||||
def should_save_snapshot(self, camera, obj: TrackedObject):
|
||||
if obj.false_positive:
|
||||
return False
|
||||
|
||||
snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots
|
||||
|
||||
if not snapshot_config.enabled:
|
||||
return False
|
||||
|
||||
# if there are required zones and there is no overlap
|
||||
required_zones = self.config.cameras[camera].snapshots.required_zones
|
||||
required_zones = snapshot_config.required_zones
|
||||
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
|
||||
logger.debug(
|
||||
f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones"
|
||||
@@ -726,6 +732,36 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
|
||||
return True
|
||||
|
||||
def should_retain_recording(self, camera, obj: TrackedObject):
|
||||
if obj.false_positive:
|
||||
return False
|
||||
|
||||
record_config: RecordConfig = self.config.cameras[camera].record
|
||||
|
||||
# Recording is disabled
|
||||
if not record_config.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(obj.entered_zones) & set(required_zones):
|
||||
logger.debug(
|
||||
f"Not creating clip for {obj.obj_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 obj.obj_data["label"] not in record_config.events.objects
|
||||
):
|
||||
logger.debug(
|
||||
f"Not creating clip for {obj.obj_data['id']} because it did not contain required objects"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def should_mqtt_snapshot(self, camera, obj: TrackedObject):
|
||||
# if there are required zones and there is no overlap
|
||||
required_zones = self.config.cameras[camera].mqtt.required_zones
|
||||
@@ -820,17 +856,7 @@ class TrackedObjectProcessor(threading.Thread):
|
||||
|
||||
# cleanup event finished queue
|
||||
while not self.event_processed_queue.empty():
|
||||
event_id, camera, clip_created = self.event_processed_queue.get()
|
||||
if clip_created:
|
||||
obj = self.camera_states[camera].tracked_objects[event_id]
|
||||
message = {
|
||||
"before": obj.previous,
|
||||
"after": obj.to_dict(),
|
||||
"type": "clip_ready",
|
||||
}
|
||||
self.client.publish(
|
||||
f"{self.topic_prefix}/events", json.dumps(message), retain=False
|
||||
)
|
||||
event_id, camera = self.event_processed_queue.get()
|
||||
self.camera_states[camera].finished(event_id)
|
||||
|
||||
logger.info(f"Exiting object processor...")
|
||||
|
||||
@@ -75,8 +75,9 @@ class BroadcastThread(threading.Thread):
|
||||
ws_iter = iter(websockets.values())
|
||||
|
||||
for ws in ws_iter:
|
||||
if not ws.terminated and ws.environ["PATH_INFO"].endswith(
|
||||
self.camera
|
||||
if (
|
||||
not ws.terminated
|
||||
and ws.environ["PATH_INFO"] == f"/{self.camera}"
|
||||
):
|
||||
try:
|
||||
ws.send(buf, binary=True)
|
||||
|
||||
@@ -14,7 +14,7 @@ import numpy as np
|
||||
from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig
|
||||
from frigate.edgetpu import LocalObjectDetector
|
||||
from frigate.motion import MotionDetector
|
||||
from frigate.object_processing import COLOR_MAP, CameraState
|
||||
from frigate.object_processing import CameraState
|
||||
from frigate.objects import ObjectTracker
|
||||
from frigate.util import (
|
||||
DictFrameManager,
|
||||
|
||||
@@ -10,8 +10,7 @@ import threading
|
||||
from pathlib import Path
|
||||
|
||||
import psutil
|
||||
|
||||
from peewee import JOIN
|
||||
from peewee import JOIN, DoesNotExist
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CACHE_DIR, RECORD_DIR
|
||||
@@ -78,7 +77,10 @@ class RecordingMaintainer(threading.Thread):
|
||||
start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
|
||||
|
||||
# Just delete files if recordings are turned off
|
||||
if not self.config.cameras[camera].record.enabled:
|
||||
if (
|
||||
not camera in self.config.cameras
|
||||
or not self.config.cameras[camera].record.enabled
|
||||
):
|
||||
Path(cache_path).unlink(missing_ok=True)
|
||||
continue
|
||||
|
||||
@@ -111,7 +113,9 @@ class RecordingMaintainer(threading.Thread):
|
||||
file_name = f"{start_time.strftime('%M.%S.mp4')}"
|
||||
file_path = os.path.join(directory, file_name)
|
||||
|
||||
shutil.move(cache_path, file_path)
|
||||
# copy then delete is required when recordings are stored on some network drives
|
||||
shutil.copyfile(cache_path, file_path)
|
||||
os.remove(cache_path)
|
||||
|
||||
rand_id = "".join(
|
||||
random.choices(string.ascii_lowercase + string.digits, k=6)
|
||||
@@ -153,18 +157,22 @@ class RecordingCleanup(threading.Thread):
|
||||
|
||||
logger.debug("Start deleted cameras.")
|
||||
# Handle deleted cameras
|
||||
expire_days = self.config.record.retain_days
|
||||
expire_before = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
no_camera_recordings: Recordings = Recordings.select().where(
|
||||
Recordings.camera.not_in(list(self.config.cameras.keys())),
|
||||
Recordings.end_time < expire_before,
|
||||
)
|
||||
|
||||
deleted_recordings = set()
|
||||
for recording in no_camera_recordings:
|
||||
expire_days = self.config.record.retain_days
|
||||
expire_before = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
if recording.end_time < expire_before:
|
||||
Path(recording.path).unlink(missing_ok=True)
|
||||
Recordings.delete_by_id(recording.id)
|
||||
Path(recording.path).unlink(missing_ok=True)
|
||||
deleted_recordings.add(recording.id)
|
||||
|
||||
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
||||
Recordings.delete().where(Recordings.id << deleted_recordings).execute()
|
||||
logger.debug("End deleted cameras.")
|
||||
|
||||
logger.debug("Start all cameras.")
|
||||
@@ -181,59 +189,61 @@ class RecordingCleanup(threading.Thread):
|
||||
).timestamp()
|
||||
expire_date = min(min_end, expire_before)
|
||||
|
||||
# Get recordings to remove
|
||||
recordings: Recordings = Recordings.select().where(
|
||||
Recordings.camera == camera,
|
||||
Recordings.end_time < expire_date,
|
||||
# Get recordings to check for expiration
|
||||
recordings: Recordings = (
|
||||
Recordings.select()
|
||||
.where(
|
||||
Recordings.camera == camera,
|
||||
Recordings.end_time < expire_date,
|
||||
)
|
||||
.order_by(Recordings.start_time)
|
||||
)
|
||||
|
||||
for recording in recordings:
|
||||
# See if there are any associated events
|
||||
events: Event = Event.select().where(
|
||||
Event.camera == recording.camera,
|
||||
(
|
||||
Event.start_time.between(
|
||||
recording.start_time, recording.end_time
|
||||
)
|
||||
| Event.end_time.between(
|
||||
recording.start_time, recording.end_time
|
||||
)
|
||||
| (
|
||||
(recording.start_time > Event.start_time)
|
||||
& (recording.end_time < Event.end_time)
|
||||
)
|
||||
),
|
||||
# Get all the events to check against
|
||||
events: Event = (
|
||||
Event.select()
|
||||
.where(
|
||||
Event.camera == camera, Event.end_time < expire_date, Event.has_clip
|
||||
)
|
||||
keep = False
|
||||
event_ids = set()
|
||||
.order_by(Event.start_time)
|
||||
.objects()
|
||||
)
|
||||
|
||||
event: Event
|
||||
for event in events:
|
||||
event_ids.add(event.id)
|
||||
# Check event/label retention and keep the recording if within window
|
||||
expire_days_event = (
|
||||
0
|
||||
if not config.record.events.enabled
|
||||
else config.record.events.retain.objects.get(
|
||||
event.label, config.record.events.retain.default
|
||||
)
|
||||
)
|
||||
expire_before_event = (
|
||||
datetime.datetime.now()
|
||||
- datetime.timedelta(days=expire_days_event)
|
||||
).timestamp()
|
||||
if recording.end_time >= expire_before_event:
|
||||
# loop over recordings and see if they overlap with any non-expired events
|
||||
event_start = 0
|
||||
deleted_recordings = set()
|
||||
for recording in recordings.objects().iterator():
|
||||
keep = False
|
||||
# Now look for a reason to keep this recording segment
|
||||
for idx in range(event_start, len(events)):
|
||||
event = events[idx]
|
||||
|
||||
# if the event starts in the future, stop checking events
|
||||
# and let this recording segment expire
|
||||
if event.start_time > recording.end_time:
|
||||
keep = False
|
||||
break
|
||||
|
||||
# if the event ends after the recording starts, keep it
|
||||
# and stop looking at events
|
||||
if event.end_time >= recording.start_time:
|
||||
keep = True
|
||||
break
|
||||
|
||||
# if the event ends before this recording segment starts, skip
|
||||
# this event and check the next event for an overlap.
|
||||
# since the events and recordings are sorted, we can skip events
|
||||
# that end before the previous recording segment started on future segments
|
||||
if event.end_time < recording.start_time:
|
||||
event_start = idx
|
||||
|
||||
# Delete recordings outside of the retention window
|
||||
if not keep:
|
||||
Path(recording.path).unlink(missing_ok=True)
|
||||
Recordings.delete_by_id(recording.id)
|
||||
if event_ids:
|
||||
# Update associated events
|
||||
Event.update(has_clip=False).where(
|
||||
Event.id.in_(list(event_ids))
|
||||
).execute()
|
||||
deleted_recordings.add(recording.id)
|
||||
|
||||
logger.debug(f"Expiring {len(deleted_recordings)} recordings")
|
||||
Recordings.delete().where(Recordings.id << deleted_recordings).execute()
|
||||
|
||||
logger.debug(f"End camera: {camera}.")
|
||||
|
||||
@@ -242,29 +252,47 @@ class RecordingCleanup(threading.Thread):
|
||||
|
||||
def expire_files(self):
|
||||
logger.debug("Start expire files (legacy).")
|
||||
|
||||
default_expire = (
|
||||
datetime.datetime.now().timestamp()
|
||||
- SECONDS_IN_DAY * self.config.record.retain_days
|
||||
)
|
||||
delete_before = {}
|
||||
|
||||
for name, camera in self.config.cameras.items():
|
||||
delete_before[name] = (
|
||||
datetime.datetime.now().timestamp()
|
||||
- SECONDS_IN_DAY * camera.record.retain_days
|
||||
)
|
||||
|
||||
for p in Path("/media/frigate/recordings").rglob("*.mp4"):
|
||||
# Ignore files that have a record in the recordings DB
|
||||
if Recordings.select().where(Recordings.path == str(p)).count():
|
||||
continue
|
||||
# find all the recordings older than the oldest recording in the db
|
||||
try:
|
||||
oldest_recording = (
|
||||
Recordings.select().order_by(Recordings.start_time.desc()).get()
|
||||
)
|
||||
|
||||
oldest_timestamp = oldest_recording.start_time
|
||||
except DoesNotExist:
|
||||
oldest_timestamp = datetime.datetime.now().timestamp()
|
||||
|
||||
logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
|
||||
process = sp.run(
|
||||
["find", RECORD_DIR, "-type", "f", "-newermt", f"@{oldest_timestamp}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
files_to_check = process.stdout.splitlines()
|
||||
|
||||
for f in files_to_check:
|
||||
p = Path(f)
|
||||
if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire):
|
||||
p.unlink(missing_ok=True)
|
||||
|
||||
logger.debug("End expire files (legacy).")
|
||||
|
||||
def run(self):
|
||||
# Expire recordings every minute, clean directories every 5 minutes.
|
||||
for counter in itertools.cycle(range(5)):
|
||||
# Expire recordings every minute, clean directories every hour.
|
||||
for counter in itertools.cycle(range(60)):
|
||||
if self.stop_event.wait(60):
|
||||
logger.info(f"Exiting recording cleanup...")
|
||||
break
|
||||
|
||||
@@ -18,8 +18,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -29,8 +32,8 @@ class TestConfig(unittest.TestCase):
|
||||
assert self.minimal == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert "coral" in runtime_config.detectors.keys()
|
||||
assert runtime_config.detectors["coral"].type == DetectorTypeEnum.edgetpu
|
||||
assert "cpu" in runtime_config.detectors.keys()
|
||||
assert runtime_config.detectors["cpu"].type == DetectorTypeEnum.cpu
|
||||
|
||||
def test_invalid_mqtt_config(self):
|
||||
config = {
|
||||
@@ -42,8 +45,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -60,8 +66,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -82,8 +91,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"objects": {"track": ["cat"]},
|
||||
}
|
||||
},
|
||||
@@ -105,8 +117,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -130,8 +145,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -152,8 +170,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"objects": {
|
||||
"track": ["person", "dog"],
|
||||
"filters": {"dog": {"threshold": 0.7}},
|
||||
@@ -179,8 +200,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"objects": {
|
||||
"mask": "0,0,1,1,0,1",
|
||||
"filters": {"dog": {"mask": "1,1,1,1,1,1"}},
|
||||
@@ -210,8 +234,11 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -233,8 +260,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"objects": {
|
||||
"track": ["person", "dog"],
|
||||
"filters": {"dog": {"threshold": 0.7}},
|
||||
@@ -260,8 +290,11 @@ class TestConfig(unittest.TestCase):
|
||||
],
|
||||
"input_args": ["-re"],
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"objects": {
|
||||
"track": ["person", "dog"],
|
||||
"filters": {"dog": {"threshold": 0.7}},
|
||||
@@ -292,8 +325,11 @@ class TestConfig(unittest.TestCase):
|
||||
],
|
||||
"input_args": "test3",
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"objects": {
|
||||
"track": ["person", "dog"],
|
||||
"filters": {"dog": {"threshold": 0.7}},
|
||||
@@ -313,7 +349,9 @@ class TestConfig(unittest.TestCase):
|
||||
def test_inherit_clips_retention(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"clips": {"retain": {"default": 20, "objects": {"person": 30}}},
|
||||
"record": {
|
||||
"events": {"retain": {"default": 20, "objects": {"person": 30}}}
|
||||
},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@@ -321,8 +359,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -330,12 +371,16 @@ class TestConfig(unittest.TestCase):
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].clips.retain.objects["person"] == 30
|
||||
assert (
|
||||
runtime_config.cameras["back"].record.events.retain.objects["person"] == 30
|
||||
)
|
||||
|
||||
def test_roles_listed_twice_throws_error(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"clips": {"retain": {"default": 20, "objects": {"person": 30}}},
|
||||
"record": {
|
||||
"events": {"retain": {"default": 20, "objects": {"person": 30}}}
|
||||
},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@@ -344,8 +389,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video2", "roles": ["detect"]},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -354,7 +402,9 @@ class TestConfig(unittest.TestCase):
|
||||
def test_zone_matching_camera_name_throws_error(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"clips": {"retain": {"default": 20, "objects": {"person": 30}}},
|
||||
"record": {
|
||||
"events": {"retain": {"default": 20, "objects": {"person": 30}}}
|
||||
},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@@ -362,8 +412,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"zones": {"back": {"coordinates": "1,1,1,1,1,1"}},
|
||||
}
|
||||
},
|
||||
@@ -373,7 +426,9 @@ class TestConfig(unittest.TestCase):
|
||||
def test_zone_assigns_color_and_contour(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"clips": {"retain": {"default": 20, "objects": {"person": 30}}},
|
||||
"record": {
|
||||
"events": {"retain": {"default": 20, "objects": {"person": 30}}}
|
||||
},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
@@ -381,8 +436,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"zones": {"test": {"coordinates": "1,1,1,1,1,1"}},
|
||||
}
|
||||
},
|
||||
@@ -399,7 +457,9 @@ class TestConfig(unittest.TestCase):
|
||||
def test_clips_should_default_to_global_objects(self):
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"clips": {"retain": {"default": 20, "objects": {"person": 30}}},
|
||||
"record": {
|
||||
"events": {"retain": {"default": 20, "objects": {"person": 30}}}
|
||||
},
|
||||
"objects": {"track": ["person", "dog"]},
|
||||
"cameras": {
|
||||
"back": {
|
||||
@@ -408,9 +468,12 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"clips": {"enabled": True},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
"record": {"events": {}},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -419,8 +482,8 @@ class TestConfig(unittest.TestCase):
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
back_camera = runtime_config.cameras["back"]
|
||||
assert back_camera.clips.objects is None
|
||||
assert back_camera.clips.retain.objects["person"] == 30
|
||||
assert back_camera.record.events.objects is None
|
||||
assert back_camera.record.events.retain.objects["person"] == 30
|
||||
|
||||
def test_role_assigned_but_not_enabled(self):
|
||||
config = {
|
||||
@@ -436,8 +499,11 @@ class TestConfig(unittest.TestCase):
|
||||
{"path": "rtsp://10.0.0.1:554/record", "roles": ["record"]},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -463,9 +529,12 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {"enabled": True},
|
||||
"detect": {
|
||||
"enabled": True,
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -490,8 +559,11 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 480,
|
||||
"width": 640,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -516,8 +588,11 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -543,8 +618,11 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -569,8 +647,11 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -596,8 +677,11 @@ class TestConfig(unittest.TestCase):
|
||||
},
|
||||
]
|
||||
},
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -608,6 +692,421 @@ class TestConfig(unittest.TestCase):
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.model.merged_labelmap[0] == "person"
|
||||
|
||||
def test_fails_on_invalid_role(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect", "clips"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
|
||||
|
||||
def test_global_detect(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"detect": {"max_disappeared": 1},
|
||||
"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"].detect.max_disappeared == 1
|
||||
assert runtime_config.cameras["back"].detect.height == 1080
|
||||
|
||||
def test_default_detect(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].detect.max_disappeared == 25
|
||||
assert runtime_config.cameras["back"].detect.height == 720
|
||||
|
||||
def test_global_detect_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"detect": {"max_disappeared": 1, "height": 720},
|
||||
"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"].detect.max_disappeared == 1
|
||||
assert runtime_config.cameras["back"].detect.height == 1080
|
||||
assert runtime_config.cameras["back"].detect.width == 1920
|
||||
|
||||
def test_global_snapshots(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"snapshots": {"enabled": True},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"snapshots": {
|
||||
"height": 100,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].snapshots.enabled
|
||||
assert runtime_config.cameras["back"].snapshots.height == 100
|
||||
|
||||
def test_default_snapshots(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].snapshots.bounding_box
|
||||
assert runtime_config.cameras["back"].snapshots.quality == 70
|
||||
|
||||
def test_global_snapshots_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"snapshots": {"bounding_box": False, "height": 300},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"snapshots": {
|
||||
"height": 150,
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].snapshots.bounding_box == False
|
||||
assert runtime_config.cameras["back"].snapshots.height == 150
|
||||
assert runtime_config.cameras["back"].snapshots.enabled
|
||||
|
||||
def test_global_rtmp(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"rtmp": {"enabled": True},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].rtmp.enabled
|
||||
|
||||
def test_default_rtmp(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].rtmp.enabled
|
||||
|
||||
def test_global_rtmp_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"rtmp": {"enabled": False},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"rtmp": {
|
||||
"enabled": True,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].rtmp.enabled
|
||||
|
||||
def test_global_live(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"live": {"quality": 4},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].live.quality == 4
|
||||
|
||||
def test_default_live(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].live.quality == 8
|
||||
|
||||
def test_global_live_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"live": {"quality": 4, "height": 480},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"live": {
|
||||
"quality": 7,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].live.quality == 7
|
||||
assert runtime_config.cameras["back"].live.height == 480
|
||||
|
||||
def test_global_timestamp_style(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"timestamp_style": {"position": "bl"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
|
||||
|
||||
def test_default_timestamp_style(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].timestamp_style.position == "tl"
|
||||
|
||||
def test_global_timestamp_style_merge(self):
|
||||
|
||||
config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"rtmp": {"enabled": False},
|
||||
"timestamp_style": {"position": "br", "thickness": 2},
|
||||
"cameras": {
|
||||
"back": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{
|
||||
"path": "rtsp://10.0.0.1:554/video",
|
||||
"roles": ["detect"],
|
||||
},
|
||||
]
|
||||
},
|
||||
"timestamp_style": {"position": "bl", "thickness": 4},
|
||||
}
|
||||
},
|
||||
}
|
||||
frigate_config = FrigateConfig(**config)
|
||||
assert config == frigate_config.dict(exclude_unset=True)
|
||||
|
||||
runtime_config = frigate_config.runtime_config
|
||||
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
|
||||
assert runtime_config.cameras["back"].timestamp_style.thickness == 4
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
@@ -18,6 +18,7 @@ import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import os
|
||||
import psutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,18 +52,32 @@ def draw_timestamp(
|
||||
timestamp,
|
||||
timestamp_format,
|
||||
font_effect=None,
|
||||
font_scale=1.0,
|
||||
font_thickness=2,
|
||||
font_color=(255, 255, 255),
|
||||
position="tl",
|
||||
):
|
||||
time_to_show = datetime.datetime.fromtimestamp(timestamp).strftime(timestamp_format)
|
||||
|
||||
# calculate a dynamic font size
|
||||
size = cv2.getTextSize(
|
||||
time_to_show,
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=1.0,
|
||||
thickness=font_thickness,
|
||||
)
|
||||
|
||||
text_width = size[0][0]
|
||||
desired_size = max(150, 0.33 * frame.shape[1])
|
||||
font_scale = desired_size / text_width
|
||||
|
||||
# calculate the actual size with the dynamic scale
|
||||
size = cv2.getTextSize(
|
||||
time_to_show,
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
fontScale=font_scale,
|
||||
thickness=font_thickness,
|
||||
)
|
||||
|
||||
image_width = frame.shape[1]
|
||||
image_height = frame.shape[0]
|
||||
text_width = size[0][0]
|
||||
@@ -520,7 +535,13 @@ def clipped(obj, frame_shape):
|
||||
|
||||
|
||||
def restart_frigate():
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
proc = psutil.Process(1)
|
||||
# if this is running via s6, sigterm pid 1
|
||||
if proc.name() == "s6-svscan":
|
||||
proc.terminate()
|
||||
# otherwise, just try and exit frigate
|
||||
else:
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
class EventsPerSecond:
|
||||
|
||||
@@ -216,6 +216,13 @@ class CameraWatchdog(threading.Thread):
|
||||
now = datetime.datetime.now().timestamp()
|
||||
|
||||
if not self.capture_thread.is_alive():
|
||||
self.logger.error(
|
||||
f"FFMPEG process crashed unexpectedly for {self.camera_name}."
|
||||
)
|
||||
self.logger.error(
|
||||
"The following ffmpeg logs include the last 100 lines prior to exit."
|
||||
)
|
||||
self.logger.error("You may have invalid args defined for this camera.")
|
||||
self.logpipe.dump()
|
||||
self.start_ffmpeg_detect()
|
||||
elif now - self.capture_thread.current_frame.value > 20:
|
||||
@@ -398,17 +405,16 @@ def detect(
|
||||
object_detector, frame, model_shape, region, objects_to_track, object_filters
|
||||
):
|
||||
tensor_input = create_tensor_input(frame, model_shape, region)
|
||||
scale = float(region[2] - region[0]) / model_shape[0]
|
||||
|
||||
detections = []
|
||||
region_detections = object_detector.detect(tensor_input)
|
||||
for d in region_detections:
|
||||
box = d[2]
|
||||
size = region[2] - region[0]
|
||||
x_min = int(max(0, box[1]) * scale + region[0])
|
||||
y_min = int(max(0, box[0]) * scale + region[1])
|
||||
x_max = int(min(frame.shape[1], box[3]) * scale + region[0])
|
||||
y_max = int(min(frame.shape[0], box[2]) * scale + region[1])
|
||||
x_min = int((box[1] * size) + region[0])
|
||||
y_min = int((box[0] * size) + region[1])
|
||||
x_max = int((box[3] * size) + region[0])
|
||||
y_max = int((box[2] * size) + region[1])
|
||||
det = (
|
||||
d[0],
|
||||
d[1],
|
||||
|
||||
@@ -5,6 +5,10 @@ import time
|
||||
import os
|
||||
import signal
|
||||
|
||||
from frigate.util import (
|
||||
restart_frigate,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -30,6 +34,6 @@ class FrigateWatchdog(threading.Thread):
|
||||
detector.start_or_restart()
|
||||
elif not detector.detect_process.is_alive():
|
||||
logger.info("Detection appears to have stopped. Exiting frigate...")
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
restart_frigate()
|
||||
|
||||
logger.info(f"Exiting watchdog...")
|
||||
|
||||
78
web/package-lock.json
generated
78
web/package-lock.json
generated
@@ -4137,17 +4137,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"@videojs/http-streaming": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.9.0.tgz",
|
||||
"integrity": "sha512-fRooepCcSYUKcplrn4h/teqL08Nr5bo4KDs8uGI6RAYOuDhGfWjaxFpmdUhr6Yme9G+ci+2Hh/hk9hHXxYGWaw==",
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.10.2.tgz",
|
||||
"integrity": "sha512-JTAlAUHzj0sTsge2WBh4DWKM2I5BDFEZYOvzxmsK/ySILmI0GRyjAHx9uid68ZECQ2qbEAIRmZW5lWp0R5PeNA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^3.0.2",
|
||||
"@videojs/vhs-utils": "3.0.3",
|
||||
"aes-decrypter": "3.1.2",
|
||||
"global": "^4.4.0",
|
||||
"m3u8-parser": "4.7.0",
|
||||
"mpd-parser": "0.17.0",
|
||||
"mux.js": "5.11.0",
|
||||
"mpd-parser": "0.19.0",
|
||||
"mux.js": "5.13.0",
|
||||
"video.js": "^6 || ^7"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -4163,9 +4163,9 @@
|
||||
}
|
||||
},
|
||||
"@videojs/vhs-utils": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.2.tgz",
|
||||
"integrity": "sha512-r8Yas1/tNGsGRNoIaDJuiWiQgM0P2yaEnobgzw5JcBiEqxnS8EXoUm4QtKH7nJtnppZ1yqBx1agBZCvBMKXA2w==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.3.tgz",
|
||||
"integrity": "sha512-bU7daxDHhzcTDbmty1cXjzsTYvx2cBGbA8hG5H2Gvpuk4sdfuvkZtMCwtCqL59p6dsleMPspyaNS+7tWXx2Y0A==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"global": "^4.4.0",
|
||||
@@ -4184,9 +4184,9 @@
|
||||
}
|
||||
},
|
||||
"@videojs/xhr": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.5.1.tgz",
|
||||
"integrity": "sha512-wV9nGESHseSK+S9ePEru2+OJZ1jq/ZbbzniGQ4weAmTIepuBMSYPx5zrxxQA0E786T5ykpO8ts+LayV+3/oI2w==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz",
|
||||
"integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"global": "~4.4.0",
|
||||
@@ -4204,6 +4204,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@xmldom/xmldom": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.4.tgz",
|
||||
"integrity": "sha512-wdxC79cvO7PjSM34jATd/RYZuYWQ8y/R7MidZl1NYYlbpFn1+spfjkiR3ZsJfcaTs2IyslBN7VwBBJwrYKM+zw=="
|
||||
},
|
||||
"abab": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
|
||||
@@ -9115,14 +9120,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"mpd-parser": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.17.0.tgz",
|
||||
"integrity": "sha512-oKS5G0jCcHHJ3sHYlcLeM9Xcbuixl08eAx7QW0Th7ChlZiI0YvLtGaHE/L0aKUBJFNvtkeksIr8XgJgSBBsS4g==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.19.0.tgz",
|
||||
"integrity": "sha512-FDLIXtZMZs99fv5iXNFg94quNFT26tobo0NUgHu7L3XgZvEq1NBarf5yxDFFJ1zzfbcmtj+NRaml6nYIxoPWvw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/vhs-utils": "^3.0.2",
|
||||
"global": "^4.4.0",
|
||||
"xmldom": "^0.5.0"
|
||||
"@xmldom/xmldom": "^0.7.2",
|
||||
"global": "^4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"global": {
|
||||
@@ -9156,9 +9161,9 @@
|
||||
}
|
||||
},
|
||||
"mux.js": {
|
||||
"version": "5.11.0",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.11.0.tgz",
|
||||
"integrity": "sha512-Q/iLfohHh5Pp6lW7EFtcxNuaCNJ3Ruywfy46pWLsY+yIxR1kXXImYY1wOhg8jLdBMs1kRaZqsiB4Zncsiw0a2Q==",
|
||||
"version": "5.13.0",
|
||||
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-5.13.0.tgz",
|
||||
"integrity": "sha512-PkmnzHcTQjUBEHp3KRPQAFoNkJtKlpCEvsYtXDfDrC+/WqbMuxHvoYfmAbHVAH7Sa/KliPVU0dT1ureO8wilog==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.2"
|
||||
}
|
||||
@@ -9565,9 +9570,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-type": {
|
||||
@@ -11808,9 +11813,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"url-toolkit": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.2.tgz",
|
||||
"integrity": "sha512-l25w6Sy+Iy3/IbogunxhWwljPaDnqpiKvrQRoLBm6DfISco7NyRIS7Zf6+Oxhy1T8kHxWdwLND7ZZba6NjXMug=="
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.3.tgz",
|
||||
"integrity": "sha512-Da75SQoxsZ+2wXS56CZBrj2nukQ4nlGUZUP/dqUBG5E1su5GKThgT94Q00x81eVII7AyS1Pn+CtTTZ4Z0pLUtQ=="
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
@@ -11887,20 +11892,20 @@
|
||||
}
|
||||
},
|
||||
"video.js": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.13.0.tgz",
|
||||
"integrity": "sha512-wJcB2R5q3/6Ez5XUfpZBZTdgF321rX/M1HkDKxXQIdfsVi/pfP4l+equ2xL9O3X0XAPHRxLEegraIEuX28mRkA==",
|
||||
"version": "7.15.4",
|
||||
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.15.4.tgz",
|
||||
"integrity": "sha512-hghxkgptLUvfkpktB4wxcIVF3VpY/hVsMkrjHSv0jpj1bW9Jplzdt8IgpTm9YhlB1KYAp07syVQeZcBFUBwhkw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@videojs/http-streaming": "2.9.0",
|
||||
"@videojs/vhs-utils": "^3.0.2",
|
||||
"@videojs/xhr": "2.5.1",
|
||||
"@videojs/http-streaming": "2.10.2",
|
||||
"@videojs/vhs-utils": "^3.0.3",
|
||||
"@videojs/xhr": "2.6.0",
|
||||
"aes-decrypter": "3.1.2",
|
||||
"global": "^4.4.0",
|
||||
"keycode": "^2.2.0",
|
||||
"m3u8-parser": "4.7.0",
|
||||
"mpd-parser": "0.17.0",
|
||||
"mux.js": "5.11.0",
|
||||
"mpd-parser": "0.19.0",
|
||||
"mux.js": "5.13.0",
|
||||
"safe-json-parse": "4.0.0",
|
||||
"videojs-font": "3.2.0",
|
||||
"videojs-vtt.js": "^0.15.3"
|
||||
@@ -12112,11 +12117,6 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true
|
||||
},
|
||||
"xmldom": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz",
|
||||
"integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"preact": "^10.5.9",
|
||||
"preact-async-route": "^2.2.1",
|
||||
"preact-router": "^3.2.1",
|
||||
"video.js": "^7.13.0",
|
||||
"video.js": "^7.15.4",
|
||||
"videojs-playlist": "^4.3.1",
|
||||
"videojs-seek-buttons": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -23,12 +23,11 @@ export default function App() {
|
||||
) : (
|
||||
<div className="flex flex-row min-h-screen w-full bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<Sidebar />
|
||||
<div className="w-full flex-auto p-2 mt-24 px-4 min-w-0">
|
||||
<div className="w-full flex-auto p-2 mt-16 px-4 min-w-0">
|
||||
<Router>
|
||||
<AsyncRoute path="/cameras/:camera/editor" getComponent={Routes.getCameraMap} />
|
||||
<AsyncRoute path="/cameras/:camera" getComponent={Routes.getCamera} />
|
||||
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
|
||||
<AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
|
||||
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
||||
<AsyncRoute path="/recording/:camera/:date?/:hour?/:seconds?" getComponent={Routes.getRecording} />
|
||||
<AsyncRoute path="/debug" getComponent={Routes.getDebug} />
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function AppBar() {
|
||||
<MenuSeparator />
|
||||
<MenuItem icon={FrigateRestartIcon} label="Restart Frigate" onSelect={handleRestart} />
|
||||
</Menu>
|
||||
) : null},
|
||||
) : null}
|
||||
{showDialog ? (
|
||||
<Dialog
|
||||
onDismiss={handleDismissRestartDialog}
|
||||
@@ -74,7 +74,7 @@ export default function AppBar() {
|
||||
{ text: 'Cancel', onClick: handleDismissRestartDialog },
|
||||
]}
|
||||
/>
|
||||
) : null},
|
||||
) : null}
|
||||
{showDialogWait ? (
|
||||
<Dialog
|
||||
title="Restart in progress"
|
||||
|
||||
@@ -10,6 +10,7 @@ import NavigationDrawer, { Destination, Separator } from './components/Navigatio
|
||||
export default function Sidebar() {
|
||||
const { data: config } = useConfig();
|
||||
const cameras = useMemo(() => Object.entries(config.cameras), [config]);
|
||||
const { birdseye } = config;
|
||||
|
||||
return (
|
||||
<NavigationDrawer header={<Header />}>
|
||||
@@ -49,7 +50,7 @@ export default function Sidebar() {
|
||||
) : null
|
||||
}
|
||||
</Match>
|
||||
<Destination href="/birdseye" text="Birdseye" />
|
||||
{birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null}
|
||||
<Destination href="/events" text="Events" />
|
||||
<Destination href="/debug" text="Debug" />
|
||||
<Separator />
|
||||
|
||||
@@ -18,7 +18,7 @@ const initialState = Object.freeze({
|
||||
|
||||
const Api = createContext(initialState);
|
||||
|
||||
function reducer(state, { type, payload, meta }) {
|
||||
function reducer(state, { type, payload }) {
|
||||
switch (type) {
|
||||
case 'REQUEST': {
|
||||
const { url, fetchId } = payload;
|
||||
@@ -36,22 +36,9 @@ function reducer(state, { type, payload, meta }) {
|
||||
}
|
||||
case 'DELETE': {
|
||||
const { eventId } = payload;
|
||||
|
||||
return produce(state, (draftState) => {
|
||||
Object.keys(draftState.queries).map((url, index) => {
|
||||
// If data has no array length then just return state.
|
||||
if (!('data' in draftState.queries[url]) || !draftState.queries[url].data.length) return state;
|
||||
|
||||
//Find the index to remove
|
||||
const removeIndex = draftState.queries[url].data.map((event) => event.id).indexOf(eventId);
|
||||
if (removeIndex === -1) return state;
|
||||
|
||||
// We need to keep track of deleted items, This will be used to re-calculate "ReachEnd" for auto load new events. Events.jsx
|
||||
const totDeleted = state.queries[url].deleted || 0;
|
||||
|
||||
// Splice the deleted index.
|
||||
draftState.queries[url].data.splice(removeIndex, 1);
|
||||
draftState.queries[url].deleted = totDeleted + 1;
|
||||
Object.keys(draftState.queries).map((url) => {
|
||||
draftState.queries[url].deletedId = eventId;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -111,9 +98,9 @@ export function useFetch(url, fetchId) {
|
||||
|
||||
const data = state.queries[url].data || null;
|
||||
const status = state.queries[url].status;
|
||||
const deleted = state.queries[url].deleted || 0;
|
||||
const deletedId = state.queries[url].deletedId || 0;
|
||||
|
||||
return { data, status, deleted };
|
||||
return { data, status, deletedId };
|
||||
}
|
||||
|
||||
export function useDelete() {
|
||||
|
||||
@@ -37,13 +37,14 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full border-b border-gray-200 dark:border-gray-700 flex items-center align-middle p-4 space-x-2 fixed left-0 right-0 z-10 bg-white dark:bg-gray-900 transform transition-all duration-200 ${
|
||||
id="appbar"
|
||||
className={`w-full border-b border-gray-200 dark:border-gray-700 flex items-center align-middle p-2 fixed left-0 right-0 z-10 bg-white dark:bg-gray-900 transform transition-all duration-200 ${
|
||||
!show ? '-translate-y-full' : 'translate-y-0'
|
||||
} ${!atZero ? 'shadow-sm' : ''}`}
|
||||
data-testid="appbar"
|
||||
>
|
||||
<div className="lg:hidden">
|
||||
<Button color="black" className="rounded-full w-12 h-12" onClick={handleShowDrawer} type="text">
|
||||
<Button color="black" className="rounded-full w-10 h-10" onClick={handleShowDrawer} type="text">
|
||||
<MenuIcon className="w-10 h-10" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -54,7 +55,7 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
|
||||
<Button
|
||||
aria-label="More options"
|
||||
color="black"
|
||||
className="rounded-full w-12 h-12"
|
||||
className="rounded-full w-9 h-9"
|
||||
onClick={onOverflowClick}
|
||||
type="text"
|
||||
>
|
||||
|
||||
@@ -12,7 +12,8 @@ export default function CameraImage({ camera, onload, searchParams = '', stretch
|
||||
const canvasRef = useRef(null);
|
||||
const [{ width: availableWidth }] = useResizeObserver(containerRef);
|
||||
|
||||
const { name, width, height } = config.cameras[camera];
|
||||
const { name } = config.cameras[camera];
|
||||
const { width, height } = config.cameras[camera].detect;
|
||||
const aspectRatio = width / height;
|
||||
|
||||
const scaledHeight = useMemo(() => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Dialog({ actions = [], portalRootID = 'dialogs', title,
|
||||
<div
|
||||
data-testid="scrim"
|
||||
key="scrim"
|
||||
className="absolute inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
|
||||
className="fixed bg-fixed inset-0 z-10 flex justify-center items-center bg-black bg-opacity-40"
|
||||
>
|
||||
<div
|
||||
role="modal"
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function NavigationDrawer({ children, header }) {
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
{header ? (
|
||||
<div className="flex-shrink-0 p-5 flex flex-row items-center justify-between border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex-shrink-0 p-2 flex flex-row items-center justify-between border-b border-gray-200 dark:border-gray-700">
|
||||
{header}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate, se
|
||||
events={recording.events}
|
||||
selected={recording.date === selectedDate}
|
||||
>
|
||||
{recording.recordings.map((item, i) => (
|
||||
{recording.recordings.slice().reverse().map((item, i) => (
|
||||
<div className="mb-2 w-full">
|
||||
<div
|
||||
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
||||
@@ -35,7 +35,7 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate, se
|
||||
</div>
|
||||
<div className="flex-1 text-right">{item.events.length} Events</div>
|
||||
</div>
|
||||
{item.events.map((event) => (
|
||||
{item.events.slice().reverse().map((event) => (
|
||||
<EventCard camera={camera} event={event} delay={item.delay} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function RelativeModal({
|
||||
}
|
||||
// too close to bottom
|
||||
if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
|
||||
newTop = relativeToY - menuHeight;
|
||||
newTop = WINDOW_PADDING;
|
||||
}
|
||||
|
||||
if (top <= WINDOW_PADDING + window.scrollY) {
|
||||
|
||||
@@ -14,9 +14,9 @@ export function Thead({ children, className, ...attrs }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Tbody({ children, className, ...attrs }) {
|
||||
export function Tbody({ children, className, reference, ...attrs }) {
|
||||
return (
|
||||
<tbody className={className} {...attrs}>
|
||||
<tbody ref={reference} className={className} {...attrs}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
@@ -30,9 +30,10 @@ export function Tfoot({ children, className = '', ...attrs }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Tr({ children, className = '', ...attrs }) {
|
||||
export function Tr({ children, className = '', reference, ...attrs }) {
|
||||
return (
|
||||
<tr
|
||||
ref={reference}
|
||||
className={`border-b border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 ${className}`}
|
||||
{...attrs}
|
||||
>
|
||||
@@ -49,9 +50,9 @@ export function Th({ children, className = '', colspan, ...attrs }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Td({ children, className = '', colspan, ...attrs }) {
|
||||
export function Td({ children, className = '', reference, colspan, ...attrs }) {
|
||||
return (
|
||||
<td className={`p-2 px-1 lg:p-4 ${className}`} colSpan={colspan} {...attrs}>
|
||||
<td ref={reference} className={`p-2 px-1 lg:p-4 ${className}`} colSpan={colspan} {...attrs}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function VideoPlayer({ children, options, seekOptions = {}, onRea
|
||||
|
||||
return (
|
||||
<div data-vjs-player>
|
||||
<video ref={playerRef} className="video-js vjs-default-skin" controls playsinline />
|
||||
<video ref={playerRef} className="small-player video-js vjs-default-skin" controls playsinline />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { render, screen } from '@testing-library/preact';
|
||||
describe('CameraImage', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Api, 'useConfig').mockImplementation(() => {
|
||||
return { data: { cameras: { front: { name: 'front', width: 1280, height: 720 } } } };
|
||||
return { data: { cameras: { front: { name: 'front', detect: { width: 1280, height: 720 } } } } };
|
||||
});
|
||||
jest.spyOn(Api, 'useApiHost').mockReturnValue('http://base-url.local:5000');
|
||||
jest.spyOn(Hooks, 'useResizeObserver').mockImplementation(() => [{ width: 0 }]);
|
||||
|
||||
22
web/src/hooks/useClickOutside.jsx
Normal file
22
web/src/hooks/useClickOutside.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
// https://stackoverflow.com/a/54292872/2693528
|
||||
export const useClickOutside = (callback) => {
|
||||
const callbackRef = useRef(); // initialize mutable ref, which stores callback
|
||||
const innerRef = useRef(); // returned to client, who marks "border" element
|
||||
|
||||
// update cb on each render, so second useEffect has access to current value
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClick);
|
||||
return () => document.removeEventListener('click', handleClick);
|
||||
function handleClick(e) {
|
||||
if (innerRef.current && callbackRef.current && !innerRef.current.contains(e.target)) callbackRef.current(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return innerRef; // convenience for client (doesn't need to init ref himself)
|
||||
};
|
||||
25
web/src/hooks/useSearchString.jsx
Normal file
25
web/src/hooks/useSearchString.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState, useCallback } from 'preact/hooks';
|
||||
|
||||
const defaultSearchString = (limit) => `include_thumbnails=0&limit=${limit}`;
|
||||
|
||||
export const useSearchString = (limit, searchParams) => {
|
||||
const { searchParams: initialSearchParams } = new URL(window.location);
|
||||
const _searchParams = searchParams || initialSearchParams.toString();
|
||||
|
||||
const [searchString, changeSearchString] = useState(`${defaultSearchString(limit)}&${_searchParams}`);
|
||||
|
||||
const setSearchString = useCallback(
|
||||
(limit, searchString) => {
|
||||
changeSearchString(`${defaultSearchString(limit)}&${searchString}`);
|
||||
},
|
||||
[changeSearchString]
|
||||
);
|
||||
|
||||
const removeDefaultSearchKeys = useCallback((searchParams) => {
|
||||
searchParams.delete('limit');
|
||||
searchParams.delete('include_thumbnails');
|
||||
searchParams.delete('before');
|
||||
}, []);
|
||||
|
||||
return { searchString, setSearchString, removeDefaultSearchKeys };
|
||||
};
|
||||
13
web/src/icons/Close.jsx
Normal file
13
web/src/icons/Close.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
|
||||
export function Close({ className = '' }) {
|
||||
return (
|
||||
<svg className={`fill-current ${className}`} viewBox="0 0 24 24">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Close);
|
||||
@@ -29,3 +29,27 @@
|
||||
.jsmpeg canvas {
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
/*
|
||||
Event.js
|
||||
Maintain aspect ratio and scale down the video container
|
||||
Could not find a proper tailwind css.
|
||||
*/
|
||||
.outer-max-width {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
/*
|
||||
Hide some videoplayer controls on mobile devices to
|
||||
align the video player and bottom control bar properly.
|
||||
*/
|
||||
@media only screen and (max-width: 700px) {
|
||||
.small-player .vjs-time-control,
|
||||
.small-player .vjs-time-divider {
|
||||
display: none;
|
||||
}
|
||||
div.vjs-control-bar > .skip-back.skip-5,
|
||||
div.vjs-control-bar > .skip-forward.skip-10 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,16 @@ export default function CameraMasks({ camera, url }) {
|
||||
|
||||
const cameraConfig = config.cameras[camera];
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
motion: { mask: motionMask },
|
||||
objects: { filters: objectFilters },
|
||||
zones,
|
||||
} = cameraConfig;
|
||||
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
} = cameraConfig.detect;
|
||||
|
||||
const [{ width: scaledWidth }] = useResizeObserver(imageRef);
|
||||
const imageScale = scaledWidth / width;
|
||||
|
||||
|
||||
@@ -1,25 +1,76 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
import { route } from 'preact-router';
|
||||
import { useCallback, useState, useEffect } from 'preact/hooks';
|
||||
import Link from '../components/Link';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Button from '../components/Button';
|
||||
import ArrowDown from '../icons/ArrowDropdown';
|
||||
import ArrowDropup from '../icons/ArrowDropup';
|
||||
import Clip from '../icons/Clip';
|
||||
import Close from '../icons/Close';
|
||||
import Delete from '../icons/Delete';
|
||||
import Snapshot from '../icons/Snapshot';
|
||||
import Dialog from '../components/Dialog';
|
||||
import Heading from '../components/Heading';
|
||||
import Link from '../components/Link';
|
||||
import VideoPlayer from '../components/VideoPlayer';
|
||||
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
|
||||
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
|
||||
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
|
||||
|
||||
export default function Event({ eventId }) {
|
||||
const ActionButtonGroup = ({ className, handleClickDelete, close }) => (
|
||||
<div className={`space-y-2 space-x-2 sm:space-y-0 xs:space-x-4 ${className}`}>
|
||||
<Button className="xs:w-auto" color="red" onClick={handleClickDelete}>
|
||||
<Delete className="w-6" /> Delete event
|
||||
</Button>
|
||||
<Button color="gray" className="xs:w-auto" onClick={() => close()}>
|
||||
<Close className="w-6" /> Close
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DownloadButtonGroup = ({ className, apiHost, eventId }) => (
|
||||
<span className={`space-y-2 sm:space-y-0 space-x-0 sm:space-x-4 ${className}`}>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
color="blue"
|
||||
href={`${apiHost}/api/events/${eventId}/clip.mp4?download=true`}
|
||||
download
|
||||
>
|
||||
<Clip className="w-6" /> Download Clip
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
color="blue"
|
||||
href={`${apiHost}/api/events/${eventId}/snapshot.jpg?download=true`}
|
||||
download
|
||||
>
|
||||
<Snapshot className="w-6" /> Download Snapshot
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
|
||||
export default function Event({ eventId, close, scrollRef }) {
|
||||
const apiHost = useApiHost();
|
||||
const { data, status } = useEvent(eventId);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
const [shouldScroll, setShouldScroll] = useState(true);
|
||||
const [deleteStatus, setDeleteStatus] = useState(FetchStatus.NONE);
|
||||
const setDeleteEvent = useDelete();
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll event into view when component has been mounted.
|
||||
if (shouldScroll && scrollRef && scrollRef[eventId]) {
|
||||
scrollRef[eventId].scrollIntoView();
|
||||
setShouldScroll(false);
|
||||
}
|
||||
return () => {
|
||||
// When opening new event window, the previous one will sometimes cause the
|
||||
// navbar to be visible, hence the "hide nav" code bellow.
|
||||
// Navbar will be hided if we add the - translate - y - full class.appBar.js
|
||||
const element = document.getElementById('appbar');
|
||||
if (element) element.classList.add('-translate-y-full');
|
||||
};
|
||||
}, [data, scrollRef, eventId, shouldScroll]);
|
||||
|
||||
const handleClickDelete = () => {
|
||||
setShowDialog(true);
|
||||
};
|
||||
@@ -40,7 +91,6 @@ export default function Event({ eventId }) {
|
||||
if (success) {
|
||||
setDeleteStatus(FetchStatus.LOADED);
|
||||
setShowDialog(false);
|
||||
route('/events', true);
|
||||
}
|
||||
}, [eventId, setShowDialog, setDeleteEvent]);
|
||||
|
||||
@@ -50,16 +100,26 @@ export default function Event({ eventId }) {
|
||||
|
||||
const startime = new Date(data.start_time * 1000);
|
||||
const endtime = new Date(data.end_time * 1000);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex">
|
||||
<Heading className="flex-grow">
|
||||
{data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
|
||||
</Heading>
|
||||
<Button className="self-start" color="red" onClick={handleClickDelete}>
|
||||
<Delete className="w-6" /> Delete event
|
||||
</Button>
|
||||
<div className="flex md:flex-row justify-between flex-wrap flex-col">
|
||||
<div className="space-y-2 xs:space-y-0 sm:space-x-4">
|
||||
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="hidden sm:inline" />
|
||||
<Button className="w-full sm:w-auto" onClick={() => setShowDetails(!showDetails)}>
|
||||
{showDetails ? (
|
||||
<Fragment>
|
||||
<ArrowDropup className="w-6" />
|
||||
Hide event Details
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<ArrowDown className="w-6" />
|
||||
Show event Details
|
||||
</Fragment>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="hidden sm:block" />
|
||||
{showDialog ? (
|
||||
<Dialog
|
||||
onDismiss={handleDismissDeleteDialog}
|
||||
@@ -78,86 +138,80 @@ export default function Event({ eventId }) {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
{showDetails ? (
|
||||
<Table class="w-full">
|
||||
<Thead>
|
||||
<Th>Key</Th>
|
||||
<Th>Value</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td>Camera</Td>
|
||||
<Td>
|
||||
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Timeframe</Td>
|
||||
<Td>
|
||||
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td>Score</Td>
|
||||
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Zones</Td>
|
||||
<Td>{data.zones.join(', ')}</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Table class="w-full">
|
||||
<Thead>
|
||||
<Th>Key</Th>
|
||||
<Th>Value</Th>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td>Camera</Td>
|
||||
<Td>
|
||||
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Timeframe</Td>
|
||||
<Td>
|
||||
{startime.toLocaleString()} – {endtime.toLocaleString()}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td>Score</Td>
|
||||
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
|
||||
</Tr>
|
||||
<Tr index={1}>
|
||||
<Td>Zones</Td>
|
||||
<Td>{data.zones.join(', ')}</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
{data.has_clip ? (
|
||||
<Fragment>
|
||||
<Heading size="lg">Clip</Heading>
|
||||
<VideoPlayer
|
||||
options={{
|
||||
sources: [
|
||||
{
|
||||
src: `${apiHost}/vod/event/${eventId}/index.m3u8`,
|
||||
type: 'application/vnd.apple.mpegurl',
|
||||
},
|
||||
],
|
||||
poster: data.has_snapshot
|
||||
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
|
||||
: `data:image/jpeg;base64,${data.thumbnail}`,
|
||||
}}
|
||||
seekOptions={{ forward: 10, back: 5 }}
|
||||
onReady={(player) => {}}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<Button
|
||||
className="mx-2"
|
||||
color="blue"
|
||||
href={`${apiHost}/api/events/${eventId}/clip.mp4?download=true`}
|
||||
download
|
||||
>
|
||||
<Clip className="w-6" /> Download Clip
|
||||
</Button>
|
||||
<Button
|
||||
className="mx-2"
|
||||
color="blue"
|
||||
href={`${apiHost}/api/events/${eventId}/snapshot.jpg?download=true`}
|
||||
download
|
||||
>
|
||||
<Snapshot className="w-6" /> Download Snapshot
|
||||
</Button>
|
||||
</div>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<Heading size="sm">{data.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
|
||||
<img
|
||||
src={
|
||||
data.has_snapshot
|
||||
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
|
||||
: `data:image/jpeg;base64,${data.thumbnail}`
|
||||
}
|
||||
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
<div className="outer-max-width xs:m-auto">
|
||||
<div className="pt-5 relative pb-20 w-screen xs:w-full">
|
||||
{data.has_clip ? (
|
||||
<Fragment>
|
||||
<Heading size="lg">Clip</Heading>
|
||||
<VideoPlayer
|
||||
options={{
|
||||
preload: 'none',
|
||||
sources: [
|
||||
{
|
||||
src: `${apiHost}/vod/event/${eventId}/index.m3u8`,
|
||||
type: 'application/vnd.apple.mpegurl',
|
||||
},
|
||||
],
|
||||
poster: data.has_snapshot
|
||||
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
|
||||
: `data:image/jpeg;base64,${data.thumbnail}`,
|
||||
}}
|
||||
seekOptions={{ forward: 10, back: 5 }}
|
||||
onReady={() => {}}
|
||||
/>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<Heading size="sm">{data.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
|
||||
<img
|
||||
src={
|
||||
data.has_snapshot
|
||||
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
|
||||
: `data:image/jpeg;base64,${data.thumbnail}`
|
||||
}
|
||||
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 xs:space-y-0">
|
||||
<DownloadButtonGroup apiHost={apiHost} eventId={eventId} className="block sm:hidden" />
|
||||
<ActionButtonGroup handleClickDelete={handleClickDelete} close={close} className="block sm:hidden" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Heading from '../components/Heading';
|
||||
import Link from '../components/Link';
|
||||
import Select from '../components/Select';
|
||||
import produce from 'immer';
|
||||
import { route } from 'preact-router';
|
||||
import { useIntersectionObserver } from '../hooks';
|
||||
import { FetchStatus, useApiHost, useConfig, useEvents } from '../api';
|
||||
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from '../components/Table';
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from 'preact/hooks';
|
||||
|
||||
const API_LIMIT = 25;
|
||||
|
||||
const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} });
|
||||
const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'APPEND_EVENTS': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
payload,
|
||||
} = action;
|
||||
|
||||
return produce(state, (draftState) => {
|
||||
draftState.searchStrings[searchString] = true;
|
||||
draftState.events.push(...payload);
|
||||
});
|
||||
}
|
||||
|
||||
case 'REACHED_END': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.reachedEnd = true;
|
||||
draftState.searchStrings[searchString] = true;
|
||||
});
|
||||
}
|
||||
|
||||
case 'RESET':
|
||||
return initialState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultSearchString = (limit) => `include_thumbnails=0&limit=${limit}`;
|
||||
function removeDefaultSearchKeys(searchParams) {
|
||||
searchParams.delete('limit');
|
||||
searchParams.delete('include_thumbnails');
|
||||
searchParams.delete('before');
|
||||
}
|
||||
|
||||
export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
|
||||
const apiHost = useApiHost();
|
||||
const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState);
|
||||
const { searchParams: initialSearchParams } = new URL(window.location);
|
||||
const [searchString, setSearchString] = useState(`${defaultSearchString(limit)}&${initialSearchParams.toString()}`);
|
||||
const { data, status, deleted } = useEvents(searchString);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && !(searchString in searchStrings)) {
|
||||
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
|
||||
}
|
||||
|
||||
if (data && Array.isArray(data) && data.length + deleted < limit) {
|
||||
dispatch({ type: 'REACHED_END', meta: { searchString } });
|
||||
}
|
||||
}, [data, limit, searchString, searchStrings, deleted]);
|
||||
|
||||
const [entry, setIntersectNode] = useIntersectionObserver();
|
||||
|
||||
useEffect(() => {
|
||||
if (entry && entry.isIntersecting) {
|
||||
const { startTime } = entry.target.dataset;
|
||||
const { searchParams } = new URL(window.location);
|
||||
searchParams.set('before', parseFloat(startTime) - 0.0001);
|
||||
|
||||
setSearchString(`${defaultSearchString(limit)}&${searchParams.toString()}`);
|
||||
}
|
||||
}, [entry, limit]);
|
||||
|
||||
const lastCellRef = useCallback(
|
||||
(node) => {
|
||||
if (node !== null && !reachedEnd) {
|
||||
setIntersectNode(node);
|
||||
}
|
||||
},
|
||||
[setIntersectNode, reachedEnd]
|
||||
);
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(searchParams) => {
|
||||
dispatch({ type: 'RESET' });
|
||||
removeDefaultSearchKeys(searchParams);
|
||||
setSearchString(`${defaultSearchString(limit)}&${searchParams.toString()}`);
|
||||
route(`${pathname}?${searchParams.toString()}`);
|
||||
},
|
||||
[limit, pathname, setSearchString]
|
||||
);
|
||||
|
||||
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
<Heading>Events</Heading>
|
||||
|
||||
<Filters onChange={handleFilter} searchParams={searchParams} />
|
||||
|
||||
<div className="min-w-0 overflow-auto">
|
||||
<Table className="min-w-full table-fixed">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Camera</Th>
|
||||
<Th>Label</Th>
|
||||
<Th>Score</Th>
|
||||
<Th>Zones</Th>
|
||||
<Th>Date</Th>
|
||||
<Th>Start</Th>
|
||||
<Th>End</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{events.map(
|
||||
(
|
||||
{ camera, id, label, start_time: startTime, end_time: endTime, thumbnail, top_score: score, zones },
|
||||
i
|
||||
) => {
|
||||
const start = new Date(parseInt(startTime * 1000, 10));
|
||||
const end = new Date(parseInt(endTime * 1000, 10));
|
||||
const ref = i === events.length - 1 ? lastCellRef : undefined;
|
||||
return (
|
||||
<Tr data-testid={`event-${id}`} key={id}>
|
||||
<Td className="w-40">
|
||||
<a href={`/events/${id}`} ref={ref} data-start-time={startTime} data-reached-end={reachedEnd}>
|
||||
<img
|
||||
width="150"
|
||||
height="150"
|
||||
style="min-height: 48px; min-width: 48px;"
|
||||
src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
|
||||
/>
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParams}
|
||||
paramName="camera"
|
||||
name={camera}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParams}
|
||||
paramName="label"
|
||||
name={label}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{(score * 100).toFixed(2)}%</Td>
|
||||
<Td>
|
||||
<ul>
|
||||
{zones.map((zone) => (
|
||||
<li>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchString}
|
||||
paramName="zone"
|
||||
name={zone}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Td>
|
||||
<Td>{start.toLocaleDateString()}</Td>
|
||||
<Td>{start.toLocaleTimeString()}</Td>
|
||||
<Td>{end.toLocaleTimeString()}</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Tbody>
|
||||
<Tfoot>
|
||||
<Tr>
|
||||
<Td className="text-center p-4" colspan="8">
|
||||
{status === FetchStatus.LOADING ? <ActivityIndicator /> : reachedEnd ? 'No more events' : null}
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tfoot>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Filterable({ onFilter, pathname, searchParams, paramName, name }) {
|
||||
const href = useMemo(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, name);
|
||||
removeDefaultSearchKeys(params);
|
||||
return `${pathname}?${params.toString()}`;
|
||||
}, [searchParams, paramName, pathname, name]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
route(href, true);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, name);
|
||||
onFilter(params);
|
||||
},
|
||||
[href, searchParams, onFilter, paramName, name]
|
||||
);
|
||||
|
||||
return (
|
||||
<Link href={href} onclick={handleClick}>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Filters({ onChange, searchParams }) {
|
||||
const { data } = useConfig();
|
||||
|
||||
const cameras = useMemo(() => Object.keys(data.cameras), [data]);
|
||||
|
||||
const zones = useMemo(
|
||||
() =>
|
||||
Object.values(data.cameras)
|
||||
.reduce((memo, camera) => {
|
||||
memo = memo.concat(Object.keys(camera.zones));
|
||||
return memo;
|
||||
}, [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i),
|
||||
[data]
|
||||
);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
return Object.values(data.cameras)
|
||||
.reduce((memo, camera) => {
|
||||
memo = memo.concat(camera.objects?.track || []);
|
||||
return memo;
|
||||
}, data.objects?.track || [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<Filter onChange={onChange} options={cameras} paramName="camera" searchParams={searchParams} />
|
||||
<Filter onChange={onChange} options={zones} paramName="zone" searchParams={searchParams} />
|
||||
<Filter onChange={onChange} options={labels} paramName="label" searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Filter({ onChange, searchParams, paramName, options }) {
|
||||
const handleSelect = useCallback(
|
||||
(key) => {
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
if (key !== 'all') {
|
||||
newParams.set(paramName, key);
|
||||
} else {
|
||||
newParams.delete(paramName);
|
||||
}
|
||||
|
||||
onChange(newParams);
|
||||
},
|
||||
[searchParams, paramName, onChange]
|
||||
);
|
||||
|
||||
const selectOptions = useMemo(() => ['all', ...options], [options]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={`${paramName.charAt(0).toUpperCase()}${paramName.substr(1)}`}
|
||||
onChange={handleSelect}
|
||||
options={selectOptions}
|
||||
selected={searchParams.get(paramName) || 'all'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
31
web/src/routes/Events/components/filter.jsx
Normal file
31
web/src/routes/Events/components/filter.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { h } from 'preact';
|
||||
import Select from '../../../components/Select';
|
||||
import { useCallback, useMemo } from 'preact/hooks';
|
||||
|
||||
const Filter = ({ onChange, searchParams, paramName, options }) => {
|
||||
const handleSelect = useCallback(
|
||||
(key) => {
|
||||
const newParams = new URLSearchParams(searchParams.toString());
|
||||
if (key !== 'all') {
|
||||
newParams.set(paramName, key);
|
||||
} else {
|
||||
newParams.delete(paramName);
|
||||
}
|
||||
|
||||
onChange(newParams);
|
||||
},
|
||||
[searchParams, paramName, onChange]
|
||||
);
|
||||
|
||||
const selectOptions = useMemo(() => ['all', ...options], [options]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={`${paramName.charAt(0).toUpperCase()}${paramName.substr(1)}`}
|
||||
onChange={handleSelect}
|
||||
options={selectOptions}
|
||||
selected={searchParams.get(paramName) || 'all'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default Filter;
|
||||
32
web/src/routes/Events/components/filterable.jsx
Normal file
32
web/src/routes/Events/components/filterable.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { h } from 'preact';
|
||||
import { useCallback, useMemo } from 'preact/hooks';
|
||||
import Link from '../../../components/Link';
|
||||
import { route } from 'preact-router';
|
||||
|
||||
const Filterable = ({ onFilter, pathname, searchParams, paramName, name, removeDefaultSearchKeys }) => {
|
||||
const href = useMemo(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, name);
|
||||
removeDefaultSearchKeys(params);
|
||||
return `${pathname}?${params.toString()}`;
|
||||
}, [searchParams, paramName, pathname, name, removeDefaultSearchKeys]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
route(href, true);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, name);
|
||||
onFilter(params);
|
||||
},
|
||||
[href, searchParams, onFilter, paramName, name]
|
||||
);
|
||||
|
||||
return (
|
||||
<Link href={href} onclick={handleClick}>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filterable;
|
||||
39
web/src/routes/Events/components/filters.jsx
Normal file
39
web/src/routes/Events/components/filters.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { h } from 'preact';
|
||||
import Filter from './filter';
|
||||
import { useConfig } from '../../../api';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
|
||||
const Filters = ({ onChange, searchParams }) => {
|
||||
const { data } = useConfig();
|
||||
|
||||
const cameras = useMemo(() => Object.keys(data.cameras), [data]);
|
||||
|
||||
const zones = useMemo(
|
||||
() =>
|
||||
Object.values(data.cameras)
|
||||
.reduce((memo, camera) => {
|
||||
memo = memo.concat(Object.keys(camera.zones));
|
||||
return memo;
|
||||
}, [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i),
|
||||
[data]
|
||||
);
|
||||
|
||||
const labels = useMemo(() => {
|
||||
return Object.values(data.cameras)
|
||||
.reduce((memo, camera) => {
|
||||
memo = memo.concat(camera.objects?.track || []);
|
||||
return memo;
|
||||
}, data.objects?.track || [])
|
||||
.filter((value, i, self) => self.indexOf(value) === i);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="flex space-x-4">
|
||||
<Filter onChange={onChange} options={cameras} paramName="camera" searchParams={searchParams} />
|
||||
<Filter onChange={onChange} options={zones} paramName="zone" searchParams={searchParams} />
|
||||
<Filter onChange={onChange} options={labels} paramName="label" searchParams={searchParams} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Filters;
|
||||
3
web/src/routes/Events/components/index.jsx
Normal file
3
web/src/routes/Events/components/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as TableHead } from './tableHead';
|
||||
export { default as TableRow } from './tableRow';
|
||||
export { default as Filters } from './filters';
|
||||
18
web/src/routes/Events/components/tableHead.jsx
Normal file
18
web/src/routes/Events/components/tableHead.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { h } from 'preact';
|
||||
import { Thead, Th, Tr } from '../../../components/Table';
|
||||
|
||||
const TableHead = () => (
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Camera</Th>
|
||||
<Th>Label</Th>
|
||||
<Th>Score</Th>
|
||||
<Th>Zones</Th>
|
||||
<Th>Date</Th>
|
||||
<Th>Start</Th>
|
||||
<Th>End</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
);
|
||||
export default TableHead;
|
||||
119
web/src/routes/Events/components/tableRow.jsx
Normal file
119
web/src/routes/Events/components/tableRow.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { h } from 'preact';
|
||||
import { memo } from 'preact/compat';
|
||||
import { useCallback, useState, useMemo } from 'preact/hooks';
|
||||
import { Tr, Td, Tbody } from '../../../components/Table';
|
||||
import Filterable from './filterable';
|
||||
import Event from '../../Event';
|
||||
import { useSearchString } from '../../../hooks/useSearchString';
|
||||
import { useClickOutside } from '../../../hooks/useClickOutside';
|
||||
|
||||
const EventsRow = memo(
|
||||
({
|
||||
id,
|
||||
apiHost,
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
scrollToRef,
|
||||
lastRowRef,
|
||||
handleFilter,
|
||||
pathname,
|
||||
limit,
|
||||
camera,
|
||||
label,
|
||||
top_score: score,
|
||||
zones,
|
||||
}) => {
|
||||
const [viewEvent, setViewEvent] = useState(null);
|
||||
const { searchString, removeDefaultSearchKeys } = useSearchString(limit);
|
||||
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||
|
||||
const innerRef = useClickOutside(() => {
|
||||
setViewEvent(null);
|
||||
});
|
||||
|
||||
const viewEventHandler = useCallback(
|
||||
(id) => {
|
||||
//Toggle event view
|
||||
if (viewEvent === id) return setViewEvent(null);
|
||||
//Set event id to be rendered.
|
||||
setViewEvent(id);
|
||||
},
|
||||
[viewEvent]
|
||||
);
|
||||
|
||||
const start = new Date(parseInt(startTime * 1000, 10));
|
||||
const end = new Date(parseInt(endTime * 1000, 10));
|
||||
|
||||
return (
|
||||
<Tbody reference={innerRef}>
|
||||
<Tr data-testid={`event-${id}`} className={`${viewEvent === id ? 'border-none' : ''}`}>
|
||||
<Td className="w-40">
|
||||
<a
|
||||
onClick={() => viewEventHandler(id)}
|
||||
ref={lastRowRef}
|
||||
data-start-time={startTime}
|
||||
// data-reached-end={reachedEnd} <-- Enable this will cause all events to re-render when reaching end.
|
||||
>
|
||||
<img
|
||||
width="150"
|
||||
height="150"
|
||||
className="cursor-pointer"
|
||||
style="min-height: 48px; min-width: 48px;"
|
||||
src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
|
||||
/>
|
||||
</a>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParams}
|
||||
paramName="camera"
|
||||
name={camera}
|
||||
removeDefaultSearchKeys={removeDefaultSearchKeys}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchParams}
|
||||
paramName="label"
|
||||
name={label}
|
||||
removeDefaultSearchKeys={removeDefaultSearchKeys}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{(score * 100).toFixed(2)}%</Td>
|
||||
<Td>
|
||||
<ul>
|
||||
{zones.map((zone) => (
|
||||
<li>
|
||||
<Filterable
|
||||
onFilter={handleFilter}
|
||||
pathname={pathname}
|
||||
searchParams={searchString}
|
||||
paramName="zone"
|
||||
name={zone}
|
||||
removeDefaultSearchKeys={removeDefaultSearchKeys}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Td>
|
||||
<Td>{start.toLocaleDateString()}</Td>
|
||||
<Td>{start.toLocaleTimeString()}</Td>
|
||||
<Td>{end.toLocaleTimeString()}</Td>
|
||||
</Tr>
|
||||
{viewEvent === id ? (
|
||||
<Tr className="border-b-1">
|
||||
<Td colSpan="8" reference={(el) => (scrollToRef[id] = el)}>
|
||||
<Event eventId={id} close={() => setViewEvent(null)} scrollRef={scrollToRef} />
|
||||
</Td>
|
||||
</Tr>
|
||||
) : null}
|
||||
</Tbody>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default EventsRow;
|
||||
107
web/src/routes/Events/index.jsx
Normal file
107
web/src/routes/Events/index.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { h } from 'preact';
|
||||
import ActivityIndicator from '../../components/ActivityIndicator';
|
||||
import Heading from '../../components/Heading';
|
||||
import { TableHead, Filters, TableRow } from './components';
|
||||
import { route } from 'preact-router';
|
||||
import { FetchStatus, useApiHost, useEvents } from '../../api';
|
||||
import { Table, Tfoot, Tr, Td } from '../../components/Table';
|
||||
import { useCallback, useEffect, useMemo, useReducer } from 'preact/hooks';
|
||||
import { reducer, initialState } from './reducer';
|
||||
import { useSearchString } from '../../hooks/useSearchString';
|
||||
import { useIntersectionObserver } from '../../hooks';
|
||||
|
||||
const API_LIMIT = 25;
|
||||
|
||||
export default function Events({ path: pathname, limit = API_LIMIT } = {}) {
|
||||
const apiHost = useApiHost();
|
||||
const { searchString, setSearchString, removeDefaultSearchKeys } = useSearchString(limit);
|
||||
const [{ events, reachedEnd, searchStrings, deleted }, dispatch] = useReducer(reducer, initialState);
|
||||
const { data, status, deletedId } = useEvents(searchString);
|
||||
|
||||
const scrollToRef = useMemo(() => Object, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data && !(searchString in searchStrings)) {
|
||||
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
|
||||
}
|
||||
|
||||
if (data && Array.isArray(data) && data.length + deleted < limit) {
|
||||
dispatch({ type: 'REACHED_END', meta: { searchString } });
|
||||
}
|
||||
|
||||
if (deletedId) {
|
||||
dispatch({ type: 'DELETE_EVENT', deletedId });
|
||||
}
|
||||
}, [data, limit, searchString, searchStrings, deleted, deletedId]);
|
||||
|
||||
const [entry, setIntersectNode] = useIntersectionObserver();
|
||||
|
||||
useEffect(() => {
|
||||
if (entry && entry.isIntersecting) {
|
||||
const { startTime } = entry.target.dataset;
|
||||
const { searchParams } = new URL(window.location);
|
||||
searchParams.set('before', parseFloat(startTime) - 0.0001);
|
||||
setSearchString(limit, searchParams.toString());
|
||||
}
|
||||
}, [entry, limit, setSearchString]);
|
||||
|
||||
const lastCellRef = useCallback(
|
||||
(node) => {
|
||||
if (node !== null && !reachedEnd) {
|
||||
setIntersectNode(node);
|
||||
}
|
||||
},
|
||||
[setIntersectNode, reachedEnd]
|
||||
);
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(searchParams) => {
|
||||
dispatch({ type: 'RESET' });
|
||||
removeDefaultSearchKeys(searchParams);
|
||||
setSearchString(limit, searchParams.toString());
|
||||
route(`${pathname}?${searchParams.toString()}`);
|
||||
},
|
||||
[limit, pathname, setSearchString, removeDefaultSearchKeys]
|
||||
);
|
||||
|
||||
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
|
||||
|
||||
const RenderTableRow = useCallback(
|
||||
(props) => (
|
||||
<TableRow
|
||||
key={props.id}
|
||||
apiHost={apiHost}
|
||||
scrollToRef={scrollToRef}
|
||||
pathname={pathname}
|
||||
limit={API_LIMIT}
|
||||
handleFilter={handleFilter}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
[apiHost, handleFilter, pathname, scrollToRef]
|
||||
);
|
||||
return (
|
||||
<div className="space-y-4 w-full">
|
||||
<Heading>Events</Heading>
|
||||
<Filters onChange={handleFilter} searchParams={searchParams} />
|
||||
<div className="min-w-0 overflow-auto">
|
||||
<Table className="min-w-full table-fixed">
|
||||
<TableHead />
|
||||
|
||||
{events.map((props, idx) => {
|
||||
const lastRowRef = idx === events.length - 1 ? lastCellRef : undefined;
|
||||
return <RenderTableRow {...props} lastRowRef={lastRowRef} idx={idx} />;
|
||||
})}
|
||||
|
||||
<Tfoot>
|
||||
<Tr>
|
||||
<Td className="text-center p-4" colSpan="8">
|
||||
{status === FetchStatus.LOADING ? <ActivityIndicator /> : reachedEnd ? 'No more events' : null}
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tfoot>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
web/src/routes/Events/reducer.jsx
Normal file
47
web/src/routes/Events/reducer.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import produce from 'immer';
|
||||
|
||||
export const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {}, deleted: 0 });
|
||||
|
||||
export const reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'DELETE_EVENT': {
|
||||
const { deletedId } = action;
|
||||
|
||||
return produce(state, (draftState) => {
|
||||
const idx = draftState.events.findIndex((e) => e.id === deletedId);
|
||||
if (idx === -1) return state;
|
||||
|
||||
draftState.events.splice(idx, 1);
|
||||
draftState.deleted++;
|
||||
});
|
||||
}
|
||||
case 'APPEND_EVENTS': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
payload,
|
||||
} = action;
|
||||
|
||||
return produce(state, (draftState) => {
|
||||
draftState.searchStrings[searchString] = true;
|
||||
draftState.events.push(...payload);
|
||||
draftState.deleted = 0;
|
||||
});
|
||||
}
|
||||
|
||||
case 'REACHED_END': {
|
||||
const {
|
||||
meta: { searchString },
|
||||
} = action;
|
||||
return produce(state, (draftState) => {
|
||||
draftState.reachedEnd = true;
|
||||
draftState.searchStrings[searchString] = true;
|
||||
});
|
||||
}
|
||||
|
||||
case 'RESET':
|
||||
return initialState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@ export async function getBirdseye(url, cb, props) {
|
||||
}
|
||||
|
||||
export async function getEvents(url, cb, props) {
|
||||
const module = await import('./Events.jsx');
|
||||
const module = await import('./Events');
|
||||
return module.default;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
xs: '480px',
|
||||
'2xl': '1536px',
|
||||
'3xl': '1720px',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user