Compare commits

..

31 Commits

Author SHA1 Message Date
Blake Blackshear
7b063a19dc remove fps arg for mjpeg 2021-09-12 14:51:59 -05:00
Blake Blackshear
0320d94ea6 docs updates 2021-09-12 14:48:21 -05:00
Jason Hunter
a7b7a45b23 allow for custom object detection model via configuration 2021-09-12 07:17:26 -05:00
Blake Blackshear
89e317a6bb store start/end event with pre/post capture to avoid expiring wanted recordings 2021-09-11 08:34:27 -05:00
Blake Blackshear
5a209caed3 Merge remote-tracking branch 'origin/master' into release-0.9.0 2021-09-08 09:03:14 -04:00
Blake Blackshear
288b1a0562 remove nested enabled config setting on events 2021-09-08 08:02:26 -05:00
Dermot Duffy
d35b09b18f Refresh the HA installation instructions. 2021-09-05 11:14:28 -05:00
Blake Blackshear
e8eb3125a5 disallow extra keys in config 2021-09-04 16:56:01 -05:00
Blake Blackshear
8109445fdd fix color config for ts (fixes #1679) 2021-09-04 16:40:10 -05:00
Blake Blackshear
f63a7cb6c0 remove font_scale in timestamp_style and calculate dynamically again 2021-09-04 16:34:48 -05:00
Blake Blackshear
7fc5297f60 aarch64 makefile fix 2021-09-03 07:13:05 -05:00
Bernt Christian Egeland
00ff76a0b9 Events performance (#1645)
* rearrange event route and splitted into several components

* useIntersectionObserver

* re-arrange

* searchstring improvement

* added xs tailwind breakpoint

* useOuterClick hook

* cleaned up

* removed some video controls for mobile devices

* lint

* moved hooks to global folder

* moved buttons for small devices

* added button groups

Co-authored-by: Bernt Christian Egeland <cbegelan@gmail.com>
2021-09-03 07:11:23 -05:00
Bernt Christian Egeland
b8df419bad hide birdseye nav if not enabled 2021-09-03 07:07:45 -05:00
Peter Campion-Bye
faf103152a Update optimizing.md
Need note about increasing GPU memory on Pi - otherwise ffmpeg hwaccel won't work
2021-09-03 07:04:21 -05:00
drinfernoo
65855e23d9 Add RTMP and timestamp style to global config (#1674)
* :memo::white_check_mark:🔧 - Make RTMP config global

Fixes #1671

* :memo::white_check_mark:🔧 - Make timestamp style config global

Fixes #1656

* fix test function names

* formatter

Co-authored-by: Blake Blackshear <blakeb@blakeshome.com>
2021-09-03 07:03:36 -05:00
Blake Blackshear
6c28613def moar speed 2021-09-03 06:31:06 -05:00
Blake Blackshear
56480dc1ef bulk delete recordings 2021-09-02 20:40:38 -05:00
Blake Blackshear
8e1c15291d optimize checking recordings for events
sorts events and recordings so you can avoid a cartesian product of checking all events against all recordings
2021-09-02 08:24:53 -05:00
Blake Blackshear
a1e52c51b1 dont expire events in two places 2021-09-01 07:06:52 -05:00
Blake Blackshear
8cc834633e reduce db queries for recording cleanup 2021-09-01 06:44:05 -05:00
Blake Blackshear
7d65c05994 properly handle scenario with no recordings 2021-08-30 06:58:50 -05:00
Blake Blackshear
d74021af47 reverse sort events within hour 2021-08-29 07:46:09 -05:00
Blake Blackshear
46fe06e779 tweak vod settings for varying iframe intervals 2021-08-28 21:26:23 -05:00
Blake Blackshear
fbea51372f sync global snapshot options (fixes #1621) 2021-08-28 09:14:00 -05:00
Blake Blackshear
fa5ec8d019 cleanup global and camera detect config (fixes #1615) 2021-08-28 08:51:51 -05:00
Blake Blackshear
11c425a7eb error on invalid role 2021-08-28 08:16:25 -05:00
Blake Blackshear
0d352f3d8a use model from frogfish release 2021-08-28 08:04:29 -05:00
Blake Blackshear
6ccff71408 handle missing camera names 2021-08-28 07:43:51 -05:00
Blake Blackshear
41fea2a531 fix match for websocket url (fixes #1633) 2021-08-28 07:42:30 -05:00
Blake Blackshear
3d6dad7e7e reverse sort within a day for recordings 2021-08-27 07:26:11 -05:00
Blake Blackshear
bddde74c06 Update issue templates 2021-08-24 07:01:29 -05:00
67 changed files with 5103 additions and 7694 deletions

View 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.

View File

@@ -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 .

View File

@@ -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/

View File

@@ -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;

View File

@@ -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,12 +33,7 @@ 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`
@@ -87,40 +43,8 @@ If you are storing your database on a network share (SMB, NFS, etc), you may get
This may need to be in a custom location if network storage is used for the media folder.
```yaml
database:
path: /media/frigate/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
```
### `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
```

View File

@@ -0,0 +1,80 @@
---
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"
```

View File

@@ -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 recordings 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
| `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:
@@ -34,524 +32,14 @@ cameras:
detect:
width: 1280
height: 720
fps: 5
```
`width`, `height`, and `fps` are only used for the `detect` role. Other streams are passed through, so there is no need to specify the resolution.
## 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"
```
![poly](/img/example-mask-poly.png)
```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.
Exported clips are also created off of these recordings. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
These recordings will not be playable in the web UI or in Home Assistant's media browser unless your camera sends video as h264.
:::caution
Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
:::
```yaml
record:
# Optional: Enable recording (default: shown below)
enabled: False
# Optional: Number of days to retain (default: shown below)
retain_days: 0
# Optional: Event recording settings
events:
# Optional: Enable event recording retention settings (default: shown below)
enabled: False
# Optional: Maximum length of time to retain video during long events. (default: shown below)
# NOTE: If an object is being tracked for longer than this amount of time, the cache
# will begin to expire and the resulting clip will be the last x seconds of the event unless retain_days under record is > 0.
max_seconds: 300
# Optional: Number of seconds before the event to include in the event (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include in the event (default: shown below)
post_capture: 5
# Optional: Objects to save event for. (default: all tracked objects)
objects:
- person
# Optional: Restrict event to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Retention settings for event
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Per object retention days
objects:
person: 15
```
## Snapshots
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
Additional cameras are simply added to the config under the `cameras` entry.
```yaml
mqtt: ...
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:
# Required: Camera level detect settings
detect:
# Required: width of the frame for the input with the detect role
width: 1280
# Required: height of the frame for the input with the detect role
height: 720
# Required: desired fps for your camera for the input with the detect role
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
fps: 5
# Optional: enables detection for the camera (default: True)
# This value can be set via MQTT and will be updated in startup based on retained value
enabled: True
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
max_disappeared: 25
# Optional: camera level motion config
motion:
# 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: 24/7 recording configuration
record:
# Optional: Enable recording (default: global setting)
enabled: False
# Optional: Number of days to retain (default: global setting)
retain_days: 30
# Optional: Event recording settings
events:
# Required: enables event recordings for the camera (default: shown below)
# This value can be set via MQTT and will be updated in startup based on retained value
enabled: False
# Optional: Number of seconds before the event to include (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include (default: shown below)
post_capture: 5
# Optional: Objects to save events for. (default: all tracked objects)
objects:
- person
# Optional: Restrict events to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Camera override for retention settings (default: global values)
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Per object retention days
objects:
person: 15
# Optional: RTMP re-stream configuration
rtmp:
# Required: Enable the RTMP stream (default: True)
enabled: True
# Optional: Live stream configuration for WebUI
live:
# Optional: Set the height of the live stream. (default: 720)
# This must be less than or equal to the height of the detect stream. Lower resolutions
# reduce bandwidth required for viewing the live stream. Width is computed to match known aspect ratio.
height: 720
# Optional: Set the encode quality of the live stream (default: shown below)
# 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources.
quality: 8
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
snapshots:
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
# 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
```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 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
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"
back: ...
front: ...
side: ...
```

View File

@@ -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,16 +30,16 @@ detectors:
device: usb:1
```
Native Coral (Dev Board):
### Native Coral (Dev Board)
```yaml
detectors:
coral:
type: edgetpu
device: ''
device: ""
```
Multiple PCIE/M.2 Corals:
### Multiple PCIE/M.2 Corals
```yaml
detectors:
@@ -51,7 +51,7 @@ detectors:
device: pci:1
```
Mixing Corals:
### Mixing Corals
```yaml
detectors:
@@ -63,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.

View 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.

View File

@@ -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:
@@ -23,12 +23,15 @@ cameras:
detect:
width: 1280
height: 720
fps: 5
```
## 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:
@@ -37,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
@@ -61,59 +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
detect:
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)
@@ -121,117 +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.
Exported clips are also created off of these recordings. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
These recordings will not be playable in the web UI or in Home Assistant's media browser unless your camera sends video as h264.
:::caution
Previous versions of frigate included `-vsync drop` in input parameters. This is not compatible with FFmpeg's segment feature and must be removed from your input parameters if you have overrides set.
:::
```yaml
record:
# Optional: Enable recording (default: shown below)
enabled: False
# Optional: Number of days to retain (default: shown below)
retain_days: 0
# Optional: Event recording settings
events:
# Optional: Enable event recording retention settings (default: shown below)
enabled: False
# Optional: Maximum length of time to retain video during long events. (default: shown below)
# NOTE: If an object is being tracked for longer than this amount of time, the cache
# will begin to expire and the resulting clip will be the last x seconds of the event unless retain_days under record is > 0.
max_seconds: 300
# Optional: Number of seconds before the event to include (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include (default: shown below)
post_capture: 5
# Optional: Objects to save recordings for. (default: all tracked objects)
objects:
- person
# Optional: Restrict recordings to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Retention settings for events
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Per object retention days
objects:
person: 15
```
## `snapshots`
Can be overridden at the camera level. Global snapshot retention settings.
```yaml
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
snapshots:
retain:
# Required: Default retention days (default: shown below)
default: 10
# 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 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: []
```

View File

@@ -0,0 +1,39 @@
---
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
```
![poly](/img/example-mask-poly.png)

View File

@@ -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,7 +99,7 @@ processes:
To further improve performance, you can set ffmpeg to skip frames in the output,
using the fps filter:
```
```yaml
output_args:
- -filter:v
- fps=fps=5

View File

@@ -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.

View File

@@ -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.

View File

@@ -0,0 +1,10 @@
---
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.
Exported clips are also created off of these recordings. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
H265 recordings can be viewed in Edge and Safari only. All other browsers require recordings to be encoded with H264.

View 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.

View 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`.

View File

@@ -0,0 +1,10 @@
---
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.

View File

@@ -1,8 +1,12 @@
---
id: troubleshooting
title: Troubleshooting and FAQ
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.

View 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.
![Resolutions](/img/resolutions.png)
### 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

View 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).

View File

@@ -7,23 +7,34 @@ title: Recommended hardware
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.

View File

@@ -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
![Diagram](/img/diagram.png)
1. Look for Motion
2. Calculate Detection Regions
3. Run Object Detection

View File

@@ -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

View File

@@ -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 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).

View File

@@ -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,10 +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>.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.

View File

@@ -8,18 +8,18 @@ The best way to integrate with Home Assistant is to use the [official integratio
## Installation
Available via HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories). To install:
### Preparation
- Add the custom repository:
The Frigate integration requires the `mqtt` integration to be installed and
manually configured first.
```
Home Assistant > HACS > Integrations > [...] > Custom Repositories
```
See the [MQTT integration
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
details.
| Key | Value |
| -------------- | ----------------------------------------------------------- |
| Repository URL | https://github.com/blakeblackshear/frigate-hass-integration |
| Category | Integration |
### Integration installation
Available via HACS as a default repository. To install:
- Use [HACS](https://hacs.xyz/) to install the integration:
@@ -38,6 +38,12 @@ 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:

View File

@@ -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

9131
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -1,16 +1,32 @@
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',
],
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/resolutions.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -170,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()
@@ -199,6 +200,7 @@ class FrigateApp:
name,
self.detection_queue,
self.detection_out_events,
model_path,
model_shape,
"cpu",
detector.num_threads,
@@ -208,6 +210,7 @@ class FrigateApp:
name,
self.detection_queue,
self.detection_out_events,
model_path,
model_shape,
detector.device,
detector.num_threads,

View File

@@ -9,7 +9,7 @@ from typing import Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt
import numpy as np
import yaml
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Extra, Field, validator
from pydantic.fields import PrivateAttr
from frigate.const import BASE_DIR, CACHE_DIR, RECORD_DIR
@@ -29,18 +29,23 @@ DEFAULT_TRACKED_OBJECTS = ["person"]
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
class FrigateBaseModel(BaseModel):
class Config:
extra = Extra.forbid
class DetectorTypeEnum(str, Enum):
edgetpu = "edgetpu"
cpu = "cpu"
class DetectorConfig(BaseModel):
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")
@@ -60,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).",
@@ -146,19 +149,22 @@ class RuntimeMotionConfig(MotionConfig):
class Config:
arbitrary_types_allowed = True
extra = Extra.ignore
class DetectConfig(BaseModel):
height: int = Field(title="Height of the stream for the detect role.")
width: int = Field(title="Width of the stream for the detect role.")
fps: int = Field(title="Number of frames per second to process through detection.")
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."
)
@@ -199,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."
@@ -242,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.")
@@ -254,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.")
@@ -301,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.",
@@ -316,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."
)
@@ -332,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."
)
@@ -363,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."
@@ -391,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.")
@@ -424,16 +451,16 @@ 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.")
best_image_timeout: int = Field(
@@ -446,12 +473,14 @@ class CameraConfig(BaseModel):
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."
@@ -460,7 +489,9 @@ class CameraConfig(BaseModel):
default_factory=ObjectConfig, title="Object configuration."
)
motion: Optional[MotionConfig] = Field(title="Motion detection configuration.")
detect: 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."
)
@@ -565,13 +596,15 @@ 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(
@@ -592,7 +625,7 @@ class ModelConfig(BaseModel):
super().__init__(**config)
self._merged_labelmap = {
**load_labels("/labelmap.txt"),
**load_labels(config.get("labelmap_path", "/labelmap.txt")),
**config.get("labelmap", {}),
}
@@ -611,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."
)
@@ -620,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."
@@ -650,6 +677,9 @@ class FrigateConfig(BaseModel):
snapshots: SnapshotsConfig = Field(
default_factory=SnapshotsConfig, title="Global snapshots configuration."
)
rtmp: RtmpConfig = Field(
default_factory=RtmpConfig, title="Global RTMP restreaming configuration."
)
birdseye: BirdseyeConfig = Field(
default_factory=BirdseyeConfig, title="Birdseye configuration."
)
@@ -662,10 +692,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:
@@ -681,10 +715,12 @@ class FrigateConfig(BaseModel):
include={
"record": ...,
"snapshots": ...,
"rtmp": ...,
"objects": ...,
"motion": ...,
"detect": ...,
"ffmpeg": ...,
"timestamp_style": ...,
},
exclude_unset=True,
)
@@ -695,6 +731,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)
@@ -742,15 +783,6 @@ class FrigateConfig(BaseModel):
**camera_config.motion.dict(exclude_unset=True),
)
# Default detect configuration
max_disappeared = camera_config.detect.fps * 5
if camera_config.detect.max_disappeared is None:
camera_config.detect.max_disappeared = max_disappeared
# Default live configuration
if camera_config.live is None:
camera_config.live = CameraLiveConfig()
config.cameras[name] = camera_config
return config

View File

@@ -45,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 = {}
@@ -64,7 +64,7 @@ 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:
@@ -77,7 +77,7 @@ class LocalObjectDetector(ObjectDetector):
"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()
@@ -133,6 +133,7 @@ def run_detector(
out_events: Dict[str, mp.Event],
avg_speed,
start,
model_path,
model_shape,
tf_device,
num_threads,
@@ -152,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():
@@ -189,6 +192,7 @@ class EdgeTPUProcess:
name,
detection_queue,
out_events,
model_path,
model_shape,
tf_device=None,
num_threads=3,
@@ -199,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
@@ -226,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,

View File

@@ -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__)
@@ -35,10 +35,8 @@ class EventProcessor(threading.Thread):
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
):
# Recording is disabled
if not record_config.enabled:
return False
# If there are required zones and there is no overlap
@@ -76,17 +74,17 @@ 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
has_clip = self.should_create_clip(camera, event_data)
event_config: EventsConfig = self.config.cameras[camera].record.events
if 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"]),

View File

@@ -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,

View File

@@ -275,9 +275,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,
)
@@ -411,9 +410,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,
)

View File

@@ -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)

View File

@@ -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
@@ -155,16 +157,16 @@ class RecordingCleanup(threading.Thread):
logger.debug("Start deleted cameras.")
# Handle deleted cameras
no_camera_recordings: Recordings = Recordings.select().where(
Recordings.camera.not_in(list(self.config.cameras.keys())),
)
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:
no_camera_recordings: Recordings = Recordings.select().where(
Recordings.camera.not_in(list(self.config.cameras.keys())),
Recordings.end_time < expire_before,
)
for recording in no_camera_recordings:
Path(recording.path).unlink(missing_ok=True)
Recordings.delete_by_id(recording.id)
logger.debug("End deleted cameras.")
@@ -183,59 +185,54 @@ class RecordingCleanup(threading.Thread):
).timestamp()
expire_date = min(min_end, expire_before)
# Get recordings to remove
recordings: Recordings = Recordings.select().where(
# Get recordings to check for expiration
recordings: Recordings = (
Recordings.select()
.where(
Recordings.camera == camera,
Recordings.end_time < expire_date,
)
.order_by(Recordings.start_time.desc())
)
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
# Get all the events to check against
events: Event = (
Event.select()
.where(
Event.camera == camera, Event.end_time < expire_date, Event.has_clip
)
| Event.end_time.between(
recording.start_time, recording.end_time
)
| (
(recording.start_time > Event.start_time)
& (recording.end_time < Event.end_time)
)
),
.order_by(Event.start_time.desc())
.objects()
)
# 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
event_ids = set()
# since the events and recordings are sorted, we can skip events
# that start after the previous recording segment ended
for idx in range(event_start, len(events)):
event = events[idx]
# if the next event ends before this segment starts, break
if event.end_time < recording.start_time:
break
# if the next event starts after the current segment ends, skip it
if event.start_time > recording.end_time:
event_start = idx
continue
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:
keep = True
# 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}.")
@@ -258,15 +255,14 @@ class RecordingCleanup(threading.Thread):
)
# 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
if oldest_recording
else datetime.datetime.now().timestamp()
)
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(

View File

@@ -32,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 = {
@@ -473,7 +473,7 @@ class TestConfig(unittest.TestCase):
"width": 1920,
"fps": 5,
},
"record": {"events": {"enabled": True}},
"record": {"events": {}},
}
},
}
@@ -692,6 +692,346 @@ 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_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)

View File

@@ -51,18 +51,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]

78
web/package-lock.json generated
View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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 />

View File

@@ -37,6 +37,7 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
return (
<div
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' : ''}`}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>
);

View 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)
};

View 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 };
};

View File

@@ -36,5 +36,20 @@ Maintain aspect ratio and scale down the video container
Could not find a proper tailwind css.
*/
.outer-max-width {
max-width: 60%;
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;
}
}

View File

@@ -1,7 +1,10 @@
import { h, Fragment } from 'preact';
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';
@@ -9,12 +12,46 @@ import Snapshot from '../icons/Snapshot';
import Dialog from '../components/Dialog';
import Heading from '../components/Heading';
import VideoPlayer from '../components/VideoPlayer';
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
import { FetchStatus, useApiHost, useEvent, useDelete } from '../api';
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();
@@ -25,6 +62,13 @@ export default function Event({ eventId, close, scrollRef }) {
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 = () => {
@@ -54,25 +98,28 @@ export default function Event({ eventId, close, scrollRef }) {
return <ActivityIndicator />;
}
const startime = new Date(data.start_time * 1000);
const endtime = new Date(data.end_time * 1000);
return (
<div className="space-y-4">
<div className="grid grid-cols-6 gap-4">
<div class="col-start-1 col-end-8 md:space-x-4">
<Button color="blue" href={`${apiHost}/api/events/${eventId}/clip.mp4?download=true`} download>
<Clip className="w-6" /> Download Clip
</Button>
<Button color="blue" href={`${apiHost}/api/events/${eventId}/snapshot.jpg?download=true`} download>
<Snapshot className="w-6" /> Download Snapshot
</Button>
</div>
<div class="col-end-10 col-span-2 space-x-4">
<Button className="self-start" color="red" onClick={handleClickDelete}>
<Delete className="w-6" /> Delete event
</Button>
<Button color="gray" className="self-start" onClick={() => close()}>
<Close className="w-6" /> Close
<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}
@@ -91,13 +138,47 @@ export default function Event({ eventId, close, scrollRef }) {
/>
) : null}
</div>
<div className="outer-max-width m-auto">
<div className="w-full pt-5 relative pb-20">
<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>
<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`,
@@ -127,6 +208,10 @@ export default function Event({ eventId, close, scrollRef }) {
)}
</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>
);
}

View File

@@ -1,326 +0,0 @@
import { h, Fragment } 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 Event from './Event';
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: {}, deleted: 0 });
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;
}
};
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, deleted }, dispatch] = useReducer(reducer, initialState);
const { searchParams: initialSearchParams } = new URL(window.location);
const [viewEvent, setViewEvent] = useState(null);
const [searchString, setSearchString] = useState(`${defaultSearchString(limit)}&${initialSearchParams.toString()}`);
const { data, status, deletedId } = useEvents(searchString);
const scrollToRef = {};
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(`${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 viewEventHandler = (id) => {
//Toggle event view
if (viewEvent === id) return setViewEvent(null);
//Set event id to be rendered.
setViewEvent(id);
};
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, 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 (
<Fragment key={id}>
<Tr data-testid={`event-${id}`} className={`${viewEvent === id ? 'border-none' : ''}`}>
<Td className="w-40">
<a
onClick={() => viewEventHandler(id)}
ref={ref}
data-start-time={startTime}
data-reached-end={reachedEnd}
>
<img
ref={(el) => (scrollToRef[id] = el)}
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}
/>
</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>
{viewEvent === id ? (
<Tr className="border-b-1">
<Td colSpan="8">
<Event eventId={id} close={() => setViewEvent(null)} scrollRef={scrollToRef} />
</Td>
</Tr>
) : null}
</Fragment>
);
}
)}
</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'}
/>
);
}

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,3 @@
export { default as TableHead } from './tableHead';
export { default as TableRow } from './tableRow';
export { default as Filters } from './filters';

View 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;

View 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;

View 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>
);
}

View 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;
}
};

View File

@@ -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;
}

View File

@@ -4,6 +4,7 @@ module.exports = {
theme: {
extend: {
screens: {
xs: '480px',
'2xl': '1536px',
'3xl': '1720px',
},