Compare commits

..

214 Commits

Author SHA1 Message Date
Blake Blackshear
bfecee9650 add missing optional comment in docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
395c16300d deregister based on max_frames setting 2022-02-18 21:18:26 -06:00
Blake Blackshear
ff19cdb773 refactor stationary config into section 2022-02-18 21:18:26 -06:00
Nicolas Mowen
5627b66a6e Always show recording link even if recordings are currently disabled (#2787)
* Always show recording link even if recordings are currently disabled

* Fix test to consider all cameras to have recording link
2022-02-18 21:18:26 -06:00
Blake Blackshear
ebdfbfe96c update birdseye to handle stationary objects 2022-02-18 21:18:26 -06:00
Blake Blackshear
1a009c7fd1 use second stream in docs example 2022-02-18 21:18:26 -06:00
Blake Blackshear
c14f986fae stop forcing detection all the way to stationary_threshold 2022-02-18 21:18:26 -06:00
Blake Blackshear
ee5b9986ad bump default stationary_threshold to 10s 2022-02-18 21:18:26 -06:00
Blake Blackshear
190f217b13 set stationary_threshold default to 5x fps 2022-02-18 21:18:26 -06:00
Blake Blackshear
e78662b924 fix the bounding box calculation position at 10 2022-02-18 21:18:26 -06:00
Blake Blackshear
5a2e395352 selectively increment position changes 2022-02-18 21:18:26 -06:00
Jason Hunter
8de15af7b4 Fix duration for long events and playback rate for top of the hour 2022-02-18 21:18:26 -06:00
Jason Hunter
21178613de Only send significant update once when motionless count reaches the defined threshold. 2022-02-18 21:18:26 -06:00
Jason Hunter
340be7f86d Allow download of in progress clips 2022-02-18 21:18:26 -06:00
Blake Blackshear
0ff4acd59c remove invalid warning 2022-02-18 21:18:26 -06:00
Jason Hunter
28dd43f8ae Fix playback rate resetting to 1 on source change 2022-02-18 21:18:26 -06:00
Jason Hunter
56d24cbf6d Update package-lock.json 2022-02-18 21:18:26 -06:00
Jason Hunter
e433bec17f Add in progress events to recordings view 2022-02-18 21:18:26 -06:00
Blake Blackshear
3189467a36 update an object once per minute 2022-02-18 21:18:26 -06:00
Blake Blackshear
63536d249f signal an update when object becomes stationary 2022-02-18 21:18:26 -06:00
Blake Blackshear
3e90f3032c make stationary_threshold configurable 2022-02-18 21:18:26 -06:00
Blake Blackshear
5cff849e59 publish an update on position changes 2022-02-18 21:18:26 -06:00
Blake Blackshear
06cc7527a9 only update db entry when a stored property changes 2022-02-18 21:18:26 -06:00
Blake Blackshear
d78dc2388c increment motionless_count 2022-02-18 21:18:26 -06:00
Blake Blackshear
583912db9c allow motion based retention when detect is disabled 2022-02-18 21:18:26 -06:00
Blake Blackshear
5792cf042e fix resolution on reolink example 2022-02-18 21:18:26 -06:00
Blake Blackshear
4524dca3ed clarify addon versions 2022-02-18 21:18:26 -06:00
Blake Blackshear
304a569c7e remove outdated output args tip 2022-02-18 21:18:26 -06:00
Blake Blackshear
10c200dc24 clarify that zones are based on the bottom center 2022-02-18 21:18:26 -06:00
Blake Blackshear
0b8617d09f update addon urls 2022-02-18 21:18:26 -06:00
Blake Blackshear
e7026dfd6e add example for ios camera live feed notification 2022-02-18 21:18:26 -06:00
Blake Blackshear
b82d75b79e avoid rare divide by zero 2022-02-18 21:18:26 -06:00
Blake Blackshear
ac30091258 note for future 2022-02-18 21:18:26 -06:00
Blake Blackshear
de58bdcc9f improve warning for retain modes 2022-02-18 21:18:26 -06:00
Blake Blackshear
329e5f8f91 invert active_count logic 2022-02-18 21:18:26 -06:00
Blake Blackshear
4a16171f96 set has_clip to false when recordings fail 2022-02-18 21:18:26 -06:00
Blake Blackshear
f0212c2aa4 adjust error messages on ffmpeg crash 2022-02-18 21:18:26 -06:00
Blake Blackshear
7401cf2399 add stacktrace to config validation errors 2022-02-18 21:18:26 -06:00
Blake Blackshear
493d16519a add new properties to the docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
a87675d3cc add additional info for non-H264 cameras 2022-02-18 21:18:26 -06:00
Blake Blackshear
0bef16bb17 upgrade npm in dev container 2022-02-18 21:18:26 -06:00
Blake Blackshear
e0078f388e package updates for docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
cf6e66c453 allow dash in camera name 2022-02-18 21:18:26 -06:00
Blake Blackshear
34fa53afcc make motion the default retain mode 2022-02-18 21:18:26 -06:00
Blake Blackshear
663bf05fd7 update stationary interval docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
f512af2563 make expire interval configurable for users wanting to minimize i/o 2022-02-18 21:18:26 -06:00
Blake Blackshear
7e7d70aa5b avoid extra tracking work on stationary frames 2022-02-18 21:18:26 -06:00
Blake Blackshear
dd1cf4d2ce use iou instead of centroid 2022-02-18 21:18:26 -06:00
Blake Blackshear
e627f4e935 dont stop scanning when there are other regions 2022-02-18 21:18:26 -06:00
Blake Blackshear
c6445898ce default periodic checks to never 2022-02-18 21:18:26 -06:00
Blake Blackshear
f1ddd0e6f7 scan the frame on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
db369a5b7f require a position change to be an active object 2022-02-18 21:18:26 -06:00
Blake Blackshear
87cd618998 randomize the region multiplier for variation 2022-02-18 21:18:26 -06:00
Blake Blackshear
338e4004d4 improve method for determining position
compares the centroid to a history of bounding boxes
2022-02-18 21:18:26 -06:00
Blake Blackshear
675f21e23a if recording not on disk, delete from db and return 2022-02-18 21:18:26 -06:00
Blake Blackshear
4d2d11193f cleanup clean snapshots on event deletion too 2022-02-18 21:18:26 -06:00
Blake Blackshear
69aaf1f8e6 require url safe camera names 2022-02-18 21:18:26 -06:00
Bernt Christian Egeland
a10970d7c9 Event Datepicker (#2428)
* new datepicker

* dev

* dev

* dev

* fix for version 0.10

* added rounded corners for date range

* lint

* Commented out some Select.test.

* improved date range selection

* improved functions with useCallback

* improved Select.test.jsx

* keyboard navigation

* keyboard navigation

* added dropdown menu icon

* Hide filters on xs, Button to show

* check if to far left before right

* Filter button text

* improved local timezone
2022-02-18 21:18:26 -06:00
Yuriy Sannikov
6eecb6780e Run python unit tests in a github actions (#2589)
* tox tests initial commit

* run tests in the Dockerfile during the build phase

* remove local tests

Co-authored-by: YS <ys@gm.com>
2022-02-18 21:18:26 -06:00
Yuriy Sannikov
80627e4989 safe refactoring (#2552)
Co-authored-by: YS <ys@gm.com>
2022-02-18 21:18:26 -06:00
TJ Horner
9e987fdebc Change JPEG mime type (#2543) 2022-02-18 21:18:26 -06:00
Blake Blackshear
e6292c719d disable disk sync on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
7c74bf2566 fix migrations 2022-02-18 21:18:26 -06:00
Blake Blackshear
2c91e7853c check for apex dir 2022-02-18 21:18:26 -06:00
Ryan McLean
1e7f196e5c #2117 change entered_zones from set to list so that they are not automatically alphabetically ordered (#2212) 2022-02-18 21:18:26 -06:00
Justin Goette
f91f4f0053 Allow for ".yaml" (#2244)
* allow for ".yaml"

* remove unused import
2022-02-18 21:18:26 -06:00
Matt Clayton
8b2622a234 Add temperature of coral tpu to telemetry mqtt message 2022-02-18 21:18:26 -06:00
Blake Blackshear
a2ddb12eb3 limit vod response cache 2022-02-18 21:18:26 -06:00
Blake Blackshear
985bd6d9bd update docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
ec3c15e4a7 expire overlapping segments based on mode 2022-02-18 21:18:26 -06:00
Blake Blackshear
188b202836 store objects and motion counts in the db 2022-02-18 21:18:26 -06:00
Blake Blackshear
01e607a14e warn when retention mismatch 2022-02-18 21:18:26 -06:00
Blake Blackshear
5b164b72dc refactor segment stats logic 2022-02-18 21:18:26 -06:00
Blake Blackshear
dcf65febba switch to retain config instead of retain_days 2022-02-18 21:18:26 -06:00
Blake Blackshear
56a2d4e64d pass processed tracked objects 2022-02-18 21:18:26 -06:00
Blake Blackshear
ef214fb80a retain frame data for recording maintenance 2022-02-18 21:18:26 -06:00
Blake Blackshear
93f418ac0b fix process_clip 2022-02-18 21:18:26 -06:00
Blake Blackshear
689af4ff87 sync recordings with disk once on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
4ab0927de8 no need to expire recordings every minute 2022-02-18 21:18:26 -06:00
Blake Blackshear
014e6fc909 ensure cache copies when events have ended 2022-02-18 21:18:26 -06:00
Blake Blackshear
6832575643 cleanup missing files from database once per hour 2022-02-18 21:18:26 -06:00
Blake Blackshear
07ad2d97b1 handle missing file edge case 2022-02-18 21:18:26 -06:00
Blake Blackshear
039f1a522e log error messages on vod endpoints 2022-02-18 21:18:26 -06:00
Blake Blackshear
24e2f84231 ensure duration > 0 for segments 2022-02-18 21:18:26 -06:00
Blake Blackshear
e0c0033852 use snapshot url to support in progress events 2022-02-18 21:18:26 -06:00
Blake Blackshear
c50e9d48bf ensure stationary interval is greater than 0 2022-02-18 21:18:26 -06:00
Blake Blackshear
173eaabddf add duration to cache 2022-02-18 21:18:26 -06:00
Blake Blackshear
a748b70da1 avoid running ffprobe for each segment multiple times 2022-02-18 21:18:26 -06:00
Blake Blackshear
8eabe5dd41 warn if no wait time 2022-02-18 21:18:26 -06:00
Blake Blackshear
114415b5e1 keep 5 segments in cache 2022-02-18 21:18:26 -06:00
Blake Blackshear
ba55b5a6db better cache handling 2022-02-18 21:18:26 -06:00
Blake Blackshear
7533f2a8ab avoid proactive messages with retain_days 0 and handle first pass 2022-02-18 21:18:26 -06:00
Blake Blackshear
543a8a1712 avoid divide by zero 2022-02-18 21:18:26 -06:00
Blake Blackshear
9b23ff597c revert switch to b/w frame prep 2022-02-18 21:18:26 -06:00
Blake Blackshear
b2ce1edd5a fix default motion comment 2022-02-18 21:18:26 -06:00
Blake Blackshear
a0235b7da4 more robust cache management 2022-02-18 21:18:26 -06:00
Blake Blackshear
87e2300855 set retain when setting switches from frontend 2022-02-18 21:18:26 -06:00
Blake Blackshear
34bc6a6457 error handling for the recording maintainer 2022-02-18 21:18:26 -06:00
Blake Blackshear
273076e7f4 don't modify ffmpeg_cmd object 2022-02-18 21:18:26 -06:00
Blake Blackshear
b29b311e92 fix ffmpeg config for env vars 2022-02-18 21:18:26 -06:00
Blake Blackshear
5a9e82c4b0 create ffmpeg commands on startup 2022-02-18 21:18:26 -06:00
Blake Blackshear
6218791708 clarify shm in docs 2022-02-18 21:18:26 -06:00
Blake Blackshear
0e43f452d2 use resolution of clip 2022-02-18 21:18:26 -06:00
Blake Blackshear
0695bb097d revamp process clip 2022-02-18 21:18:26 -06:00
Blake Blackshear
294c79a271 no longer make motion settings dynamic 2022-02-18 21:18:26 -06:00
Blake Blackshear
e351e132f5 remove min frame height of 180 and increase contour area 2022-02-18 21:18:26 -06:00
Blake Blackshear
258215a3ae consolidate regions 2022-02-18 21:18:26 -06:00
Blake Blackshear
08ddfc100f improve contrast 2022-02-18 21:18:26 -06:00
Blake Blackshear
8ab6cba521 check for overlapping motion boxes 2022-02-18 21:18:26 -06:00
Blake Blackshear
eb16de7395 config option for stationary detection interval 2022-02-18 21:18:26 -06:00
Blake Blackshear
dde0498ed3 drop high overlap detections 2022-02-18 21:18:26 -06:00
Blake Blackshear
75c8570913 reduce detection rate for stationary objects 2022-02-18 21:18:26 -06:00
Blake Blackshear
e36099a342 improve box merging and keep tracking 2022-02-18 21:18:26 -06:00
Blake Blackshear
2f2329ba44 only save recordings when an event is in progress 2022-02-18 21:18:26 -06:00
Blake Blackshear
6c8b184d2c version tick 2022-02-18 21:18:26 -06:00
DataBitz
32878bd016 Another missing slash (#2803)
2nd attempt to fix link to full configuration
2022-02-14 07:33:29 -06:00
DataBitz
12d13988c4 Missing slash in url (#2797)
Missing slash in url to full-configuration-reference
2022-02-13 07:38:24 -06:00
atinsley
9e4d921488 Update advanced.md (#2794)
Add details about how to specify a custom database location in config.yml
2022-02-12 06:28:56 -06:00
Blake Blackshear
edc1884c4e add warning to storage docs 2022-02-11 06:15:15 -06:00
Alex Yao
a2d1bd2c67 Document JPEG streams (#2586)
* Document JPEG streams

* Update camera_specific.md
2022-02-02 07:27:22 -06:00
Felipe Santos
bb68a2405b Improve audio conversion tip (#2140)
* Improve audio convert guide

* Mention faq in RTMP configuration

* Add example for audio conversion tip

* Change comma to period

* Explain why this is needed
2021-12-29 08:57:32 -06:00
MrNorm
42ac4172ff Add passthrough information for PCIe Coral TPU (#2200) 2021-12-12 09:31:52 -06:00
hcooper
998921ae63 Update objects.mdx
Mention that `person` is the only tracked object by default. Minor reformat.
2021-12-01 07:33:16 -06:00
Blake Blackshear
26ae6084ea fix rtmp again 2021-10-24 13:53:43 -05:00
Blake Blackshear
76142e9699 version tick 2021-10-24 13:53:43 -05:00
Blake Blackshear
5e692acfbb add links in docs to other sites 2021-10-23 09:41:32 -05:00
Blake Blackshear
a67b8ab84d validate with runtime config (fixes #2055) 2021-10-23 08:21:15 -05:00
Blake Blackshear
4cf55ad8e2 Revert switch to mpegts format and audio default 2021-10-23 08:21:15 -05:00
Blake Blackshear
c1132e6897 update ignore files 2021-10-23 08:21:15 -05:00
Blake Blackshear
d6104f2eb2 add storage info to docs 2021-10-23 08:21:15 -05:00
Blake Blackshear
b0e0abe385 improve performance of cache loop 2021-10-23 08:21:15 -05:00
Blake Blackshear
4916e1cd1d hide banner for ffmpeg conversion 2021-10-23 08:21:15 -05:00
Blake Blackshear
cd87f3e6f4 fix old style recording cleanup 2021-10-23 08:21:15 -05:00
Blake Blackshear
18f4ab2644 version tick 2021-10-23 08:21:15 -05:00
Lindsay Ward
0bd3be94ec Clarify environment variables
Based on issue #1976 - specify explicitly that these fields can include environment variables to avoid interpretation that environment variables could be used anywhere.
I am participating in #hacktoberfest, so I would appreciate if you could add the 'hacktoberfest-accepted' label (or add #hacktoberfest topic to your repo). Thanks!
2021-10-23 06:42:53 -05:00
Blake Blackshear
25bb515afc Merge pull request #2026 from blakeblackshear/recording_fix
0.9.2
2021-10-19 20:43:25 -05:00
Blake Blackshear
7ab6961ee1 use live dimensions 2021-10-17 08:48:59 -05:00
Blake Blackshear
ae24cf3bb2 set max width/height for live view 2021-10-17 07:48:56 -05:00
Blake Blackshear
2e494477a6 backwards compatibility for segment_type 2021-10-16 10:36:13 -05:00
Blake Blackshear
80b72c75d9 revert jest update 2021-10-16 08:12:22 -05:00
Blake Blackshear
9494bb7f5f frontend dependency updates 2021-10-16 07:57:59 -05:00
Blake Blackshear
86a741b6e6 assign roles when single input and consolidate validation 2021-10-16 07:46:39 -05:00
Blake Blackshear
f738275d21 yell about config validation errors
for the people in the back
2021-10-16 07:17:36 -05:00
Blake Blackshear
e297e02800 store audio by default 2021-10-16 06:06:49 -05:00
Blake Blackshear
b2e05afff2 prevent oldest recording from being deleted 2021-10-15 21:56:03 -05:00
Blake Blackshear
05fc35fc3d update hardware docs 2021-10-15 21:29:36 -05:00
Blake Blackshear
c809494c98 switch to mpegts format for cache and create mp4 with faststart 2021-10-15 21:08:43 -05:00
Blake Blackshear
ef82c5c691 fix expiration when event spans the exire date 2021-10-15 07:30:55 -05:00
Blake Blackshear
c0e2a75715 version tick 2021-10-15 07:30:35 -05:00
Blake Blackshear
01ddd00bc5 Merge pull request #1975 from blakeblackshear/hassos_docs
update hassos warning
2021-10-10 07:39:11 -05:00
Blake Blackshear
d150f01a2c update hassos warning 2021-10-10 07:32:55 -05:00
Blake Blackshear
f9e159deaf Merge pull request #1968 from FM-17/patch-1
warning for dev board incompatibility post-0.9.x
2021-10-09 11:57:46 -05:00
FM-17
381b00157e warning for dev board incompatibility post-0.9.x
Hoped to investigate this with my dev board at some point. In the meantime, added a warning for others who may experience it when upgrading to the new stable release.
2021-10-09 11:23:51 -03:00
Blake Blackshear
800f33e7be version tick 2021-10-05 19:02:38 -05:00
Blake Blackshear
b8218876be Merge pull request #1922 from blakeblackshear/fix_logo
fix logo used for birdseye
2021-10-05 18:57:07 -05:00
Blake Blackshear
5669f4c161 fix logo used for birdseye 2021-10-05 18:40:46 -05:00
Blake Blackshear
c492b30adb Merge pull request #825 from blakeblackshear/release-0.9.0
Release 0.9.0
2021-10-05 17:59:25 -05:00
Kevin Pelzel
eb48722126 added white background to apple-touch-icon 2021-10-05 17:37:18 -05:00
Blake Blackshear
8e881b60f0 update hardware recommendations 2021-10-05 07:13:13 -05:00
Blake Blackshear
0260d824a6 further doc clarifications 2021-10-05 06:57:17 -05:00
Blake Blackshear
0877a7dec7 Create config.yml 2021-10-04 17:20:58 -05:00
Blake Blackshear
4c7919ad69 updated links 2021-10-04 08:54:35 -05:00
Blake Blackshear
4e997124b3 update latest recommendations for reolink 2021-10-04 07:18:53 -05:00
Blake Blackshear
8b040f5c95 optimize images for web 2021-10-04 07:00:30 -05:00
Blake Blackshear
96156805ed Delete bug_report.md 2021-10-03 08:53:19 -05:00
Blake Blackshear
b8d48d7e62 Create support_request.yml 2021-10-03 08:51:53 -05:00
Blake Blackshear
8ca12806ca revert rockchip support for aarch64 2021-10-03 07:43:55 -05:00
Blake Blackshear
de811b7018 delete clean snapshot when duplicate 2021-10-02 06:59:02 -05:00
Blake Blackshear
7bf7365f6c better log message when corrupt segment detected 2021-10-02 06:58:29 -05:00
Blake Blackshear
1daffd92fd docs updates 2021-10-01 07:37:47 -05:00
Blake Blackshear
74986982a0 update docs url 2021-09-26 16:43:26 -05:00
Blake Blackshear
aa807d25ed add affiliate links 2021-09-26 13:37:42 -05:00
Blake Blackshear
cd28869649 fix path 2021-09-26 12:32:41 -05:00
Blake Blackshear
ae97692883 docs config update for netlify 2021-09-26 12:27:01 -05:00
Blake Blackshear
e8e778c6d4 instantiate area field 2021-09-26 09:43:31 -05:00
Kevin Pelzel
5c552a0d71 change theme color from red 2021-09-25 11:11:49 -05:00
Blake Blackshear
0f5dfea9de add support for rockchip hwaccel 2021-09-25 08:25:00 -05:00
Blake Blackshear
e6cdb6a7a2 install docs clarification 2021-09-24 06:45:15 -05:00
Blake Blackshear
1d25936f31 add region/bbox/area to event table 2021-09-23 07:31:48 -05:00
Blake Blackshear
1049673413 run nginx as root
this addresses an issue many have had when using network shares
2021-09-20 19:02:59 -05:00
Blake Blackshear
c3109f808c allow partial days in retention settings 2021-09-20 18:59:16 -05:00
Blake Blackshear
a943ac1308 use s6 to shutdown frigate 2021-09-18 07:40:27 -05:00
Blake Blackshear
96319e795c docs clarification for masks 2021-09-17 19:21:03 -05:00
Blake Blackshear
5a8016de87 simplify logic and fix wrong segments expiring (fixes #1779) 2021-09-17 17:15:16 -05:00
Blake Blackshear
bc350644bd make expiration of deleted camera footage faster 2021-09-17 17:12:03 -05:00
Blake Blackshear
c793500ad2 add udp camera example to docs 2021-09-15 07:33:50 -05:00
Blake Blackshear
1b2134c49e remove clip_ready event type
this doesnt really mean anything more than "end" anymore. new has_clip property added
2021-09-15 07:16:52 -05:00
Blake Blackshear
86a5b46c68 more docs updates 2021-09-14 08:07:36 -05:00
Blake Blackshear
f83d4a58dd add version to the logs on startup 2021-09-13 22:02:23 -05:00
Blake Blackshear
a5f241d5bd cleanup ha notification docs 2021-09-13 22:02:12 -05:00
Blake Blackshear
661f7baa21 fix global live config 2021-09-13 20:33:00 -05:00
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
bddde74c06 Update issue templates 2021-08-24 07:01:29 -05:00
124 changed files with 38093 additions and 9274 deletions

View File

@@ -7,4 +7,5 @@ config/
.git .git
core core
*.mp4 *.mp4
*.db *.db
*.ts

View File

@@ -1,56 +0,0 @@
---
name: Bug report or Support request
about: Bug report or Support request
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what your issue is.
**Version of frigate**
Output from `/api/version`
**Config file**
Include your full config file wrapped in triple back ticks.
```yaml
config here
```
**Frigate container logs**
```
Include relevant log output here
```
**Frigate stats**
```json
Output from frigate's /api/stats endpoint
```
**FFprobe from your camera**
Run the following command and paste output below
```
ffprobe <stream_url>
```
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Computer Hardware**
- OS: [e.g. Ubuntu, Windows]
- Install method: [e.g. Addon, Docker Compose, Docker Command]
- Virtualization: [e.g. Proxmox, Virtualbox]
- Coral Version: [e.g. USB, PCIe, None]
- Network Setup: [e.g. Wired, WiFi]
**Camera Info:**
- Manufacturer: [e.g. Dahua]
- Model: [e.g. IPC-HDW5231R-ZE]
- Resolution: [e.g. 720p]
- FPS: [e.g. 5]
**Additional context**
Add any other context about the problem here.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

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

@@ -0,0 +1,107 @@
name: Support Request
description: Support for Frigate setup or configuration
title: "[Support]: "
labels: ["support", "triage"]
assignees: []
body:
- type: textarea
id: description
attributes:
label: Describe the problem you are having
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: Visible on the Debug page in the Web UI
validations:
required: true
- type: textarea
id: config
attributes:
label: Frigate config file
description: This will be automatically formatted into code, so no need for backticks.
render: yaml
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
id: ffprobe
attributes:
label: FFprobe output from your camera
description: Run `ffprobe <camera_url>` and provide output below
render: shell
validations:
required: true
- type: textarea
id: stats
attributes:
label: Frigate stats
description: Output from frigate's /api/stats endpoint
render: json
- type: dropdown
id: os
attributes:
label: Operating system
options:
- HassOS
- Debian
- Other Linux
- Proxmox
- UNRAID
- Windows
- Other
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Install method
options:
- HassOS Addon
- Docker Compose
- Docker CLI
validations:
required: true
- type: dropdown
id: coral
attributes:
label: Coral version
options:
- USB
- PCIe
- M.2
- Dev Board
- Other
- CPU (no coral)
validations:
required: true
- type: dropdown
id: network
attributes:
label: Network connection
options:
- Wired
- Wireless
- Mixed
validations:
required: true
- type: input
id: camera
attributes:
label: Camera make and model
description: Dahua, hikvision, amcrest, reolink, etc and model number
validations:
required: true
- type: textarea
id: other
attributes:
label: Any other information that may be helpful

View File

@@ -44,3 +44,27 @@ jobs:
- name: Test - name: Test
run: npm run test run: npm run test
working-directory: ./web working-directory: ./web
docker_tests_on_aarch64:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and run tests
run: make run_tests PLATFORM="linux/arm64/v8" ARCH="aarch64"
docker_tests_on_amd64:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and run tests
run: make run_tests PLATFORM="linux/amd64" ARCH="amd64"

View File

@@ -1,28 +0,0 @@
name: On push
on:
push:
branches:
- master
- release-0.8.0
jobs:
deploy-docs:
name: Deploy docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 12.x
- run: npm install
- name: Build docs
run: npm run build
- name: Deploy documentation
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build

2
.gitignore vendored
View File

@@ -6,7 +6,9 @@ debug
config/config.yml config/config.yml
models models
*.mp4 *.mp4
*.ts
*.db *.db
*.csv
frigate/version.py frigate/version.py
web/build web/build
web/node_modules web/node_modules

View File

@@ -3,7 +3,7 @@ default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1) COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version: version:
echo "VERSION='0.9.0-$(COMMIT_HASH)'" > frigate/version.py echo "VERSION='0.10.0-$(COMMIT_HASH)'" > frigate/version.py
web: web:
docker build --tag frigate-web --file docker/Dockerfile.web web/ docker build --tag frigate-web --file docker/Dockerfile.web web/
@@ -39,13 +39,13 @@ aarch64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels . docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels .
aarch64_ffmpeg: aarch64_ffmpeg:
docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.2.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 . docker build --no-cache --pull --tag blakeblackshear/frigate-ffmpeg:1.3.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
aarch64_frigate: version web 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-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 . 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: armv7_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels . docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels .
@@ -59,4 +59,16 @@ armv7_frigate: version web
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
.PHONY: web run_tests:
# PLATFORM: linux/arm64/v8 linux/amd64 or linux/arm/v7
# ARCH: aarch64 amd64 or armv7
@cat docker/Dockerfile.base docker/Dockerfile.$(ARCH) > docker/Dockerfile.test
@sed -i "s/FROM frigate-web as web/#/g" docker/Dockerfile.test
@sed -i "s/COPY --from=web \/opt\/frigate\/build web\//#/g" docker/Dockerfile.test
@sed -i "s/FROM frigate-base/#/g" docker/Dockerfile.test
@echo "" >> docker/Dockerfile.test
@echo "RUN python3 -m unittest" >> docker/Dockerfile.test
@docker buildx build --platform=$(PLATFORM) --tag frigate-base --build-arg NGINX_VERSION=1.0.2 --build-arg FFMPEG_VERSION=1.0.0 --build-arg ARCH=$(ARCH) --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.test .
@rm docker/Dockerfile.test
.PHONY: web run_tests

View File

@@ -20,7 +20,7 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
## Documentation ## Documentation
View the documentation at https://blakeblackshear.github.io/frigate View the documentation at https://docs.frigate.video
## Donations ## Donations

View File

@@ -22,3 +22,5 @@ RUN pip3 install pylint black
# Install Node 14 # Install Node 14
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \ RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get install -y nodejs && apt-get install -y nodejs
RUN npm install -g npm@latest

View File

@@ -9,7 +9,7 @@ WORKDIR /tmp/workdir
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -yqq update && \ RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 && \ apt-get install -yq --no-install-recommends ca-certificates expat libgomp1 xutils-dev && \
apt-get autoremove -y && \ apt-get autoremove -y && \
apt-get clean -y apt-get clean -y
@@ -18,7 +18,7 @@ FROM base as build
ENV FFMPEG_VERSION=4.3.2 \ ENV FFMPEG_VERSION=4.3.2 \
AOM_VERSION=v1.0.0 \ AOM_VERSION=v1.0.0 \
FDKAAC_VERSION=0.1.5 \ FDKAAC_VERSION=0.1.5 \
FREETYPE_VERSION=2.5.5 \ FREETYPE_VERSION=2.11.0 \
FRIBIDI_VERSION=0.19.7 \ FRIBIDI_VERSION=0.19.7 \
KVAZAAR_VERSION=1.2.0 \ KVAZAAR_VERSION=1.2.0 \
LAME_VERSION=3.100 \ LAME_VERSION=3.100 \
@@ -43,7 +43,7 @@ ENV FFMPEG_VERSION=4.3.2 \
LIBZMQ_VERSION=4.3.2 \ LIBZMQ_VERSION=4.3.2 \
SRC=/usr/local SRC=/usr/local
ARG FREETYPE_SHA256SUM="5d03dd76c2171a7601e9ce10551d52d4471cf92cd205948e60289251daddffa8 freetype-2.5.5.tar.gz" ARG FREETYPE_SHA256SUM="a45c6b403413abd5706f3582f04c8339d26397c4304b78fa552f2215df64101f freetype-2.11.0.tar.gz"
ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz" ARG FRIBIDI_SHA256SUM="3fc96fa9473bd31dcb5500bdf1aa78b337ba13eb8c301e7c28923fea982453a8 0.19.7.tar.gz"
ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz" ARG LIBVIDSTAB_SHA256SUM="14d2a053e56edad4f397be0cb3ef8eb1ec3150404ce99a426c4eb641861dc0bb v1.1.0.tar.gz"
ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz" ARG OGG_SHA256SUM="e19ee34711d7af328cb26287f4137e70630e7261b17cbe3cd41011d73a654692 libogg-1.3.2.tar.gz"
@@ -392,6 +392,16 @@ RUN \
make -j $(nproc) install && \ make -j $(nproc) install && \
rm -rf ${DIR} rm -rf ${DIR}
RUN \
DIR=/tmp/rkmpp && \
mkdir -p ${DIR} && \
cd ${DIR} && \
git clone https://github.com/rockchip-linux/libdrm-rockchip && git clone https://github.com/rockchip-linux/mpp && \
cd libdrm-rockchip && bash autogen.sh && ./configure && make && make install && \
cd ../mpp && cmake -DRKPLATFORM=ON -DHAVE_DRM=ON && make -j6 && make install && \
rm -rf ${DIR}
## ffmpeg https://ffmpeg.org/ ## ffmpeg https://ffmpeg.org/
RUN \ RUN \
DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \ DIR=/tmp/ffmpeg && mkdir -p ${DIR} && cd ${DIR} && \
@@ -434,6 +444,8 @@ RUN \
--enable-libkvazaar \ --enable-libkvazaar \
--enable-libaom \ --enable-libaom \
--extra-libs=-lpthread \ --extra-libs=-lpthread \
--enable-rkmpp \
--enable-libdrm \
# --enable-omx \ # --enable-omx \
# --enable-omx-rpi \ # --enable-omx-rpi \
# --enable-mmal \ # --enable-mmal \

View File

@@ -1,4 +1,5 @@
daemon off; daemon off;
user root;
worker_processes 1; worker_processes 1;
error_log /usr/local/nginx/logs/error.log warn; error_log /usr/local/nginx/logs/error.log warn;
@@ -57,7 +58,7 @@ http {
# vod caches # vod caches
vod_metadata_cache metadata_cache 512m; vod_metadata_cache metadata_cache 512m;
vod_mapping_cache mapping_cache 5m; vod_mapping_cache mapping_cache 5m 10m;
# gzip manifests # gzip manifests
gzip on; gzip on;

View File

@@ -1,50 +1,11 @@
--- ---
id: advanced id: advanced
title: Advanced title: Advanced Options
sidebar_label: Advanced sidebar_label: Advanced Options
--- ---
## Advanced configuration ## 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` ### `logger`
Change the default log level for troubleshooting purposes. Change the default log level for troubleshooting purposes.
@@ -72,12 +33,7 @@ Examples of available modules are:
### `environment_vars` ### `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) This section can be used to set environment variables for those unable to modify the environment of the container (ie. within HassOS)
```yaml
environment_vars:
EXAMPLE_VAR: value
```
### `database` ### `database`
@@ -89,23 +45,7 @@ This may need to be in a custom location if network storage is used for the medi
```yaml ```yaml
database: database:
path: /media/frigate/frigate.db path: /path/to/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` ### `model`
@@ -116,11 +56,14 @@ The labelmap can be customized to your needs. A common reason to do this is to c
```yaml ```yaml
model: model:
# Required: height of the trained model
height: 320
# Required: width of the trained model
width: 320
# Optional: labelmap overrides
labelmap: labelmap:
7: car 2: vehicle
3: vehicle
5: vehicle
7: vehicle
15: animal
16: animal
17: animal
``` ```
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.

View File

@@ -0,0 +1,115 @@
---
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
```
### JPEG Stream Cameras
Cameras using a live changing jpeg image will need input parameters as below
```yaml
input_args:
- -r
- 5 # << enter FPS here
- -stream_loop
- -1
- -f
- image2
- -avoid_negative_ts
- make_zero
- -fflags
- nobuffer
- -flags
- low_delay
- -strict
- experimental
- -fflags
- +genpts+discardcorrupt
- -use_wallclock_as_timestamps
- 1
```
Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras)
### 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 -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv
```
### Reolink 410/520 (possibly others)
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/1713#issuecomment-932976305), the http video streams seem to be the most reliable for Reolink.
```yaml
cameras:
reolink:
ffmpeg:
hwaccel_args:
input_args:
- -avoid_negative_ts
- make_zero
- -fflags
- nobuffer+genpts+discardcorrupt
- -flags
- low_delay
- -strict
- experimental
- -analyzeduration
- 1000M
- -probesize
- 1000M
- -rw_timeout
- "5000000"
inputs:
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password
roles:
- record
- rtmp
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password
roles:
- detect
detect:
width: 896
height: 672
fps: 7
```
![Resolutions](/img/reolink-settings.png)
### Blue Iris RTSP Cameras
You will need to remove `nobuffer` flag for Blue Iris RTSP cameras
```yaml
ffmpeg:
input_args: -avoid_negative_ts make_zero -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
```
### UDP Only Cameras
If your cameras do not support TCP connections for RTSP, you can use UDP.
```yaml
ffmpeg:
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -stimeout 5000000 -use_wallclock_as_timestamps 1
```

View File

@@ -5,17 +5,15 @@ title: Cameras
## Setting Up Camera Inputs ## 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: Each role can only be assigned to one input per camera. The options for roles are as follows:
| Role | Description | | Role | Description |
| -------- | ------------------------------------------------------------------------------------- | | -------- | ----------------------------------------------------------------------------------------------- |
| `detect` | Main feed for object detection | | `detect` | Main feed for object detection |
| `record` | Saves segments of the video feed based on configuration settings. [docs](#recordings) | | `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](#rtmp-streams) | | `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](/configuration/rtmp) |
### Example
```yaml ```yaml
mqtt: mqtt:
@@ -34,524 +32,14 @@ cameras:
detect: detect:
width: 1280 width: 1280
height: 720 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. Additional cameras are simply added to the config under the `cameras` entry.
## 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
```yaml ```yaml
mqtt: ...
cameras: cameras:
# Required: name of the camera back: ...
back: front: ...
# Required: ffmpeg settings for the camera side: ...
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:
# 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: 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"
``` ```

View File

@@ -3,13 +3,13 @@ id: detectors
title: 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). 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. **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 ```yaml
detectors: detectors:
@@ -18,7 +18,7 @@ detectors:
device: usb device: usb
``` ```
Multiple USB Corals: ### Multiple USB Corals
```yaml ```yaml
detectors: detectors:
@@ -30,16 +30,17 @@ detectors:
device: usb:1 device: usb:1
``` ```
Native Coral (Dev Board): ### Native Coral (Dev Board)
_warning: may have [compatibility issues](https://github.com/blakeblackshear/frigate/issues/1706) after `v0.9.x`_
```yaml ```yaml
detectors: detectors:
coral: coral:
type: edgetpu type: edgetpu
device: '' device: ""
``` ```
Multiple PCIE/M.2 Corals: ### Multiple PCIE/M.2 Corals
```yaml ```yaml
detectors: detectors:
@@ -51,7 +52,7 @@ detectors:
device: pci:1 device: pci:1
``` ```
Mixing Corals: ### Mixing Corals
```yaml ```yaml
detectors: detectors:
@@ -63,12 +64,16 @@ detectors:
device: pci device: pci
``` ```
CPU Detectors (not recommended): ### CPU Detectors (not recommended)
```yaml ```yaml
detectors: detectors:
cpu1: cpu1:
type: cpu type: cpu
num_threads: 3
cpu2: cpu2:
type: cpu 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 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 ```yaml
mqtt: mqtt:
@@ -23,12 +23,15 @@ cameras:
detect: detect:
width: 1280 width: 1280
height: 720 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 ```yaml
mqtt: mqtt:
@@ -37,16 +40,16 @@ mqtt:
# Optional: port (default: shown below) # Optional: port (default: shown below)
port: 1883 port: 1883
# Optional: topic prefix (default: shown below) # 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 topic_prefix: frigate
# Optional: client id (default: shown below) # 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 client_id: frigate
# Optional: user # Optional: user
user: mqtt_user user: mqtt_user
# Optional: password # Optional: password
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}. # NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
# eg. password: '{FRIGATE_MQTT_PASSWORD}' # e.g. password: '{FRIGATE_MQTT_PASSWORD}'
password: password password: password
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None) # Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
tls_ca_certs: /path/to/ca.crt tls_ca_certs: /path/to/ca.crt
@@ -61,59 +64,39 @@ mqtt:
tls_insecure: false tls_insecure: false
# Optional: interval in seconds for publishing stats (default: shown below) # Optional: interval in seconds for publishing stats (default: shown below)
stats_interval: 60 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. # Optional: Database configuration
```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
database: database:
# The path to store the SQLite DB (default: shown below) # The path to store the SQLite DB (default: shown below)
path: /media/frigate/frigate.db path: /media/frigate/frigate.db
```
### `model`
```yaml
# Optional: model modifications # Optional: model modifications
model: 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) # Required: Object detection model input width (default: shown below)
width: 320 width: 320
# Required: Object detection model input height (default: shown below) # Required: Object detection model input height (default: shown below)
height: 320 height: 320
# Optional: Label name modifications # Optional: Label name modifications. These are merged into the standard labelmap.
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 # Optional: logger verbosity settings
logger: logger:
# Optional: Default log verbosity (default: shown below) # Optional: Default log verbosity (default: shown below)
@@ -121,102 +104,12 @@ logger:
# Optional: Component specific logger overrides # Optional: Component specific logger overrides
logs: logs:
frigate.event: debug 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. # Optional: birdseye configuration
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
```
### `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
birdseye: birdseye:
# Optional: Enable birdseye view (default: shown below) # Optional: Enable birdseye view (default: shown below)
enabled: True enabled: True
@@ -232,4 +125,310 @@ birdseye:
# motion - cameras are included if motion was detected in the last 30 seconds # motion - cameras are included if motion was detected in the last 30 seconds
# continuous - all cameras are included always # continuous - all cameras are included always
mode: objects 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: Configuration for stationary object tracking
stationary:
# Optional: Frequency for running detection on stationary objects (default: shown below)
# When set to 0, object detection will never be run on stationary objects. If set to 10, it will be run on every 10th frame.
interval: 0
# Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s)
threshold: 50
# Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever)
# This can help with false positives for objects that should only be stationary for a limited amount of time.
# It can also be used to disable stationary object tracking. For example, you may want to set a value for person, but leave
# car at the default.
max_frames:
# Optional: Default for all object types (default: not set, track forever)
default: 3000
# Optional: Object specific values
objects:
person: 1000
# 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: 30)
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will
# make motion detection more sensitive to smaller moving objects.
# As a rule of thumb:
# - 15 - high sensitivity
# - 30 - medium sensitivity
# - 50 - low sensitivity
contour_area: 30
# 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: 50)
# 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: 50
# 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)
# WARNING: Frigate does not currently support limiting recordings based
# on available disk space automatically. If using recordings,
# you must specify retention settings for a number of days that
# will fit within the available disk space of your drive or Frigate
# will crash.
enabled: False
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
expire_interval: 60
# Optional: Retention settings for recording
retain:
# 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.
days: 0
# Optional: Mode for retention. Available options are: all, motion, and active_objects
# all - save all recording segments regardless of activity
# motion - save all recordings segments with any detected motion
# active_objects - save all recording segments with active/moving objects
# NOTE: this mode only applies when the days setting above is greater than 0
mode: all
# 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: Mode for retention. (default: shown below)
# all - save all recording segments for events regardless of activity
# motion - save all recordings segments for events with any detected motion
# active_objects - save all recording segments for event with active/moving objects
#
# NOTE: If the retain mode for the camera is more restrictive than the mode configured
# here, the segments will already be gone by the time this mode is applied.
# For example, if the camera retain mode is "motion", the segments without motion are
# never stored, so setting the mode to "all" here won't bring them back.
mode: motion
# 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: path may include environment variables, which must begin with 'FRIGATE_' and 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: Presence in a zone is evaluated only based on the bottom center of the objects bounding box.
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,77 @@
---
id: masks
title: Masks
---
There are two types of masks available:
**Motion masks**: Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the debug feed with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. Over masking will make it more difficult for objects to be tracked. To see this effect, create a mask, and then watch the video feed with `Motion Boxes` enabled again.
**Object filter masks**: Object filter masks are used to filter out false positives for a given object type based on location. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell frigate that anything in your yard is a false positive.
To create a poly mask:
1. Visit the Web UI
1. Click the camera you wish to create a mask for
1. Select "Debug" at the top
1. Expand the "Options" below the video feed
1. Click "Mask & Zone creator"
1. Click "Add" on the type of mask or zone you would like to create
1. Click on the camera's latest image to create a masked area. The yaml representation will be updated in real-time
1. When you've finished creating your mask, click "Copy" and paste the contents into your config file and restart Frigate
Example of a finished row corresponding to the below example image:
```yaml
motion:
mask: "0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432"
```
Multiple masks can be listed.
```yaml
motion:
mask:
- 458,1346,336,973,317,869,375,866,432
- 0,461,3,0,1919,0,1919,843,1699,492,1344
```
![poly](/img/example-mask-poly-min.png)
### Further Clarification
This is a response to a [question posed on reddit](https://www.reddit.com/r/homeautomation/comments/ppxdve/replacing_my_doorbell_with_a_security_camera_a_6/hd876w4?utm_source=share&utm_medium=web2x&context=3):
It is helpful to understand a bit about how Frigate uses motion detection and object detection together.
First, Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.
Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer.
After the area with motion is identified, Frigate creates a "region" (the green boxes in the debug viewer) to run object detection on. The models are trained on square images, so these regions are always squares. It adds a margin around the motion area in hopes of capturing a cropped view of the object moving that fills most of the image passed to object detection, but doesn't cut anything off. It also takes into consideration the location of the bounding box from the previous frame if it is tracking an object.
After object detection runs, if there are detected objects that seem to be cut off, Frigate reframes the region and runs object detection again on the same frame to get a better look.
All of this happens for each area of motion and tracked object.
> Are you simply saying that INITIAL triggering of any kind of detection will only happen in un-masked areas, but that once this triggering happens, the masks become irrelevant and object detection takes precedence?
Essentially, yes. I wouldn't describe it as object detection taking precedence though. The motion masks just prevent those areas from being counted as motion. Those masks do not modify the regions passed to object detection in any way, so you can absolutely detect objects in areas masked for motion.
> If so, this is completely expected and intuitive behavior for me. Because obviously if a "foot" starts motion detection the camera should be able to check if it's an entire person before it fully crosses into the zone. The docs imply this is the behavior, so I also don't understand why this would be detrimental to object detection on the whole.
When just a foot is triggering motion, Frigate will zoom in and look only at the foot. If that even qualifies as a person, it will determine the object is being cut off and look again and again until it zooms back out enough to find the whole person.
It is also detrimental to how Frigate tracks a moving object. Motion nearby the bounding box from the previous frame is used to intelligently determine where the region should be in the next frame. With too much masking, tracking is hampered and if an object walks from an unmasked area into a fully masked area, they essentially disappear and will be picked up as a "new" object if they leave the masked area. This is important because Frigate uses the history of scores while tracking an object to determine if it is a false positive or not. It takes a minimum of 3 frames for Frigate to determine is the object type it thinks it is, and the median score must be greater than the threshold. If a person meets this threshold while on the sidewalk before they walk into your stoop, you will get an alert the instant they step a single foot into a zone.
> I thought the main point of this feature was to cut down on CPU use when motion is happening in unnecessary areas.
It is, but the definition of "unnecessary" varies. I want to ignore areas of motion that I know are definitely not being triggered by objects of interest. Timestamps, trees, sky, rooftops. I don't want to ignore motion from objects that I want to track and know where they go.
> For me, giving my masks ANY padding results in a lot of people detection I'm not interested in. I live in the city and catch a lot of the sidewalk on my camera. People walk by my front door all the time and the margin between the sidewalk and actually walking onto my stoop is very thin, so I basically have everything but the exact contours of my stoop masked out. This results in very tidy detections but this info keeps throwing me off. Am I just overthinking it?
This is what `required_zones` are for. You should define a zone (remember this is evaluated based on the bottom center of the bounding box) and make it required to save snapshots and clips (now events in 0.9.0). You can also use this in your conditions for a notification.
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.

View File

@@ -1,6 +1,6 @@
--- ---
id: nvdec id: nvdec
title: nVidia hardware decoder title: NVIDIA hardware decoder
--- ---
Certain nvidia cards include a hardware decoder, which can greatly improve the Certain nvidia cards include a hardware decoder, which can greatly improve the
@@ -9,6 +9,8 @@ ffmpeg with NVDEC support is required. The special docker architecture 'amd64nvi
includes this support for amd64 platforms. An aarch64 for the Jetson, which includes this support for amd64 platforms. An aarch64 for the Jetson, which
also includes NVDEC may be added in the future. also includes NVDEC may be added in the future.
Some more detailed setup instructions are also available in [this issue](https://github.com/blakeblackshear/frigate/issues/1847#issuecomment-932076731).
## Docker setup ## Docker setup
### Requirements ### Requirements
@@ -23,7 +25,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: In a docker compose file, these lines need to be set:
``` ```yaml
services: services:
frigate: frigate:
... ...
@@ -41,7 +43,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) 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..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264) V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc) V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
@@ -57,10 +59,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 For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
`-c:v hevc_cuvid` to your ffmpeg input arguments: `-c:v hevc_cuvid` to your ffmpeg input arguments:
``` ```yaml
ffmpeg: ffmpeg:
input_args: input_args: ...
...
- -c:v - -c:v
- hevc_cuvid - hevc_cuvid
``` ```
@@ -96,15 +97,3 @@ processes:
| 0 N/A N/A 12827 C ffmpeg 417MiB | | 0 N/A N/A 12827 C ffmpeg 417MiB |
+-----------------------------------------------------------------------------+ +-----------------------------------------------------------------------------+
``` ```
To further improve performance, you can set ffmpeg to skip frames in the output,
using the fps filter:
```
output_args:
- -filter:v
- fps=fps=5
```
This setting, for example, allows Frigate to consume my 10-15fps camera streams on
my relatively low powered Haswell machine with relatively low cpu usage.

View File

@@ -1,12 +1,15 @@
--- ---
id: objects id: objects
title: Default available objects title: Objects
sidebar_label: Available objects
--- ---
import labels from "../../../labelmap.txt"; import labels from "../../../labelmap.txt";
By default, Frigate includes the following object models from the Google Coral test data. Frigate includes the object models listed below from the Google Coral test data.
Please note:
- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
- `person` is the only tracked object by default. See the [full configuration reference](https://docs.frigate.video/configuration/index#full-configuration-reference) for an example of expanding the list of tracked objects.
<ul> <ul>
{labels.split("\n").map((label) => ( {labels.split("\n").map((label) => (
@@ -22,4 +25,4 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
- EdgeTPU Model: `/edgetpu_model.tflite` - EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt` - 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,24 @@
---
id: record
title: Recording
---
Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy in the config. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
H265 recordings can be viewed in Edge and Safari only. All other browsers require recordings to be encoded with H264.
## What if I don't want 24/7 recordings?
If you only used clips in previous versions with recordings disabled, you can use the following config to get the same behavior. This is also the default behavior when recordings are enabled.
```yaml
record:
enabled: True
events:
retain:
default: 10
```
This configuration will retain recording segments that overlap with events and have active tracked objects for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs.
When `retain_days` is set to `0`, segments will be deleted from the cache if no events are in progress.

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. Some more information about it can be found [here](../faqs#audio-in-recordings).

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,40 @@
---
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. Presence in a zone is evaluated based on the bottom center of the bounding box for the object. It does not matter how much of the bounding box overlaps with the zone.
Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
During testing, enable the Zones option for the debug feed so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
To create a zone, follow [the steps for a "Motion mask"](/configuration/masks), but use the section of the web UI for creating a zone instead.
### Restricting zones to specific objects
Sometimes you want to limit a zone to specific object types to have more granular control of when events/snapshots are saved. The following example will limit one zone to person objects and the other to cars.
```yaml
camera:
record:
events:
required_zones:
- entire_yard
- front_yard_street
snapshots:
required_zones:
- entire_yard
- front_yard_street
zones:
entire_yard:
coordinates: ... (everywhere you want a person)
objects:
- person
front_yard_street:
coordinates: ... (just the street)
objects:
- car
```
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street.

View File

@@ -1,21 +1,40 @@
--- ---
id: troubleshooting id: faqs
title: Troubleshooting and FAQ 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. ### 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. A solid green image means that frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly.
### How can I get sound or audio in my recordings? ### How can I get sound or audio in my recordings? {#audio-in-recordings}
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/frigate/configuration/index#ffmpeg). By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/configuration/index/#full-configuration-reference).
:::tip
When using `-c:a aac`, do not forget to replace `-c copy` with `-c:v copy`. Example:
```diff title="frigate.yml"
ffmpeg:
output_args:
- record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
+ record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac
```
This is needed because the `-c` flag (without `:a` or `:v`) applies for both audio and video, thus making it conflicting with `-c:a aac`.
:::
### My mjpeg stream or snapshots look green and crazy ### My mjpeg stream or snapshots look green and crazy
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards. This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
![mismatched-resolution](/img/mismatched-resolution.jpg) ![mismatched-resolution](/img/mismatched-resolution-min.jpg)
### I can't view events or recordings in the Web UI. ### I can't view events or recordings in the Web UI.

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-min.jpg)
### 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,201 @@
---
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
rtmp:
enabled: False # <-- RTMP should be disabled if your stream is not H264
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 H264 RTSP cameras that support TCP connections. If you do not have H264 cameras, make sure you have disabled RTMP. It is possible to enable it, but you must tell ffmpeg to re-encode the video with customized output args.
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
- path: rtsp://10.0.10.10:554/high_res_stream # <----- Add high res stream
roles:
- record
detect: ...
record: # <----- Enable recording
enabled: True
motion: ...
```
If you don't have separate streams for detect and record, you would just add the record role to the list on the first input.
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

@@ -0,0 +1,80 @@
---
id: ha_notifications
title: Home Assistant notifications
---
The best way to get started with notifications for Frigate is to use the [Blueprint](https://community.home-assistant.io/t/frigate-mobile-app-notifications/311091). You can use the yaml generated from the Blueprint as a starting point and customize from there.
It is generally recommended to trigger notifications based on the `frigate/events` mqtt topic. This provides the event_id needed to fetch [thumbnails/snapshots/clips](/integrations/home-assistant#notification-api) and other useful information to customize when and where you want to receive alerts. The data is published in the form of a change feed, which means you can reference the "previous state" of the object in the `before` section and the "current state" of the object in the `after` section. You can see an example [here](/integrations/mqtt#frigateevents).
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android'
tag: '{{trigger.payload_json["after"]["id"]}}'
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
```
Note that iOS devices support live previews of cameras by adding a camera entity id to the message data.
```yaml
automation:
- alias: Security_Frigate_Notifications
description: ""
trigger:
- platform: mqtt
topic: frigate/events
payload: new
value_template: "{{ value_json.type }}"
action:
- service: notify.mobile_app_iphone
data:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
image: >-
https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg
tag: '{{trigger.payload_json["after"]["id"]}}'
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
entity_id: camera.{{trigger.payload_json["after"]["camera"]}}
mode: single
```
## Conditions
Conditions with the `before` and `after` values allow a high degree of customization for automations.
When a person enters a zone named yard
```yaml
condition:
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
```
When a person leaves a zone named yard
```yaml
condition:
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
```
Notify for dogs in the front with a high top score
```yaml
condition:
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
```

View File

@@ -0,0 +1,37 @@
---
id: stationary_objects
title: Avoiding stationary objects
---
Many people use Frigate to detect cars entering their driveway, and they often run into an issue with repeated events of a parked car being repeatedly detected. This is because object tracking stops when motion ends and the event ends. Motion detection works by determining if a sufficient number of pixels have changed between frames. Shadows or other lighting changes will be detected as motion. This will often cause a new event for a parked car.
You can use zones to restrict events and notifications to objects that have entered specific areas.
:::caution
It is not recommended to use masks to try and eliminate parked cars in your driveway. Masks are designed to prevent motion from triggering object detection and/or to indicate areas that are guaranteed false positives.
Frigate is designed to track objects as they move and over-masking can prevent it from knowing that an object in the current frame is the same as the previous frame. You want Frigate to detect objects everywhere and configure your events and alerts to be based on the location of the object with zones.
:::
To only be notified of cars that enter your driveway from the street, you could create multiple zones that cover your driveway. For cars, you would only notify if `entered_zones` from the events MQTT topic has more than 1 zone.
See [this example](/configuration/zones#restricting-zones-to-specific-objects) from the Zones documentation to see how to restrict zones to certain object types.
![Driveway Zones](/img/driveway_zones-min.png)
To limit snapshots and events, you can list the zone for the entrance of your driveway under `required_zones` in your configuration file. Example below.
```yaml
camera:
record:
events:
required_zones:
- zone_2
zones:
zone_1:
coordinates: ... (parking area)
zone_2:
coordinates: ... (entrance to driveway)
```

View File

@@ -7,23 +7,62 @@ 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. 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.
| Name | Inference Speed | Notes | Many users have reported various issues with Reolink cameras, so I do not recommend them. If you are using Reolink, I suggest the [Reolink specific configuration](configuration/camera_specific#reolink-410520-possibly-others). Wifi cameras are also not recommended. Their streams are less reliable and cause connection loss and/or lost video data.
| ----------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Atomic Pi | 16ms | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| 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. |
| 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 Here are some of the camera's I recommend:
Many people have powerful enough NAS devices or home servers to also run docker. There is a Unraid Community App. - <a href="https://amzn.to/3uFLtxB" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) T5442TM-AS-LED</a> (affiliate link)
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) - <a href="https://amzn.to/3isJ3gU" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) IPC-T5442TM-AS</a> (affiliate link)
- <a href="https://amzn.to/2ZWNWIA" target="_blank" rel="nofollow noopener sponsored">Amcrest IP5M-T1179EW-28MM</a> (affiliate link)
| Name | Inference Speed | Notes | I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
| ------------------------------------ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [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) | ## Server
My current favorite is the Odyssey X86 Blue J4125 because the Coral M.2 compatibility and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
| Name | Inference Speed | Coral Compatibility | Notes |
| -------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Odyssey X86 Blue J4125</a> (affiliate link) | 9-10ms | M.2 B+M | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3oxEC8m" target="_blank" rel="nofollow noopener sponsored">Minisforum GK41</a> (affiliate link) | 9-10ms | USB | Great alternative to a NUC. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3ixJFlb" target="_blank" rel="nofollow noopener sponsored">Minisforum GK50</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
| <a href="https://amzn.to/3l7vCEI" target="_blank" rel="nofollow noopener sponsored">Intel NUC</a> (affiliate link) | 8-10ms | USB | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. |
| <a href="https://amzn.to/3a6TBh8" target="_blank" rel="nofollow noopener sponsored">BMAX B2 Plus</a> (affiliate link) | 10-12ms | USB | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
| <a href="https://amzn.to/2YjpY9m" target="_blank" rel="nofollow noopener sponsored">Atomic Pi</a> (affiliate link) | 16ms | USB | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| <a href="https://amzn.to/2WIpwRU" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 3B (32bit)</a> (affiliate link) | 60ms | USB | Can handle a small number of cameras, but the detection speeds are slow due to USB 2.0. |
| <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (32bit)</a> (affiliate link) | 15-20ms | USB | Can handle a small number of cameras. The 2GB version runs fine. |
| <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (64bit)</a> (affiliate link) | 10-15ms | USB | Can handle a small number of cameras. The 2GB version runs fine. |
## Google Coral TPU
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
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.
### What does Frigate use the CPU for and what does it use the Coral for? (ELI5 Version)
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
CPU Usage: I am a CPU, Mendel is a Google Coral
My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it.
Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score!
_What happens when I increase the resolution of my camera?_
However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby.
Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it.
### Do hwaccel args help if I am using a Coral?
YES! The Coral does not help with decoding video streams.
Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://blog.video.ibm.com/streaming-video-tips/keyframes-interframe-video-compression/). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work.

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 id: index
title: Frigate title: Introduction
sidebar_label: Features
slug: / 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. 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) - 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 - Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
@@ -15,11 +14,12 @@ 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 - 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 - Object detection with TensorFlow runs in separate processes for maximum FPS
- Communicates over MQTT for easy integration into other systems - 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 - Re-streaming via RTMP to reduce the number of connections to your camera
- A dynamic combined camera view of all tracked cameras.
## Screenshots ## Screenshots
![Media Browser](/img/media_browser.png) ![Media Browser](/img/media_browser-min.png)
![Notification](/img/notification.png) ![Notification](/img/notification-min.png)

View File

@@ -3,25 +3,105 @@ id: installation
title: 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/). Note that a Home Assistant Addon is **not** the same thing as the integration. The [integration](integrations/home-assistant) is required to integrate Frigate into Home Assistant.
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. ## Dependencies
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. **MQTT broker** - Frigate requires an MQTT broker. If using Home Assistant, Frigate and Home Assistant must be connected to the same MQTT broker.
## HassOS Addon ## Preparing your hardware
HassOS users can install via the addon repository. Frigate requires an MQTT server. ### Operating System
1. Navigate to Supervisor > Add-on Store > Repositories 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, but [not in all circumstances](https://github.com/blakeblackshear/frigate/discussions/1837).
2. Add https://github.com/blakeblackshear/frigate-hass-addons
3. Setup your network configuration in the `Configuration` tab if deisred 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.
4. Create the file `frigate.yml` in your `config` directory with your detailed Frigate configuration
5. Start the addon container ### Storage
6. If you are using hardware acceleration for ffmpeg, you will need to disable "Protection mode"
Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine.
:::caution
Note that Frigate does not currently support limiting recordings based on available disk space automatically. If using recordings, you must specify retention settings for a number of days that will fit within the available disk space of your drive or Frigate will crash.
:::
- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
- `/media/frigate/frigate.db`: Default location for the sqlite database. You will also see several files alongside this file while frigate is running. If moving the database location (often needed when using a network drive at `/media/frigate`), it is recommended to mount a volume with docker at `/db` and change the storage location of the database to `/db/frigate.db` in the config file.
- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder.
- `/dev/shm`: It is not recommended to modify this directory or map it with docker. This is the location for raw decoded frames in shared memory and it's size is impacted by the `shm-size` calculations below.
- `/config/config.yml`: Default location of the config file.
#### Common docker compose storage configurations
Writing to a local disk or external USB drive:
```yaml
version: "3.9"
services:
frigate:
...
volumes:
- /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:
size: 1000000000
...
```
Writing to a network drive with database on a local drive:
```yaml
version: "3.9"
services:
frigate:
...
volumes:
- /path/to/your/config.yml:/config/config.yml:ro
- /path/to/network/storage:/media/frigate
- /path/to/local/disk:/db
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
target: /tmp/cache
tmpfs:
size: 1000000000
...
```
frigate.yml
```yaml
database:
path: /db/frigate.db
```
### 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 using the resolution specified for detect:
```
(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 <a href="https://amzn.to/3a2mH0P" target="_blank" rel="nofollow noopener sponsored">this</a> (affiliate link).
## Docker ## Docker
Running in Docker directly is the recommended install method.
Make sure you choose the right image for your architecture: Make sure you choose the right image for your architecture:
| Arch | Image Name | | Arch | Image Name |
@@ -41,13 +121,15 @@ services:
privileged: true # this may not be necessary for all setups privileged: true # this may not be necessary for all setups
restart: unless-stopped restart: unless-stopped
image: blakeblackshear/frigate:<specify_version_tag> image: blakeblackshear/frigate:<specify_version_tag>
shm_size: "64mb" # update for your cameras based on calculation above
devices: 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/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware - /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- <path_to_config_file>:/config/config.yml:ro - /path/to/your/config.yml:/config/config.yml:ro
- <path_to_directory_for_media>:/media/frigate - /path/to/your/storage:/media/frigate
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear - type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
target: /tmp/cache target: /tmp/cache
tmpfs: tmpfs:
@@ -68,8 +150,9 @@ docker run -d \
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \ --mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \
--device /dev/bus/usb:/dev/bus/usb \ --device /dev/bus/usb:/dev/bus/usb \
--device /dev/dri/renderD128 \ --device /dev/dri/renderD128 \
-v <path_to_directory_for_media>:/media/frigate \ --shm-size=64m \
-v <path_to_config_file>:/config/config.yml:ro \ -v /path/to/your/storage:/media/frigate \
-v /path/to/your/config.yml:/config/config.yml:ro \
-v /etc/localtime:/etc/localtime:ro \ -v /etc/localtime:/etc/localtime:ro \
-e FRIGATE_RTSP_PASSWORD='password' \ -e FRIGATE_RTSP_PASSWORD='password' \
-p 5000:5000 \ -p 5000:5000 \
@@ -77,48 +160,71 @@ docker run -d \
blakeblackshear/frigate:<specify_version_tag> 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, utilizing external storage for recordings or snapshots requires [modifying udev rules manually](https://community.home-assistant.io/t/solved-mount-usb-drive-in-hassio-to-be-used-on-the-media-folder-with-udev-customization/258406/46).
``` :::
(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"
There are several versions of the addon available:
| Addon Version | Description |
| ------------------------------ | ---------------------------------------------------------- |
| Frigate NVR | Current release with protection mode on |
| Frigate NVR (Full Access) | Current release with the option to disable protection mode |
| Frigate NVR Beta | Beta release with protection mode on |
| Frigate NVR Beta (Full Access) | Beta release with the option 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 ## Kubernetes
Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate). 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.
``` ## ESX
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
For details on running Frigate under ESX, see details [here](https://github.com/blakeblackshear/frigate/issues/305). 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. 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` ### `GET /api/events/<id>/snapshot.jpg`
Returns the snapshot image for the event id. Works while the event is in progress and after completion. 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) | | `crop` | int | Crop the snapshot to the (0 or 1) |
| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. | | `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. 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. 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

@@ -0,0 +1,192 @@
---
id: home-assistant
title: Home Assistant Integration
---
The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration).
## Installation
### Preparation
The Frigate integration requires the `mqtt` integration to be installed and
manually configured first.
See the [MQTT integration
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
details.
### Integration installation
Available via HACS as a default repository. To install:
- Use [HACS](https://hacs.xyz/) to install the integration:
```
Home Assistant > HACS > Integrations > "Explore & Add Integrations" > Frigate
```
- Restart Home Assistant.
- Then add/configure the integration:
```
Home Assistant > Configuration > Integrations > Add Integration > Frigate
```
Note: You will also need
[media_source](https://www.home-assistant.io/integrations/media_source/) enabled
in your Home Assistant configuration for the Media Browser to appear.
### (Optional) Lovelace Card Installation
To install the optional companion Lovelace card, please see the [separate
installation instructions](https://github.com/dermotduffy/frigate-hass-card) for
that card.
## Configuration
When configuring the integration, you will be asked for the `URL` of your frigate instance which is the URL you use to access Frigate in the browser. This may look like `http://<host>:5000/`. If you are using HassOS with the addon, the URL should be one of the following depending on which addon version you are using. Note that if you are using the Proxy Addon, you do NOT point the integration at the proxy URL. Just enter the URL used to access frigate directly from your network.
| Addon Version | URL |
| ------------------------------ | -------------------------------------- |
| Frigate NVR | `http://ccab4aaf-frigate:5000` |
| Frigate NVR (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
| Frigate NVR Beta | `http://ccab4aaf-frigate-beta:5000` |
| Frigate NVR Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
<a name="options"></a>
## Options
```
Home Assistant > Configuration > Integrations > Frigate > Options
```
| Option | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| RTMP URL Template | A [jinja2](https://jinja.palletsprojects.com/) template that is used to override the standard RTMP stream URL (e.g. for use with reverse proxies). This option is only shown to users who have [advanced mode](https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode) enabled. See [RTMP streams](#streams) below. |
## Entities Provided
| Platform | Description |
| --------------- | --------------------------------------------------------------------------------- |
| `camera` | Live camera stream (requires RTMP), camera for image of the last detected object. |
| `sensor` | States to monitor Frigate performance, object counts for all zones and cameras. |
| `switch` | Switch entities to toggle detection, recordings and snapshots. |
| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. |
## Media Browser Support
The integration provides:
- Browsing event recordings with thumbnails
- Browsing snapshots
- Browsing recordings by month, day, camera, time
This is accessible via "Media Browser" on the left menu panel in Home Assistant.
<a name="api"></a>
## Notification API
Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications.
To load a thumbnail for an event:
```
https://HA_URL/api/frigate/notifications/<event-id>/thumbnail.jpg
```
To load a snapshot for an event:
```
https://HA_URL/api/frigate/notifications/<event-id>/snapshot.jpg
```
To load a video clip of an event:
```
https://HA_URL/api/frigate/notifications/<event-id>/clip.mp4
```
<a name="streams"></a>
## RTMP stream
In order for the live streams to function they need to be accessible on the RTMP
port (default: `1935`) at `<frigatehost>:1935`. Home Assistant will directly
connect to that streaming port when the live camera is viewed.
#### RTMP URL Template
For advanced usecases, this behavior can be changed with the [RTMP URL
template](#options) option. When set, this string will override the default stream
address that is derived from the default behavior described above. This option supports
[jinja2 templates](https://jinja.palletsprojects.com/) and has the `camera` dict
variables from [Frigate API](https://blakeblackshear.github.io/frigate/usage/api#apiconfig)
available for the template. Note that no Home Assistant state is available to the
template, only the camera dict from Frigate.
This is potentially useful when Frigate is behind a reverse proxy, and/or when
the default stream port is otherwise not accessible to Home Assistant (e.g.
firewall rules).
###### RTMP URL Template Examples
Use a different port number:
```
rtmp://<frigate_host>:2000/live/front_door
```
Use the camera name in the stream URL:
```
rtmp://<frigate_host>:2000/live/{{ name }}
```
Use the camera name in the stream URL, converting it to lowercase first:
```
rtmp://<frigate_host>:2000/live/{{ name|lower }}
```
## Multiple Instance Support
The Frigate integration seamlessly supports the use of multiple Frigate servers.
### Requirements for Multiple Instances
In order for multiple Frigate instances to function correctly, the
`topic_prefix` and `client_id` parameters must be set differently per server.
See [MQTT
configuration](https://blakeblackshear.github.io/frigate/configuration/index#mqtt)
for how to set these.
#### API URLs
When multiple Frigate instances are configured, [API](#api) URLs should include an
identifier to tell Home Assistant which Frigate instance to refer to. The
identifier used is the MQTT `client_id` paremeter included in the configuration,
and is used like so:
```
https://HA_URL/api/frigate/<client-id>/notifications/<event-id>/thumbnail.jpg
```
```
https://HA_URL/api/frigate/<client-id>/clips/front_door-1624599978.427826-976jaa.mp4
```
#### Default Treatment
When a single Frigate instance is configured, the `client-id` parameter need not
be specified in URLs/identifiers -- that single instance is assumed. When
multiple Frigate instances are configured, the user **must** explicitly specify
which server they are referring to.
## FAQ
#### If I am detecting multiple objects, how do I assign the correct `binary_sensor` to the camera in HomeKit?
The [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) randomly links one of the binary sensors (motion sensor entities) grouped with the camera device in Home Assistant. You can specify a `linked_motion_sensor` in the Home Assistant [HomeKit configuration](https://www.home-assistant.io/integrations/homekit/#linked_motion_sensor) for each camera.

View File

@@ -36,7 +36,7 @@ Message published for each changed event. The first message is published when th
```json ```json
{ {
"type": "update", // new, update, end or clip_ready "type": "update", // new, update, end
"before": { "before": {
"id": "1607123955.475377-mxklsc", "id": "1607123955.475377-mxklsc",
"camera": "front_door", "camera": "front_door",
@@ -53,7 +53,12 @@ Message published for each changed event. The first message is published when th
"region": [264, 450, 667, 853], "region": [264, 450, 667, 853],
"current_zones": ["driveway"], "current_zones": ["driveway"],
"entered_zones": ["yard", "driveway"], "entered_zones": ["yard", "driveway"],
"thumbnail": null "thumbnail": null,
"has_snapshot": false,
"has_clip": false,
"stationary": false, // whether or not the object is considered stationary
"motionless_count": 0, // number of frames the object has been motionless
"position_changes": 2 // number of times the object has moved from a stationary position
}, },
"after": { "after": {
"id": "1607123955.475377-mxklsc", "id": "1607123955.475377-mxklsc",
@@ -71,7 +76,12 @@ Message published for each changed event. The first message is published when th
"region": [218, 440, 693, 915], "region": [218, 440, 693, 915],
"current_zones": ["yard", "driveway"], "current_zones": ["yard", "driveway"],
"entered_zones": ["yard", "driveway"], "entered_zones": ["yard", "driveway"],
"thumbnail": null "thumbnail": null,
"has_snapshot": false,
"has_clip": false,
"stationary": false, // whether or not the object is considered stationary
"motionless_count": 0, // number of frames the object has been motionless
"position_changes": 2 // number of times the object has changed position
} }
} }
``` ```

View File

@@ -1,281 +0,0 @@
---
id: home-assistant
title: Integration with Home Assistant
sidebar_label: Home Assistant
---
The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration).
## Installation
Available via HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories). To install:
- Add the custom repository:
```
Home Assistant > HACS > Integrations > [...] > Custom Repositories
```
| Key | Value |
| -------------- | ----------------------------------------------------------- |
| Repository URL | https://github.com/blakeblackshear/frigate-hass-integration |
| Category | Integration |
- Use [HACS](https://hacs.xyz/) to install the integration:
```
Home Assistant > HACS > Integrations > "Explore & Add Integrations" > Frigate
```
- Restart Home Assistant.
- Then add/configure the integration:
```
Home Assistant > Configuration > Integrations > Add Integration > Frigate
```
Note: You will also need
[media_source](https://www.home-assistant.io/integrations/media_source/) enabled
in your Home Assistant configuration for the Media Browser to appear.
## Configuration
When configuring the integration, you will be asked for the following parameters:
| Variable | Description |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| URL | The `URL` of your frigate instance, the URL you use to access Frigate in the browser. This may look like `http://<host>:5000/`. If you are using HassOS with the addon, the URL should be `http://ccab4aaf-frigate:5000` (or `http://ccab4aaf-frigate-beta:5000` if your are using the beta version of the addon). Live streams required port 1935, see [RTMP streams](#streams) |
<a name="options"></a>
## Options
```
Home Assistant > Configuration > Integrations > Frigate > Options
```
| Option | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| RTMP URL Template | A [jinja2](https://jinja.palletsprojects.com/) template that is used to override the standard RTMP stream URL (e.g. for use with reverse proxies). This option is only shown to users who have [advanced mode](https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode) enabled. See [RTMP streams](#streams) below. |
## Entities Provided
| Platform | Description |
| --------------- | --------------------------------------------------------------------------------- |
| `camera` | Live camera stream (requires RTMP), camera for image of the last detected object. |
| `sensor` | States to monitor Frigate performance, object counts for all zones and cameras. |
| `switch` | Switch entities to toggle detection, recordings and snapshots. |
| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. |
## Media Browser Support
The integration provides:
- Rich UI with thumbnails for browsing event recordings
- Rich UI for browsing 24/7 recordings by month, day, camera, time
This is accessible via "Media Browser" on the left menu panel in Home Assistant.
<a name="api"></a>
## API
- Notification API with public facing endpoints for images in notifications
### Notifications
Frigate publishes event information in the form of a change feed via MQTT. This
allows lots of customization for notifications to meet your needs. Event changes
are published with `before` and `after` information as shown
[here](#frigateevents). Note that some people may not want to expose frigate to
the web, so you can leverage the HA API that frigate custom_integration ties
into (which is exposed to the web, and thus can be used for mobile notifications
etc):
To load an image taken by frigate from Home Assistants API see below:
```
https://HA_URL/api/frigate/notifications/<event-id>/thumbnail.jpg
```
To load a video clip taken by frigate from Home Assistants API :
```
https://HA_URL/api/frigate/notifications/<event-id>/<camera>/clip.mp4
```
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android'
tag: '{{trigger.payload_json["after"]["id"]}}'
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
```
```yaml
automation:
- alias: When a person enters a zone named yard
trigger:
platform: mqtt
topic: frigate/events
condition:
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
```
```yaml
- alias: When a person leaves a zone named yard
trigger:
platform: mqtt
topic: frigate/events
condition:
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: "A {{trigger.payload_json['after']['label']}} has left the yard."
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
```
```yaml
- alias: Notify for dogs in the front with a high top score
trigger:
platform: mqtt
topic: frigate/events
condition:
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: "High confidence dog detection."
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
when: '{{trigger.payload_json["after"]["start_time"]|int}}'
```
If you are using telegram, you can fetch the image directly from Frigate:
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.telegram_full
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
photo:
# this url should work for addon users
- url: 'http://ccab4aaf-frigate:5000/api/events/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg'
caption: 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
```
<a name="streams"></a>
## RTMP stream
In order for the live streams to function they need to be accessible on the RTMP
port (default: `1935`) at `<frigatehost>:1935`. Home Assistant will directly
connect to that streaming port when the live camera is viewed.
#### RTMP URL Template
For advanced usecases, this behavior can be changed with the [RTMP URL
template](#options) option. When set, this string will override the default stream
address that is derived from the default behavior described above. This option supports
[jinja2 templates](https://jinja.palletsprojects.com/) and has the `camera` dict
variables from [Frigate API](https://blakeblackshear.github.io/frigate/usage/api#apiconfig)
available for the template. Note that no Home Assistant state is available to the
template, only the camera dict from Frigate.
This is potentially useful when Frigate is behind a reverse proxy, and/or when
the default stream port is otherwise not accessible to Home Assistant (e.g.
firewall rules).
###### RTMP URL Template Examples
Use a different port number:
```
rtmp://<frigate_host>:2000/live/front_door
```
Use the camera name in the stream URL:
```
rtmp://<frigate_host>:2000/live/{{ name }}
```
Use the camera name in the stream URL, converting it to lowercase first:
```
rtmp://<frigate_host>:2000/live/{{ name|lower }}
```
## Multiple Instance Support
The Frigate integration seamlessly supports the use of multiple Frigate servers.
### Requirements for Multiple Instances
In order for multiple Frigate instances to function correctly, the
`topic_prefix` and `client_id` parameters must be set differently per server.
See [MQTT
configuration](https://blakeblackshear.github.io/frigate/configuration/index#mqtt)
for how to set these.
#### API URLs
When multiple Frigate instances are configured, [API](#api) URLs should include an
identifier to tell Home Assistant which Frigate instance to refer to. The
identifier used is the MQTT `client_id` paremeter included in the configuration,
and is used like so:
```
https://HA_URL/api/frigate/<client-id>/notifications/<event-id>/thumbnail.jpg
```
```
https://HA_URL/api/frigate/<client-id>/clips/front_door-1624599978.427826-976jaa.mp4
```
#### Default Treatment
When a single Frigate instance is configured, the `client-id` parameter need not
be specified in URLs/identifiers -- that single instance is assumed. When
multiple Frigate instances are configured, the user **must** explicitly specify
which server they are referring to.
## FAQ
### If I am detecting multiple objects, how do I assign the correct `binary_sensor` to the camera in HomeKit?
The [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) randomly links one of the binary sensors (motion sensor entities) grouped with the camera device in Home Assistant. You can specify a `linked_motion_sensor` in the Home Assistant [HomeKit configuration](https://www.home-assistant.io/integrations/homekit/#linked_motion_sensor) for each camera.

View File

@@ -1,11 +0,0 @@
---
id: howtos
title: Community Guides
sidebar_label: Community Guides
---
## Communitiy Guides/How-To's
- Best Camera AI Person & Object Detection - How to Setup Frigate w/ Home Assistant - digiblurDIY [YouTube](https://youtu.be/V8vGdoYO6-Y) - [Article](https://www.digiblur.com/2021/05/how-to-setup-frigate-home-assistant.html)
- Even More Free Local Object Detection with Home Assistant - Frigate Install - Everything Smart Home [YouTube](https://youtu.be/pqDCEZSVeRk)
- Home Assistant Frigate integration for local image recognition - KPeyanski [YouTube](https://youtu.be/Q2UT78lFQpo) - [Article](https://peyanski.com/home-assistant-frigate-integration/)

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

View File

@@ -3,8 +3,8 @@ const path = require('path');
module.exports = { module.exports = {
title: 'Frigate', title: 'Frigate',
tagline: 'NVR With Realtime Object Detection for IP Cameras', tagline: 'NVR With Realtime Object Detection for IP Cameras',
url: 'https://blakeblackshear.github.io', url: 'https://docs.frigate.video',
baseUrl: '/frigate/', baseUrl: '/',
onBrokenLinks: 'throw', onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn', onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico', favicon: 'img/favicon.ico',
@@ -29,6 +29,16 @@ module.exports = {
label: 'Docs', label: 'Docs',
position: 'left', position: 'left',
}, },
{
href: 'https://frigate.video',
label: 'Website',
position: 'right',
},
{
href: 'https://demo.frigate.video',
label: 'Demo',
position: 'right',
},
{ {
href: 'https://github.com/blakeblackshear/frigate', href: 'https://github.com/blakeblackshear/frigate',
label: 'GitHub', label: 'GitHub',

23980
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,13 +12,13 @@
"clear": "docusaurus clear" "clear": "docusaurus clear"
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "2.0.0-alpha.70", "@docusaurus/core": "^2.0.0-beta.15",
"@docusaurus/preset-classic": "2.0.0-alpha.70", "@docusaurus/preset-classic": "^2.0.0-beta.15",
"@mdx-js/react": "^1.6.21", "@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1", "clsx": "^1.1.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react": "^16.8.4", "react": "^16.14.0",
"react-dom": "^16.8.4" "react-dom": "^16.14.0"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -31,5 +31,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@types/react": "^16.14.0"
} }
} }

View File

@@ -1,16 +1,34 @@
module.exports = { module.exports = {
docs: { docs: {
Frigate: ['index', 'how-it-works', 'hardware', 'installation', 'troubleshooting'], Frigate: [
'index',
'hardware',
'installation',
],
Guides: [
'guides/camera_setup',
'guides/getting_started',
'guides/false_positives',
'guides/ha_notifications',
'guides/stationary_objects',
],
Configuration: [ Configuration: [
'configuration/index', 'configuration/index',
'configuration/cameras',
'configuration/optimizing',
'configuration/detectors', 'configuration/detectors',
'configuration/false_positives', 'configuration/cameras',
'configuration/masks',
'configuration/record',
'configuration/snapshots',
'configuration/objects', 'configuration/objects',
'configuration/rtmp',
'configuration/zones',
'configuration/advanced', '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'], Development: ['contributing'],
}, },
}; };

BIN
docs/static/img/driveway_zones-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
docs/static/img/driveway_zones.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/static/img/media_browser-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
docs/static/img/notification-min.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
docs/static/img/reolink-settings.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
docs/static/img/resolutions-min.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/static/img/resolutions.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -8,10 +8,12 @@ import threading
from logging.handlers import QueueHandler from logging.handlers import QueueHandler
from typing import Dict, List from typing import Dict, List
import traceback
import yaml import yaml
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase from playhouse.sqliteq import SqliteQueueDatabase
from pydantic import ValidationError
from frigate.config import DetectorTypeEnum, FrigateConfig from frigate.config import DetectorTypeEnum, FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
@@ -20,14 +22,14 @@ from frigate.events import EventCleanup, EventProcessor
from frigate.http import create_app from frigate.http import create_app
from frigate.log import log_process, root_configurer from frigate.log import log_process, root_configurer
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.mqtt import create_mqtt_client, MqttSocketRelay from frigate.mqtt import MqttSocketRelay, create_mqtt_client
from frigate.object_processing import TrackedObjectProcessor from frigate.object_processing import TrackedObjectProcessor
from frigate.output import output_frames from frigate.output import output_frames
from frigate.record import RecordingCleanup, RecordingMaintainer from frigate.record import RecordingCleanup, RecordingMaintainer
from frigate.stats import StatsEmitter, stats_init from frigate.stats import StatsEmitter, stats_init
from frigate.version import VERSION
from frigate.video import capture_camera, track_camera from frigate.video import capture_camera, track_camera
from frigate.watchdog import FrigateWatchdog from frigate.watchdog import FrigateWatchdog
from frigate.zeroconf import broadcast_zeroconf
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -66,6 +68,12 @@ class FrigateApp:
def init_config(self): def init_config(self):
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml") config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
# Check if we can use .yaml instead of .yml
config_file_yaml = config_file.replace(".yml", ".yaml")
if os.path.isfile(config_file_yaml):
config_file = config_file_yaml
user_config = FrigateConfig.parse_file(config_file) user_config = FrigateConfig.parse_file(config_file)
self.config = user_config.runtime_config self.config = user_config.runtime_config
@@ -85,29 +93,6 @@ class FrigateApp:
"frame_queue": mp.Queue(maxsize=2), "frame_queue": mp.Queue(maxsize=2),
} }
def check_config(self):
for name, camera in self.config.cameras.items():
assigned_roles = list(
set([r for i in camera.ffmpeg.inputs for r in i.roles])
)
if not camera.record.enabled and "record" in assigned_roles:
logger.warning(
f"Camera {name} has record assigned to an input, but record is not enabled."
)
elif camera.record.enabled and not "record" in assigned_roles:
logger.warning(
f"Camera {name} has record enabled, but record is not assigned to an input."
)
if not camera.rtmp.enabled and "rtmp" in assigned_roles:
logger.warning(
f"Camera {name} has rtmp assigned to an input, but rtmp is not enabled."
)
elif camera.rtmp.enabled and not "rtmp" in assigned_roles:
logger.warning(
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
)
def set_log_levels(self): def set_log_levels(self):
logging.getLogger().setLevel(self.config.logger.default.value.upper()) logging.getLogger().setLevel(self.config.logger.default.value.upper())
for log, level in self.config.logger.logs.items(): for log, level in self.config.logger.logs.items():
@@ -127,6 +112,9 @@ class FrigateApp:
maxsize=len(self.config.cameras.keys()) * 2 maxsize=len(self.config.cameras.keys()) * 2
) )
# Queue for recordings info
self.recordings_info_queue = mp.Queue()
def init_database(self): def init_database(self):
# Migrate DB location # Migrate DB location
old_db_path = os.path.join(CLIPS_DIR, "frigate.db") old_db_path = os.path.join(CLIPS_DIR, "frigate.db")
@@ -170,6 +158,7 @@ class FrigateApp:
self.mqtt_relay.start() self.mqtt_relay.start()
def start_detectors(self): def start_detectors(self):
model_path = self.config.model.path
model_shape = (self.config.model.height, self.config.model.width) model_shape = (self.config.model.height, self.config.model.width)
for name in self.config.cameras.keys(): for name in self.config.cameras.keys():
self.detection_out_events[name] = mp.Event() self.detection_out_events[name] = mp.Event()
@@ -199,6 +188,7 @@ class FrigateApp:
name, name,
self.detection_queue, self.detection_queue,
self.detection_out_events, self.detection_out_events,
model_path,
model_shape, model_shape,
"cpu", "cpu",
detector.num_threads, detector.num_threads,
@@ -208,6 +198,7 @@ class FrigateApp:
name, name,
self.detection_queue, self.detection_queue,
self.detection_out_events, self.detection_out_events,
model_path,
model_shape, model_shape,
detector.device, detector.device,
detector.num_threads, detector.num_threads,
@@ -222,6 +213,7 @@ class FrigateApp:
self.event_queue, self.event_queue,
self.event_processed_queue, self.event_processed_queue,
self.video_output_queue, self.video_output_queue,
self.recordings_info_queue,
self.stop_event, self.stop_event,
) )
self.detected_frames_processor.start() self.detected_frames_processor.start()
@@ -289,7 +281,9 @@ class FrigateApp:
self.event_cleanup.start() self.event_cleanup.start()
def start_recording_maintainer(self): def start_recording_maintainer(self):
self.recording_maintainer = RecordingMaintainer(self.config, self.stop_event) self.recording_maintainer = RecordingMaintainer(
self.config, self.recordings_info_queue, self.stop_event
)
self.recording_maintainer.start() self.recording_maintainer.start()
def start_recording_cleanup(self): def start_recording_cleanup(self):
@@ -312,16 +306,29 @@ class FrigateApp:
def start(self): def start(self):
self.init_logger() self.init_logger()
logger.info(f"Starting Frigate ({VERSION})")
try: try:
try: try:
self.init_config() self.init_config()
except Exception as e: except Exception as e:
print(f"Error parsing config: {e}") print("*************************************************************")
print("*************************************************************")
print("*** Your config file is not valid! ***")
print("*** Please check the docs at ***")
print("*** https://docs.frigate.video/configuration/index ***")
print("*************************************************************")
print("*************************************************************")
print("*** Config Validation Errors ***")
print("*************************************************************")
print(e)
print(traceback.format_exc())
print("*************************************************************")
print("*** End Config Validation Errors ***")
print("*************************************************************")
self.log_process.terminate() self.log_process.terminate()
sys.exit(1) sys.exit(1)
self.set_environment_vars() self.set_environment_vars()
self.ensure_dirs() self.ensure_dirs()
self.check_config()
self.set_log_levels() self.set_log_levels()
self.init_queues() self.init_queues()
self.init_database() self.init_database()

BIN
frigate/birdseye.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -9,12 +9,11 @@ from typing import Dict, List, Optional, Tuple, Union
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import yaml import yaml
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Extra, Field, validator
from pydantic.fields import PrivateAttr from pydantic.fields import PrivateAttr
from frigate.const import BASE_DIR, CACHE_DIR, RECORD_DIR from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT
from frigate.edgetpu import load_labels from frigate.util import create_mask, deep_merge, load_labels
from frigate.util import create_mask, deep_merge
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,18 +28,23 @@ DEFAULT_TRACKED_OBJECTS = ["person"]
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}} DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
class FrigateBaseModel(BaseModel):
class Config:
extra = Extra.forbid
class DetectorTypeEnum(str, Enum): class DetectorTypeEnum(str, Enum):
edgetpu = "edgetpu" edgetpu = "edgetpu"
cpu = "cpu" cpu = "cpu"
class DetectorConfig(BaseModel): class DetectorConfig(FrigateBaseModel):
type: DetectorTypeEnum = Field(default=DetectorTypeEnum.cpu, title="Detector Type") type: DetectorTypeEnum = Field(default=DetectorTypeEnum.cpu, title="Detector Type")
device: str = Field(default="usb", title="Device Type") device: str = Field(default="usb", title="Device Type")
num_threads: int = Field(default=3, title="Number of detection threads") num_threads: int = Field(default=3, title="Number of detection threads")
class MqttConfig(BaseModel): class MqttConfig(FrigateBaseModel):
host: str = Field(title="MQTT Host") host: str = Field(title="MQTT Host")
port: int = Field(default=1883, title="MQTT Port") port: int = Field(default=1883, title="MQTT Port")
topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix") topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix")
@@ -60,50 +64,68 @@ class MqttConfig(BaseModel):
return v return v
class RetainConfig(BaseModel): class RetainModeEnum(str, Enum):
default: int = Field(default=10, title="Default retention period.") all = "all"
objects: Dict[str, int] = Field( motion = "motion"
active_objects = "active_objects"
class RetainConfig(FrigateBaseModel):
default: float = Field(default=10, title="Default retention period.")
mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.")
objects: Dict[str, float] = Field(
default_factory=dict, title="Object retention period." default_factory=dict, title="Object retention period."
) )
# DEPRECATED: Will eventually be removed class EventsConfig(FrigateBaseModel):
class ClipsConfig(BaseModel): max_seconds: int = Field(default=300, title="Maximum event duration.")
enabled: bool = Field(default=False, title="Save clips.") pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
max_seconds: int = Field(default=300, title="Maximum clip duration.") post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
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.")
required_zones: List[str] = Field( required_zones: List[str] = Field(
default_factory=list, 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( 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( retain: RetainConfig = Field(
default_factory=RetainConfig, title="Clip retention settings." default_factory=RetainConfig, title="Event retention settings."
) )
class RecordConfig(BaseModel): class RecordRetainConfig(FrigateBaseModel):
days: float = Field(default=0, title="Default retention period.")
mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
class RecordConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Enable record on all cameras.") enabled: bool = Field(default=False, title="Enable record on all cameras.")
retain_days: int = Field(default=0, title="Recording retention period in days.") expire_interval: int = Field(
events: ClipsConfig = Field( default=60,
default_factory=ClipsConfig, title="Event specific settings." title="Number of minutes to wait between cleanup runs.",
)
# deprecated - to be removed in a future version
retain_days: Optional[float] = Field(title="Recording retention period in days.")
retain: RecordRetainConfig = Field(
default_factory=RecordRetainConfig, title="Record retention settings."
)
events: EventsConfig = Field(
default_factory=EventsConfig, title="Event specific settings."
) )
class MotionConfig(BaseModel): class MotionConfig(FrigateBaseModel):
threshold: int = Field( threshold: int = Field(
default=25, default=25,
title="Motion detection threshold (1-255).", title="Motion detection threshold (1-255).",
ge=1, ge=1,
le=255, le=255,
) )
contour_area: Optional[int] = Field(title="Contour Area") contour_area: Optional[int] = Field(default=30, title="Contour Area")
delta_alpha: float = Field(default=0.2, title="Delta Alpha") delta_alpha: float = Field(default=0.2, title="Delta Alpha")
frame_alpha: float = Field(default=0.2, title="Frame Alpha") frame_alpha: float = Field(default=0.2, title="Frame Alpha")
frame_height: Optional[int] = Field(title="Frame Height") frame_height: Optional[int] = Field(default=50, title="Frame Height")
mask: Union[str, List[str]] = Field( mask: Union[str, List[str]] = Field(
default="", title="Coordinates polygon for the motion mask." default="", title="Coordinates polygon for the motion mask."
) )
@@ -116,15 +138,6 @@ class RuntimeMotionConfig(MotionConfig):
def __init__(self, **config): def __init__(self, **config):
frame_shape = config.get("frame_shape", (1, 1)) frame_shape = config.get("frame_shape", (1, 1))
if "frame_height" not in config:
config["frame_height"] = max(frame_shape[0] // 6, 180)
if "contour_area" not in config:
frame_width = frame_shape[1] * config["frame_height"] / frame_shape[0]
config["contour_area"] = (
config["frame_height"] * frame_width * 0.00173611111
)
mask = config.get("mask", "") mask = config.get("mask", "")
config["raw_mask"] = mask config["raw_mask"] = mask
@@ -146,9 +159,33 @@ class RuntimeMotionConfig(MotionConfig):
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
extra = Extra.ignore
class DetectConfig(BaseModel): class StationaryMaxFramesConfig(FrigateBaseModel):
default: Optional[int] = Field(title="Default max frames.", ge=1)
objects: Dict[str, int] = Field(
default_factory=dict, title="Object specific max frames."
)
class StationaryConfig(FrigateBaseModel):
interval: Optional[int] = Field(
default=0,
title="Frame interval for checking stationary objects.",
ge=0,
)
threshold: Optional[int] = Field(
title="Number of frames without a position change for an object to be considered stationary",
ge=1,
)
max_frames: StationaryMaxFramesConfig = Field(
default_factory=StationaryMaxFramesConfig,
title="Max frames for stationary objects.",
)
class DetectConfig(FrigateBaseModel):
height: int = Field(default=720, title="Height of the stream for the detect role.") 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.") width: int = Field(default=1280, title="Width of the stream for the detect role.")
fps: int = Field( fps: int = Field(
@@ -158,9 +195,13 @@ class DetectConfig(BaseModel):
max_disappeared: Optional[int] = Field( max_disappeared: Optional[int] = Field(
title="Maximum number of frames the object can dissapear before detection ends." title="Maximum number of frames the object can dissapear before detection ends."
) )
stationary: StationaryConfig = Field(
default_factory=StationaryConfig,
title="Stationary objects config.",
)
class FilterConfig(BaseModel): class FilterConfig(FrigateBaseModel):
min_area: int = Field( min_area: int = Field(
default=0, title="Minimum area of bounding box for object to be counted." default=0, title="Minimum area of bounding box for object to be counted."
) )
@@ -201,8 +242,10 @@ class RuntimeFilterConfig(FilterConfig):
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
extra = Extra.ignore
# this uses the base model because the color is an extra attribute
class ZoneConfig(BaseModel): class ZoneConfig(BaseModel):
filters: Dict[str, FilterConfig] = Field( filters: Dict[str, FilterConfig] = Field(
default_factory=dict, title="Zone filters." default_factory=dict, title="Zone filters."
@@ -244,7 +287,7 @@ class ZoneConfig(BaseModel):
self._contour = np.array([]) self._contour = np.array([])
class ObjectConfig(BaseModel): class ObjectConfig(FrigateBaseModel):
track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.") track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.") filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.")
mask: Union[str, List[str]] = Field(default="", title="Object mask.") mask: Union[str, List[str]] = Field(default="", title="Object mask.")
@@ -256,7 +299,7 @@ class BirdseyeModeEnum(str, Enum):
continuous = "continuous" continuous = "continuous"
class BirdseyeConfig(BaseModel): class BirdseyeConfig(FrigateBaseModel):
enabled: bool = Field(default=True, title="Enable birdseye view.") enabled: bool = Field(default=True, title="Enable birdseye view.")
width: int = Field(default=1280, title="Birdseye width.") width: int = Field(default=1280, title="Birdseye width.")
height: int = Field(default=720, title="Birdseye height.") height: int = Field(default=720, title="Birdseye height.")
@@ -303,7 +346,7 @@ RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
] ]
class FfmpegOutputArgsConfig(BaseModel): class FfmpegOutputArgsConfig(FrigateBaseModel):
detect: Union[str, List[str]] = Field( detect: Union[str, List[str]] = Field(
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT, default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
title="Detect role FFmpeg output arguments.", title="Detect role FFmpeg output arguments.",
@@ -318,7 +361,7 @@ class FfmpegOutputArgsConfig(BaseModel):
) )
class FfmpegConfig(BaseModel): class FfmpegConfig(FrigateBaseModel):
global_args: Union[str, List[str]] = Field( global_args: Union[str, List[str]] = Field(
default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments." default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments."
) )
@@ -340,7 +383,7 @@ class CameraRoleEnum(str, Enum):
detect = "detect" detect = "detect"
class CameraInput(BaseModel): class CameraInput(FrigateBaseModel):
path: str = Field(title="Camera input path.") path: str = Field(title="Camera input path.")
roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.") roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.")
global_args: Union[str, List[str]] = Field( global_args: Union[str, List[str]] = Field(
@@ -371,7 +414,7 @@ class CameraFfmpegConfig(FfmpegConfig):
return v return v
class SnapshotsConfig(BaseModel): class SnapshotsConfig(FrigateBaseModel):
enabled: bool = Field(default=False, title="Snapshots enabled.") enabled: bool = Field(default=False, title="Snapshots enabled.")
clean_copy: bool = Field( clean_copy: bool = Field(
default=True, title="Create a clean copy of the snapshot image." default=True, title="Create a clean copy of the snapshot image."
@@ -399,22 +442,35 @@ class SnapshotsConfig(BaseModel):
) )
class ColorConfig(BaseModel): class ColorConfig(FrigateBaseModel):
red: int = Field(default=255, le=0, ge=255, title="Red") red: int = Field(default=255, ge=0, le=255, title="Red")
green: int = Field(default=255, le=0, ge=255, title="Green") green: int = Field(default=255, ge=0, le=255, title="Green")
blue: int = Field(default=255, le=0, ge=255, title="Blue") blue: int = Field(default=255, ge=0, le=255, title="Blue")
class TimestampStyleConfig(BaseModel): class TimestampPositionEnum(str, Enum):
position: str = Field(default="tl", title="Timestamp position.") 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.") format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.")
color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.") 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.") 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.") enabled: bool = Field(default=True, title="Send image over MQTT.")
timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.") timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.")
bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.") bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.")
@@ -432,17 +488,17 @@ class CameraMqttConfig(BaseModel):
) )
class CameraRtmpConfig(BaseModel): class RtmpConfig(FrigateBaseModel):
enabled: bool = Field(default=True, title="RTMP restreaming enabled.") 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") height: int = Field(default=720, title="Live camera view height")
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality") 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.") name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.") ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
best_image_timeout: int = Field( best_image_timeout: int = Field(
default=60, default=60,
@@ -454,8 +510,8 @@ class CameraConfig(BaseModel):
record: RecordConfig = Field( record: RecordConfig = Field(
default_factory=RecordConfig, title="Record configuration." default_factory=RecordConfig, title="Record configuration."
) )
rtmp: CameraRtmpConfig = Field( rtmp: RtmpConfig = Field(
default_factory=CameraRtmpConfig, title="RTMP restreaming configuration." default_factory=RtmpConfig, title="RTMP restreaming configuration."
) )
live: CameraLiveConfig = Field( live: CameraLiveConfig = Field(
default_factory=CameraLiveConfig, title="Live playback settings." default_factory=CameraLiveConfig, title="Live playback settings."
@@ -476,6 +532,7 @@ class CameraConfig(BaseModel):
timestamp_style: TimestampStyleConfig = Field( timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig, title="Timestamp style configuration." default_factory=TimestampStyleConfig, title="Timestamp style configuration."
) )
_ffmpeg_cmds: List[Dict[str, List[str]]] = PrivateAttr()
def __init__(self, **config): def __init__(self, **config):
# Set zone colors # Set zone colors
@@ -486,6 +543,10 @@ class CameraConfig(BaseModel):
for idx, (name, z) in enumerate(config["zones"].items()) for idx, (name, z) in enumerate(config["zones"].items())
} }
# add roles to the input if there is only one
if len(config["ffmpeg"]["inputs"]) == 1:
config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
super().__init__(**config) super().__init__(**config)
@property @property
@@ -498,6 +559,11 @@ class CameraConfig(BaseModel):
@property @property
def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]: def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
return self._ffmpeg_cmds
def create_ffmpeg_cmds(self):
if "_ffmpeg_cmds" in self:
return
ffmpeg_cmds = [] ffmpeg_cmds = []
for ffmpeg_input in self.ffmpeg.inputs: for ffmpeg_input in self.ffmpeg.inputs:
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input) ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
@@ -505,7 +571,7 @@ class CameraConfig(BaseModel):
continue continue
ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd}) ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
return ffmpeg_cmds self._ffmpeg_cmds = ffmpeg_cmds
def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput): def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
ffmpeg_output_args = [] ffmpeg_output_args = []
@@ -541,6 +607,7 @@ class CameraConfig(BaseModel):
if isinstance(self.ffmpeg.output_args.record, list) if isinstance(self.ffmpeg.output_args.record, list)
else self.ffmpeg.output_args.record.split(" ") else self.ffmpeg.output_args.record.split(" ")
) )
ffmpeg_output_args = ( ffmpeg_output_args = (
record_args record_args
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"] + [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
@@ -577,13 +644,15 @@ class CameraConfig(BaseModel):
return [part for part in cmd if part != ""] return [part for part in cmd if part != ""]
class DatabaseConfig(BaseModel): class DatabaseConfig(FrigateBaseModel):
path: str = Field( path: str = Field(
default=os.path.join(BASE_DIR, "frigate.db"), title="Database path." 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.") width: int = Field(default=320, title="Object detection model input width.")
height: int = Field(default=320, title="Object detection model input height.") height: int = Field(default=320, title="Object detection model input height.")
labelmap: Dict[int, str] = Field( labelmap: Dict[int, str] = Field(
@@ -597,14 +666,14 @@ class ModelConfig(BaseModel):
return self._merged_labelmap return self._merged_labelmap
@property @property
def colormap(self) -> Dict[int, tuple[int, int, int]]: def colormap(self) -> Dict[int, Tuple[int, int, int]]:
return self._colormap return self._colormap
def __init__(self, **config): def __init__(self, **config):
super().__init__(**config) super().__init__(**config)
self._merged_labelmap = { self._merged_labelmap = {
**load_labels("/labelmap.txt"), **load_labels(config.get("labelmap_path", "/labelmap.txt")),
**config.get("labelmap", {}), **config.get("labelmap", {}),
} }
@@ -623,7 +692,7 @@ class LogLevelEnum(str, Enum):
critical = "critical" critical = "critical"
class LoggerConfig(BaseModel): class LoggerConfig(FrigateBaseModel):
default: LogLevelEnum = Field( default: LogLevelEnum = Field(
default=LogLevelEnum.info, title="Default logging level." default=LogLevelEnum.info, title="Default logging level."
) )
@@ -632,7 +701,7 @@ class LoggerConfig(BaseModel):
) )
class FrigateConfig(BaseModel): class FrigateConfig(FrigateBaseModel):
mqtt: MqttConfig = Field(title="MQTT Configuration.") mqtt: MqttConfig = Field(title="MQTT Configuration.")
database: DatabaseConfig = Field( database: DatabaseConfig = Field(
default_factory=DatabaseConfig, title="Database configuration." default_factory=DatabaseConfig, title="Database configuration."
@@ -656,6 +725,12 @@ class FrigateConfig(BaseModel):
snapshots: SnapshotsConfig = Field( snapshots: SnapshotsConfig = Field(
default_factory=SnapshotsConfig, title="Global snapshots configuration." default_factory=SnapshotsConfig, title="Global snapshots configuration."
) )
live: CameraLiveConfig = Field(
default_factory=CameraLiveConfig, title="Global live configuration."
)
rtmp: RtmpConfig = Field(
default_factory=RtmpConfig, title="Global RTMP restreaming configuration."
)
birdseye: BirdseyeConfig = Field( birdseye: BirdseyeConfig = Field(
default_factory=BirdseyeConfig, title="Birdseye configuration." default_factory=BirdseyeConfig, title="Birdseye configuration."
) )
@@ -672,6 +747,10 @@ class FrigateConfig(BaseModel):
default_factory=DetectConfig, title="Global object tracking configuration." default_factory=DetectConfig, title="Global object tracking configuration."
) )
cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.") cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig,
title="Global timestamp style configuration.",
)
@property @property
def runtime_config(self) -> FrigateConfig: def runtime_config(self) -> FrigateConfig:
@@ -687,10 +766,13 @@ class FrigateConfig(BaseModel):
include={ include={
"record": ..., "record": ...,
"snapshots": ..., "snapshots": ...,
"live": ...,
"rtmp": ...,
"objects": ..., "objects": ...,
"motion": ..., "motion": ...,
"detect": ..., "detect": ...,
"ffmpeg": ..., "ffmpeg": ...,
"timestamp_style": ...,
}, },
exclude_unset=True, exclude_unset=True,
) )
@@ -706,6 +788,11 @@ class FrigateConfig(BaseModel):
if camera_config.detect.max_disappeared is None: if camera_config.detect.max_disappeared is None:
camera_config.detect.max_disappeared = max_disappeared camera_config.detect.max_disappeared = max_disappeared
# Default stationary_threshold configuration
stationary_threshold = camera_config.detect.fps * 10
if camera_config.detect.stationary.threshold is None:
camera_config.detect.stationary.threshold = stationary_threshold
# FFMPEG input substitution # FFMPEG input substitution
for input in camera_config.ffmpeg.inputs: for input in camera_config.ffmpeg.inputs:
input.path = input.path.format(**FRIGATE_ENV_VARS) input.path = input.path.format(**FRIGATE_ENV_VARS)
@@ -753,6 +840,44 @@ class FrigateConfig(BaseModel):
**camera_config.motion.dict(exclude_unset=True), **camera_config.motion.dict(exclude_unset=True),
) )
# check runtime config
assigned_roles = list(
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
)
if camera_config.record.enabled and not "record" in assigned_roles:
raise ValueError(
f"Camera {name} has record enabled, but record is not assigned to an input."
)
if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
raise ValueError(
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
)
# backwards compatibility for retain_days
if not camera_config.record.retain_days is None:
logger.warning(
"The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'"
)
if camera_config.record.retain.days == 0:
camera_config.record.retain.days = camera_config.record.retain_days
# warning if the higher level record mode is potentially more restrictive than the events
rank_map = {
RetainModeEnum.all: 0,
RetainModeEnum.motion: 1,
RetainModeEnum.active_objects: 2,
}
if (
camera_config.record.retain.days != 0
and rank_map[camera_config.record.retain.mode]
> rank_map[camera_config.record.events.retain.mode]
):
logger.warning(
f"{name}: Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied."
)
# generage the ffmpeg commands
camera_config.create_ffmpeg_cmds()
config.cameras[name] = camera_config config.cameras[name] = camera_config
return config return config
@@ -770,7 +895,7 @@ class FrigateConfig(BaseModel):
with open(config_file) as f: with open(config_file) as f:
raw_config = f.read() raw_config = f.read()
if config_file.endswith(".yml"): if config_file.endswith(YAML_EXT):
config = yaml.safe_load(raw_config) config = yaml.safe_load(raw_config)
elif config_file.endswith(".json"): elif config_file.endswith(".json"):
config = json.loads(raw_config) config = json.loads(raw_config)

View File

@@ -2,3 +2,4 @@ BASE_DIR = "/media/frigate"
CLIPS_DIR = f"{BASE_DIR}/clips" CLIPS_DIR = f"{BASE_DIR}/clips"
RECORD_DIR = f"{BASE_DIR}/recordings" RECORD_DIR = f"{BASE_DIR}/recordings"
CACHE_DIR = "/tmp/cache" CACHE_DIR = "/tmp/cache"
YAML_EXT = (".yaml", ".yml")

View File

@@ -13,31 +13,11 @@ import tflite_runtime.interpreter as tflite
from setproctitle import setproctitle from setproctitle import setproctitle
from tflite_runtime.interpreter import load_delegate from tflite_runtime.interpreter import load_delegate
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen, load_labels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def load_labels(path, encoding="utf-8"):
"""Loads labels from file (with or without index numbers).
Args:
path: path to label file.
encoding: label file encoding.
Returns:
Dictionary mapping indices to labels.
"""
with open(path, "r", encoding=encoding) as f:
lines = f.readlines()
if not lines:
return {}
if lines[0].split(" ", maxsplit=1)[0].isdigit():
pairs = [line.split(" ", maxsplit=1) for line in lines]
return {int(index): label.strip() for index, label in pairs}
else:
return {index: line.strip() for index, line in enumerate(lines)}
class ObjectDetector(ABC): class ObjectDetector(ABC):
@abstractmethod @abstractmethod
def detect(self, tensor_input, threshold=0.4): def detect(self, tensor_input, threshold=0.4):
@@ -45,7 +25,7 @@ class ObjectDetector(ABC):
class LocalObjectDetector(ObjectDetector): 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() self.fps = EventsPerSecond()
if labels is None: if labels is None:
self.labels = {} self.labels = {}
@@ -64,7 +44,7 @@ class LocalObjectDetector(ObjectDetector):
edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config) edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config)
logger.info("TPU found") logger.info("TPU found")
self.interpreter = tflite.Interpreter( self.interpreter = tflite.Interpreter(
model_path="/edgetpu_model.tflite", model_path=model_path or "/edgetpu_model.tflite",
experimental_delegates=[edge_tpu_delegate], experimental_delegates=[edge_tpu_delegate],
) )
except ValueError: except ValueError:
@@ -77,7 +57,7 @@ class LocalObjectDetector(ObjectDetector):
"CPU detectors are not recommended and should only be used for testing or for trial purposes." "CPU detectors are not recommended and should only be used for testing or for trial purposes."
) )
self.interpreter = tflite.Interpreter( 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() self.interpreter.allocate_tensors()
@@ -133,6 +113,7 @@ def run_detector(
out_events: Dict[str, mp.Event], out_events: Dict[str, mp.Event],
avg_speed, avg_speed,
start, start,
model_path,
model_shape, model_shape,
tf_device, tf_device,
num_threads, num_threads,
@@ -152,7 +133,9 @@ def run_detector(
signal.signal(signal.SIGINT, receiveSignal) signal.signal(signal.SIGINT, receiveSignal)
frame_manager = SharedMemoryFrameManager() 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 = {} outputs = {}
for name in out_events.keys(): for name in out_events.keys():
@@ -189,6 +172,7 @@ class EdgeTPUProcess:
name, name,
detection_queue, detection_queue,
out_events, out_events,
model_path,
model_shape, model_shape,
tf_device=None, tf_device=None,
num_threads=3, num_threads=3,
@@ -199,6 +183,7 @@ class EdgeTPUProcess:
self.avg_inference_speed = mp.Value("d", 0.01) self.avg_inference_speed = mp.Value("d", 0.01)
self.detection_start = mp.Value("d", 0.0) self.detection_start = mp.Value("d", 0.0)
self.detect_process = None self.detect_process = None
self.model_path = model_path
self.model_shape = model_shape self.model_shape = model_shape
self.tf_device = tf_device self.tf_device = tf_device
self.num_threads = num_threads self.num_threads = num_threads
@@ -226,6 +211,7 @@ class EdgeTPUProcess:
self.out_events, self.out_events,
self.avg_inference_speed, self.avg_inference_speed,
self.detection_start, self.detection_start,
self.model_path,
self.model_shape, self.model_shape,
self.tf_device, self.tf_device,
self.num_threads, self.num_threads,

View File

@@ -6,15 +6,25 @@ import threading
import time import time
from pathlib import Path 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 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__) logger = logging.getLogger(__name__)
def should_update_db(prev_event, current_event):
return (
prev_event["top_score"] != current_event["top_score"]
or prev_event["entered_zones"] != current_event["entered_zones"]
or prev_event["thumbnail"] != current_event["thumbnail"]
or prev_event["has_clip"] != current_event["has_clip"]
or prev_event["has_snapshot"] != current_event["has_snapshot"]
)
class EventProcessor(threading.Thread): class EventProcessor(threading.Thread):
def __init__( def __init__(
self, config, camera_processes, event_queue, event_processed_queue, stop_event self, config, camera_processes, event_queue, event_processed_queue, stop_event
@@ -29,41 +39,12 @@ class EventProcessor(threading.Thread):
self.events_in_process = {} self.events_in_process = {}
self.stop_event = stop_event self.stop_event = stop_event
def should_create_clip(self, camera, event_data):
if event_data["false_positive"]:
return False
record_config: RecordConfig = self.config.cameras[camera].record
# Recording clips is disabled
if not record_config.enabled or (
record_config.retain_days == 0 and not record_config.events.enabled
):
return False
# If there are required zones and there is no overlap
required_zones = record_config.events.required_zones
if len(required_zones) > 0 and not set(event_data["entered_zones"]) & set(
required_zones
):
logger.debug(
f"Not creating clip for {event_data['id']} because it did not enter required zones"
)
return False
# If the required objects are not present
if (
record_config.events.objects is not None
and event_data["label"] not in record_config.events.objects
):
logger.debug(
f"Not creating clip for {event_data['id']} because it did not contain required objects"
)
return False
return True
def run(self): def run(self):
# set an end_time on events without an end_time on startup
Event.update(end_time=Event.start_time + 30).where(
Event.end_time == None
).execute()
while not self.stop_event.is_set(): while not self.stop_event.is_set():
try: try:
event_type, camera, event_data = self.event_queue.get(timeout=10) event_type, camera, event_data = self.event_queue.get(timeout=10)
@@ -72,32 +53,60 @@ class EventProcessor(threading.Thread):
logger.debug(f"Event received: {event_type} {camera} {event_data['id']}") logger.debug(f"Event received: {event_type} {camera} {event_data['id']}")
event_config: EventsConfig = self.config.cameras[camera].record.events
if event_type == "start": if event_type == "start":
self.events_in_process[event_data["id"]] = event_data self.events_in_process[event_data["id"]] = event_data
if event_type == "end": elif event_type == "update" and should_update_db(
record_config: RecordConfig = self.config.cameras[camera].record self.events_in_process[event_data["id"]], event_data
):
has_clip = self.should_create_clip(camera, event_data) self.events_in_process[event_data["id"]] = event_data
# TODO: this will generate a lot of db activity possibly
if has_clip or event_data["has_snapshot"]: if event_data["has_clip"] or event_data["has_snapshot"]:
Event.create( Event.replace(
id=event_data["id"], id=event_data["id"],
label=event_data["label"], label=event_data["label"],
camera=camera, camera=camera,
start_time=event_data["start_time"], start_time=event_data["start_time"] - event_config.pre_capture,
end_time=event_data["end_time"], end_time=None,
top_score=event_data["top_score"], top_score=event_data["top_score"],
false_positive=event_data["false_positive"], false_positive=event_data["false_positive"],
zones=list(event_data["entered_zones"]), zones=list(event_data["entered_zones"]),
thumbnail=event_data["thumbnail"], thumbnail=event_data["thumbnail"],
has_clip=has_clip, region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"], has_snapshot=event_data["has_snapshot"],
) ).execute()
elif event_type == "end":
if event_data["has_clip"] or event_data["has_snapshot"]:
Event.replace(
id=event_data["id"],
label=event_data["label"],
camera=camera,
start_time=event_data["start_time"] - event_config.pre_capture,
end_time=event_data["end_time"] + event_config.post_capture,
top_score=event_data["top_score"],
false_positive=event_data["false_positive"],
zones=list(event_data["entered_zones"]),
thumbnail=event_data["thumbnail"],
region=event_data["region"],
box=event_data["box"],
area=event_data["area"],
has_clip=event_data["has_clip"],
has_snapshot=event_data["has_snapshot"],
).execute()
del self.events_in_process[event_data["id"]] del self.events_in_process[event_data["id"]]
self.event_processed_queue.put((event_data["id"], camera, has_clip)) self.event_processed_queue.put((event_data["id"], camera))
# set an end_time on events without an end_time before exiting
Event.update(end_time=datetime.datetime.now().timestamp()).where(
Event.end_time == None
).execute()
logger.info(f"Exiting event processor...") logger.info(f"Exiting event processor...")
@@ -225,12 +234,12 @@ class EventCleanup(threading.Thread):
for event in duplicate_events: for event in duplicate_events:
logger.debug(f"Removing duplicate: {event.id}") logger.debug(f"Removing duplicate: {event.id}")
media_name = f"{event.camera}-{event.id}" media_name = f"{event.camera}-{event.id}"
if event.has_snapshot: media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") media_path.unlink(missing_ok=True)
media_path.unlink(missing_ok=True) media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
if event.has_clip: media_path.unlink(missing_ok=True)
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4") media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
media_path.unlink(missing_ok=True) media_path.unlink(missing_ok=True)
( (
Event.delete() Event.delete()

View File

@@ -1,6 +1,7 @@
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import copy
import json import json
import glob import glob
import logging import logging
@@ -132,6 +133,8 @@ def delete_event(id):
if event.has_snapshot: if event.has_snapshot:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg") media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
media.unlink(missing_ok=True) media.unlink(missing_ok=True)
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
media.unlink(missing_ok=True)
if event.has_clip: if event.has_clip:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4") media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
media.unlink(missing_ok=True) media.unlink(missing_ok=True)
@@ -181,7 +184,7 @@ def event_thumbnail(id):
thumbnail_bytes = jpg.tobytes() thumbnail_bytes = jpg.tobytes()
response = make_response(thumbnail_bytes) response = make_response(thumbnail_bytes)
response.headers["Content-Type"] = "image/jpg" response.headers["Content-Type"] = "image/jpeg"
return response return response
@@ -190,7 +193,7 @@ def event_snapshot(id):
download = request.args.get("download", type=bool) download = request.args.get("download", type=bool)
jpg_bytes = None jpg_bytes = None
try: try:
event = Event.get(Event.id == id) event = Event.get(Event.id == id, Event.end_time != None)
if not event.has_snapshot: if not event.has_snapshot:
return "Snapshot not available", 404 return "Snapshot not available", 404
# read snapshot from disk # read snapshot from disk
@@ -222,7 +225,7 @@ def event_snapshot(id):
return "Event not found", 404 return "Event not found", 404
response = make_response(jpg_bytes) response = make_response(jpg_bytes)
response.headers["Content-Type"] = "image/jpg" response.headers["Content-Type"] = "image/jpeg"
if download: if download:
response.headers[ response.headers[
"Content-Disposition" "Content-Disposition"
@@ -242,14 +245,14 @@ def event_clip(id):
if not event.has_clip: if not event.has_clip:
return "Clip not available", 404 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" file_name = f"{event.camera}-{id}.mp4"
clip_path = os.path.join(CLIPS_DIR, file_name) clip_path = os.path.join(CLIPS_DIR, file_name)
if not os.path.isfile(clip_path): if not os.path.isfile(clip_path):
return recording_clip(event.camera, start_ts, end_ts) end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time
)
return recording_clip(event.camera, event.start_time, end_ts)
response = make_response() response = make_response()
response.headers["Content-Description"] = "File Transfer" response.headers["Content-Description"] = "File Transfer"
@@ -324,7 +327,7 @@ def config():
# add in the ffmpeg_cmds # add in the ffmpeg_cmds
for camera_name, camera in current_app.frigate_config.cameras.items(): for camera_name, camera in current_app.frigate_config.cameras.items():
camera_dict = config["cameras"][camera_name] camera_dict = config["cameras"][camera_name]
camera_dict["ffmpeg_cmds"] = camera.ffmpeg_cmds camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds)
for cmd in camera_dict["ffmpeg_cmds"]: for cmd in camera_dict["ffmpeg_cmds"]:
cmd["cmd"] = " ".join(cmd["cmd"]) cmd["cmd"] = " ".join(cmd["cmd"])
@@ -361,9 +364,16 @@ def best(camera_name, label):
crop = bool(request.args.get("crop", 0, type=int)) crop = bool(request.args.get("crop", 0, type=int))
if crop: if crop:
box = best_object.get("box", (0, 0, 300, 300)) box_size = 300
box = best_object.get("box", (0, 0, box_size, box_size))
region = calculate_region( region = calculate_region(
best_frame.shape, box[0], box[1], box[2], box[3], 1.1 best_frame.shape,
box[0],
box[1],
box[2],
box[3],
box_size,
multiplier=1.1,
) )
best_frame = best_frame[region[1] : region[3], region[0] : region[2]] best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
@@ -378,7 +388,7 @@ def best(camera_name, label):
".jpg", best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality] ".jpg", best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
) )
response = make_response(jpg.tobytes()) response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpg" response.headers["Content-Type"] = "image/jpeg"
return response return response
else: else:
return "Camera named {} not found".format(camera_name), 404 return "Camera named {} not found".format(camera_name), 404
@@ -440,7 +450,7 @@ def latest_frame(camera_name):
".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality] ".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
) )
response = make_response(jpg.tobytes()) response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpg" response.headers["Content-Type"] = "image/jpeg"
return response return response
else: else:
return "Camera named {} not found".format(camera_name), 404 return "Camera named {} not found".format(camera_name), 404
@@ -517,12 +527,17 @@ def recordings(camera_name):
FROM C2 FROM C2
WHERE cnt = 0 WHERE cnt = 0
) )
SELECT id, label, camera, top_score, start_time, end_time
FROM event
WHERE camera = ? AND end_time IS NULL
UNION ALL
SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time
FROM C3 FROM C3
GROUP BY label, grpnum GROUP BY label, grpnum
ORDER BY start_time;""", ORDER BY start_time;""",
camera_name, camera_name,
camera_name, camera_name,
camera_name,
) )
event: Event event: Event
@@ -660,10 +675,15 @@ def vod_ts(camera, start_ts, end_ts):
# Determine if we need to end the last clip early # Determine if we need to end the last clip early
if recording.end_time > end_ts: if recording.end_time > end_ts:
duration -= int((recording.end_time - end_ts) * 1000) duration -= int((recording.end_time - end_ts) * 1000)
clips.append(clip)
durations.append(duration) if duration > 0:
clips.append(clip)
durations.append(duration)
else:
logger.warning(f"Recording clip is missing or empty: {recording.path}")
if not clips: if not clips:
logger.error("No recordings found for the requested time range")
return "No recordings found.", 404 return "No recordings found.", 404
hour_ago = datetime.now() - timedelta(hours=1) hour_ago = datetime.now() - timedelta(hours=1)
@@ -692,20 +712,30 @@ def vod_event(id):
try: try:
event: Event = Event.get(Event.id == id) event: Event = Event.get(Event.id == id)
except DoesNotExist: except DoesNotExist:
logger.error(f"Event not found: {id}")
return "Event not found.", 404 return "Event not found.", 404
if not event.has_clip: if not event.has_clip:
return "Clip not available", 404 logger.error(f"Event does not have recordings: {id}")
return "Recordings 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") clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
if not os.path.isfile(clip_path): if not os.path.isfile(clip_path):
return vod_ts(event.camera, start_ts, end_ts) end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time
)
vod_response = vod_ts(event.camera, event.start_time, end_ts)
# If the recordings are not found, set has_clip to false
if (
type(vod_response) == tuple
and len(vod_response) == 2
and vod_response[1] == 404
):
Event.update(has_clip=False).where(Event.id == id).execute()
return vod_response
duration = int((end_ts - start_ts) * 1000) duration = int((event.end_time - event.start_time) * 1000)
return jsonify( return jsonify(
{ {
"cache": True, "cache": True,

View File

@@ -15,6 +15,9 @@ class Event(Model):
thumbnail = TextField() thumbnail = TextField()
has_clip = BooleanField(default=True) has_clip = BooleanField(default=True)
has_snapshot = BooleanField(default=True) has_snapshot = BooleanField(default=True)
region = JSONField()
box = JSONField()
area = IntegerField()
class Recordings(Model): class Recordings(Model):
@@ -24,3 +27,5 @@ class Recordings(Model):
start_time = DateTimeField() start_time = DateTimeField()
end_time = DateTimeField() end_time = DateTimeField()
duration = FloatField() duration = FloatField()
motion = IntegerField(null=True)
objects = IntegerField(null=True)

View File

@@ -23,6 +23,7 @@ class MotionDetector:
interpolation=cv2.INTER_LINEAR, interpolation=cv2.INTER_LINEAR,
) )
self.mask = np.where(resized_mask == [0]) self.mask = np.where(resized_mask == [0])
self.save_images = False
def detect(self, frame): def detect(self, frame):
motion_boxes = [] motion_boxes = []
@@ -36,10 +37,15 @@ class MotionDetector:
interpolation=cv2.INTER_LINEAR, interpolation=cv2.INTER_LINEAR,
) )
# TODO: can I improve the contrast of the grayscale image here? # Improve contrast
minval = np.percentile(resized_frame, 4)
# convert to grayscale maxval = np.percentile(resized_frame, 96)
# resized_frame = cv2.cvtColor(resized_frame, cv2.COLOR_BGR2GRAY) # don't adjust if the image is a single color
if minval < maxval:
resized_frame = np.clip(resized_frame, minval, maxval)
resized_frame = (
((resized_frame - minval) / (maxval - minval)) * 255
).astype(np.uint8)
# mask frame # mask frame
resized_frame[self.mask] = [255] resized_frame[self.mask] = [255]
@@ -49,6 +55,8 @@ class MotionDetector:
if self.frame_counter < 30: if self.frame_counter < 30:
self.frame_counter += 1 self.frame_counter += 1
else: else:
if self.save_images:
self.frame_counter += 1
# compare to average # compare to average
frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame)) frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
@@ -58,7 +66,6 @@ class MotionDetector:
cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha) cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
# compute the threshold image for the current frame # compute the threshold image for the current frame
# TODO: threshold
current_thresh = cv2.threshold( current_thresh = cv2.threshold(
frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY frameDelta, self.config.threshold, 255, cv2.THRESH_BINARY
)[1] )[1]
@@ -75,8 +82,10 @@ class MotionDetector:
# dilate the thresholded image to fill in holes, then find contours # dilate the thresholded image to fill in holes, then find contours
# on thresholded image # on thresholded image
thresh = cv2.dilate(thresh, None, iterations=2) thresh_dilated = cv2.dilate(thresh, None, iterations=2)
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cnts = cv2.findContours(
thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
cnts = imutils.grab_contours(cnts) cnts = imutils.grab_contours(cnts)
# loop over the contours # loop over the contours
@@ -94,6 +103,35 @@ class MotionDetector:
) )
) )
if self.save_images:
thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR)
# print("--------")
# print(self.frame_counter)
for c in cnts:
contour_area = cv2.contourArea(c)
# print(contour_area)
if contour_area > self.config.contour_area:
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(
thresh_dilated,
(x, y),
(x + w, y + h),
(0, 0, 255),
2,
)
# print("--------")
image_row_1 = cv2.hconcat(
[
cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR),
cv2.cvtColor(avg_delta_image, cv2.COLOR_GRAY2BGR),
]
)
image_row_2 = cv2.hconcat(
[cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), thresh_dilated]
)
combined_image = cv2.vconcat([image_row_1, image_row_2])
cv2.imwrite(f"motion/motion-{self.frame_counter}.jpg", combined_image)
if len(motion_boxes) > 0: if len(motion_boxes) > 0:
self.motion_frame_count += 1 self.motion_frame_count += 1
if self.motion_frame_count >= 10: if self.motion_frame_count >= 10:

View File

@@ -107,7 +107,7 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
+ str(rc) + str(rc)
) )
logger.info("MQTT connected") logger.debug("MQTT connected")
client.subscribe(f"{mqtt_config.topic_prefix}/#") client.subscribe(f"{mqtt_config.topic_prefix}/#")
client.publish(mqtt_config.topic_prefix + "/available", "online", retain=True) client.publish(mqtt_config.topic_prefix + "/available", "online", retain=True)

View File

@@ -16,14 +16,14 @@ from typing import Callable, Dict
import cv2 import cv2
import numpy as np import numpy as np
from frigate.config import CameraConfig, FrigateConfig from frigate.config import CameraConfig, SnapshotsConfig, RecordConfig, FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
from frigate.edgetpu import load_labels
from frigate.util import ( from frigate.util import (
SharedMemoryFrameManager, SharedMemoryFrameManager,
calculate_region, calculate_region,
draw_box_with_label, draw_box_with_label,
draw_timestamp, draw_timestamp,
load_labels,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -71,8 +71,10 @@ class TrackedObject:
self.camera_config = camera_config self.camera_config = camera_config
self.frame_cache = frame_cache self.frame_cache = frame_cache
self.current_zones = [] self.current_zones = []
self.entered_zones = set() self.entered_zones = []
self.false_positive = True self.false_positive = True
self.has_clip = False
self.has_snapshot = False
self.top_score = self.computed_score = 0.0 self.top_score = self.computed_score = 0.0
self.thumbnail_data = None self.thumbnail_data = None
self.last_updated = 0 self.last_updated = 0
@@ -99,14 +101,13 @@ class TrackedObject:
return median(scores) return median(scores)
def update(self, current_frame_time, obj_data): def update(self, current_frame_time, obj_data):
significant_update = False thumb_update = False
zone_change = False significant_change = False
self.obj_data.update(obj_data)
# if the object is not in the current frame, add a 0.0 to the score history # if the object is not in the current frame, add a 0.0 to the score history
if self.obj_data["frame_time"] != current_frame_time: if obj_data["frame_time"] != current_frame_time:
self.score_history.append(0.0) self.score_history.append(0.0)
else: else:
self.score_history.append(self.obj_data["score"]) self.score_history.append(obj_data["score"])
# only keep the last 10 scores # only keep the last 10 scores
if len(self.score_history) > 10: if len(self.score_history) > 10:
self.score_history = self.score_history[-10:] self.score_history = self.score_history[-10:]
@@ -120,24 +121,24 @@ class TrackedObject:
if not self.false_positive: if not self.false_positive:
# determine if this frame is a better thumbnail # determine if this frame is a better thumbnail
if self.thumbnail_data is None or is_better_thumbnail( if self.thumbnail_data is None or is_better_thumbnail(
self.thumbnail_data, self.obj_data, self.camera_config.frame_shape self.thumbnail_data, obj_data, self.camera_config.frame_shape
): ):
self.thumbnail_data = { self.thumbnail_data = {
"frame_time": self.obj_data["frame_time"], "frame_time": obj_data["frame_time"],
"box": self.obj_data["box"], "box": obj_data["box"],
"area": self.obj_data["area"], "area": obj_data["area"],
"region": self.obj_data["region"], "region": obj_data["region"],
"score": self.obj_data["score"], "score": obj_data["score"],
} }
significant_update = True thumb_update = True
# check zones # check zones
current_zones = [] current_zones = []
bottom_center = (self.obj_data["centroid"][0], self.obj_data["box"][3]) bottom_center = (obj_data["centroid"][0], obj_data["box"][3])
# check each zone # check each zone
for name, zone in self.camera_config.zones.items(): for name, zone in self.camera_config.zones.items():
# if the zone is not for this object type, skip # if the zone is not for this object type, skip
if len(zone.objects) > 0 and not self.obj_data["label"] in zone.objects: if len(zone.objects) > 0 and not obj_data["label"] in zone.objects:
continue continue
contour = zone.contour contour = zone.contour
# check if the object is in the zone # check if the object is in the zone
@@ -145,14 +146,32 @@ class TrackedObject:
# if the object passed the filters once, dont apply again # if the object passed the filters once, dont apply again
if name in self.current_zones or not zone_filtered(self, zone.filters): if name in self.current_zones or not zone_filtered(self, zone.filters):
current_zones.append(name) current_zones.append(name)
self.entered_zones.add(name) if name not in self.entered_zones:
self.entered_zones.append(name)
# if the zones changed, signal an update if not self.false_positive:
if not self.false_positive and set(self.current_zones) != set(current_zones): # if the zones changed, signal an update
zone_change = True if set(self.current_zones) != set(current_zones):
significant_change = True
# if the position changed, signal an update
if self.obj_data["position_changes"] != obj_data["position_changes"]:
significant_change = True
# if the motionless_count reaches the stationary threshold
if (
self.obj_data["motionless_count"]
== self.camera_config.detect.stationary.threshold
):
significant_change = True
# update at least once per minute
if self.obj_data["frame_time"] - self.previous["frame_time"] > 60:
significant_change = True
self.obj_data.update(obj_data)
self.current_zones = current_zones self.current_zones = current_zones
return (significant_update, zone_change) return (thumb_update, significant_change)
def to_dict(self, include_thumbnail: bool = False): def to_dict(self, include_thumbnail: bool = False):
snapshot_time = ( snapshot_time = (
@@ -174,8 +193,14 @@ class TrackedObject:
"box": self.obj_data["box"], "box": self.obj_data["box"],
"area": self.obj_data["area"], "area": self.obj_data["area"],
"region": self.obj_data["region"], "region": self.obj_data["region"],
"stationary": self.obj_data["motionless_count"]
> self.camera_config.detect.stationary.threshold,
"motionless_count": self.obj_data["motionless_count"],
"position_changes": self.obj_data["position_changes"],
"current_zones": self.current_zones.copy(), "current_zones": self.current_zones.copy(),
"entered_zones": list(self.entered_zones).copy(), "entered_zones": self.entered_zones.copy(),
"has_clip": self.has_clip,
"has_snapshot": self.has_snapshot,
} }
if include_thumbnail: if include_thumbnail:
@@ -258,8 +283,15 @@ class TrackedObject:
if crop: if crop:
box = self.thumbnail_data["box"] box = self.thumbnail_data["box"]
box_size = 300
region = calculate_region( region = calculate_region(
best_frame.shape, box[0], box[1], box[2], box[3], 1.1 best_frame.shape,
box[0],
box[1],
box[2],
box[3],
box_size,
multiplier=1.1,
) )
best_frame = best_frame[region[1] : region[3], region[0] : region[2]] best_frame = best_frame[region[1] : region[3], region[0] : region[2]]
@@ -275,9 +307,8 @@ class TrackedObject:
self.thumbnail_data["frame_time"], self.thumbnail_data["frame_time"],
self.camera_config.timestamp_style.format, self.camera_config.timestamp_style.format,
font_effect=self.camera_config.timestamp_style.effect, 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_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, position=self.camera_config.timestamp_style.position,
) )
@@ -411,9 +442,8 @@ class CameraState:
frame_time, frame_time,
self.camera_config.timestamp_style.format, self.camera_config.timestamp_style.format,
font_effect=self.camera_config.timestamp_style.effect, 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_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, position=self.camera_config.timestamp_style.position,
) )
@@ -454,11 +484,11 @@ class CameraState:
for id in updated_ids: for id in updated_ids:
updated_obj = tracked_objects[id] updated_obj = tracked_objects[id]
significant_update, zone_change = updated_obj.update( thumb_update, significant_update = updated_obj.update(
frame_time, current_detections[id] frame_time, current_detections[id]
) )
if significant_update: if thumb_update:
# ensure this frame is stored in the cache # ensure this frame is stored in the cache
if ( if (
updated_obj.thumbnail_data["frame_time"] == frame_time updated_obj.thumbnail_data["frame_time"] == frame_time
@@ -468,13 +498,13 @@ class CameraState:
updated_obj.last_updated = frame_time updated_obj.last_updated = frame_time
# if it has been more than 5 seconds since the last publish # if it has been more than 5 seconds since the last thumb update
# and the last update is greater than the last publish or # and the last update is greater than the last publish or
# the object has changed zones # the object has changed significantly
if ( if (
frame_time - updated_obj.last_published > 5 frame_time - updated_obj.last_published > 5
and updated_obj.last_updated > updated_obj.last_published and updated_obj.last_updated > updated_obj.last_published
) or zone_change: ) or significant_update:
# call event handlers # call event handlers
for c in self.callbacks["update"]: for c in self.callbacks["update"]:
c(self.name, updated_obj, frame_time) c(self.name, updated_obj, frame_time)
@@ -582,6 +612,7 @@ class TrackedObjectProcessor(threading.Thread):
event_queue, event_queue,
event_processed_queue, event_processed_queue,
video_output_queue, video_output_queue,
recordings_info_queue,
stop_event, stop_event,
): ):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@@ -593,6 +624,7 @@ class TrackedObjectProcessor(threading.Thread):
self.event_queue = event_queue self.event_queue = event_queue
self.event_processed_queue = event_processed_queue self.event_processed_queue = event_processed_queue
self.video_output_queue = video_output_queue self.video_output_queue = video_output_queue
self.recordings_info_queue = recordings_info_queue
self.stop_event = stop_event self.stop_event = stop_event
self.camera_states: Dict[str, CameraState] = {} self.camera_states: Dict[str, CameraState] = {}
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
@@ -601,6 +633,8 @@ class TrackedObjectProcessor(threading.Thread):
self.event_queue.put(("start", camera, obj.to_dict())) self.event_queue.put(("start", camera, obj.to_dict()))
def update(camera, obj: TrackedObject, current_frame_time): def update(camera, obj: TrackedObject, current_frame_time):
obj.has_snapshot = self.should_save_snapshot(camera, obj)
obj.has_clip = self.should_retain_recording(camera, obj)
after = obj.to_dict() after = obj.to_dict()
message = { message = {
"before": obj.previous, "before": obj.previous,
@@ -611,11 +645,51 @@ class TrackedObjectProcessor(threading.Thread):
f"{self.topic_prefix}/events", json.dumps(message), retain=False f"{self.topic_prefix}/events", json.dumps(message), retain=False
) )
obj.previous = after obj.previous = after
self.event_queue.put(
("update", camera, obj.to_dict(include_thumbnail=True))
)
def end(camera, obj: TrackedObject, current_frame_time): def end(camera, obj: TrackedObject, current_frame_time):
snapshot_config = self.config.cameras[camera].snapshots # populate has_snapshot
event_data = obj.to_dict(include_thumbnail=True) obj.has_snapshot = self.should_save_snapshot(camera, obj)
event_data["has_snapshot"] = False obj.has_clip = self.should_retain_recording(camera, obj)
# write the snapshot to disk
if obj.has_snapshot:
snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots
jpg_bytes = obj.get_jpg_bytes(
timestamp=snapshot_config.timestamp,
bounding_box=snapshot_config.bounding_box,
crop=snapshot_config.crop,
height=snapshot_config.height,
quality=snapshot_config.quality,
)
if jpg_bytes is None:
logger.warning(f"Unable to save snapshot for {obj.obj_data['id']}.")
else:
with open(
os.path.join(CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"),
"wb",
) as j:
j.write(jpg_bytes)
# write clean snapshot if enabled
if snapshot_config.clean_copy:
png_bytes = obj.get_clean_png()
if png_bytes is None:
logger.warning(
f"Unable to save clean snapshot for {obj.obj_data['id']}."
)
else:
with open(
os.path.join(
CLIPS_DIR,
f"{camera}-{obj.obj_data['id']}-clean.png",
),
"wb",
) as p:
p.write(png_bytes)
if not obj.false_positive: if not obj.false_positive:
message = { message = {
"before": obj.previous, "before": obj.previous,
@@ -625,46 +699,8 @@ class TrackedObjectProcessor(threading.Thread):
self.client.publish( self.client.publish(
f"{self.topic_prefix}/events", json.dumps(message), retain=False f"{self.topic_prefix}/events", json.dumps(message), retain=False
) )
# write snapshot to disk if enabled
if snapshot_config.enabled and self.should_save_snapshot(camera, obj):
jpg_bytes = obj.get_jpg_bytes(
timestamp=snapshot_config.timestamp,
bounding_box=snapshot_config.bounding_box,
crop=snapshot_config.crop,
height=snapshot_config.height,
quality=snapshot_config.quality,
)
if jpg_bytes is None:
logger.warning(
f"Unable to save snapshot for {obj.obj_data['id']}."
)
else:
with open(
os.path.join(
CLIPS_DIR, f"{camera}-{obj.obj_data['id']}.jpg"
),
"wb",
) as j:
j.write(jpg_bytes)
event_data["has_snapshot"] = True
# write clean snapshot if enabled self.event_queue.put(("end", camera, obj.to_dict(include_thumbnail=True)))
if snapshot_config.clean_copy:
png_bytes = obj.get_clean_png()
if png_bytes is None:
logger.warning(
f"Unable to save clean snapshot for {obj.obj_data['id']}."
)
else:
with open(
os.path.join(
CLIPS_DIR,
f"{camera}-{obj.obj_data['id']}-clean.png",
),
"wb",
) as p:
p.write(png_bytes)
self.event_queue.put(("end", camera, event_data))
def snapshot(camera, obj: TrackedObject, current_frame_time): def snapshot(camera, obj: TrackedObject, current_frame_time):
mqtt_config = self.config.cameras[camera].mqtt mqtt_config = self.config.cameras[camera].mqtt
@@ -713,9 +749,21 @@ class TrackedObjectProcessor(threading.Thread):
self.zone_data = defaultdict(lambda: defaultdict(dict)) self.zone_data = defaultdict(lambda: defaultdict(dict))
def should_save_snapshot(self, camera, obj: TrackedObject): def should_save_snapshot(self, camera, obj: TrackedObject):
if obj.false_positive:
return False
snapshot_config: SnapshotsConfig = self.config.cameras[camera].snapshots
if not snapshot_config.enabled:
return False
# object never changed position
if obj.obj_data["position_changes"] == 0:
return False
# if there are required zones and there is no overlap # if there are required zones and there is no overlap
required_zones = self.config.cameras[camera].snapshots.required_zones required_zones = snapshot_config.required_zones
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones): if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
logger.debug( logger.debug(
f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones" f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones"
) )
@@ -723,10 +771,48 @@ class TrackedObjectProcessor(threading.Thread):
return True return True
def should_retain_recording(self, camera, obj: TrackedObject):
if obj.false_positive:
return False
record_config: RecordConfig = self.config.cameras[camera].record
# Recording is disabled
if not record_config.enabled:
return False
# object never changed position
if obj.obj_data["position_changes"] == 0:
return False
# If there are required zones and there is no overlap
required_zones = record_config.events.required_zones
if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
logger.debug(
f"Not creating clip for {obj.obj_data['id']} because it did not enter required zones"
)
return False
# If the required objects are not present
if (
record_config.events.objects is not None
and obj.obj_data["label"] not in record_config.events.objects
):
logger.debug(
f"Not creating clip for {obj.obj_data['id']} because it did not contain required objects"
)
return False
return True
def should_mqtt_snapshot(self, camera, obj: TrackedObject): def should_mqtt_snapshot(self, camera, obj: TrackedObject):
# object never changed position
if obj.obj_data["position_changes"] == 0:
return False
# if there are required zones and there is no overlap # if there are required zones and there is no overlap
required_zones = self.config.cameras[camera].mqtt.required_zones required_zones = self.config.cameras[camera].mqtt.required_zones
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones): if len(required_zones) > 0 and not set(obj.entered_zones) & set(required_zones):
logger.debug( logger.debug(
f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones" f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones"
) )
@@ -769,11 +855,26 @@ class TrackedObjectProcessor(threading.Thread):
frame_time, current_tracked_objects, motion_boxes, regions frame_time, current_tracked_objects, motion_boxes, regions
) )
tracked_objects = [
o.to_dict() for o in camera_state.tracked_objects.values()
]
self.video_output_queue.put( self.video_output_queue.put(
( (
camera, camera,
frame_time, frame_time,
current_tracked_objects, tracked_objects,
motion_boxes,
regions,
)
)
# send info on this frame to the recordings maintainer
self.recordings_info_queue.put(
(
camera,
frame_time,
tracked_objects,
motion_boxes, motion_boxes,
regions, regions,
) )
@@ -817,17 +918,7 @@ class TrackedObjectProcessor(threading.Thread):
# cleanup event finished queue # cleanup event finished queue
while not self.event_processed_queue.empty(): while not self.event_processed_queue.empty():
event_id, camera, clip_created = self.event_processed_queue.get() event_id, camera = self.event_processed_queue.get()
if clip_created:
obj = self.camera_states[camera].tracked_objects[event_id]
message = {
"before": obj.previous,
"after": obj.to_dict(),
"type": "clip_ready",
}
self.client.publish(
f"{self.topic_prefix}/events", json.dumps(message), retain=False
)
self.camera_states[camera].finished(event_id) self.camera_states[camera].finished(event_id)
logger.info(f"Exiting object processor...") logger.info(f"Exiting object processor...")

View File

@@ -13,31 +13,133 @@ import numpy as np
from scipy.spatial import distance as dist from scipy.spatial import distance as dist
from frigate.config import DetectConfig from frigate.config import DetectConfig
from frigate.util import draw_box_with_label from frigate.util import intersection_over_union
class ObjectTracker: class ObjectTracker:
def __init__(self, config: DetectConfig): def __init__(self, config: DetectConfig):
self.tracked_objects = {} self.tracked_objects = {}
self.disappeared = {} self.disappeared = {}
self.positions = {}
self.max_disappeared = config.max_disappeared self.max_disappeared = config.max_disappeared
self.detect_config = config
def register(self, index, obj): def register(self, index, obj):
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6)) rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
id = f"{obj['frame_time']}-{rand_id}" id = f"{obj['frame_time']}-{rand_id}"
obj["id"] = id obj["id"] = id
obj["start_time"] = obj["frame_time"] obj["start_time"] = obj["frame_time"]
obj["motionless_count"] = 0
obj["position_changes"] = 0
self.tracked_objects[id] = obj self.tracked_objects[id] = obj
self.disappeared[id] = 0 self.disappeared[id] = 0
self.positions[id] = {
"xmins": [],
"ymins": [],
"xmaxs": [],
"ymaxs": [],
"xmin": 0,
"ymin": 0,
"xmax": self.detect_config.width,
"ymax": self.detect_config.height,
}
def deregister(self, id): def deregister(self, id):
del self.tracked_objects[id] del self.tracked_objects[id]
del self.disappeared[id] del self.disappeared[id]
# tracks the current position of the object based on the last N bounding boxes
# returns False if the object has moved outside its previous position
def update_position(self, id, box):
position = self.positions[id]
position_box = (
position["xmin"],
position["ymin"],
position["xmax"],
position["ymax"],
)
xmin, ymin, xmax, ymax = box
iou = intersection_over_union(position_box, box)
# if the iou drops below the threshold
# assume the object has moved to a new position and reset the computed box
if iou < 0.6:
self.positions[id] = {
"xmins": [xmin],
"ymins": [ymin],
"xmaxs": [xmax],
"ymaxs": [ymax],
"xmin": xmin,
"ymin": ymin,
"xmax": xmax,
"ymax": ymax,
}
return False
# if there are less than 10 entries for the position, add the bounding box
# and recompute the position box
if len(position["xmins"]) < 10:
position["xmins"].append(xmin)
position["ymins"].append(ymin)
position["xmaxs"].append(xmax)
position["ymaxs"].append(ymax)
# by using percentiles here, we hopefully remove outliers
position["xmin"] = np.percentile(position["xmins"], 15)
position["ymin"] = np.percentile(position["ymins"], 15)
position["xmax"] = np.percentile(position["xmaxs"], 85)
position["ymax"] = np.percentile(position["ymaxs"], 85)
return True
def is_expired(self, id):
obj = self.tracked_objects[id]
# get the max frames for this label type or the default
max_frames = self.detect_config.stationary.max_frames.objects.get(
obj["label"], self.detect_config.stationary.max_frames.default
)
# if there is no max_frames for this label type, continue
if max_frames is None:
return False
# if the object has exceeded the max_frames setting, deregister
if (
obj["motionless_count"] - self.detect_config.stationary.threshold
> max_frames
):
print(f"expired: {obj['motionless_count']}")
return True
def update(self, id, new_obj): def update(self, id, new_obj):
self.disappeared[id] = 0 self.disappeared[id] = 0
# update the motionless count if the object has not moved to a new position
if self.update_position(id, new_obj["box"]):
self.tracked_objects[id]["motionless_count"] += 1
if self.is_expired(id):
self.deregister(id)
return
else:
# register the first position change and then only increment if
# the object was previously stationary
if (
self.tracked_objects[id]["position_changes"] == 0
or self.tracked_objects[id]["motionless_count"]
>= self.detect_config.stationary.threshold
):
self.tracked_objects[id]["position_changes"] += 1
self.tracked_objects[id]["motionless_count"] = 0
self.tracked_objects[id].update(new_obj) self.tracked_objects[id].update(new_obj)
def update_frame_times(self, frame_time):
for id in list(self.tracked_objects.keys()):
self.tracked_objects[id]["frame_time"] = frame_time
self.tracked_objects[id]["motionless_count"] += 1
if self.is_expired(id):
self.deregister(id)
def match_and_update(self, frame_time, new_objects): def match_and_update(self, frame_time, new_objects):
# group by name # group by name
new_object_groups = defaultdict(lambda: []) new_object_groups = defaultdict(lambda: [])

View File

@@ -104,7 +104,7 @@ class BirdsEyeFrameManager:
self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16 self.blank_frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]] = 16
# find and copy the logo on the blank frame # find and copy the logo on the blank frame
logo_files = glob.glob("/opt/frigate/web/apple-touch-icon.*.png") logo_files = glob.glob("/opt/frigate/frigate/birdseye.png")
frigate_logo = None frigate_logo = None
if len(logo_files) > 0: if len(logo_files) > 0:
frigate_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED) frigate_logo = cv2.imread(logo_files[0], cv2.IMREAD_UNCHANGED)
@@ -184,10 +184,7 @@ class BirdsEyeFrameManager:
if self.mode == BirdseyeModeEnum.continuous: if self.mode == BirdseyeModeEnum.continuous:
return True return True
if ( if self.mode == BirdseyeModeEnum.motion and motion_box_count > 0:
self.mode == BirdseyeModeEnum.motion
and object_box_count + motion_box_count > 0
):
return True return True
if self.mode == BirdseyeModeEnum.objects and object_box_count > 0: if self.mode == BirdseyeModeEnum.objects and object_box_count > 0:
@@ -418,7 +415,7 @@ def output_frames(config: FrigateConfig, video_output_queue):
): ):
if birdseye_manager.update( if birdseye_manager.update(
camera, camera,
len(current_tracked_objects), len([o for o in current_tracked_objects if not o["stationary"]]),
len(motion_boxes), len(motion_boxes),
frame_time, frame_time,
frame, frame,

View File

@@ -1,21 +1,25 @@
import datetime import datetime
import itertools import itertools
import logging import logging
import multiprocessing as mp
import os import os
import queue
import random import random
import shutil import shutil
import string import string
import subprocess as sp import subprocess as sp
import threading import threading
import time
from collections import defaultdict
from pathlib import Path from pathlib import Path
import psutil import psutil
from peewee import JOIN, DoesNotExist
from peewee import JOIN from frigate.config import RetainModeEnum, FrigateConfig
from frigate.config import FrigateConfig
from frigate.const import CACHE_DIR, RECORD_DIR from frigate.const import CACHE_DIR, RECORD_DIR
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.util import area
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,20 +43,27 @@ def remove_empty_directories(directory):
class RecordingMaintainer(threading.Thread): class RecordingMaintainer(threading.Thread):
def __init__(self, config: FrigateConfig, stop_event): def __init__(
self, config: FrigateConfig, recordings_info_queue: mp.Queue, stop_event
):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "recording_maint" self.name = "recording_maint"
self.config = config self.config = config
self.recordings_info_queue = recordings_info_queue
self.stop_event = stop_event self.stop_event = stop_event
self.recordings_info = defaultdict(list)
self.end_time_cache = {}
def move_files(self): def move_files(self):
recordings = [ cache_files = sorted(
d [
for d in os.listdir(CACHE_DIR) d
if os.path.isfile(os.path.join(CACHE_DIR, d)) for d in os.listdir(CACHE_DIR)
and d.endswith(".mp4") if os.path.isfile(os.path.join(CACHE_DIR, d))
and not d.startswith("clip_") and d.endswith(".mp4")
] and not d.startswith("clip_")
]
)
files_in_use = [] files_in_use = []
for process in psutil.process_iter(): for process in psutil.process_iter():
@@ -67,7 +78,9 @@ class RecordingMaintainer(threading.Thread):
except: except:
continue continue
for f in recordings: # group recordings by camera
grouped_recordings = defaultdict(list)
for f in cache_files:
# Skip files currently in use # Skip files currently in use
if f in files_in_use: if f in files_in_use:
continue continue
@@ -77,45 +90,187 @@ class RecordingMaintainer(threading.Thread):
camera, date = basename.rsplit("-", maxsplit=1) camera, date = basename.rsplit("-", maxsplit=1)
start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S") start_time = datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
# Just delete files if recordings are turned off grouped_recordings[camera].append(
if ( {
not camera in self.config.cameras "cache_path": cache_path,
or not self.config.cameras[camera].record.enabled "start_time": start_time,
): }
Path(cache_path).unlink(missing_ok=True)
continue
ffprobe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
f"{cache_path}",
]
p = sp.run(ffprobe_cmd, capture_output=True)
if p.returncode == 0:
duration = float(p.stdout.decode().strip())
end_time = start_time + datetime.timedelta(seconds=duration)
else:
logger.info(f"bad file: {f}")
Path(cache_path).unlink(missing_ok=True)
continue
directory = os.path.join(
RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera
) )
if not os.path.exists(directory): # delete all cached files past the most recent 5
os.makedirs(directory) keep_count = 5
for camera in grouped_recordings.keys():
if len(grouped_recordings[camera]) > keep_count:
to_remove = grouped_recordings[camera][:-keep_count]
for f in to_remove:
Path(f["cache_path"]).unlink(missing_ok=True)
self.end_time_cache.pop(f["cache_path"], None)
grouped_recordings[camera] = grouped_recordings[camera][-keep_count:]
file_name = f"{start_time.strftime('%M.%S.mp4')}" for camera, recordings in grouped_recordings.items():
file_path = os.path.join(directory, file_name)
# clear out all the recording info for old frames
while (
len(self.recordings_info[camera]) > 0
and self.recordings_info[camera][0][0]
< recordings[0]["start_time"].timestamp()
):
self.recordings_info[camera].pop(0)
# get all events with the end time after the start of the oldest cache file
# or with end_time None
events: Event = (
Event.select()
.where(
Event.camera == camera,
(Event.end_time == None)
| (Event.end_time >= recordings[0]["start_time"].timestamp()),
Event.has_clip,
)
.order_by(Event.start_time)
)
for r in recordings:
cache_path = r["cache_path"]
start_time = r["start_time"]
# Just delete files if recordings are turned off
if (
not camera in self.config.cameras
or not self.config.cameras[camera].record.enabled
):
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
continue
if cache_path in self.end_time_cache:
end_time, duration = self.end_time_cache[cache_path]
else:
ffprobe_cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
f"{cache_path}",
]
p = sp.run(ffprobe_cmd, capture_output=True)
if p.returncode == 0:
duration = float(p.stdout.decode().strip())
end_time = start_time + datetime.timedelta(seconds=duration)
self.end_time_cache[cache_path] = (end_time, duration)
else:
logger.warning(f"Discarding a corrupt recording segment: {f}")
Path(cache_path).unlink(missing_ok=True)
continue
# if cached file's start_time is earlier than the retain days for the camera
if start_time <= (
(
datetime.datetime.now()
- datetime.timedelta(
days=self.config.cameras[camera].record.retain.days
)
)
):
# if the cached segment overlaps with the events:
overlaps = False
for event in events:
# if the event starts in the future, stop checking events
# and remove this segment
if event.start_time > end_time.timestamp():
overlaps = False
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
break
# if the event is in progress or ends after the recording starts, keep it
# and stop looking at events
if (
event.end_time is None
or event.end_time >= start_time.timestamp()
):
overlaps = True
break
if overlaps:
record_mode = self.config.cameras[
camera
].record.events.retain.mode
# move from cache to recordings immediately
self.store_segment(
camera,
start_time,
end_time,
duration,
cache_path,
record_mode,
)
# else retain days includes this segment
else:
record_mode = self.config.cameras[camera].record.retain.mode
self.store_segment(
camera, start_time, end_time, duration, cache_path, record_mode
)
def segment_stats(self, camera, start_time, end_time):
active_count = 0
motion_count = 0
for frame in self.recordings_info[camera]:
# frame is after end time of segment
if frame[0] > end_time.timestamp():
break
# frame is before start time of segment
if frame[0] < start_time.timestamp():
continue
active_count += len(
[
o
for o in frame[1]
if not o["false_positive"] and o["motionless_count"] == 0
]
)
motion_count += sum([area(box) for box in frame[2]])
return (motion_count, active_count)
def store_segment(
self,
camera,
start_time,
end_time,
duration,
cache_path,
store_mode: RetainModeEnum,
):
motion_count, active_count = self.segment_stats(camera, start_time, end_time)
# check if the segment shouldn't be stored
if (store_mode == RetainModeEnum.motion and motion_count == 0) or (
store_mode == RetainModeEnum.active_objects and active_count == 0
):
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
return
directory = os.path.join(RECORD_DIR, start_time.strftime("%Y-%m/%d/%H"), camera)
if not os.path.exists(directory):
os.makedirs(directory)
file_name = f"{start_time.strftime('%M.%S.mp4')}"
file_path = os.path.join(directory, file_name)
try:
start_frame = datetime.datetime.now().timestamp()
# copy then delete is required when recordings are stored on some network drives # copy then delete is required when recordings are stored on some network drives
shutil.copyfile(cache_path, file_path) shutil.copyfile(cache_path, file_path)
logger.debug(
f"Copied {file_path} in {datetime.datetime.now().timestamp()-start_frame} seconds."
)
os.remove(cache_path) os.remove(cache_path)
rand_id = "".join( rand_id = "".join(
@@ -128,12 +283,56 @@ class RecordingMaintainer(threading.Thread):
start_time=start_time.timestamp(), start_time=start_time.timestamp(),
end_time=end_time.timestamp(), end_time=end_time.timestamp(),
duration=duration, duration=duration,
motion=motion_count,
# TODO: update this to store list of active objects at some point
objects=active_count,
) )
except Exception as e:
logger.error(f"Unable to store recording segment {cache_path}")
Path(cache_path).unlink(missing_ok=True)
logger.error(e)
# clear end_time cache
self.end_time_cache.pop(cache_path, None)
def run(self): def run(self):
# Check for new files every 5 seconds # Check for new files every 5 seconds
while not self.stop_event.wait(5): wait_time = 5
self.move_files() while not self.stop_event.wait(wait_time):
run_start = datetime.datetime.now().timestamp()
# empty the recordings info queue
while True:
try:
(
camera,
frame_time,
current_tracked_objects,
motion_boxes,
regions,
) = self.recordings_info_queue.get(False)
if self.config.cameras[camera].record.enabled:
self.recordings_info[camera].append(
(
frame_time,
current_tracked_objects,
motion_boxes,
regions,
)
)
except queue.Empty:
break
try:
self.move_files()
except Exception as e:
logger.error(
"Error occurred when attempting to maintain recording cache"
)
logger.error(e)
duration = datetime.datetime.now().timestamp() - run_start
wait_time = max(0, 5 - duration)
logger.info(f"Exiting recording maintenance...") logger.info(f"Exiting recording maintenance...")
@@ -158,18 +357,22 @@ class RecordingCleanup(threading.Thread):
logger.debug("Start deleted cameras.") logger.debug("Start deleted cameras.")
# Handle deleted cameras # Handle deleted cameras
expire_days = self.config.record.retain.days
expire_before = (
datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp()
no_camera_recordings: Recordings = Recordings.select().where( no_camera_recordings: Recordings = Recordings.select().where(
Recordings.camera.not_in(list(self.config.cameras.keys())), Recordings.camera.not_in(list(self.config.cameras.keys())),
Recordings.end_time < expire_before,
) )
deleted_recordings = set()
for recording in no_camera_recordings: for recording in no_camera_recordings:
expire_days = self.config.record.retain_days Path(recording.path).unlink(missing_ok=True)
expire_before = ( deleted_recordings.add(recording.id)
datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp() logger.debug(f"Expiring {len(deleted_recordings)} recordings")
if recording.end_time < expire_before: Recordings.delete().where(Recordings.id << deleted_recordings).execute()
Path(recording.path).unlink(missing_ok=True)
Recordings.delete_by_id(recording.id)
logger.debug("End deleted cameras.") logger.debug("End deleted cameras.")
logger.debug("Start all cameras.") logger.debug("Start all cameras.")
@@ -180,65 +383,83 @@ class RecordingCleanup(threading.Thread):
datetime.datetime.now() datetime.datetime.now()
- datetime.timedelta(seconds=config.record.events.max_seconds) - datetime.timedelta(seconds=config.record.events.max_seconds)
).timestamp() ).timestamp()
expire_days = config.record.retain_days expire_days = config.record.retain.days
expire_before = ( expire_before = (
datetime.datetime.now() - datetime.timedelta(days=expire_days) datetime.datetime.now() - datetime.timedelta(days=expire_days)
).timestamp() ).timestamp()
expire_date = min(min_end, expire_before) expire_date = min(min_end, expire_before)
# Get recordings to remove # Get recordings to check for expiration
recordings: Recordings = Recordings.select().where( recordings: Recordings = (
Recordings.camera == camera, Recordings.select()
Recordings.end_time < expire_date, .where(
Recordings.camera == camera,
Recordings.end_time < expire_date,
)
.order_by(Recordings.start_time)
) )
for recording in recordings: # Get all the events to check against
# See if there are any associated events events: Event = (
events: Event = Event.select().where( Event.select()
Event.camera == recording.camera, .where(
( Event.camera == camera,
Event.start_time.between( # need to ensure segments for all events starting
recording.start_time, recording.end_time # before the expire date are included
) Event.start_time < expire_date,
| Event.end_time.between( Event.has_clip,
recording.start_time, recording.end_time
)
| (
(recording.start_time > Event.start_time)
& (recording.end_time < Event.end_time)
)
),
) )
.order_by(Event.start_time)
.objects()
)
# loop over recordings and see if they overlap with any non-expired events
# TODO: expire segments based on segment stats according to config
event_start = 0
deleted_recordings = set()
for recording in recordings.objects().iterator():
keep = False keep = False
event_ids = set() # Now look for a reason to keep this recording segment
for idx in range(event_start, len(events)):
event = events[idx]
event: Event # if the event starts in the future, stop checking events
for event in events: # and let this recording segment expire
event_ids.add(event.id) if event.start_time > recording.end_time:
# Check event/label retention and keep the recording if within window keep = False
expire_days_event = ( break
0
if not config.record.events.enabled # if the event is in progress or ends after the recording starts, keep it
else config.record.events.retain.objects.get( # and stop looking at events
event.label, config.record.events.retain.default if event.end_time is None or event.end_time >= recording.start_time:
)
)
expire_before_event = (
datetime.datetime.now()
- datetime.timedelta(days=expire_days_event)
).timestamp()
if recording.end_time >= expire_before_event:
keep = True keep = True
break
# Delete recordings outside of the retention window # if the event ends before this recording segment starts, skip
if not keep: # this event and check the next event for an overlap.
# since the events and recordings are sorted, we can skip events
# that end before the previous recording segment started on future segments
if event.end_time < recording.start_time:
event_start = idx
# Delete recordings outside of the retention window or based on the retention mode
if (
not keep
or (
config.record.events.retain.mode == RetainModeEnum.motion
and recording.motion == 0
)
or (
config.record.events.retain.mode
== RetainModeEnum.active_objects
and recording.objects == 0
)
):
Path(recording.path).unlink(missing_ok=True) Path(recording.path).unlink(missing_ok=True)
Recordings.delete_by_id(recording.id) deleted_recordings.add(recording.id)
if event_ids:
# Update associated events logger.debug(f"Expiring {len(deleted_recordings)} recordings")
Event.update(has_clip=False).where( Recordings.delete().where(Recordings.id << deleted_recordings).execute()
Event.id.in_(list(event_ids))
).execute()
logger.debug(f"End camera: {camera}.") logger.debug(f"End camera: {camera}.")
@@ -250,30 +471,32 @@ class RecordingCleanup(threading.Thread):
default_expire = ( default_expire = (
datetime.datetime.now().timestamp() datetime.datetime.now().timestamp()
- SECONDS_IN_DAY * self.config.record.retain_days - SECONDS_IN_DAY * self.config.record.retain.days
) )
delete_before = {} delete_before = {}
for name, camera in self.config.cameras.items(): for name, camera in self.config.cameras.items():
delete_before[name] = ( delete_before[name] = (
datetime.datetime.now().timestamp() datetime.datetime.now().timestamp()
- SECONDS_IN_DAY * camera.record.retain_days - SECONDS_IN_DAY * camera.record.retain.days
) )
# find all the recordings older than the oldest recording in the db # find all the recordings older than the oldest recording in the db
oldest_recording = ( try:
Recordings.select().order_by(Recordings.start_time.desc()).get() oldest_recording = Recordings.select().order_by(Recordings.start_time).get()
)
oldest_timestamp = ( p = Path(oldest_recording.path)
oldest_recording.start_time oldest_timestamp = p.stat().st_mtime - 1
if oldest_recording except DoesNotExist:
else datetime.datetime.now().timestamp() oldest_timestamp = datetime.datetime.now().timestamp()
) except FileNotFoundError:
logger.warning(f"Unable to find file from recordings database: {p}")
Recordings.delete().where(Recordings.id == oldest_recording.id).execute()
return
logger.debug(f"Oldest recording in the db: {oldest_timestamp}") logger.debug(f"Oldest recording in the db: {oldest_timestamp}")
process = sp.run( process = sp.run(
["find", RECORD_DIR, "-type", "f", "-newermt", f"@{oldest_timestamp}"], ["find", RECORD_DIR, "-type", "f", "!", "-newermt", f"@{oldest_timestamp}"],
capture_output=True, capture_output=True,
text=True, text=True,
) )
@@ -281,21 +504,52 @@ class RecordingCleanup(threading.Thread):
for f in files_to_check: for f in files_to_check:
p = Path(f) p = Path(f)
if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire): try:
p.unlink(missing_ok=True) if p.stat().st_mtime < delete_before.get(p.parent.name, default_expire):
p.unlink(missing_ok=True)
except FileNotFoundError:
logger.warning(f"Attempted to expire missing file: {f}")
logger.debug("End expire files (legacy).") logger.debug("End expire files (legacy).")
def sync_recordings(self):
logger.debug("Start sync recordings.")
# get all recordings in the db
recordings: Recordings = Recordings.select()
# get all recordings files on disk
process = sp.run(
["find", RECORD_DIR, "-type", "f"],
capture_output=True,
text=True,
)
files_on_disk = process.stdout.splitlines()
recordings_to_delete = []
for recording in recordings.objects().iterator():
if not recording.path in files_on_disk:
recordings_to_delete.append(recording.id)
logger.debug(
f"Deleting {len(recordings_to_delete)} recordings with missing files"
)
Recordings.delete().where(Recordings.id << recordings_to_delete).execute()
logger.debug("End sync recordings.")
def run(self): def run(self):
# Expire recordings every minute, clean directories every hour. # on startup sync recordings with disk (disabled due to too much CPU usage)
for counter in itertools.cycle(range(60)): # self.sync_recordings()
# Expire tmp clips every minute, recordings and clean directories every hour.
for counter in itertools.cycle(range(self.config.record.expire_interval)):
if self.stop_event.wait(60): if self.stop_event.wait(60):
logger.info(f"Exiting recording cleanup...") logger.info(f"Exiting recording cleanup...")
break break
self.expire_recordings()
self.clean_tmp_clips() self.clean_tmp_clips()
if counter == 0: if counter == 0:
self.expire_recordings()
self.expire_files() self.expire_files()
remove_empty_directories(RECORD_DIR) remove_empty_directories(RECORD_DIR)

View File

@@ -4,6 +4,7 @@ import threading
import time import time
import psutil import psutil
import shutil import shutil
import os
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
@@ -31,6 +32,28 @@ def get_fs_type(path):
return fsType return fsType
def read_temperature(path):
if os.path.isfile(path):
with open(path) as f:
line = f.readline().strip()
return int(line) / 1000
return None
def get_temperatures():
temps = {}
# Get temperatures for all attached Corals
base = "/sys/class/apex/"
if os.path.isdir(base):
for apex in os.listdir(base):
temp = read_temperature(os.path.join(base, apex, "temp"))
if temp is not None:
temps[apex] = temp
return temps
def stats_snapshot(stats_tracking): def stats_snapshot(stats_tracking):
camera_metrics = stats_tracking["camera_metrics"] camera_metrics = stats_tracking["camera_metrics"]
stats = {} stats = {}
@@ -61,6 +84,7 @@ def stats_snapshot(stats_tracking):
"uptime": (int(time.time()) - stats_tracking["started"]), "uptime": (int(time.time()) - stats_tracking["started"]),
"version": VERSION, "version": VERSION,
"storage": {}, "storage": {},
"temperatures": get_temperatures(),
} }
for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]: for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]:

View File

@@ -473,7 +473,7 @@ class TestConfig(unittest.TestCase):
"width": 1920, "width": 1920,
"fps": 5, "fps": 5,
}, },
"record": {"events": {"enabled": True}}, "record": {"events": {}},
} }
}, },
} }
@@ -572,7 +572,7 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].motion.frame_height >= 120 assert runtime_config.cameras["back"].motion.frame_height == 50
def test_motion_contour_area_dynamic(self): def test_motion_contour_area_dynamic(self):
@@ -601,7 +601,7 @@ class TestConfig(unittest.TestCase):
assert config == frigate_config.dict(exclude_unset=True) assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config runtime_config = frigate_config.runtime_config
assert round(runtime_config.cameras["back"].motion.contour_area) == 99 assert round(runtime_config.cameras["back"].motion.contour_area) == 30
def test_merge_labelmap(self): def test_merge_labelmap(self):
@@ -702,7 +702,11 @@ class TestConfig(unittest.TestCase):
"inputs": [ "inputs": [
{ {
"path": "rtsp://10.0.0.1:554/video", "path": "rtsp://10.0.0.1:554/video",
"roles": ["detect", "clips"], "roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["clips"],
}, },
] ]
}, },
@@ -717,6 +721,87 @@ class TestConfig(unittest.TestCase):
self.assertRaises(ValidationError, lambda: FrigateConfig(**config)) self.assertRaises(ValidationError, lambda: FrigateConfig(**config))
def test_fails_on_missing_role(self):
config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
"rtmp": {"enabled": True},
}
},
}
frigate_config = FrigateConfig(**config)
self.assertRaises(ValueError, lambda: frigate_config.runtime_config)
def test_works_on_missing_role_multiple_cams(self):
config = {
"mqtt": {"host": "mqtt"},
"rtmp": {"enabled": False},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
},
"cam2": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
"detect": {
"height": 1080,
"width": 1920,
"fps": 5,
},
},
},
}
frigate_config = FrigateConfig(**config)
runtime_config = frigate_config.runtime_config
def test_global_detect(self): def test_global_detect(self):
config = { config = {
@@ -884,6 +969,305 @@ class TestConfig(unittest.TestCase):
assert runtime_config.cameras["back"].snapshots.height == 150 assert runtime_config.cameras["back"].snapshots.height == 150
assert runtime_config.cameras["back"].snapshots.enabled 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_rtmp_default(self):
config = {
"mqtt": {"host": "mqtt"},
"rtmp": {"enabled": False},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
{
"path": "rtsp://10.0.0.1:554/video2",
"roles": ["record"],
},
]
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert not runtime_config.cameras["back"].rtmp.enabled
def test_global_live(self):
config = {
"mqtt": {"host": "mqtt"},
"live": {"quality": 4},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].live.quality == 4
def test_default_live(self):
config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
}
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].live.quality == 8
def test_global_live_merge(self):
config = {
"mqtt": {"host": "mqtt"},
"live": {"quality": 4, "height": 480},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
"live": {
"quality": 7,
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].live.quality == 7
assert runtime_config.cameras["back"].live.height == 480
def test_global_timestamp_style(self):
config = {
"mqtt": {"host": "mqtt"},
"timestamp_style": {"position": "bl"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
def test_default_timestamp_style(self):
config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
}
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].timestamp_style.position == "tl"
def test_global_timestamp_style_merge(self):
config = {
"mqtt": {"host": "mqtt"},
"rtmp": {"enabled": False},
"timestamp_style": {"position": "br", "thickness": 2},
"cameras": {
"back": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
"timestamp_style": {"position": "bl", "thickness": 4},
}
},
}
frigate_config = FrigateConfig(**config)
assert config == frigate_config.dict(exclude_unset=True)
runtime_config = frigate_config.runtime_config
assert runtime_config.cameras["back"].timestamp_style.position == "bl"
assert runtime_config.cameras["back"].timestamp_style.thickness == 4
def test_allow_retain_to_be_a_decimal(self):
config = {
"mqtt": {"host": "mqtt"},
"snapshots": {"retain": {"default": 1.5}},
"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.retain.default == 1.5
def test_fails_on_bad_camera_name(self):
config = {
"mqtt": {"host": "mqtt"},
"snapshots": {"retain": {"default": 1.5}},
"cameras": {
"back camer#": {
"ffmpeg": {
"inputs": [
{
"path": "rtsp://10.0.0.1:554/video",
"roles": ["detect"],
},
]
},
}
},
}
frigate_config = FrigateConfig(**config)
self.assertRaises(
ValidationError, lambda: frigate_config.runtime_config.cameras
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)

View File

@@ -0,0 +1,26 @@
import numpy as np
from unittest import TestCase, main
from frigate.video import box_overlaps, reduce_boxes
class TestBoxOverlaps(TestCase):
def test_overlap(self):
assert box_overlaps((100, 100, 200, 200), (50, 50, 150, 150))
def test_overlap_2(self):
assert box_overlaps((50, 50, 150, 150), (100, 100, 200, 200))
def test_no_overlap(self):
assert not box_overlaps((100, 100, 200, 200), (250, 250, 350, 350))
class TestReduceBoxes(TestCase):
def test_cluster(self):
clusters = reduce_boxes(
[(144, 290, 221, 459), (225, 178, 426, 341), (343, 105, 584, 250)]
)
assert len(clusters) == 2
if __name__ == "__main__":
main(verbosity=2)

View File

@@ -18,6 +18,7 @@ import cv2
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import os import os
import psutil
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,18 +52,32 @@ def draw_timestamp(
timestamp, timestamp,
timestamp_format, timestamp_format,
font_effect=None, font_effect=None,
font_scale=1.0,
font_thickness=2, font_thickness=2,
font_color=(255, 255, 255), font_color=(255, 255, 255),
position="tl", position="tl",
): ):
time_to_show = datetime.datetime.fromtimestamp(timestamp).strftime(timestamp_format) 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( size = cv2.getTextSize(
time_to_show, time_to_show,
cv2.FONT_HERSHEY_SIMPLEX, cv2.FONT_HERSHEY_SIMPLEX,
fontScale=font_scale, fontScale=font_scale,
thickness=font_thickness, thickness=font_thickness,
) )
image_width = frame.shape[1] image_width = frame.shape[1]
image_height = frame.shape[0] image_height = frame.shape[0]
text_width = size[0][0] text_width = size[0][0]
@@ -174,12 +189,12 @@ def draw_box_with_label(
) )
def calculate_region(frame_shape, xmin, ymin, xmax, ymax, multiplier=2): def calculate_region(frame_shape, xmin, ymin, xmax, ymax, model_size, multiplier=2):
# size is the longest edge and divisible by 4 # size is the longest edge and divisible by 4
size = int(max(xmax - xmin, ymax - ymin) // 4 * 4 * multiplier) size = int((max(xmax - xmin, ymax - ymin) * multiplier) // 4 * 4)
# dont go any smaller than 300 # dont go any smaller than the model_size
if size < 300: if size < model_size:
size = 300 size = model_size
# x_offset is midpoint of bounding box minus half the size # x_offset is midpoint of bounding box minus half the size
x_offset = int((xmax - xmin) / 2.0 + xmin - size / 2.0) x_offset = int((xmax - xmin) / 2.0 + xmin - size / 2.0)
@@ -520,7 +535,13 @@ def clipped(obj, frame_shape):
def restart_frigate(): def restart_frigate():
os.kill(os.getpid(), signal.SIGTERM) proc = psutil.Process(1)
# if this is running via s6, sigterm pid 1
if proc.name() == "s6-svscan":
proc.terminate()
# otherwise, just try and exit frigate
else:
os.kill(os.getpid(), signal.SIGTERM)
class EventsPerSecond: class EventsPerSecond:
@@ -546,6 +567,9 @@ class EventsPerSecond:
# compute the (approximate) events in the last n seconds # compute the (approximate) events in the last n seconds
now = datetime.datetime.now().timestamp() now = datetime.datetime.now().timestamp()
seconds = min(now - self._start, last_n_seconds) seconds = min(now - self._start, last_n_seconds)
# avoid divide by zero
if seconds == 0:
seconds = 1
return ( return (
len([t for t in self._timestamps if t > (now - last_n_seconds)]) / seconds len([t for t in self._timestamps if t > (now - last_n_seconds)]) / seconds
) )
@@ -581,6 +605,26 @@ def add_mask(mask, mask_img):
cv2.fillPoly(mask_img, pts=[contour], color=(0)) cv2.fillPoly(mask_img, pts=[contour], color=(0))
def load_labels(path, encoding="utf-8"):
"""Loads labels from file (with or without index numbers).
Args:
path: path to label file.
encoding: label file encoding.
Returns:
Dictionary mapping indices to labels.
"""
with open(path, "r", encoding=encoding) as f:
lines = f.readlines()
if not lines:
return {}
if lines[0].split(" ", maxsplit=1)[0].isdigit():
pairs = [line.split(" ", maxsplit=1) for line in lines]
return {int(index): label.strip() for index, label in pairs}
else:
return {index: line.strip() for index, line in enumerate(lines)}
class FrameManager(ABC): class FrameManager(ABC):
@abstractmethod @abstractmethod
def create(self, name, size) -> AnyStr: def create(self, name, size) -> AnyStr:

View File

@@ -3,18 +3,19 @@ import itertools
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import queue import queue
import subprocess as sp import random
import signal import signal
import subprocess as sp
import threading import threading
import time import time
from collections import defaultdict from collections import defaultdict
from setproctitle import setproctitle
from typing import Dict, List from typing import Dict, List
from cv2 import cv2
import numpy as np import numpy as np
from cv2 import cv2, reduce
from setproctitle import setproctitle
from frigate.config import CameraConfig from frigate.config import CameraConfig, DetectConfig
from frigate.edgetpu import RemoteObjectDetector from frigate.edgetpu import RemoteObjectDetector
from frigate.log import LogPipe from frigate.log import LogPipe
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
@@ -23,8 +24,11 @@ from frigate.util import (
EventsPerSecond, EventsPerSecond,
FrameManager, FrameManager,
SharedMemoryFrameManager, SharedMemoryFrameManager,
area,
calculate_region, calculate_region,
clipped, clipped,
intersection,
intersection_over_union,
listen, listen,
yuv_region_2_rgb, yuv_region_2_rgb,
) )
@@ -149,10 +153,10 @@ def capture_frames(
try: try:
frame_buffer[:] = ffmpeg_process.stdout.read(frame_size) frame_buffer[:] = ffmpeg_process.stdout.read(frame_size)
except Exception as e: except Exception as e:
logger.info(f"{camera_name}: ffmpeg sent a broken frame. {e}") logger.error(f"{camera_name}: Unable to read frames from ffmpeg process.")
if ffmpeg_process.poll() != None: if ffmpeg_process.poll() != None:
logger.info( logger.error(
f"{camera_name}: ffmpeg process is not running. exiting capture thread..." f"{camera_name}: ffmpeg process is not running. exiting capture thread..."
) )
frame_manager.delete(frame_name) frame_manager.delete(frame_name)
@@ -217,12 +221,11 @@ class CameraWatchdog(threading.Thread):
if not self.capture_thread.is_alive(): if not self.capture_thread.is_alive():
self.logger.error( self.logger.error(
f"FFMPEG process crashed unexpectedly for {self.camera_name}." f"Ffmpeg process crashed unexpectedly for {self.camera_name}."
) )
self.logger.error( self.logger.error(
"The following ffmpeg logs include the last 100 lines prior to exit." "The following ffmpeg logs include the last 100 lines prior to exit."
) )
self.logger.error("You may have invalid args defined for this camera.")
self.logpipe.dump() self.logpipe.dump()
self.start_ffmpeg_detect() self.start_ffmpeg_detect()
elif now - self.capture_thread.current_frame.value > 20: elif now - self.capture_thread.current_frame.value > 20:
@@ -364,6 +367,7 @@ def track_camera(
frame_queue, frame_queue,
frame_shape, frame_shape,
model_shape, model_shape,
config.detect,
frame_manager, frame_manager,
motion_detector, motion_detector,
object_detector, object_detector,
@@ -379,26 +383,36 @@ def track_camera(
logger.info(f"{name}: exiting subprocess") logger.info(f"{name}: exiting subprocess")
def reduce_boxes(boxes): def box_overlaps(b1, b2):
if len(boxes) == 0: if b1[2] < b2[0] or b1[0] > b2[2] or b1[1] > b2[3] or b1[3] < b2[1]:
return [] return False
reduced_boxes = cv2.groupRectangles( return True
[list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2
)[0]
return [tuple(b) for b in reduced_boxes] def reduce_boxes(boxes, iou_threshold=0.0):
clusters = []
for box in boxes:
matched = 0
for cluster in clusters:
if intersection_over_union(box, cluster) > iou_threshold:
matched = 1
cluster[0] = min(cluster[0], box[0])
cluster[1] = min(cluster[1], box[1])
cluster[2] = max(cluster[2], box[2])
cluster[3] = max(cluster[3], box[3])
if not matched:
clusters.append(list(box))
return [tuple(c) for c in clusters]
# modified from https://stackoverflow.com/a/40795835
def intersects_any(box_a, boxes): def intersects_any(box_a, boxes):
for box in boxes: for box in boxes:
if ( if box_overlaps(box_a, box):
box_a[2] < box[0] return True
or box_a[0] > box[2] return False
or box_a[1] > box[3]
or box_a[3] < box[1]
):
continue
return True
def detect( def detect(
@@ -434,6 +448,7 @@ def process_frames(
frame_queue: mp.Queue, frame_queue: mp.Queue,
frame_shape, frame_shape,
model_shape, model_shape,
detect_config: DetectConfig,
frame_manager: FrameManager, frame_manager: FrameManager,
motion_detector: MotionDetector, motion_detector: MotionDetector,
object_detector: RemoteObjectDetector, object_detector: RemoteObjectDetector,
@@ -454,6 +469,8 @@ def process_frames(
fps_tracker = EventsPerSecond() fps_tracker = EventsPerSecond()
fps_tracker.start() fps_tracker.start()
startup_scan_counter = 0
while not stop_event.is_set(): while not stop_event.is_set():
if exit_on_empty and frame_queue.empty(): if exit_on_empty and frame_queue.empty():
logger.info(f"Exiting track_objects...") logger.info(f"Exiting track_objects...")
@@ -474,122 +491,219 @@ def process_frames(
logger.info(f"{camera_name}: frame {frame_time} is not in memory store.") logger.info(f"{camera_name}: frame {frame_time} is not in memory store.")
continue continue
if not detection_enabled.value:
fps.value = fps_tracker.eps()
object_tracker.match_and_update(frame_time, [])
detected_objects_queue.put(
(camera_name, frame_time, object_tracker.tracked_objects, [], [])
)
detection_fps.value = object_detector.fps.eps()
frame_manager.close(f"{camera_name}{frame_time}")
continue
# look for motion # look for motion
motion_boxes = motion_detector.detect(frame) motion_boxes = motion_detector.detect(frame)
# only get the tracked object boxes that intersect with motion regions = []
tracked_object_boxes = [
obj["box"]
for obj in object_tracker.tracked_objects.values()
if intersects_any(obj["box"], motion_boxes)
]
# combine motion boxes with known locations of existing objects # if detection is disabled
combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes) if not detection_enabled.value:
object_tracker.match_and_update(frame_time, [])
# compute regions else:
regions = [ # get stationary object ids
calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.2) # check every Nth frame for stationary objects
for a in combined_boxes # disappeared objects are not stationary
] # also check for overlapping motion boxes
stationary_object_ids = [
# combine overlapping regions obj["id"]
combined_regions = reduce_boxes(regions) for obj in object_tracker.tracked_objects.values()
# if there hasn't been motion for 10 frames
# re-compute regions if obj["motionless_count"] >= 10
regions = [ # and it isn't due for a periodic check
calculate_region(frame_shape, a[0], a[1], a[2], a[3], 1.0) and (
for a in combined_regions detect_config.stationary.interval == 0
] or obj["motionless_count"] % detect_config.stationary.interval != 0
# resize regions and detect
detections = []
for region in regions:
detections.extend(
detect(
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
) )
) # and it hasn't disappeared
and object_tracker.disappeared[obj["id"]] == 0
# and it doesn't overlap with any current motion boxes
and not intersects_any(obj["box"], motion_boxes)
]
######### # get tracked object boxes that aren't stationary
# merge objects, check for clipped objects and look again up to 4 times tracked_object_boxes = [
######### obj["box"]
refining = True for obj in object_tracker.tracked_objects.values()
refine_count = 0 if not obj["id"] in stationary_object_ids
while refining and refine_count < 4: ]
refining = False
# group by name # combine motion boxes with known locations of existing objects
detected_object_groups = defaultdict(lambda: []) combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
for detection in detections:
detected_object_groups[detection[0]].append(detection)
selected_objects = [] region_min_size = max(model_shape[0], model_shape[1])
for group in detected_object_groups.values(): # compute regions
regions = [
calculate_region(
frame_shape,
a[0],
a[1],
a[2],
a[3],
region_min_size,
multiplier=random.uniform(1.2, 1.5),
)
for a in combined_boxes
]
# apply non-maxima suppression to suppress weak, overlapping bounding boxes # consolidate regions with heavy overlap
boxes = [ regions = [
(o[2][0], o[2][1], o[2][2] - o[2][0], o[2][3] - o[2][1]) calculate_region(
for o in group frame_shape, a[0], a[1], a[2], a[3], region_min_size, multiplier=1.0
] )
confidences = [o[1] for o in group] for a in reduce_boxes(regions, 0.4)
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4) ]
for index in idxs: # if starting up, get the next startup scan region
obj = group[index[0]] if startup_scan_counter < 9:
if clipped(obj, frame_shape): ymin = int(frame_shape[0] / 3 * startup_scan_counter / 3)
box = obj[2] ymax = int(frame_shape[0] / 3 + ymin)
# calculate a new region that will hopefully get the entire object xmin = int(frame_shape[1] / 3 * startup_scan_counter / 3)
region = calculate_region( xmax = int(frame_shape[1] / 3 + xmin)
frame_shape, box[0], box[1], box[2], box[3] regions.append(
) calculate_region(
frame_shape,
xmin,
ymin,
xmax,
ymax,
region_min_size,
multiplier=1.2,
)
)
startup_scan_counter += 1
regions.append(region) # resize regions and detect
# seed with stationary objects
detections = [
(
obj["label"],
obj["score"],
obj["box"],
obj["area"],
obj["region"],
)
for obj in object_tracker.tracked_objects.values()
if obj["id"] in stationary_object_ids
]
selected_objects.extend( for region in regions:
detect( detections.extend(
object_detector, detect(
frame, object_detector,
model_shape, frame,
region, model_shape,
objects_to_track, region,
object_filters, objects_to_track,
object_filters,
)
)
#########
# merge objects, check for clipped objects and look again up to 4 times
#########
refining = len(regions) > 0
refine_count = 0
while refining and refine_count < 4:
refining = False
# group by name
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
detected_object_groups[detection[0]].append(detection)
selected_objects = []
for group in detected_object_groups.values():
# apply non-maxima suppression to suppress weak, overlapping bounding boxes
boxes = [
(o[2][0], o[2][1], o[2][2] - o[2][0], o[2][3] - o[2][1])
for o in group
]
confidences = [o[1] for o in group]
idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
for index in idxs:
obj = group[index[0]]
if clipped(obj, frame_shape):
box = obj[2]
# calculate a new region that will hopefully get the entire object
region = calculate_region(
frame_shape,
box[0],
box[1],
box[2],
box[3],
region_min_size,
) )
)
refining = True regions.append(region)
else:
selected_objects.append(obj)
# set the detections list to only include top, complete objects
# and new detections
detections = selected_objects
if refining: selected_objects.extend(
refine_count += 1 detect(
object_detector,
frame,
model_shape,
region,
objects_to_track,
object_filters,
)
)
# Limit to the detections overlapping with motion areas refining = True
# to avoid picking up stationary background objects else:
detections_with_motion = [ selected_objects.append(obj)
d for d in detections if intersects_any(d[2], motion_boxes) # set the detections list to only include top, complete objects
] # and new detections
detections = selected_objects
# now that we have refined our detections, we need to track objects if refining:
object_tracker.match_and_update(frame_time, detections_with_motion) refine_count += 1
## drop detections that overlap too much
consolidated_detections = []
# if detection was run on this frame, consolidate
if len(regions) > 0:
# group by name
detected_object_groups = defaultdict(lambda: [])
for detection in detections:
detected_object_groups[detection[0]].append(detection)
# loop over detections grouped by label
for group in detected_object_groups.values():
# if the group only has 1 item, skip
if len(group) == 1:
consolidated_detections.append(group[0])
continue
# sort smallest to largest by area
sorted_by_area = sorted(group, key=lambda g: g[3])
for current_detection_idx in range(0, len(sorted_by_area)):
current_detection = sorted_by_area[current_detection_idx][2]
overlap = 0
for to_check_idx in range(
min(current_detection_idx + 1, len(sorted_by_area)),
len(sorted_by_area),
):
to_check = sorted_by_area[to_check_idx][2]
# if 90% of smaller detection is inside of another detection, consolidate
if (
area(intersection(current_detection, to_check))
/ area(current_detection)
> 0.9
):
overlap = 1
break
if overlap == 0:
consolidated_detections.append(
sorted_by_area[current_detection_idx]
)
# now that we have refined our detections, we need to track objects
object_tracker.match_and_update(frame_time, consolidated_detections)
# else, just update the frame times for the stationary objects
else:
object_tracker.update_frame_times(frame_time)
# add to the queue if not full # add to the queue if not full
if detected_objects_queue.full(): if detected_objects_queue.full():

View File

@@ -5,6 +5,10 @@ import time
import os import os
import signal import signal
from frigate.util import (
restart_frigate,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,6 +34,6 @@ class FrigateWatchdog(threading.Thread):
detector.start_or_restart() detector.start_or_restart()
elif not detector.detect_process.is_alive(): elif not detector.detect_process.is_alive():
logger.info("Detection appears to have stopped. Exiting frigate...") logger.info("Detection appears to have stopped. Exiting frigate...")
os.kill(os.getpid(), signal.SIGTERM) restart_frigate()
logger.info(f"Exiting watchdog...") logger.info(f"Exiting watchdog...")

View File

@@ -28,17 +28,19 @@ SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs): def migrate(migrator, database, fake=False, **kwargs):
migrator.create_model(Recordings) migrator.sql(
'CREATE TABLE IF NOT EXISTS "recordings" ("id" VARCHAR(30) NOT NULL PRIMARY KEY, "camera" VARCHAR(20) NOT NULL, "path" VARCHAR(255) NOT NULL, "start_time" DATETIME NOT NULL, "end_time" DATETIME NOT NULL, "duration" REAL NOT NULL)'
def add_index(): )
# First add the index here, because there is a bug in peewee_migrate migrator.sql(
# when trying to create an multi-column index in the same migration 'CREATE INDEX IF NOT EXISTS "recordings_camera" ON "recordings" ("camera")'
# as the table: https://github.com/klen/peewee_migrate/issues/19 )
Recordings.add_index("start_time", "end_time") migrator.sql(
Recordings.create_table() 'CREATE UNIQUE INDEX IF NOT EXISTS "recordings_path" ON "recordings" ("path")'
)
migrator.python(add_index) migrator.sql(
'CREATE INDEX IF NOT EXISTS "recordings_start_time_end_time" ON "recordings" (start_time, end_time)'
)
def rollback(migrator, database, fake=False, **kwargs): def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_model(Recordings) pass

View File

@@ -0,0 +1,48 @@
"""Peewee migrations -- 004_add_bbox_region_area.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Event,
region=JSONField(default=[]),
box=JSONField(default=[]),
area=pw.IntegerField(default=0),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Event, ["region", "box", "area"])

View File

@@ -0,0 +1,43 @@
"""Peewee migrations -- 004_add_bbox_region_area.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Event
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.drop_not_null(Event, "end_time")
def rollback(migrator, database, fake=False, **kwargs):
pass

View File

@@ -0,0 +1,47 @@
"""Peewee migrations -- 004_add_bbox_region_area.py.
Some examples (model - class or model name)::
> Model = migrator.orm['model_name'] # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.python(func, *args, **kwargs) # Run python code
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.drop_index(model, *col_names)
> migrator.add_not_null(model, *field_names)
> migrator.drop_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
"""
import datetime as dt
import peewee as pw
from playhouse.sqlite_ext import *
from decimal import ROUND_HALF_EVEN
from frigate.models import Recordings
try:
import playhouse.postgres_ext as pw_pext
except ImportError:
pass
SQL = pw.SQL
def migrate(migrator, database, fake=False, **kwargs):
migrator.add_fields(
Recordings,
objects=pw.IntegerField(null=True),
motion=pw.IntegerField(null=True),
)
def rollback(migrator, database, fake=False, **kwargs):
migrator.remove_fields(Recordings, ["objects", "motion"])

View File

@@ -1,23 +1,26 @@
import datetime import sys
from typing_extensions import runtime
sys.path.append("/lab/frigate")
import json import json
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os import os
import subprocess as sp import subprocess as sp
import sys import sys
from unittest import TestCase, main
import click import click
import csv
import cv2 import cv2
import numpy as np import numpy as np
from frigate.config import FRIGATE_CONFIG_SCHEMA, FrigateConfig from frigate.config import FrigateConfig
from frigate.edgetpu import LocalObjectDetector from frigate.edgetpu import LocalObjectDetector
from frigate.motion import MotionDetector from frigate.motion import MotionDetector
from frigate.object_processing import CameraState from frigate.object_processing import CameraState
from frigate.objects import ObjectTracker from frigate.objects import ObjectTracker
from frigate.util import ( from frigate.util import (
DictFrameManager,
EventsPerSecond, EventsPerSecond,
SharedMemoryFrameManager, SharedMemoryFrameManager,
draw_box_with_label, draw_box_with_label,
@@ -96,20 +99,22 @@ class ProcessClip:
ffmpeg_process.wait() ffmpeg_process.wait()
ffmpeg_process.communicate() ffmpeg_process.communicate()
def process_frames(self, objects_to_track=["person"], object_filters={}): def process_frames(
self, object_detector, objects_to_track=["person"], object_filters={}
):
mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8) mask = np.zeros((self.frame_shape[0], self.frame_shape[1], 1), np.uint8)
mask[:] = 255 mask[:] = 255
motion_detector = MotionDetector( motion_detector = MotionDetector(self.frame_shape, self.camera_config.motion)
self.frame_shape, mask, self.camera_config.motion motion_detector.save_images = False
)
object_detector = LocalObjectDetector(labels="/labelmap.txt")
object_tracker = ObjectTracker(self.camera_config.detect) object_tracker = ObjectTracker(self.camera_config.detect)
process_info = { process_info = {
"process_fps": mp.Value("d", 0.0), "process_fps": mp.Value("d", 0.0),
"detection_fps": mp.Value("d", 0.0), "detection_fps": mp.Value("d", 0.0),
"detection_frame": mp.Value("d", 0.0), "detection_frame": mp.Value("d", 0.0),
} }
detection_enabled = mp.Value("d", 1)
stop_event = mp.Event() stop_event = mp.Event()
model_shape = (self.config.model.height, self.config.model.width) model_shape = (self.config.model.height, self.config.model.width)
@@ -118,6 +123,7 @@ class ProcessClip:
self.frame_queue, self.frame_queue,
self.frame_shape, self.frame_shape,
model_shape, model_shape,
self.camera_config.detect,
self.frame_manager, self.frame_manager,
motion_detector, motion_detector,
object_detector, object_detector,
@@ -126,25 +132,16 @@ class ProcessClip:
process_info, process_info,
objects_to_track, objects_to_track,
object_filters, object_filters,
mask, detection_enabled,
stop_event, stop_event,
exit_on_empty=True, exit_on_empty=True,
) )
def top_object(self, debug_path=None): def stats(self, debug_path=None):
obj_detected = False total_regions = 0
top_computed_score = 0.0 total_motion_boxes = 0
object_ids = set()
def handle_event(name, obj, frame_time): total_frames = 0
nonlocal obj_detected
nonlocal top_computed_score
if obj.computed_score > top_computed_score:
top_computed_score = obj.computed_score
if not obj.false_positive:
obj_detected = True
self.camera_state.on("new", handle_event)
self.camera_state.on("update", handle_event)
while not self.detected_objects_queue.empty(): while not self.detected_objects_queue.empty():
( (
@@ -154,7 +151,8 @@ class ProcessClip:
motion_boxes, motion_boxes,
regions, regions,
) = self.detected_objects_queue.get() ) = self.detected_objects_queue.get()
if not debug_path is None:
if debug_path:
self.save_debug_frame( self.save_debug_frame(
debug_path, frame_time, current_tracked_objects.values() debug_path, frame_time, current_tracked_objects.values()
) )
@@ -162,10 +160,26 @@ class ProcessClip:
self.camera_state.update( self.camera_state.update(
frame_time, current_tracked_objects, motion_boxes, regions frame_time, current_tracked_objects, motion_boxes, regions
) )
total_regions += len(regions)
total_motion_boxes += len(motion_boxes)
top_score = 0
for id, obj in self.camera_state.tracked_objects.items():
if not obj.false_positive:
object_ids.add(id)
if obj.top_score > top_score:
top_score = obj.top_score
self.frame_manager.delete(self.camera_state.previous_frame_id) total_frames += 1
return {"object_detected": obj_detected, "top_score": top_computed_score} self.frame_manager.delete(self.camera_state.previous_frame_id)
return {
"total_regions": total_regions,
"total_motion_boxes": total_motion_boxes,
"true_positive_objects": len(object_ids),
"total_frames": total_frames,
"top_score": top_score,
}
def save_debug_frame(self, debug_path, frame_time, tracked_objects): def save_debug_frame(self, debug_path, frame_time, tracked_objects):
current_frame = cv2.cvtColor( current_frame = cv2.cvtColor(
@@ -178,7 +192,6 @@ class ProcessClip:
for obj in tracked_objects: for obj in tracked_objects:
thickness = 2 thickness = 2
color = (0, 0, 175) color = (0, 0, 175)
if obj["frame_time"] != frame_time: if obj["frame_time"] != frame_time:
thickness = 1 thickness = 1
color = (255, 0, 0) color = (255, 0, 0)
@@ -221,10 +234,9 @@ class ProcessClip:
@click.command() @click.command()
@click.option("-p", "--path", required=True, help="Path to clip or directory to test.") @click.option("-p", "--path", required=True, help="Path to clip or directory to test.")
@click.option("-l", "--label", default="person", help="Label name to detect.") @click.option("-l", "--label", default="person", help="Label name to detect.")
@click.option("-t", "--threshold", default=0.85, help="Threshold value for objects.") @click.option("-o", "--output", default=None, help="File to save csv of data")
@click.option("-s", "--scores", default=None, help="File to save csv of top scores")
@click.option("--debug-path", default=None, help="Path to output frames for debugging.") @click.option("--debug-path", default=None, help="Path to output frames for debugging.")
def process(path, label, threshold, scores, debug_path): def process(path, label, output, debug_path):
clips = [] clips = []
if os.path.isdir(path): if os.path.isdir(path):
files = os.listdir(path) files = os.listdir(path)
@@ -235,51 +247,79 @@ def process(path, label, threshold, scores, debug_path):
json_config = { json_config = {
"mqtt": {"host": "mqtt"}, "mqtt": {"host": "mqtt"},
"detectors": {"coral": {"type": "edgetpu", "device": "usb"}},
"cameras": { "cameras": {
"camera": { "camera": {
"ffmpeg": { "ffmpeg": {
"inputs": [ "inputs": [
{ {
"path": "path.mp4", "path": "path.mp4",
"global_args": "", "global_args": "-hide_banner",
"input_args": "", "input_args": "-loglevel info",
"roles": ["detect"], "roles": ["detect"],
} }
] ]
}, },
"height": 1920, "rtmp": {"enabled": False},
"width": 1080, "record": {"enabled": False},
} }
}, },
} }
object_detector = LocalObjectDetector(labels="/labelmap.txt")
results = [] results = []
for c in clips: for c in clips:
logger.info(c) logger.info(c)
frame_shape = get_frame_shape(c) frame_shape = get_frame_shape(c)
json_config["cameras"]["camera"]["height"] = frame_shape[0] json_config["cameras"]["camera"]["detect"] = {
json_config["cameras"]["camera"]["width"] = frame_shape[1] "height": frame_shape[0],
"width": frame_shape[1],
}
json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c json_config["cameras"]["camera"]["ffmpeg"]["inputs"][0]["path"] = c
config = FrigateConfig(config=FRIGATE_CONFIG_SCHEMA(json_config)) frigate_config = FrigateConfig(**json_config)
runtime_config = frigate_config.runtime_config
runtime_config.cameras["camera"].create_ffmpeg_cmds()
process_clip = ProcessClip(c, frame_shape, config) process_clip = ProcessClip(c, frame_shape, runtime_config)
process_clip.load_frames() process_clip.load_frames()
process_clip.process_frames(objects_to_track=[label]) process_clip.process_frames(object_detector, objects_to_track=[label])
results.append((c, process_clip.top_object(debug_path))) results.append((c, process_clip.stats(debug_path)))
if not scores is None: positive_count = sum(
with open(scores, "w") as writer: 1 for result in results if result[1]["true_positive_objects"] > 0
for result in results: )
writer.write(f"{result[0]},{result[1]['top_score']}\n")
positive_count = sum(1 for result in results if result[1]["object_detected"])
print( print(
f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s)." f"Objects were detected in {positive_count}/{len(results)}({positive_count/len(results)*100:.2f}%) clip(s)."
) )
if output:
# now we will open a file for writing
data_file = open(output, "w")
# create the csv writer object
csv_writer = csv.writer(data_file)
# Counter variable used for writing
# headers to the CSV file
count = 0
for result in results:
if count == 0:
# Writing headers of CSV file
header = ["file"] + list(result[1].keys())
csv_writer.writerow(header)
count += 1
# Writing data of CSV file
csv_writer.writerow([result[0]] + list(result[1].values()))
data_file.close()
if __name__ == "__main__": if __name__ == "__main__":
process() process()

15641
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,11 +14,11 @@
"@cycjimmy/jsmpeg-player": "^5.0.1", "@cycjimmy/jsmpeg-player": "^5.0.1",
"date-fns": "^2.21.3", "date-fns": "^2.21.3",
"idb-keyval": "^5.0.2", "idb-keyval": "^5.0.2",
"immer": "^8.0.1", "immer": "^9.0.6",
"preact": "^10.5.9", "preact": "^10.5.9",
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^3.2.1", "preact-router": "^3.2.1",
"video.js": "^7.13.0", "video.js": "^7.15.4",
"videojs-playlist": "^4.3.1", "videojs-playlist": "^4.3.1",
"videojs-seek-buttons": "^2.0.1" "videojs-seek-buttons": "^2.0.1"
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -11,7 +11,8 @@
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3b82f7" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3b82f7" />
<meta name="msapplication-TileColor" content="#3b82f7" /> <meta name="msapplication-TileColor" content="#3b82f7" />
<meta name="theme-color" content="#ff0000" /> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#111827" media="(prefers-color-scheme: dark)" />
</head> </head>
<body> <body>
<div id="root" class="z-0"></div> <div id="root" class="z-0"></div>

View File

@@ -13,7 +13,7 @@
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ff0000", "theme_color": "#ffffff",
"background_color": "#ff0000", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }

View File

@@ -10,6 +10,7 @@ import NavigationDrawer, { Destination, Separator } from './components/Navigatio
export default function Sidebar() { export default function Sidebar() {
const { data: config } = useConfig(); const { data: config } = useConfig();
const cameras = useMemo(() => Object.entries(config.cameras), [config]); const cameras = useMemo(() => Object.entries(config.cameras), [config]);
const { birdseye } = config;
return ( return (
<NavigationDrawer header={<Header />}> <NavigationDrawer header={<Header />}>
@@ -49,7 +50,7 @@ export default function Sidebar() {
) : null ) : null
} }
</Match> </Match>
<Destination href="/birdseye" text="Birdseye" /> {birdseye?.enabled ? <Destination href="/birdseye" text="Birdseye" /> : null}
<Destination href="/events" text="Events" /> <Destination href="/events" text="Events" />
<Destination href="/debug" text="Debug" /> <Destination href="/debug" text="Debug" />
<Separator /> <Separator />
@@ -60,7 +61,7 @@ export default function Sidebar() {
<Separator /> <Separator />
</Fragment> </Fragment>
) : null} ) : null}
<Destination className="self-end" href="https://blakeblackshear.github.io/frigate" text="Documentation" /> <Destination className="self-end" href="https://docs.frigate.video" text="Documentation" />
<Destination className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" /> <Destination className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
</NavigationDrawer> </NavigationDrawer>
); );

View File

@@ -121,12 +121,12 @@ describe('MqttProvider', () => {
</MqttProvider> </MqttProvider>
); );
await screen.findByTestId('data'); await screen.findByTestId('data');
expect(screen.getByTestId('front/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON"}'); expect(screen.getByTestId('front/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON","retain":true}');
expect(screen.getByTestId('front/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('front/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON"}'); expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON","retain":true}');
expect(screen.getByTestId('side/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('side/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
expect(screen.getByTestId('side/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('side/recordings/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}'); expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF","retain":true}');
}); });
}); });

View File

@@ -42,9 +42,9 @@ export function MqttProvider({
useEffect(() => { useEffect(() => {
Object.keys(config.cameras).forEach((camera) => { Object.keys(config.cameras).forEach((camera) => {
const { name, record, detect, snapshots } = config.cameras[camera]; const { name, record, detect, snapshots } = config.cameras[camera];
dispatch({ topic: `${name}/recordings/state`, payload: record.enabled ? 'ON' : 'OFF' }); dispatch({ topic: `${name}/recordings/state`, payload: record.enabled ? 'ON' : 'OFF', retain: true });
dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF' }); dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF', retain: true });
dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF' }); dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF', retain: true });
}); });
}, [config]); }, [config]);

View File

@@ -37,7 +37,8 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
return ( return (
<div <div
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 ${ 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' !show ? '-translate-y-full' : 'translate-y-0'
} ${!atZero ? 'shadow-sm' : ''}`} } ${!atZero ? 'shadow-sm' : ''}`}
data-testid="appbar" data-testid="appbar"

View File

@@ -0,0 +1,329 @@
import { h } from 'preact';
import { useEffect, useState, useCallback, useMemo, useRef } from 'preact/hooks';
import ArrowRight from '../icons/ArrowRight';
import ArrowRightDouble from '../icons/ArrowRightDouble';
const todayTimestamp = new Date().setHours(0, 0, 0, 0).valueOf();
const Calender = ({ onChange, calenderRef, close }) => {
const keyRef = useRef([]);
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth();
const daysMap = useMemo(() => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], []);
const monthMap = useMemo(
() => [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
],
[]
);
const [state, setState] = useState({
getMonthDetails: [],
year,
month,
selectedDay: null,
timeRange: { before: null, after: null },
monthDetails: null,
});
const getNumberOfDays = useCallback((year, month) => {
return 40 - new Date(year, month, 40).getDate();
}, []);
const getDayDetails = useCallback(
(args) => {
const date = args.index - args.firstDay;
const day = args.index % 7;
let prevMonth = args.month - 1;
let prevYear = args.year;
if (prevMonth < 0) {
prevMonth = 11;
prevYear--;
}
const prevMonthNumberOfDays = getNumberOfDays(prevYear, prevMonth);
const _date = (date < 0 ? prevMonthNumberOfDays + date : date % args.numberOfDays) + 1;
const month = date < 0 ? -1 : date >= args.numberOfDays ? 1 : 0;
const timestamp = new Date(args.year, args.month, _date).getTime();
return {
date: _date,
day,
month,
timestamp,
dayString: daysMap[day],
};
},
[getNumberOfDays, daysMap]
);
const getMonthDetails = useCallback(
(year, month) => {
const firstDay = new Date(year, month).getDay();
const numberOfDays = getNumberOfDays(year, month);
const monthArray = [];
const rows = 6;
let currentDay = null;
let index = 0;
const cols = 7;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
currentDay = getDayDetails({
index,
numberOfDays,
firstDay,
year,
month,
});
monthArray.push(currentDay);
index++;
}
}
return monthArray;
},
[getNumberOfDays, getDayDetails]
);
useEffect(() => {
setState((prev) => ({ ...prev, selectedDay: todayTimestamp, monthDetails: getMonthDetails(year, month) }));
}, [year, month, getMonthDetails]);
useEffect(() => {
// add refs for keyboard navigation
if (state.monthDetails) {
keyRef.current = keyRef.current.slice(0, state.monthDetails.length);
}
// set today date in focus for keyboard navigation
const todayDate = new Date(todayTimestamp).getDate();
keyRef.current.find((t) => t.tabIndex === todayDate)?.focus();
}, [state.monthDetails]);
const isCurrentDay = (day) => day.timestamp === todayTimestamp;
const isSelectedRange = useCallback(
(day) => {
if (!state.timeRange.after || !state.timeRange.before) return;
return day.timestamp < state.timeRange.before && day.timestamp >= state.timeRange.after;
},
[state.timeRange]
);
const isFirstDayInRange = useCallback(
(day) => {
if (isCurrentDay(day)) return;
return state.timeRange.after === day.timestamp;
},
[state.timeRange.after]
);
const isLastDayInRange = useCallback(
(day) => {
return state.timeRange.before === new Date(day.timestamp).setHours(24, 0, 0, 0);
},
[state.timeRange.before]
);
const getMonthStr = useCallback(
(month) => {
return monthMap[Math.max(Math.min(11, month), 0)] || 'Month';
},
[monthMap]
);
const onDateClick = (day) => {
const { before, after } = state.timeRange;
let timeRange = { before: null, after: null };
// user has selected a date < after, reset values
if (after === null || day.timestamp < after) {
timeRange = { before: new Date(day.timestamp).setHours(24, 0, 0, 0), after: day.timestamp };
}
// user has selected a date > after
if (after !== null && before !== new Date(day.timestamp).setHours(24, 0, 0, 0) && day.timestamp > after) {
timeRange = {
after,
before:
day.timestamp >= todayTimestamp
? new Date(todayTimestamp).setHours(24, 0, 0, 0)
: new Date(day.timestamp).setHours(24, 0, 0, 0),
};
}
// reset values
if (before === new Date(day.timestamp).setHours(24, 0, 0, 0)) {
timeRange = { before: null, after: null };
}
setState((prev) => ({
...prev,
timeRange,
selectedDay: day.timestamp,
}));
if (onChange) {
onChange(timeRange.after ? { before: timeRange.before / 1000, after: timeRange.after / 1000 } : ['all']);
}
};
const setYear = useCallback(
(offset) => {
const year = state.year + offset;
const month = state.month;
setState((prev) => {
return {
...prev,
year,
monthDetails: getMonthDetails(year, month),
};
});
},
[state.year, state.month, getMonthDetails]
);
const setMonth = (offset) => {
let year = state.year;
let month = state.month + offset;
if (month === -1) {
month = 11;
year--;
} else if (month === 12) {
month = 0;
year++;
}
setState((prev) => {
return {
...prev,
year,
month,
monthDetails: getMonthDetails(year, month),
};
});
};
const handleKeydown = (e, day, index) => {
if ((keyRef.current && e.key === 'Enter') || e.keyCode === 32) {
e.preventDefault();
day.month === 0 && onDateClick(day);
}
if (e.key === 'ArrowLeft') {
index > 0 && keyRef.current[index - 1].focus();
}
if (e.key === 'ArrowRight') {
index < 41 && keyRef.current[index + 1].focus();
}
if (e.key === 'ArrowUp') {
e.preventDefault();
index > 6 && keyRef.current[index - 7].focus();
}
if (e.key === 'ArrowDown') {
e.preventDefault();
index < 36 && keyRef.current[index + 7].focus();
}
if (e.key === 'Escape') {
close();
}
};
const renderCalendar = () => {
const days =
state.monthDetails &&
state.monthDetails.map((day, idx) => {
return (
<div
onClick={() => onDateClick(day)}
onkeydown={(e) => handleKeydown(e, day, idx)}
ref={(ref) => (keyRef.current[idx] = ref)}
tabIndex={day.month === 0 ? day.date : null}
className={`h-12 w-12 float-left flex flex-shrink justify-center items-center cursor-pointer ${
day.month !== 0 ? ' opacity-50 bg-gray-700 dark:bg-gray-700 pointer-events-none' : ''
}
${isFirstDayInRange(day) ? ' rounded-l-xl ' : ''}
${isSelectedRange(day) ? ' bg-blue-600 dark:hover:bg-blue-600' : ''}
${isLastDayInRange(day) ? ' rounded-r-xl ' : ''}
${isCurrentDay(day) && !isLastDayInRange(day) ? 'rounded-full bg-gray-100 dark:hover:bg-gray-100 ' : ''}`}
key={idx}
>
<div className="font-light">
<span className="text-gray-400">{day.date}</span>
</div>
</div>
);
});
return (
<div>
<div className="w-full flex justify-start flex-shrink">
{['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map((d, i) => (
<div key={i} className="w-12 text-xs font-light text-center">
{d}
</div>
))}
</div>
<div className="w-full h-56">{days}</div>
</div>
);
};
return (
<div className="select-none w-96 flex flex-shrink" ref={calenderRef}>
<div className="py-4 px-6">
<div className="flex items-center">
<div className="w-1/6 relative flex justify-around">
<div
tabIndex={100}
className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
onClick={() => setYear(-1)}
>
<ArrowRightDouble className="h-2/6 transform rotate-180 " />
</div>
</div>
<div className="w-1/6 relative flex justify-around ">
<div
tabIndex={101}
className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
onClick={() => setMonth(-1)}
>
<ArrowRight className="h-2/6 transform rotate-180 red" />
</div>
</div>
<div className="w-1/3">
<div className="text-3xl text-center text-gray-200 font-extralight">{state.year}</div>
<div className="text-center text-gray-400 font-extralight">{getMonthStr(state.month)}</div>
</div>
<div className="w-1/6 relative flex justify-around ">
<div
tabIndex={102}
className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800"
onClick={() => setMonth(1)}
>
<ArrowRight className="h-2/6" />
</div>
</div>
<div className="w-1/6 relative flex justify-around " tabIndex={104} onClick={() => setYear(1)}>
<div className="flex justify-center items-center cursor-pointer absolute -mt-4 text-center rounded-full w-10 h-10 bg-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800">
<ArrowRightDouble className="h-2/6" />
</div>
</div>
</div>
<div className="mt-3">{renderCalendar()}</div>
</div>
</div>
);
};
export default Calender;

View File

@@ -0,0 +1,162 @@
import { h } from 'preact';
import { useCallback, useEffect, useState } from 'preact/hooks';
export const DateFilterOptions = [
{
label: 'All',
value: ['all'],
},
{
label: 'Today',
value: {
//Before
before: new Date().setHours(24, 0, 0, 0) / 1000,
//After
after: new Date().setHours(0, 0, 0, 0) / 1000,
},
},
{
label: 'Yesterday',
value: {
//Before
before: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(24, 0, 0, 0) / 1000,
//After
after: new Date(new Date().setDate(new Date().getDate() - 1)).setHours(0, 0, 0, 0) / 1000,
},
},
{
label: 'Last 7 Days',
value: {
//Before
before: new Date().setHours(24, 0, 0, 0) / 1000,
//After
after: new Date(new Date().setDate(new Date().getDate() - 7)).setHours(0, 0, 0, 0) / 1000,
},
},
{
label: 'This Month',
value: {
//Before
before: new Date().setHours(24, 0, 0, 0) / 1000,
//After
after: new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000,
},
},
{
label: 'Last Month',
value: {
//Before
before: new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime() / 1000,
//After
after: new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1).getTime() / 1000,
},
},
{
label: 'Custom Range',
value: 'custom_range',
},
];
export default function DatePicker({
helpText,
keyboardType = 'text',
inputRef,
label,
leadingIcon: LeadingIcon,
onBlur,
onChangeText,
onFocus,
readonly,
trailingIcon: TrailingIcon,
value: propValue = '',
...props
}) {
const [isFocused, setFocused] = useState(false);
const [value, setValue] = useState(propValue);
useEffect(() => {
if (propValue !== value) {
setValue(propValue);
}
}, [propValue, setValue, value]);
const handleFocus = useCallback(
(event) => {
setFocused(true);
onFocus && onFocus(event);
},
[onFocus]
);
const handleBlur = useCallback(
(event) => {
setFocused(false);
onBlur && onBlur(event);
},
[onBlur]
);
const handleChange = useCallback(
(event) => {
const { value } = event.target;
setValue(value);
onChangeText && onChangeText(value);
},
[onChangeText, setValue]
);
const onClick = (e) => {
props.onclick(e);
};
const labelMoved = isFocused || value !== '';
return (
<div className="w-full">
{props.children}
<div
className={`bg-gray-100 dark:bg-gray-700 rounded rounded-b-none border-gray-400 border-b p-1 pl-4 pr-3 ${
isFocused ? 'border-blue-500 dark:border-blue-500' : ''
}`}
ref={inputRef}
>
<label
className="flex space-x-2 items-center"
data-testid={`label-${label.toLowerCase().replace(/[^\w]+/g, '_')}`}
>
{LeadingIcon ? (
<div className="w-10 h-full">
<LeadingIcon />
</div>
) : null}
<div className="relative w-full">
<input
className="h-6 mt-6 w-full bg-transparent focus:outline-none focus:ring-0"
type={keyboardType}
readOnly
onBlur={handleBlur}
onFocus={handleFocus}
onInput={handleChange}
tabIndex="0"
onClick={onClick}
value={propValue}
{...props}
/>
<div
className={`absolute top-3 transition transform text-gray-600 dark:text-gray-400 ${
labelMoved ? 'text-xs -translate-y-2' : ''
} ${isFocused ? 'text-blue-500 dark:text-blue-500' : ''}`}
>
<p>{label}</p>
</div>
</div>
{TrailingIcon ? (
<div className="w-10 h-10">
<TrailingIcon />
</div>
) : null}
</label>
</div>
{helpText ? <div className="text-xs pl-3 pt-1">{helpText}</div> : null}
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { baseUrl } from '../api/baseUrl';
import { useRef, useEffect } from 'preact/hooks'; import { useRef, useEffect } from 'preact/hooks';
import JSMpeg from '@cycjimmy/jsmpeg-player'; import JSMpeg from '@cycjimmy/jsmpeg-player';
export default function JSMpegPlayer({ camera }) { export default function JSMpegPlayer({ camera, width, height }) {
const playerRef = useRef(); const playerRef = useRef();
const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}` const url = `${baseUrl.replace(/^http/, 'ws')}/live/${camera}`
@@ -32,6 +32,6 @@ export default function JSMpegPlayer({ camera }) {
}, [url]); }, [url]);
return ( return (
<div ref={playerRef} class="jsmpeg" /> <div ref={playerRef} class="jsmpeg" style={`max-height: ${height}px; max-width: ${width}px`} />
); );
} }

View File

@@ -1,6 +1,14 @@
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import { addSeconds, differenceInSeconds, fromUnixTime, format, parseISO, startOfHour } from 'date-fns'; import {
differenceInSeconds,
fromUnixTime,
format,
parseISO,
startOfHour,
differenceInMinutes,
differenceInHours,
} from 'date-fns';
import ArrowDropdown from '../icons/ArrowDropdown'; import ArrowDropdown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup'; import ArrowDropup from '../icons/ArrowDropup';
import Link from '../components/Link'; import Link from '../components/Link';
@@ -21,25 +29,31 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate, se
events={recording.events} events={recording.events}
selected={recording.date === selectedDate} selected={recording.date === selectedDate}
> >
{recording.recordings.slice().reverse().map((item, i) => ( {recording.recordings
<div className="mb-2 w-full"> .slice()
<div .reverse()
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${ .map((item, i) => (
i === 0 ? 'border-t border-white border-opacity-50' : '' <div className="mb-2 w-full">
}`} <div
> className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
<div className="flex-1"> i === 0 ? 'border-t border-white border-opacity-50' : ''
<Link href={`/recording/${camera}/${recording.date}/${item.hour}`} type="text"> }`}
{item.hour}:00 >
</Link> <div className="flex-1">
<Link href={`/recording/${camera}/${recording.date}/${item.hour}`} type="text">
{item.hour}:00
</Link>
</div>
<div className="flex-1 text-right">{item.events.length} Events</div>
</div> </div>
<div className="flex-1 text-right">{item.events.length} Events</div> {item.events
.slice()
.reverse()
.map((event) => (
<EventCard camera={camera} event={event} delay={item.delay} />
))}
</div> </div>
{item.events.map((event) => ( ))}
<EventCard camera={camera} event={event} delay={item.delay} />
))}
</div>
))}
</ExpandableList> </ExpandableList>
); );
} }
@@ -83,8 +97,17 @@ export function ExpandableList({ title, events = 0, children, selected = false }
export function EventCard({ camera, event, delay }) { export function EventCard({ camera, event, delay }) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const start = fromUnixTime(event.start_time); const start = fromUnixTime(event.start_time);
const end = fromUnixTime(event.end_time); let duration = 'In Progress';
const duration = addSeconds(new Date(0), differenceInSeconds(end, start)); if (event.end_time) {
const end = fromUnixTime(event.end_time);
const hours = differenceInHours(end, start);
const minutes = differenceInMinutes(end, start) - hours * 60;
const seconds = differenceInSeconds(end, start) - hours * 60 - minutes * 60;
duration = '';
if (hours) duration += `${hours}h `;
if (minutes) duration += `${minutes}m `;
duration += `${seconds}s`;
}
const position = differenceInSeconds(start, startOfHour(start)); const position = differenceInSeconds(start, startOfHour(start));
const offset = Object.entries(delay) const offset = Object.entries(delay)
.map(([p, d]) => (position > p ? d : 0)) .map(([p, d]) => (position > p ? d : 0))
@@ -102,7 +125,7 @@ export function EventCard({ camera, event, delay }) {
<div className="flex-1"> <div className="flex-1">
<div className="text-2xl text-white leading-tight capitalize">{event.label}</div> <div className="text-2xl text-white leading-tight capitalize">{event.label}</div>
<div className="text-xs md:text-normal text-gray-300">Start: {format(start, 'HH:mm:ss')}</div> <div className="text-xs md:text-normal text-gray-300">Start: {format(start, 'HH:mm:ss')}</div>
<div className="text-xs md:text-normal text-gray-300">Duration: {format(duration, 'mm:ss')}</div> <div className="text-xs md:text-normal text-gray-300">Duration: {duration}</div>
</div> </div>
<div className="text-lg text-white text-right leading-tight">{(event.top_score * 100).toFixed(1)}%</div> <div className="text-lg text-white text-right leading-tight">{(event.top_score * 100).toFixed(1)}%</div>
</div> </div>

View File

@@ -27,7 +27,7 @@ export default function RelativeModal({
const handleKeydown = useCallback( const handleKeydown = useCallback(
(event) => { (event) => {
const focusable = ref.current.querySelectorAll('[tabindex]'); const focusable = ref.current && ref.current.querySelectorAll('[tabindex]');
if (event.key === 'Tab' && focusable.length) { if (event.key === 'Tab' && focusable.length) {
if (event.shiftKey && document.activeElement === focusable[0]) { if (event.shiftKey && document.activeElement === focusable[0]) {
focusable[focusable.length - 1].focus(); focusable[focusable.length - 1].focus();
@@ -69,14 +69,15 @@ export default function RelativeModal({
let newTop = top; let newTop = top;
let newLeft = left; let newLeft = left;
// too far right
if (newLeft + width + WINDOW_PADDING >= windowWidth - WINDOW_PADDING) {
newLeft = windowWidth - width - WINDOW_PADDING;
}
// too far left // too far left
else if (left < WINDOW_PADDING) { if (left < WINDOW_PADDING) {
newLeft = WINDOW_PADDING; newLeft = WINDOW_PADDING;
} }
// too far right
else if (newLeft + width + WINDOW_PADDING >= windowWidth - WINDOW_PADDING) {
newLeft = windowWidth - width - WINDOW_PADDING;
}
// too close to bottom // too close to bottom
if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) { if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
newTop = WINDOW_PADDING; newTop = WINDOW_PADDING;

View File

@@ -3,74 +3,27 @@ import ArrowDropdown from '../icons/ArrowDropdown';
import ArrowDropup from '../icons/ArrowDropup'; import ArrowDropup from '../icons/ArrowDropup';
import Menu, { MenuItem } from './Menu'; import Menu, { MenuItem } from './Menu';
import TextField from './TextField'; import TextField from './TextField';
import DatePicker from './DatePicker';
import Calender from './Calender';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
export default function Select({ label, onChange, options: inputOptions = [], selected: propSelected }) { export default function Select({
type,
label,
onChange,
paramName,
options: inputOptions = [],
selected: propSelected,
}) {
const options = useMemo( const options = useMemo(
() => () =>
typeof inputOptions[0] === 'string' ? inputOptions.map((opt) => ({ value: opt, label: opt })) : inputOptions, typeof inputOptions[0] === 'string' ? inputOptions.map((opt) => ({ value: opt, label: opt })) : inputOptions,
[inputOptions] [inputOptions]
); );
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [selected, setSelected] = useState( const [selected, setSelected] = useState();
Math.max( const [datePickerValue, setDatePickerValue] = useState();
options.findIndex(({ value }) => value === propSelected),
0
)
);
const [focused, setFocused] = useState(null);
const ref = useRef(null);
const handleSelect = useCallback(
(value, label) => {
setSelected(options.findIndex((opt) => opt.value === value));
onChange && onChange(value, label);
setShowMenu(false);
},
[onChange, options]
);
const handleClick = useCallback(() => {
setShowMenu(true);
}, [setShowMenu]);
const handleKeydown = useCallback(
(event) => {
switch (event.key) {
case 'Enter': {
if (!showMenu) {
setShowMenu(true);
setFocused(selected);
} else {
setSelected(focused);
onChange && onChange(options[focused].value, options[focused].label);
setShowMenu(false);
}
break;
}
case 'ArrowDown': {
const newIndex = focused + 1;
newIndex < options.length && setFocused(newIndex);
break;
}
case 'ArrowUp': {
const newIndex = focused - 1;
newIndex > -1 && setFocused(newIndex);
break;
}
// no default
}
},
[onChange, options, showMenu, setShowMenu, setFocused, focused, selected]
);
const handleDismiss = useCallback(() => {
setShowMenu(false);
}, [setShowMenu]);
// Reset the state if the prop value changes // Reset the state if the prop value changes
useEffect(() => { useEffect(() => {
@@ -85,25 +38,219 @@ export default function Select({ label, onChange, options: inputOptions = [], se
// DO NOT include `selected` // DO NOT include `selected`
}, [options, propSelected]); // eslint-disable-line react-hooks/exhaustive-deps }, [options, propSelected]); // eslint-disable-line react-hooks/exhaustive-deps
return ( useEffect(() => {
<Fragment> if (type === 'datepicker') {
<TextField if ('after' && 'before' in propSelected) {
inputRef={ref} if (!propSelected.before || !propSelected.after) return setDatePickerValue('all');
label={label}
onchange={onChange} for (let i = 0; i < inputOptions.length; i++) {
onclick={handleClick} if (
onkeydown={handleKeydown} inputOptions[i].value &&
readonly Object.entries(inputOptions[i].value).sort().toString() === Object.entries(propSelected).sort().toString()
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown} ) {
value={options[selected]?.label} setDatePickerValue(inputOptions[i]?.label);
/> break;
{showMenu ? ( } else {
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative> setDatePickerValue(
{options.map(({ value, label }, i) => ( `${new Date(propSelected.after * 1000).toLocaleDateString()} -> ${new Date(
<MenuItem key={value} label={label} focus={focused === i} onSelect={handleSelect} value={value} /> propSelected.before * 1000 - 1
))} ).toLocaleDateString()}`
</Menu> );
) : null} }
</Fragment> }
}
}
if (type === 'dropdown') {
setSelected(
Math.max(
options.findIndex(({ value }) => Object.values(propSelected).includes(value)),
0
)
);
}
}, [type, options, inputOptions, propSelected, setSelected]);
const [focused, setFocused] = useState(null);
const [showCalender, setShowCalender] = useState(false);
const calenderRef = useRef(null);
const ref = useRef(null);
const handleSelect = useCallback(
(value) => {
setSelected(options.findIndex(({ value }) => Object.values(propSelected).includes(value)));
setShowMenu(false);
//show calender date range picker
if (value === 'custom_range') return setShowCalender(true);
onChange && onChange(value);
},
[onChange, options, propSelected, setSelected]
); );
const handleDateRange = useCallback(
(range) => {
onChange && onChange(range);
setShowMenu(false);
},
[onChange]
);
const handleClick = useCallback(() => {
setShowMenu(true);
}, [setShowMenu]);
const handleKeydownDatePicker = useCallback(
(event) => {
switch (event.key) {
case 'Enter': {
if (!showMenu) {
setShowMenu(true);
setFocused(selected);
} else {
setSelected(focused);
if (options[focused].value === 'custom_range') {
setShowMenu(false);
return setShowCalender(true);
}
onChange && onChange(options[focused].value);
setShowMenu(false);
}
break;
}
case 'ArrowDown': {
event.preventDefault();
const newIndex = focused + 1;
newIndex < options.length && setFocused(newIndex);
break;
}
case 'ArrowUp': {
event.preventDefault();
const newIndex = focused - 1;
newIndex > -1 && setFocused(newIndex);
break;
}
// no default
}
},
[onChange, options, showMenu, setShowMenu, setFocused, focused, selected]
);
const handleKeydown = useCallback(
(event) => {
switch (event.key) {
case 'Enter': {
if (!showMenu) {
setShowMenu(true);
setFocused(selected);
} else {
setSelected(focused);
onChange && onChange({ [paramName]: options[focused].value });
setShowMenu(false);
}
break;
}
case 'ArrowDown': {
event.preventDefault();
const newIndex = focused + 1;
newIndex < options.length && setFocused(newIndex);
break;
}
case 'ArrowUp': {
event.preventDefault();
const newIndex = focused - 1;
newIndex > -1 && setFocused(newIndex);
break;
}
// no default
}
},
[onChange, options, showMenu, setShowMenu, setFocused, focused, selected, paramName]
);
const handleDismiss = useCallback(() => {
setShowMenu(false);
}, [setShowMenu]);
const findDOMNodes = (component) => {
return (component && (component.base || (component.nodeType === 1 && component))) || null;
};
useEffect(() => {
const addBackDrop = (e) => {
if (showCalender && !findDOMNodes(calenderRef.current).contains(e.target)) {
setShowCalender(false);
}
};
window.addEventListener('click', addBackDrop);
return function cleanup() {
window.removeEventListener('click', addBackDrop);
};
}, [showCalender]);
switch (type) {
case 'datepicker':
return (
<Fragment>
<DatePicker
inputRef={ref}
label={label}
onchange={onChange}
onclick={handleClick}
onkeydown={handleKeydownDatePicker}
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown}
value={datePickerValue}
/>
{showCalender && (
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref}>
<Calender onChange={handleDateRange} calenderRef={calenderRef} close={() => setShowCalender(false)} />
</Menu>
)}
{showMenu ? (
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative>
{options.map(({ value, label }, i) => (
<MenuItem key={value} label={label} focus={focused === i} onSelect={handleSelect} value={value} />
))}
</Menu>
) : null}
</Fragment>
);
// case 'dropdown':
default:
return (
<Fragment>
<TextField
inputRef={ref}
label={label}
onchange={onChange}
onclick={handleClick}
onkeydown={handleKeydown}
readonly
trailingIcon={showMenu ? ArrowDropup : ArrowDropdown}
value={options[selected]?.label}
/>
{showMenu ? (
<Menu className="rounded-t-none" onDismiss={handleDismiss} relativeTo={ref} widthRelative>
{options.map(({ value, label }, i) => (
<MenuItem
key={value}
label={label}
focus={focused === i}
onSelect={handleSelect}
value={{ [paramName]: value }}
/>
))}
</Menu>
) : null}
</Fragment>
);
}
} }

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 ( return (
<tbody className={className} {...attrs}> <tbody ref={reference} className={className} {...attrs}>
{children} {children}
</tbody> </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 ( return (
<tr <tr
ref={reference}
className={`border-b border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 ${className}`} className={`border-b border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 ${className}`}
{...attrs} {...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 ( 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} {children}
</td> </td>
); );

View File

@@ -88,7 +88,7 @@ export default function VideoPlayer({ children, options, seekOptions = {}, onRea
return ( return (
<div data-vjs-player> <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} {children}
</div> </div>
); );

View File

@@ -5,21 +5,40 @@ import { fireEvent, render, screen } from '@testing-library/preact';
describe('Select', () => { describe('Select', () => {
test('on focus, shows a menu', async () => { test('on focus, shows a menu', async () => {
const handleChange = jest.fn(); const handleChange = jest.fn();
render(<Select label="Tacos" onChange={handleChange} options={['tacos', 'burritos']} />); render(
<Select
label="Tacos"
type="dropdown"
onChange={handleChange}
options={['all', 'tacos', 'burritos']}
paramName={['dinner']}
selected=""
/>
);
expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('textbox')); fireEvent.click(screen.getByRole('textbox'));
expect(screen.queryByRole('listbox')).toBeInTheDocument(); expect(screen.queryByRole('listbox')).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'all' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'tacos' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'tacos' })).toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'burritos' })).toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'burritos' })).toBeInTheDocument();
fireEvent.click(screen.queryByRole('option', { name: 'burritos' })); fireEvent.click(screen.queryByRole('option', { name: 'tacos' }));
expect(handleChange).toHaveBeenCalledWith('burritos', 'burritos'); expect(handleChange).toHaveBeenCalledWith({ dinner: 'tacos' });
}); });
test('allows keyboard navigation', async () => { test('allows keyboard navigation', async () => {
const handleChange = jest.fn(); const handleChange = jest.fn();
render(<Select label="Tacos" onChange={handleChange} options={['tacos', 'burritos']} />); render(
<Select
label="Tacos"
type="dropdown"
onChange={handleChange}
options={['tacos', 'burritos']}
paramName={['dinner']}
selected=""
/>
);
expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
const input = screen.getByRole('textbox'); const input = screen.getByRole('textbox');
@@ -29,6 +48,6 @@ describe('Select', () => {
fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' });
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(handleChange).toHaveBeenCalledWith('burritos', 'burritos'); expect(handleChange).toHaveBeenCalledWith({ dinner: 'burritos' });
}); });
}); });

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

Some files were not shown because too many files have changed in this diff Show More