Compare commits

..

330 Commits

Author SHA1 Message Date
Paul Armstrong
5043040530 fix(web): ensure tooltips and menus don't cause scrollbar reflow 2021-02-25 06:34:36 -06:00
Paul Armstrong
3c60aeeef9 fix(web): set events api limit to 25 2021-02-25 06:34:36 -06:00
Blake Blackshear
0344d61b26 use gevent sleep to prevent mjpeg from blocking 2021-02-25 06:34:36 -06:00
Blake Blackshear
0e8467782b version tick 2021-02-25 06:34:36 -06:00
Paul Armstrong
423ea26266 Add paularmstrong to funding.yml 2021-02-24 20:58:44 -06:00
Paul Armstrong
2f3339ba85 docs: add contributing docs 2021-02-23 07:37:19 -06:00
Blake Blackshear
9433b50785 add stalebot 2021-02-22 07:20:39 -06:00
Blake Blackshear
1e7b53dc0e clarify h264 in docs 2021-02-22 07:20:32 -06:00
Blake Blackshear
bc94748f2a clips not playable 2021-02-22 07:10:56 -06:00
Blake Blackshear
2395f93ed1 Update bug_report.md 2021-02-22 06:43:21 -06:00
Blake Blackshear
d771726c2a version tick 2021-02-21 09:32:45 -06:00
Blake Blackshear
b2a2fe898c ensure base url works for websockets 2021-02-21 09:32:45 -06:00
Blake Blackshear
31d408a746 dynamic ws/wss selection 2021-02-20 08:20:17 -06:00
Blake Blackshear
4a74f295e7 docs updates 2021-02-20 08:20:17 -06:00
Paul Armstrong
b6ba6459fb feat(web): detect, clips, snapshots toggles 2021-02-20 08:20:17 -06:00
Paul Armstrong
e399790442 feat(web): mqtt for stats 2021-02-20 08:20:17 -06:00
Blake Blackshear
20c65b9a31 fix link and clarify audio encoding (fixes #800) 2021-02-20 08:20:17 -06:00
Blake Blackshear
1a7853a47e subscribe in the connect callback (fixes #814) 2021-02-20 08:20:17 -06:00
Blake Blackshear
683c3a4c90 update wheels again 2021-02-20 08:20:17 -06:00
Blake Blackshear
4a8d998afe unpin numpy 2021-02-20 08:20:17 -06:00
Paul Armstrong
fe59d90c51 web(test): routes/Events 2021-02-20 08:20:17 -06:00
Paul Armstrong
f87813805a test(web): RelativeModal 2021-02-20 08:20:17 -06:00
Paul Armstrong
a7e5b9978f test(web): Select 2021-02-20 08:20:17 -06:00
Paul Armstrong
0a3959af86 test(web): TextField 2021-02-20 08:20:17 -06:00
Paul Armstrong
9ba6054140 test(web): Sidebar 2021-02-20 08:20:17 -06:00
Paul Armstrong
3348f04889 test(web): App 2021-02-20 08:20:17 -06:00
Paul Armstrong
c12aec7c8f test(web): routes/Event 2021-02-20 08:20:17 -06:00
Paul Armstrong
05f66b8f24 test(web): routes/Debug 2021-02-20 08:20:17 -06:00
Paul Armstrong
d8b80f0fe9 test(web): routes/Cameras 2021-02-20 08:20:17 -06:00
Paul Armstrong
7314572d97 feat(web): allow CameraImage to stretch 2021-02-20 08:20:17 -06:00
Paul Armstrong
52a29ed00a test(web): routes/Camera 2021-02-20 08:20:17 -06:00
Paul Armstrong
5eaf8a5448 test(web): Switch (and add label back in) 2021-02-20 08:20:17 -06:00
Paul Armstrong
f70fb12c3d test(web): NavigationDrawer 2021-02-20 08:20:17 -06:00
Paul Armstrong
ece6c1203c test(web): Menu, MenuItem 2021-02-20 08:20:17 -06:00
Paul Armstrong
9c7e3177a2 test(web): Link 2021-02-20 08:20:17 -06:00
Paul Armstrong
058c0affaf test(web): Heading 2021-02-20 08:20:17 -06:00
Paul Armstrong
5ee7146884 test(web): Card 2021-02-20 08:20:17 -06:00
Paul Armstrong
1aa9a7a093 test(web): CameraImage (basic)
Testing Image and Canvas calls requires a lot of heavy dependencies, so this skips that part of the tests
2021-02-20 08:20:17 -06:00
Paul Armstrong
a202c44a0f test(web): Button 2021-02-20 08:20:17 -06:00
Paul Armstrong
85776cc7d0 test(web): fix switch case indent lint 2021-02-20 08:20:17 -06:00
Paul Armstrong
6d133ef724 test(web): api/index.jsx 2021-02-20 08:20:17 -06:00
Paul Armstrong
53288d361c test(web): AutoUpdatingCameraImage 2021-02-20 08:20:17 -06:00
Paul Armstrong
e729bd52aa refactor(web): Split AppBar and add tests 2021-02-20 08:20:17 -06:00
Paul Armstrong
ddb6127519 test(web): add ActivityIndicator test 2021-02-20 08:20:17 -06:00
Blake Blackshear
50b42eb6fe add gevent to prebuilt wheels 2021-02-20 08:20:17 -06:00
Blake Blackshear
b6572b7272 add some error handling to mqtt relay 2021-02-20 08:20:17 -06:00
Blake Blackshear
57ced2c284 constrain websockets to frigate topics 2021-02-20 08:20:17 -06:00
Blake Blackshear
26a3491466 revise log messages 2021-02-20 08:20:17 -06:00
Blake Blackshear
eed8463832 relay messages from sockets to mqtt 2021-02-20 08:20:17 -06:00
Blake Blackshear
718b4f3fd7 relay mqtt to clients 2021-02-20 08:20:17 -06:00
Blake Blackshear
22461d1728 simple echo websocket working 2021-02-20 08:20:17 -06:00
Blake Blackshear
69cab1e6bb update docker commands to avoid privileged mode 2021-02-20 08:20:17 -06:00
Blake Blackshear
a661fddaf3 fix cache cleanup for large full disks 2021-02-20 08:20:17 -06:00
Blake Blackshear
1b85e561b9 only save the event to the database if a snapshot or clip exists 2021-02-20 08:20:17 -06:00
Paul Armstrong
a803ab8577 test(web): add unit test framework 2021-02-20 08:20:17 -06:00
Paul Armstrong
daa759cc55 test(web): add eslint and PR lint validation 2021-02-20 08:20:17 -06:00
Blake Blackshear
513a099c24 better error handling (fixes #739) 2021-02-20 08:20:17 -06:00
Blake Blackshear
e299e73a68 ignore detections that don't overlap with motion 2021-02-20 08:20:17 -06:00
Blake Blackshear
9550ac7422 fix intersection calculation 2021-02-20 08:20:17 -06:00
Patrick Decat
07bd376649 fix(web): fix CameraMap.jsx import of api after move to routes/ 2021-02-20 08:20:17 -06:00
Patrick Decat
ec6a1ed9d1 Add nginx sub_filter to fix resources from /dist with HA ingress 2021-02-20 08:20:17 -06:00
Paul Armstrong
7aee28d080 refactor(web): async routing 2021-02-20 08:20:17 -06:00
Paul Armstrong
24ec13e36d fix(web): svgs may need explicit height/width in rare cases on linux 2021-02-20 08:20:17 -06:00
Paul Armstrong
d2e7c360b9 fix(web): build fixes after rebase 2021-02-20 08:20:17 -06:00
Paul Armstrong
f00628f4e5 refactor(web): menu positioning 2021-02-20 08:20:17 -06:00
Paul Armstrong
19bd5ace7d perf(web): memoize icon components 2021-02-20 08:20:17 -06:00
Paul Armstrong
3e2506136c fix(web): debug tables scrollable on small width screens 2021-02-20 08:20:17 -06:00
Paul Armstrong
4e03acc944 fix(web): ensure drawer can slide in/out and not just appear 2021-02-20 08:20:17 -06:00
Paul Armstrong
188eb6b9ea fix(web): relative modal height, top position, and z-indexing 2021-02-20 08:20:17 -06:00
Paul Armstrong
c89e1a5735 fix(web): remove cards from event page 2021-02-20 08:20:17 -06:00
Paul Armstrong
e50cc59f0d refactor(web): datatables 2021-02-20 08:20:17 -06:00
Paul Armstrong
96f87caff0 refactor(web): camera view + bugfixes 2021-02-20 08:20:17 -06:00
Paul Armstrong
b422a83b57 fix(web): ensure relative modal respects scrollY 2021-02-20 08:20:17 -06:00
Paul Armstrong
15ae3bee55 refactor(web): update shadows for material specs 2021-02-20 08:20:17 -06:00
Paul Armstrong
0cac2fec2a feat(web): add button types 2021-02-20 08:20:17 -06:00
Paul Armstrong
5965da88c3 fix(web): dark mode for portals 2021-02-20 08:20:17 -06:00
Paul Armstrong
ba0338e9d5 refactor(web): NavigationBar (sidebar) styles 2021-02-20 08:20:17 -06:00
Paul Armstrong
ff62338359 feat(web): icons and better menu handling for dark mode 2021-02-20 08:20:17 -06:00
Paul Armstrong
9867f4eeee fix(web): ensure relative modals have proper padding 2021-02-20 08:20:17 -06:00
Paul Armstrong
ba278dfc3d refactor(web): add 3xl breakpoint 2021-02-20 08:20:17 -06:00
Paul Armstrong
063030bcf3 fix(web): make app bar and sidebar fully responsive 2021-02-20 08:20:17 -06:00
Paul Armstrong
276ce8710c feat(web): persist darkmode preference 2021-02-20 08:20:17 -06:00
Paul Armstrong
5ed7a17f46 refactor(web): styles and styleguide 2021-02-20 08:20:17 -06:00
Blake Blackshear
01c3b4fa6e try and ensure database closes cleanly 2021-02-20 08:20:17 -06:00
Blake Blackshear
165ca8fbc7 purge duplicate events during cleanup 2021-02-20 08:20:17 -06:00
Blake Blackshear
ce90ae343c add global object mask 2021-02-20 08:20:17 -06:00
Paul Armstrong
a99f360a64 refactor(web): use snowpack-plugin-hash 2021-02-20 08:20:17 -06:00
Blake Blackshear
d51e9446ff add camera level ffmpeg params 2021-02-20 08:20:17 -06:00
Blake Blackshear
d3524ee46f adjust jpg quality in other locations too 2021-02-20 08:20:17 -06:00
Blake Blackshear
121ea37825 allow defining required zones for snapshots/clips/mqtt 2021-02-20 08:20:17 -06:00
Blake Blackshear
9592d95599 proactively clean up cache when above 90% use 2021-02-20 08:20:17 -06:00
Blake Blackshear
d6faa18adb increase default max_disappeared to 5x FPS 2021-02-20 08:20:17 -06:00
Blake Blackshear
1cbe6f77ee only run detection on objects that intersect with motion 2021-02-20 08:20:17 -06:00
Blake Blackshear
4f5d4e36b7 add disk usage to stats 2021-02-20 08:20:17 -06:00
Paul Armstrong
163025c1f2 fix(app): reduce JPEG quality to drastically improve size 2021-02-20 08:20:17 -06:00
Paul Armstrong
880178d62e refactor(web): render CameraImage to a canvas 2021-02-20 08:20:17 -06:00
Blake Blackshear
d285ff7e54 increment version 2021-02-20 08:20:17 -06:00
jaburges
54671fc522 Added the base urls for image and video 2021-02-10 20:59:00 -06:00
jaburges
53e3e6545d Added Unraid and M2 coral edge tpu
Recommended hardware docs
2021-02-10 20:58:11 -06:00
Blake Blackshear
91cb49c4a3 Update issue templates 2021-02-10 06:51:52 -06:00
Blake Blackshear
c065cb48f2 fix notification examples 2021-02-03 06:56:14 -06:00
Blake Blackshear
d376f6b1d2 increment version 2021-01-31 06:20:59 -06:00
Paul Armstrong
45526a7652 feat(web): activity indicator while loading 2021-01-31 06:18:35 -06:00
Paul Armstrong
cc7929932b feat(nginx): enable gzip compression and cache control for static files 2021-01-31 06:18:35 -06:00
Paul Armstrong
e6516235fa feat(web): auto-paginate events page 2021-01-31 06:18:35 -06:00
Blake Blackshear
40d5a9f890 change default log level 2021-01-31 06:18:35 -06:00
Blake Blackshear
ee3e744cc6 tail last 100 lines of ffmpeg logs and dump when failure detected 2021-01-31 06:18:35 -06:00
Blake Blackshear
b55bd1e027 add param to reduce response sizes by excluding thumbnails in api response 2021-01-31 06:18:35 -06:00
Jeff Billimek
9a96df0319 update docs to reflect new chart home
Signed-off-by: Jeff Billimek <jeff@billimek.com>
2021-01-30 22:15:20 -06:00
Blake Blackshear
e9b1618364 add note about protection mode for tmpfs fixes #658 2021-01-29 06:42:55 -06:00
Blake Blackshear
6dc6ed1e94 more detailed reolink args 2021-01-29 06:40:32 -06:00
Blake Blackshear
1943a49274 add audio info to docs 2021-01-29 06:35:54 -06:00
Paul Armstrong
a8c00edc94 fix(web): reduce transferred/unused assets on html load 2021-01-29 06:27:32 -06:00
Blake Blackshear
faa8abb2b9 docs updates 2021-01-28 08:21:04 -06:00
Blake Blackshear
f6cd2fc68e clarifying addon docs 2021-01-28 07:45:09 -06:00
Paul Armstrong
6482000d6b fix(web): image loading for firefox 2021-01-28 07:05:45 -06:00
Justin Goette
dcf7209706 Fix camera.md links 2021-01-28 06:43:43 -06:00
Paul Armstrong
2ec921593e refactor(web): responsive images on content size, throttle AutoUpdatingCameraImage 2021-01-26 21:40:33 -06:00
Paul Armstrong
75a01f657e feat(web): make it possible to add to object masks 2021-01-26 21:40:33 -06:00
Paul Armstrong
d4e512c1fc fix(web): object mask editing not showing points 2021-01-26 21:40:33 -06:00
Paul Armstrong
26e7d34f18 fix(web): ensure all links on events page include pathname 2021-01-26 21:40:33 -06:00
Blake Blackshear
15b5ffddd4 updating for docusaurus2 docs 2021-01-26 21:40:33 -06:00
Paul Armstrong
f0f3764992 fix(web): make camera latest.jpg responsive 2021-01-26 21:40:33 -06:00
Blake Blackshear
2beb44b591 add search to docs 2021-01-26 21:40:33 -06:00
Blake Blackshear
27b659dde1 tweaking the docs 2021-01-26 21:40:33 -06:00
Blake Blackshear
630c2ee6f6 use sqlitequeuedb 2021-01-26 21:40:33 -06:00
James Carlos
600477c487 Update documentation link in sidebar to new docs 2021-01-26 21:40:33 -06:00
Blake Blackshear
d31c295598 add debug log when cache is cleaned up 2021-01-26 21:40:33 -06:00
Blake Blackshear
a7bb0931c4 if detection stopped, assume the container needs a restart 2021-01-26 21:40:33 -06:00
Blake Blackshear
ff99a01423 fix table in docs 2021-01-26 21:40:33 -06:00
Blake Blackshear
ea6e311318 readme update 2021-01-26 21:40:33 -06:00
Paul Armstrong
6790467bbc docs: move docs to docusaurus 2021-01-26 21:40:33 -06:00
Blake Blackshear
d315dbea22 rate limit tracked object updates to every 5 seconds 2021-01-26 21:40:33 -06:00
Blake Blackshear
8db7ab6724 add snapshot endpoint that works during the event fixes #575 2021-01-26 21:40:33 -06:00
Blake Blackshear
9a2c034ae8 get the thumbnail instead of the full frame 2021-01-26 21:40:33 -06:00
Blake Blackshear
2885b80a13 dont wait forever for the cache 2021-01-26 21:40:33 -06:00
Blake Blackshear
4a85156e87 fix initial switch state 2021-01-26 21:40:33 -06:00
Blake Blackshear
1785c69e1b handle exception when frame isnt in cache 2021-01-26 21:40:33 -06:00
Paul Armstrong
a862ba8348 feat(web): AutoUpdatingCameraImage to replace MJPEG feed 2021-01-26 21:40:33 -06:00
Paul Armstrong
633d45d02f fix(web): set default path to cameras view 2021-01-26 21:40:33 -06:00
Blake Blackshear
7f4e042dfa update index.js to use baseUrl 2021-01-26 21:40:33 -06:00
Blake Blackshear
507ec13848 first pass at subfilter for ingress support 2021-01-26 21:40:33 -06:00
Paul Armstrong
2132352639 fix(web): dark mode text color fixes
fixes #544
2021-01-26 21:40:33 -06:00
Blake Blackshear
11016b8486 ensure error message with missing config is printed 2021-01-26 21:40:33 -06:00
Blake Blackshear
8615f14407 update notification example 2021-01-26 21:40:33 -06:00
Blake Blackshear
1e84f08018 fix mqtt switch handling 2021-01-26 21:40:33 -06:00
Blake Blackshear
7f663328dc initialize detection correctly from config 2021-01-26 21:40:33 -06:00
Blake Blackshear
ea53068432 update wheels version 2021-01-26 21:40:33 -06:00
Blake Blackshear
144aff9b4e pin numpy 2021-01-26 21:40:33 -06:00
Paul Armstrong
18db6daf0a feat(web): layout & auto-update debug page 2021-01-26 21:40:33 -06:00
Paul Armstrong
26ba29b538 fix(web): ensure button bg colors show in prod builds 2021-01-26 21:40:33 -06:00
Blake Blackshear
70167a34b6 fix zone config 2021-01-26 21:40:33 -06:00
Blake Blackshear
ccb668a1b6 no longer need special aarch64 wheels build 2021-01-26 21:40:33 -06:00
Blake Blackshear
0989c64eab versioning wheels image 2021-01-26 21:40:33 -06:00
Blake Blackshear
c082fc5cb2 move wheels to build container 2021-01-26 21:40:33 -06:00
Paul Armstrong
d39111a294 fix(web): mask zone editor to handle object filter masks
Includes additional handlers for adding/removing masks, as well as click to copy configs

fixes #523
2021-01-26 21:40:33 -06:00
Paul Armstrong
3c072f94b0 feat(web): hash build files to avoid cache issues 2021-01-26 21:40:33 -06:00
Paul Armstrong
7f8ae2ce5c fix(web): ensure mask editing works in firefox 2021-01-26 21:40:33 -06:00
Blake Blackshear
d84b75168c docs updates for notification changes 2021-01-26 21:40:33 -06:00
Blake Blackshear
eb0a5e1c55 rename snapshot endpoint to thumbnail 2021-01-26 21:40:33 -06:00
Blake Blackshear
47ac77dbb0 mqtt tweaks for switches 2021-01-26 21:40:33 -06:00
Blake Blackshear
ec84847be7 allow summary data to be filtered 2021-01-26 21:40:33 -06:00
Blake Blackshear
e7839bfd40 update readme 2021-01-26 21:40:33 -06:00
Blake Blackshear
8762da627b snapshots config typo 2021-01-26 21:40:33 -06:00
Blake Blackshear
3fab321045 update object filters to inherit like motion settings 2021-01-26 21:40:33 -06:00
Blake Blackshear
9451048574 remove support for image masks 2021-01-26 21:40:33 -06:00
Blake Blackshear
46c002038b don't fallback to the CPU
fixes #381
2021-01-26 21:40:33 -06:00
Blake Blackshear
d1d833ea9a add change type to events topic
#476
2021-01-26 21:40:33 -06:00
Blake Blackshear
c1f0750526 ensure each camera has a detect role set 2021-01-26 21:40:33 -06:00
Blake Blackshear
89e02b6956 add detection enable to config
fixes #482
2021-01-26 21:40:33 -06:00
Blake Blackshear
97e8258288 add env vars to config
fixes #509
2021-01-26 21:40:33 -06:00
Blake Blackshear
39040c1874 enable and disable detection via mqtt 2021-01-26 21:40:33 -06:00
Blake Blackshear
c709851888 move setproctitle to prebuilt wheel location 2021-01-26 21:40:33 -06:00
Blake Blackshear
b022bec1fa switch to docker based web builds 2021-01-26 21:40:33 -06:00
Blake Blackshear
bca0531963 handle null thumbnail data 2021-01-26 21:40:33 -06:00
Blake Blackshear
b2c7fc8f5b add mask as object filter 2021-01-26 21:40:33 -06:00
Blake Blackshear
96ac2c29d6 add object masks and move moton mask 2021-01-26 21:40:33 -06:00
Blake Blackshear
14a5118b4d add missing global shapshots config 2021-01-26 21:40:33 -06:00
Patrick Decat
232fa1ffe8 Add missing migrations in docker images 2021-01-26 21:40:33 -06:00
Paul Armstrong
d2e91754e9 fix(web): ensure postcss and postcss-cli are marked as deps 2021-01-26 21:40:33 -06:00
Patrick Decat
4d9066a58d Fix Makefile to ignore gpg signatures in commits 2021-01-26 21:40:33 -06:00
Paul Armstrong
c618867941 feat!: web user interface 2021-01-26 21:40:33 -06:00
Blake Blackshear
5ad4017510 try to cleanup some migration logging 2021-01-26 21:40:33 -06:00
Blake Blackshear
63e14a98f9 add retention settings for snapshots 2021-01-26 21:40:33 -06:00
Blake Blackshear
25e3fe8eab init variables on camera state 2021-01-26 21:40:33 -06:00
Blake Blackshear
840f046572 handle process exit exceptions 2021-01-26 21:40:33 -06:00
Blake Blackshear
89e3c2e4b1 store has_clip and has_snapshot on events 2021-01-26 21:40:33 -06:00
Blake Blackshear
c770470b58 add database migrations 2021-01-26 21:40:33 -06:00
Nat Morris
4619836122 Set titles for forked processes 2021-01-26 21:40:33 -06:00
Nat Morris
76403bba8e New stats module, refactor stats generation out of http module.
StatsEmitter thread to send stats to MQTT every 60 seconds by default, optional stats_interval config value.

New service stats attribute, containing uptime in seconds and version.
2021-01-26 21:40:33 -06:00
Blake Blackshear
a9afa303a2 turn off snapshots via mqtt 2021-01-26 21:40:33 -06:00
Blake Blackshear
e5399ae07a enable turning clips on and off via mqtt 2021-01-26 21:40:33 -06:00
Blake Blackshear
80a5a7b129 cleanup save_Clips/clips inconsistency 2021-01-26 21:40:33 -06:00
Blake Blackshear
9dc97d4b6b add jpg snapshots to disk and clean up config 2021-01-26 21:40:33 -06:00
Paul Armstrong
d8c9169af2 fix: ensure timestamp is drawn above mask 2021-01-26 21:40:33 -06:00
Leonardo Merza
ec256f7130 add notes for Blue Iris RTSP support 2021-01-26 21:40:33 -06:00
yllar
19bbfce4ed Update README.md
change tmpfs size from 100MB to 1GB
2021-01-26 21:40:33 -06:00
kluszczyn
b0b2d9d972 Recordings - fix expire_file 2021-01-26 21:40:33 -06:00
Blake Blackshear
6f5f5c9461 add clips endpoint to readme 2021-01-26 21:40:33 -06:00
Blake Blackshear
fc04bc6046 better mask error handling 2021-01-26 21:40:33 -06:00
Blake Blackshear
9f504253fb fix tmpfs 2021-01-26 21:40:33 -06:00
Blake Blackshear
961997e078 remove redundant error output 2021-01-26 21:40:33 -06:00
Blake Blackshear
363594a9a2 use CACHE_DIR constant 2021-01-26 21:40:33 -06:00
Blake Blackshear
247e2677f3 enable mounting tmpfs volume on start 2021-01-26 21:40:33 -06:00
Blake Blackshear
5b5159f4dd docs and issue template 2021-01-26 21:40:33 -06:00
Blake Blackshear
bc8b85860c update process clip for latest changes 2021-01-26 21:40:33 -06:00
Blake Blackshear
44d45c5880 publish event updates on zone change 2021-01-26 21:40:33 -06:00
Blake Blackshear
a6d8e4fc3f readme updates 2021-01-26 21:40:33 -06:00
Blake Blackshear
2cc9a15f6a handle scenario with empty cache 2021-01-26 21:40:33 -06:00
Blake Blackshear
151f9fb2ee add qsv support to amd64 image 2021-01-26 21:40:33 -06:00
Blake Blackshear
32fb76b3d1 add num_threads fixes #322 2021-01-26 21:40:33 -06:00
Blake Blackshear
8d52e2635a optimize clips fixes #299 2021-01-26 21:40:33 -06:00
Blake Blackshear
f20e1f20a6 add post_capture option 2021-01-26 21:40:33 -06:00
Blake Blackshear
af8594c5c6 re-crop to the object rather than the region 2021-01-26 21:40:33 -06:00
Blake Blackshear
899d41f361 allow runtime drawing settings for mjpeg and latest 2021-01-26 21:40:33 -06:00
Blake Blackshear
7dc6382c90 allow the mask to be a list of masks 2021-01-26 21:40:33 -06:00
Blake Blackshear
e8009c2d26 adding version endpoint 2021-01-26 21:40:33 -06:00
Blake Blackshear
3bc7cdaab6 configurable motion and detect settings 2021-01-26 21:40:33 -06:00
Blake Blackshear
724d8187c6 update gitignore 2021-01-26 21:40:33 -06:00
Blake Blackshear
8f68df60c7 fix test 2021-01-26 21:40:33 -06:00
Blake Blackshear
6af3cb6134 switch default threshold to .7 2021-01-26 21:40:33 -06:00
Blake Blackshear
2ff0c3907f allow process clips to output a csv of scores 2021-01-26 21:40:33 -06:00
Blake Blackshear
dd102ff01d allow db path to be customized 2021-01-26 21:40:33 -06:00
Blake Blackshear
f20b1d75e6 add telegram example 2021-01-26 21:40:33 -06:00
Blake Blackshear
a4b88ac4a7 fix process clip 2021-01-26 21:40:33 -06:00
Blake Blackshear
93b9d586d2 handle empty string args 2021-01-26 21:40:33 -06:00
Blake Blackshear
41dd4447cc allow region to extend beyond the frame 2021-01-26 21:40:33 -06:00
tubalainen
8b9c8a2e80 Updated file
ref: https://github.com/blakeblackshear/frigate/issues/373
2021-01-26 21:40:33 -06:00
Blake Blackshear
a63ff1bb99 swap width and height to reduce confusion 2021-01-26 21:40:33 -06:00
Blake Blackshear
d5e3b59245 updating compose example to reduce confusion 2021-01-26 21:40:33 -06:00
Blake Blackshear
d0470fffcc allow defining model shape and switch to mobiledet as default model 2021-01-26 21:40:33 -06:00
Blake Blackshear
5053305e17 add model dimensions to config 2021-01-26 21:40:33 -06:00
Patrick Decat
9c79392060 Document beta addon host 2021-01-26 21:40:33 -06:00
Blake Blackshear
708c3278bf make shm consistent with compose 2021-01-26 21:40:33 -06:00
tubalainen
c0249f6e59 Updated docker command line...
...to correspond with 0.8.0 feature set.
2021-01-26 21:40:33 -06:00
Blake Blackshear
afd8aefac2 readme cleanup fixes #332 2021-01-26 21:40:33 -06:00
Blake Blackshear
3c07767138 handle and warn if roles dont match enabled features 2021-01-26 21:40:33 -06:00
Blake Blackshear
953c442f13 camera recommendations 2021-01-26 21:40:33 -06:00
Blake Blackshear
e147852878 catch all psutil errors 2021-01-26 21:40:33 -06:00
Blake Blackshear
db7ee6cfb3 clarify height width and fps 2021-01-26 21:40:33 -06:00
Blake Blackshear
3b41b6cc33 readme updates 2021-01-26 21:40:33 -06:00
Blake Blackshear
5e79888370 tweak screenshots 2021-01-26 21:40:33 -06:00
Blake Blackshear
a6aa9bdd59 readme updates 2021-01-26 21:40:33 -06:00
Blake Blackshear
d78b7cc110 set ffmpeg image versions 2021-01-26 21:40:33 -06:00
Blake Blackshear
e7cdace0ab comment you zeroconf 2021-01-26 21:40:33 -06:00
Blake Blackshear
f60eb4e977 fix flask logger config 2021-01-26 21:40:33 -06:00
Blake Blackshear
7aecf6c6de fix graceful exits 2021-01-26 21:40:33 -06:00
Blake Blackshear
75d62096a6 better exception handling 2021-01-26 21:40:33 -06:00
Blake Blackshear
7c44994070 fix default args 2021-01-26 21:40:33 -06:00
Blake Blackshear
f49f3fd9c3 fix fontconfig issue 2021-01-26 21:40:33 -06:00
Blake Blackshear
5ea86d636c doc updates 2021-01-26 21:40:33 -06:00
Blake Blackshear
4c6e90717a update some default config values 2021-01-26 21:40:33 -06:00
Blake Blackshear
d60ca9d783 log level configuration 2021-01-26 21:40:33 -06:00
Blake Blackshear
d304718ea0 no need to write jpg disk 2021-01-26 21:40:33 -06:00
Blake Blackshear
c787c8948e dont delete the recordings directory 2021-01-26 21:40:33 -06:00
Blake Blackshear
62728ef7fb default save_clips objects 2021-01-26 21:40:33 -06:00
Blake Blackshear
47e256f03d add logging for directory creation 2021-01-26 21:40:33 -06:00
Blake Blackshear
527db52d5e exit on config errors 2021-01-26 21:40:33 -06:00
Blake Blackshear
f78b2c48a7 add zeroconf discovery 2021-01-26 21:40:33 -06:00
Blake Blackshear
90c965a32a optional android notification aspect ratio 2021-01-26 21:40:33 -06:00
Blake Blackshear
d4afcde6c9 reduce min timestamp size 2021-01-26 21:40:33 -06:00
Blake Blackshear
257de89ce4 publish object counts rather than on/off 2021-01-26 21:40:33 -06:00
Blake Blackshear
735cc3962b make directories constants 2021-01-26 21:40:33 -06:00
Blake Blackshear
feb42181de cleanup empty directories 2021-01-26 21:40:33 -06:00
Blake Blackshear
f5c4bfa7b4 serve up recordings with nginx 2021-01-26 21:40:33 -06:00
Blake Blackshear
65ddd91855 add recording maintenance 2021-01-26 21:40:33 -06:00
Blake Blackshear
6d7d838613 add record settings to config 2021-01-26 21:40:33 -06:00
Blake Blackshear
5edf7b7f00 fix log timeout 2021-01-26 21:40:33 -06:00
Blake Blackshear
117569830d ensure zones dont have the same name as a camera 2021-01-26 21:40:33 -06:00
Blake Blackshear
d62aec7287 graceful exit of subprocesses 2021-01-26 21:40:33 -06:00
Blake Blackshear
4e0cf3681e add multiple streams per camera 2021-01-26 21:40:33 -06:00
Blake Blackshear
d98751102a fix fontconfig error 2021-01-26 21:40:33 -06:00
Blake Blackshear
1acbeb813e add support for rebroadcasting as rtmp 2021-01-26 21:40:33 -06:00
Blake Blackshear
b87ec752cf avoid null error 2021-01-26 21:40:33 -06:00
Blake Blackshear
753df31fa6 minimize logging 2021-01-26 21:40:33 -06:00
Blake Blackshear
bd77b74689 oops 2021-01-26 21:40:33 -06:00
Blake Blackshear
810c23d8ee only publish end events for true positives 2021-01-26 21:40:33 -06:00
Blake Blackshear
34d9b2983e ensure all events are cleaned up 2021-01-26 21:40:33 -06:00
Blake Blackshear
63c5c8412a publish events like a change feed 2021-01-26 21:40:33 -06:00
Blake Blackshear
60207723d1 pull from memory if event in progress 2021-01-26 21:40:33 -06:00
Blake Blackshear
f4117ad096 add endpoint for event thumbnail 2021-01-26 21:40:33 -06:00
Blake Blackshear
8bed4e9970 add service to get by id 2021-01-26 21:40:33 -06:00
Blake Blackshear
f72eaf781c add zones to summary data 2021-01-26 21:40:33 -06:00
Blake Blackshear
e9ecc20a36 sleep in the right place 2021-01-26 21:40:33 -06:00
Blake Blackshear
addfa2a32d manage events for unlisted cameras 2021-01-26 21:40:33 -06:00
Blake Blackshear
0dad9bc393 add event cleanup thread 2021-01-26 21:40:33 -06:00
Blake Blackshear
5155875a72 add clip retention to config 2021-01-26 21:40:33 -06:00
Blake Blackshear
4ed1217366 use localtime in group by 2021-01-26 21:40:33 -06:00
Blake Blackshear
50e898a684 new http endpoints 2021-01-26 21:40:33 -06:00
Blake Blackshear
251c7fa982 add parameters to event query 2021-01-26 21:40:33 -06:00
Blake Blackshear
00c75e9f98 only save events when a clip is created 2021-01-26 21:40:33 -06:00
Blake Blackshear
1b5b02d286 add bas64 encoded thumbnail to the database 2021-01-26 21:40:33 -06:00
Blake Blackshear
946d655cee check for None value thumbnail_data 2021-01-26 21:40:33 -06:00
Blake Blackshear
d56710b0b5 only set thumbnail data if object is a true positive 2021-01-26 21:40:33 -06:00
Blake Blackshear
0cf78277b5 add some debug logging to frame cache 2021-01-26 21:40:33 -06:00
Blake Blackshear
ce2a583ff9 dont use a property 2021-01-26 21:40:33 -06:00
Blake Blackshear
84bddad30e attempt to fix missing thumbs 2021-01-26 21:40:33 -06:00
Blake Blackshear
0ff682504a better frame handling for best images 2021-01-26 21:40:33 -06:00
Blake Blackshear
5d5984166f cleanup false_positive attribute 2021-01-26 21:40:33 -06:00
Blake Blackshear
b825eb44fe ensure some valid thumbnail is available 2021-01-26 21:40:33 -06:00
Blake Blackshear
7015eb66f2 don't save thumbnails for false positives 2021-01-26 21:40:33 -06:00
Blake Blackshear
494eeb16a5 cleanup 2021-01-26 21:40:33 -06:00
Blake Blackshear
692fdc8d5d reduce logging 2021-01-26 21:40:33 -06:00
Blake Blackshear
ec1a8ebd4a fixes 2021-01-26 21:40:33 -06:00
Blake Blackshear
59daa6597b update nginx config 2021-01-26 21:40:33 -06:00
Blake Blackshear
3941ce4ad1 stop writing json file to disk 2021-01-26 21:40:33 -06:00
Blake Blackshear
aff87d4372 create tracked object class and save thumbnails 2021-01-26 21:40:33 -06:00
Blake Blackshear
373ca87887 maintain thumbnail frames for tracked objects 2021-01-26 21:40:33 -06:00
Blake Blackshear
03c855ecbe sort imports 2021-01-26 21:40:33 -06:00
Blake Blackshear
3a3cb24631 naming threads and processes for logs 2021-01-26 21:40:33 -06:00
Blake Blackshear
4c3fea25a5 use a queue for logging 2021-01-26 21:40:33 -06:00
Blake Blackshear
af303cbf2a create typed config classes 2021-01-26 21:40:33 -06:00
Blake Blackshear
b7c09a9b38 add nginx and change default file locations 2021-01-26 21:40:33 -06:00
Blake Blackshear
eced06eea8 config setup 2021-01-26 21:40:33 -06:00
Blake Blackshear
15d989255c add watchdog 2021-01-26 21:40:33 -06:00
Blake Blackshear
095566b9c2 add back all endpoints 2021-01-26 21:40:33 -06:00
Blake Blackshear
b77a65d446 add event processor 2021-01-26 21:40:33 -06:00
Blake Blackshear
9778a748fc add capture processes 2021-01-26 21:40:33 -06:00
Blake Blackshear
a89dddcafa add camera processors 2021-01-26 21:40:33 -06:00
Blake Blackshear
75973fd4c0 add detected_frames_processor 2021-01-26 21:40:33 -06:00
Blake Blackshear
514036f9d1 add detector processes 2021-01-26 21:40:33 -06:00
Blake Blackshear
36fbedab20 init db/http/mqtt 2021-01-26 21:40:33 -06:00
Blake Blackshear
180baeba50 app container and config schema 2021-01-26 21:40:33 -06:00
Blake Blackshear
cce82fe2a5 move primary script into the module 2021-01-26 21:40:33 -06:00
Blake Blackshear
5512bb2e06 saving events and simple endpoint 2021-01-26 21:40:33 -06:00
Blake Blackshear
be1fcbbdf8 basic database model and api endpoint 2021-01-26 21:40:33 -06:00
Blake Blackshear
422cd52049 store events in tinydb 2021-01-26 21:40:33 -06:00
Blake Blackshear
d67a56d37e update events model 2021-01-26 21:40:33 -06:00
Marc Seeger
070c9721b6 Add support for AMD Ryzen iGPU (fixes #311)
This package will add support for the iGPU of AMD Ryzen and presumably a few more AMD cards.
See details of the package here: https://packages.ubuntu.com/focal/mesa-va-drivers
It also adds support for the open source Nvidia Nouveau driver according to https://wiki.debian.org/HardwareVideoAcceleration
2021-01-26 21:40:33 -06:00
Michael Wei
0219834dd1 Use cv2.bitwise_and instead of numpy.where 2021-01-26 21:40:33 -06:00
164 changed files with 28979 additions and 6559 deletions

4
.github/FUNDING.yml vendored
View File

@@ -1 +1,3 @@
github: blakeblackshear
github:
- blakeblackshear
- paularmstrong

View File

@@ -1,6 +1,6 @@
---
name: Bug report or Support request
about: ''
about: Bug report or Support request
title: ''
labels: ''
assignees: ''
@@ -11,7 +11,7 @@ assignees: ''
A clear and concise description of what your issue is.
**Version of frigate**
Output from `/version`
Output from `/api/version`
**Config file**
Include your full config file wrapped in triple back ticks.
@@ -26,7 +26,7 @@ Include relevant log output here
**Frigate stats**
```json
Output from frigate's /stats endpoint
Output from frigate's /api/stats endpoint
```
**FFprobe from your camera**

17
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 3
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

46
.github/workflows/pull_request.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: On pull request
on: pull_request
jobs:
web_lint:
name: Web - Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
- run: npm install
working-directory: ./web
- name: Lint
run: npm run lint:cmd
working-directory: ./web
web_build:
name: Web - Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
- run: npm install
working-directory: ./web
- name: Build
run: npm run build
working-directory: ./web
web_test:
name: Web - Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/setup-node@master
with:
node-version: 14.x
- run: npm install
working-directory: ./web
- name: Test
run: npm run test
working-directory: ./web

28
.github/workflows/push.yml vendored Normal file
View File

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

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ models
frigate/version.py
web/build
web/node_modules
web/coverage

View File

@@ -3,55 +3,55 @@ default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version:
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
echo "VERSION='0.8.4-$(COMMIT_HASH)'" > frigate/version.py
web:
docker build --tag frigate-web --file docker/Dockerfile.web web/
amd64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.1-amd64 --file docker/Dockerfile.wheels .
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64 --file docker/Dockerfile.wheels .
amd64_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.1.0-amd64 --file docker/Dockerfile.ffmpeg.amd64 .
amd64_frigate: version web
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
docker build --tag frigate-base --build-arg ARCH=amd64 --build-arg FFMPEG_VERSION=1.1.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.amd64 .
amd64_all: amd64_wheels amd64_ffmpeg amd64_frigate
amd64nvidia_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.1-amd64nvidia --file docker/Dockerfile.wheels .
docker build --tag blakeblackshear/frigate-wheels:1.0.3-amd64nvidia --file docker/Dockerfile.wheels .
amd64nvidia_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-amd64nvidia --file docker/Dockerfile.ffmpeg.amd64nvidia .
amd64nvidia_frigate: version web
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
docker build --tag frigate-base --build-arg ARCH=amd64nvidia --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.amd64nvidia .
amd64nvidia_all: amd64nvidia_wheels amd64nvidia_ffmpeg amd64nvidia_frigate
aarch64_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.1-aarch64 --file docker/Dockerfile.wheels .
docker build --tag blakeblackshear/frigate-wheels:1.0.3-aarch64 --file docker/Dockerfile.wheels .
aarch64_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-aarch64 --file docker/Dockerfile.ffmpeg.aarch64 .
aarch64_frigate: version web
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
docker build --tag frigate-base --build-arg ARCH=aarch64 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.aarch64 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate
armv7_wheels:
docker build --tag blakeblackshear/frigate-wheels:1.0.1-armv7 --file docker/Dockerfile.wheels .
docker build --tag blakeblackshear/frigate-wheels:1.0.3-armv7 --file docker/Dockerfile.wheels .
armv7_ffmpeg:
docker build --tag blakeblackshear/frigate-ffmpeg:1.0.0-armv7 --file docker/Dockerfile.ffmpeg.armv7 .
armv7_frigate: version web
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.1 --file docker/Dockerfile.base .
docker build --tag frigate-base --build-arg ARCH=armv7 --build-arg FFMPEG_VERSION=1.0.0 --build-arg WHEELS_VERSION=1.0.3 --file docker/Dockerfile.base .
docker build --tag frigate --file docker/Dockerfile.armv7 .
armv7_all: armv7_wheels armv7_ffmpeg armv7_frigate

1116
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,10 @@ RUN apt-get -qq update \
RUN pip3 install \
peewee_migrate \
zeroconf \
voluptuous
voluptuous\
Flask-Sockets \
gevent \
gevent-websocket
COPY nginx/nginx.conf /etc/nginx/nginx.conf

View File

@@ -24,8 +24,7 @@ RUN pip3 install scikit-build
RUN pip3 wheel --wheel-dir=/wheels \
opencv-python-headless \
# pinning due to issue in 1.19.5 https://github.com/numpy/numpy/issues/18131
numpy==1.19.4 \
numpy \
imutils \
scipy \
psutil \
@@ -35,7 +34,8 @@ RUN pip3 wheel --wheel-dir=/wheels \
matplotlib \
click \
setproctitle \
peewee
peewee \
gevent
FROM scratch

20
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

5
docs/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Website
This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
For installation and contributing instructions, please follow the [Contributing Docs](https://blakeblackshear.github.io/frigate/contributing).

3
docs/babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

View File

@@ -1,42 +0,0 @@
# Camera Specific Configuration
Frigate should work with most RTSP cameras and h264 feeds such as Dahua.
## 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'
```
## 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

@@ -0,0 +1,139 @@
---
id: advanced
title: Advanced
sidebar_label: Advanced
---
## 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
# 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)
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense of higher CPU usage.
# Lower values result in less CPU, but small changes may not register as motion.
frame_height: 180
```
### `detect`
Global object detection settings. These may also be defined at the camera level.
```yaml
detect:
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
max_disappeared: 25
```
### `logger`
Change the default log level for troubleshooting purposes.
```yaml
logger:
# Optional: default log level (default: shown below)
default: info
# Optional: module by module log level configuration
logs:
frigate.mqtt: error
```
Available log levels are: `debug`, `info`, `warning`, `error`, `critical`
Examples of available modules are:
- `frigate.app`
- `frigate.mqtt`
- `frigate.edgetpu`
- `frigate.zeroconf`
- `detector.<detector_name>`
- `watchdog.<camera_name>`
- `ffmpeg.<camera_name>.<sorted_roles>` NOTE: All FFmpeg logs are sent as `error` level.
### `environment_vars`
This section can be used to set environment variables for those unable to modify the environment of the container (ie. within Hass.io)
```yaml
environment_vars:
EXAMPLE_VAR: value
```
### `database`
Event and clip information is managed in a sqlite database at `/media/frigate/clips/frigate.db`. If that database is deleted, clips will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within HomeAssistant.
If you are storing your clips on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database in the config if necessary.
This may need to be in a custom location if network storage is used for clips.
```yaml
database:
path: /media/frigate/clips/frigate.db
```
### `detectors`
```yaml
detectors:
# Required: name of the detector
coral:
# Required: type of the detector
# Valid values are 'edgetpu' (requires device property below) and 'cpu'. type: edgetpu
# Optional: device name as defined here: https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
device: usb
# Optional: num_threads value passed to the tflite.Interpreter (default: shown below)
# This value is only used for CPU types
num_threads: 3
```
### `model`
```yaml
model:
# Required: height of the trained model
height: 320
# Required: width of the trained model
width: 320
```
## Custom Models
Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use your own models with volume mounts:
- CPU Model: `/cpu_model.tflite`
- EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt`
You also need to update the model width/height in the config if they differ from the defaults.
### Customizing the Labelmap
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. You must retain the same number of labels, but you can change the names. To change:
- Download the [COCO labelmap](https://dl.google.com/coral/canned_models/coco_labels.txt)
- Modify the label names as desired. For example, change `7 truck` to `7 car`
- Mount the new file at `/labelmap.txt` in the container with an additional volume
```
-v ./config/labelmap.txt:/labelmap.txt
```

View File

@@ -0,0 +1,452 @@
---
id: cameras
title: Cameras
---
## Setting Up Camera Inputs
Up to 4 inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create clips from a higher resolution stream, or vice versa.
Each role can only be assigned to one input per camera. The options for roles are as follows:
| Role | Description |
| -------- | ------------------------------------------------------------------------------------ |
| `detect` | Main feed for object detection |
| `clips` | Clips of events from objects detected in the `detect` feed. [docs](#recording-clips) |
| `record` | Saves 60 second segments of the video feed. [docs](#247-recordings) |
| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](#rtmp-streams) |
### Example
```yaml
mqtt:
host: mqtt.server.com
cameras:
back:
ffmpeg:
inputs:
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
roles:
- detect
- rtmp
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/live
roles:
- clips
- record
width: 1280
height: 720
fps: 5
```
## Masks & Zones
### Masks
Masks are used to ignore initial detection in areas of your camera's field of view.
There are two types of masks available:
- **Motion masks**: Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the video feed with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. Over masking will make it more difficult for objects to be tracked. To see this effect, create a mask, and then watch the video feed with `Motion Boxes` enabled again.
- **Object filter masks**: Object filter masks are used to filter out false positives for a given object type. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell frigate that anything in your yard is a false positive.
To create a poly mask:
1. Visit the [web UI](/usage/web)
1. Click the camera you wish to create a mask for
1. Click "Mask & Zone creator"
1. Click "Add" on the type of mask or zone you would like to create
1. Click on the camera's latest image to create a masked area. The yaml representation will be updated in real-time
1. When you've finished creating your mask, click "Copy" and paste the contents into your `config.yaml` file and restart Frigate
Example of a finished row corresponding to the below example image:
```yaml
motion:
mask: '0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432'
```
![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: 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
```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
```
## Clips
Frigate can save video clips without any CPU overhead for encoding by simply copying the stream directly with FFmpeg. It leverages FFmpeg's segment functionality to maintain a cache of video for each camera. The cache files are written to disk at `/tmp/cache` and do not introduce memory overhead. When an object is being tracked, it will extend the cache to ensure it can assemble a clip when the event ends. Once the event ends, it again uses FFmpeg to assemble a clip by combining the video clips without any encoding by the CPU. Assembled clips are are saved to `/media/frigate/clips`. Clips are retained according to the retention settings defined on the config for each object type.
These clips will not be playable in the web UI or in HomeAssistant'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
clips:
# Required: enables clips for the camera (default: shown below)
# This value can be set via MQTT and will be updated in startup based on retained value
enabled: False
# Optional: Number of seconds before the event to include in the clips (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include in the clips (default: shown below)
post_capture: 5
# Optional: Objects to save clips for. (default: all tracked objects)
objects:
- person
# Optional: Restrict clips to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Camera override for retention settings (default: global values)
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Per object retention days
objects:
person: 15
```
## 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: 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
```
## 24/7 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 HomeAssistant's media browser. Each camera supports a configurable retention policy in the config.
:::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
# 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
```
## RTMP streams
Frigate can re-stream your video feed as a RTMP feed for other applications such as HomeAssistant 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 HomeAssistant 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.
## Full example
The following is a full example of all of the options together for a camera configuration
```yaml
cameras:
# Required: name of the camera
back:
# Required: ffmpeg settings for the camera
ffmpeg:
# Required: A list of input streams for the camera. See documentation for more information.
inputs:
# Required: the path to the stream
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
# Required: list of roles for this stream. valid values are: detect,record,clips,rtmp
# NOTICE: In addition to assigning the record, clips, and rtmp roles,
# they must also be enabled in the camera config.
roles:
- detect
- rtmp
# Optional: stream specific global args (default: inherit)
global_args:
# Optional: stream specific hwaccel args (default: inherit)
hwaccel_args:
# Optional: stream specific input args (default: inherit)
input_args:
# Optional: camera specific global args (default: inherit)
global_args:
# Optional: camera specific hwaccel args (default: inherit)
hwaccel_args:
# Optional: camera specific input args (default: inherit)
input_args:
# Optional: camera specific output args (default: inherit)
output_args:
# Required: width of the frame for the input with the detect role
width: 1280
# Required: height of the frame for the input with the detect role
height: 720
# Optional: desired fps for your camera for the input with the detect role
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
# Frigate will attempt to autodetect if not specified.
fps: 5
# Optional: camera level motion config
motion:
# Optional: motion mask
# NOTE: see docs for more detailed info on creating masks
mask: 0,900,1080,900,1080,1920,0,1920
# Optional: timeout for highest scoring image before allowing it
# to be replaced by a newer image. (default: shown below)
best_image_timeout: 60
# Optional: zones for this camera
zones:
# Required: name of the zone
# NOTE: This must be different than any camera names, but can match with another zone on another
# camera.
front_steps:
# Required: List of x,y coordinates to define the polygon of the zone.
# NOTE: Coordinates can be generated at https://www.image-map.net/
coordinates: 545,1077,747,939,788,805
# Optional: Zone level object filters.
# NOTE: The global and camera filters are applied upstream.
filters:
person:
min_area: 5000
max_area: 100000
threshold: 0.7
# Optional: Camera level detect settings
detect:
# Optional: enables detection for the camera (default: True)
# This value can be set via MQTT and will be updated in startup based on retained value
enabled: True
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
max_disappeared: 25
# Optional: save clips configuration
clips:
# Required: enables clips for the camera (default: shown below)
# This value can be set via MQTT and will be updated in startup based on retained value
enabled: False
# Optional: Number of seconds before the event to include in the clips (default: shown below)
pre_capture: 5
# Optional: Number of seconds after the event to include in the clips (default: shown below)
post_capture: 5
# Optional: Objects to save clips for. (default: all tracked objects)
objects:
- person
# Optional: Restrict clips to objects that entered any of the listed zones (default: no required zones)
required_zones: []
# Optional: Camera override for retention settings (default: global values)
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Per object retention days
objects:
person: 15
# Optional: 24/7 recording configuration
record:
# Optional: Enable recording (default: global setting)
enabled: False
# Optional: Number of days to retain (default: global setting)
retain_days: 30
# Optional: RTMP re-stream configuration
rtmp:
# Required: Enable the live stream (default: True)
enabled: True
# 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: 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
```
## Camera specific configuration
### 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

@@ -0,0 +1,53 @@
---
id: detectors
title: Detectors
---
The default config will look for a USB Coral device. If you do not have a Coral, you will need to configure a CPU detector. If you have PCI or multiple Coral devices, you need to configure your detector devices in the config file. When using multiple detectors, they run in dedicated processes, but pull from a common queue of requested detections across all cameras.
Frigate supports `edgetpu` and `cpu` as detector types. The device value should be specified according to the [Documentation for the TensorFlow Lite Python API](https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api).
**Note**: There is no support for Nvidia GPUs to perform object detection with tensorflow. It can be used for ffmpeg decoding, but not object detection.
Single USB Coral:
```yaml
detectors:
coral:
type: edgetpu
device: usb
```
Multiple USB Corals:
```yaml
detectors:
coral1:
type: edgetpu
device: usb:0
coral2:
type: edgetpu
device: usb:1
```
Mixing Corals:
```yaml
detectors:
coral_usb:
type: edgetpu
device: usb
coral_pci:
type: edgetpu
device: pci
```
CPU Detectors (not recommended):
```yaml
detectors:
cpu1:
type: cpu
cpu2:
type: cpu
```

View File

@@ -0,0 +1,19 @@
---
id: false_positives
title: Reducing false positives
---
Tune your object filters to adjust false positives: `min_area`, `max_area`, `min_score`, `threshold`.
For object filters in your configuration, any single detection below `min_score` will be ignored as a false positive. `threshold` is based on the median of the history of scores (padded to 3 values) for a tracked object. Consider the following frames when `min_score` is set to 0.6 and threshold is set to 0.85:
| Frame | Current Score | Score History | Computed Score | Detected Object |
| ----- | ------------- | --------------------------------- | -------------- | --------------- |
| 1 | 0.7 | 0.0, 0, 0.7 | 0.0 | No |
| 2 | 0.55 | 0.0, 0.7, 0.0 | 0.0 | No |
| 3 | 0.85 | 0.7, 0.0, 0.85 | 0.7 | No |
| 4 | 0.90 | 0.7, 0.85, 0.95, 0.90 | 0.875 | Yes |
| 5 | 0.88 | 0.7, 0.85, 0.95, 0.90, 0.88 | 0.88 | Yes |
| 6 | 0.95 | 0.7, 0.85, 0.95, 0.90, 0.88, 0.95 | 0.89 | Yes |
In frame 2, the score is below the `min_score` value, so frigate ignores it and it becomes a 0.0. The computed score is the median of the score history (padding to at least 3 values), and only when that computed score crosses the `threshold` is the object marked as a true positive. That happens in frame 4 in the example.

View File

@@ -0,0 +1,138 @@
---
id: index
title: Configuration
---
HassOS users can manage their configuration directly in the addon Configuration tab. For 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).
It is recommended to start with a minimal configuration and add to it:
```yaml
mqtt:
host: mqtt.server.com
cameras:
back:
ffmpeg:
inputs:
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
roles:
- detect
- rtmp
width: 1280
height: 720
fps: 5
```
## Required
## `mqtt`
```yaml
mqtt:
# Required: host name
host: mqtt.server.com
# Optional: port (default: shown below)
port: 1883
# Optional: topic prefix (default: shown below)
# WARNING: must be unique if you are running multiple instances
topic_prefix: frigate
# Optional: client id (default: shown below)
# WARNING: must be unique if you are running multiple instances
client_id: frigate
# Optional: user
user: mqtt_user
# Optional: password
# NOTE: Environment variables that begin with 'FRIGATE_' may be referenced in {}.
# eg. password: '{FRIGATE_MQTT_PASSWORD}'
password: password
# Optional: interval in seconds for publishing stats (default: shown below)
stats_interval: 60
```
## `cameras`
Each of your cameras must be configured. The following is the minimum required to register a camera in Frigate. Check the [camera configuration page](cameras.md) for a complete list of options.
```yaml
cameras:
# Name of your camera
front_door:
ffmpeg:
inputs:
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
roles:
- detect
- rtmp
width: 1280
height: 720
fps: 5
```
## Optional
### `clips`
```yaml
clips:
# 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.
max_seconds: 300
# Optional: size of tmpfs mount to create for cache files (default: not set)
# mount -t tmpfs -o size={tmpfs_cache_size} tmpfs /tmp/cache
# NOTICE: Addon users must have Protection mode disabled for the addon when using this setting.
# Also, if you have mounted a tmpfs volume through docker, this value should not be set in your config.
tmpfs_cache_size: 256m
# Optional: Retention settings for clips (default: shown below)
retain:
# Required: Default retention days (default: shown below)
default: 10
# Optional: Per object retention days
objects:
person: 15
```
### `ffmpeg`
```yaml
ffmpeg:
# Optional: global ffmpeg args (default: shown below)
global_args: -hide_banner -loglevel warning
# Optional: global hwaccel args (default: shown below)
# NOTE: See hardware acceleration docs for your specific device
hwaccel_args: []
# Optional: global input args (default: shown below)
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -stimeout 5000000 -use_wallclock_as_timestamps 1
# Optional: global output args
output_args:
# Optional: output args for detect streams (default: shown below)
detect: -f rawvideo -pix_fmt yuv420p
# Optional: output args for record streams (default: shown below)
record: -f segment -segment_time 60 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
# Optional: output args for clips streams (default: shown below)
clips: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
# Optional: output args for rtmp streams (default: shown below)
rtmp: -c copy -f flv
```
### `objects`
Can be overridden at the camera level
```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
```

View File

@@ -1,14 +1,18 @@
# nVidia hardware decoder (NVDEC)
---
id: nvdec
title: nVidia hardware decoder
---
Certain nvidia cards include a hardware decoder, which can greatly improve the
performance of video decoding. In order to use NVDEC, a special build of
performance of video decoding. In order to use NVDEC, a special build of
ffmpeg with NVDEC support is required. The special docker architecture 'amd64nvidia'
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.
## Docker setup
### Requirements
[nVidia closed source driver](https://www.nvidia.com/en-us/drivers/unix/) required to access NVDEC.
[nvidia-docker](https://github.com/NVIDIA/nvidia-docker) required to pass NVDEC to docker.
@@ -18,6 +22,7 @@ In order to pass NVDEC, the docker engine must be set to `nvidia` and the enviro
`NVIDIA_VISIBLE_DEVICES=all` and `NVIDIA_DRIVER_CAPABILITIES=compute,utility,video` must be set.
In a docker compose file, these lines need to be set:
```
services:
frigate:
@@ -26,15 +31,16 @@ services:
runtime: nvidia
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
- NVIDIA_DRIVER_CAPABILITIES=compute,utility,video
```
### Setting up the configuration file
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
The decoder you choose will depend on the input video.
In your frigate config.yml, you'll need to set ffmpeg to use the hardware decoder.
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)
```
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
@@ -46,7 +52,7 @@ A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the c
V..... vc1_cuvid Nvidia CUVID VC1 decoder (codec vc1)
V..... vp8_cuvid Nvidia CUVID VP8 decoder (codec vp8)
V..... vp9_cuvid Nvidia CUVID VP9 decoder (codec vp9)
```
```
For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
`-c:v hevc_covid` to your ffmpeg input arguments:
@@ -55,7 +61,7 @@ For example, for H265 video (hevc), you'll select `hevc_cuvid`. Add
ffmpeg:
input_args:
...
- -c:v
- -c:v
- hevc_cuvid
```
@@ -75,7 +81,7 @@ processes:
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
@@ -96,10 +102,9 @@ using the fps filter:
```
output_args:
- -filter:v
- -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

@@ -0,0 +1,72 @@
---
id: optimizing
title: Optimizing performance
---
- **Google Coral**: It is strongly recommended to use a Google Coral, but Frigate will 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.

131
docs/docs/contributing.md Normal file
View File

@@ -0,0 +1,131 @@
---
id: contributing
title: Contributing
---
## Getting the source
### Core, Web, Docker, and Documentation
This repository holds the main Frigate application and all of its dependencies.
Fork [blakeblackshear/frigate](https://github.com/blakeblackshear/frigate.git) to your own GitHub profile, then clone the forked repo to your local machine.
From here, follow the guides for:
- [Core](#core)
- [Web Interface](#web-interface)
- [Documentation](#documentation)
### Frigate Home Assistant Addon
This repository holds the Home Assistant Addon, for use with Home Assistant OS and compatible installations. It is the piece that allows you to run Frigate from your Home Assistant Supervisor tab.
Fork [blakeblackshear/frigate-hass-addons](https://github.com/blakeblackshear/frigate-hass-addons) to your own Github profile, then clone the forked repo to your local machine.
### Frigate Home Assistant Integration
This repository holds the custom integration that allows your Home Assistant installation to automatically create entities for your Frigate instance, whether you run that with the [addon](#frigate-home-assistant-addon) or in a separate Docker instance.
Fork [blakeblackshear/frigate-hass-integration](https://github.com/blakeblackshear/frigate-hass-integration) to your own GitHub profile, then clone the forked repo to your local machine.
## Core
### Prerequisites
- [Frigate source code](#frigate-core-web-and-docs)
- GNU make
- Docker
## Web Interface
### Prerequisites
- [Frigate source code](#frigate-core-web-and-docs)
- All [core](#core) prerequisites _or_ another running Frigate instance locally available
- Node.js 14
### Making changes
#### 1. Set up a Frigate instance
The Web UI requires an instance of Frigate to interact with for all of its data. You can either run an instance locally (recommended) or attach to a separate instance accessible on your network.
To run the local instance, follow the [core](#core) development instructions.
If you won't be making any changes to the Frigate HTTP API, you can attach the web development server to any Frigate instance on your network. Skip this step and go to [3a](#3a-run-the-development-server-against-a-non-local-instance).
#### 2. Install dependencies
```console
cd web && npm install
```
#### 3. Run the development server
```console
cd web && npm run start
```
#### 3a. Run the development server against a non-local instance
To run the development server against a non-local instance, you will need to provide an environment variable, `SNOWPACK_PUBLIC_API_HOST` that tells the web application how to connect to the Frigate API:
```console
cd web && SNOWPACK_PUBLIC_API_HOST=http://<ip-address-to-your-frigate-instance>:5000 npm run start
```
#### 4. Making changes
The Web UI is built using [Snowpack](https://www.snowpack.dev/), [Preact](https://preactjs.com), and [Tailwind CSS](https://tailwindcss.com).
Light guidelines and advice:
- Avoid adding more dependencies. The web UI intends to be lightweight and fast to load.
- Do not make large sweeping changes. [Open a discussion on GitHub](https://github.com/blakeblackshear/frigate/discussions/new) for any large or architectural ideas.
- Ensure `lint` passes. This command will ensure basic conformance to styles, applying as many automatic fixes as possible, including Prettier formatting.
```console
npm run lint
```
- Add to unit tests and ensure they pass. As much as possible, you should strive to _increase_ test coverage whenever making changes. This will help ensure features do not accidentally become broken in the future.
```console
npm run test
```
- Test in different browsers. Firefox, Chrome, and Safari all have different quirks that make them unique targets to interact with.
## Documentation
### Prerequisites
- [Frigate source code](#frigate-core-web-and-docs)
- Node.js 14
### Making changes
#### 1. Installation
```console
npm run install
```
#### 2. Local Development
```console
npm run start
```
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
The docs are built using [Docusaurus v2](https://v2.docusaurus.io). Please refer to the Docusaurus docs for more information on how to modify Frigate's documentation.
#### 3. Build (optional)
```console
npm run build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.

29
docs/docs/hardware.md Normal file
View File

@@ -0,0 +1,29 @@
---
id: hardware
title: Recommended hardware
---
## Cameras
Cameras that output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and HomeAssistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, clips, and recordings without re-encoding.
## Computer
| Name | Inference Speed | Notes |
| ----------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Atomic Pi | 16ms | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
| 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
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)
| Name | Inference Speed | Notes |
| ----------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [M2 Coral Edge TPU](http://coral.ai) | 6.2ms | Little complicated to get installed, as needs drivers on the host OS, [info here](https://forums.unraid.net/topic/98064-support-blakeblackshear-frigate/?do=findComment&comment=945776) |

13
docs/docs/how-it-works.md Normal file
View File

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

25
docs/docs/index.md Normal file
View File

@@ -0,0 +1,25 @@
---
id: index
title: Frigate
sidebar_label: Features
slug: /
---
A complete and local NVR designed for HomeAssistant 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.
- Tight integration with HomeAssistant 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
- Leverages multiprocessing heavily with an emphasis on realtime over processing every frame
- Uses a very low overhead motion detection to determine where to run object detection
- Object detection with TensorFlow runs in separate processes for maximum FPS
- Communicates over MQTT for easy integration into other systems
- 24/7 recording
- Re-streaming via RTMP to reduce the number of connections to your camera
## Screenshots
![Media Browser](/img/media_browser.png)
![Notification](/img/notification.png)

123
docs/docs/installation.md Normal file
View File

@@ -0,0 +1,123 @@
---
id: installation
title: Installation
---
Frigate is a Docker container that can be run on any Docker host including as a [HassOS Addon](https://www.home-assistant.io/addons/). See instructions below for installing the HassOS addon.
For HomeAssistant users, there is also a [custom component (aka integration)](https://github.com/blakeblackshear/frigate-hass-integration). This custom component adds tighter integration with HomeAssistant by automatically setting up camera entities, sensors, media browser for clips and recordings, and a public API to simplify notifications.
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.
## HassOS Addon
HassOS users can install via the addon repository. Frigate requires an MQTT server.
1. Navigate to Supervisor > Add-on Store > Repositories
1. Add https://github.com/blakeblackshear/frigate-hass-addons
1. Setup your configuration in the `Configuration` tab
1. Start the addon container
1. If you are using hardware acceleration for ffmpeg, you will need to disable "Protection mode"
## Docker
Make sure you choose the right image for your architecture:
|Arch|Image Name|
|-|-|
|amd64|blakeblackshear/frigate:stable-amd64|
|amd64nvidia|blakeblackshear/frigate:stable-amd64nvidia|
|armv7|blakeblackshear/frigate:stable-armv7|
|aarch64|blakeblackshear/frigate:stable-aarch64|
It is recommended to run with docker-compose:
```yaml
version: '3.9'
services:
frigate:
container_name: frigate
privileged: true # this may not be necessary for all setups
restart: unless-stopped
image: blakeblackshear/frigate:<specify_version_tag>
devices:
- /dev/bus/usb:/dev/bus/usb
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
volumes:
- /etc/localtime:/etc/localtime:ro
- <path_to_config_file>:/config/config.yml:ro
- <path_to_directory_for_media>:/media/frigate
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
target: /tmp/cache
tmpfs:
size: 1000000000
ports:
- '5000:5000'
- '1935:1935' # RTMP feeds
environment:
FRIGATE_RTSP_PASSWORD: 'password'
```
If you can't use docker compose, you can run the container with something similar to this:
```bash
docker run -d \
--name frigate \
--restart=unless-stopped \
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \
--device /dev/bus/usb:/dev/bus/usb \
--device /dev/dri/renderD128
-v <path_to_directory_for_media>:/media/frigate \
-v <path_to_config_file>:/config/config.yml:ro \
-v /etc/localtime:/etc/localtime:ro \
-e FRIGATE_RTSP_PASSWORD='password' \
-p 5000:5000 \
-p 1935:1935 \
blakeblackshear/frigate:<specify_version_tag>
```
### Calculating shm-size
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.
You can calculate the necessary shm-size for each camera with the following formula:
```
(width * height * 1.5 * 7 + 270480)/1048576 = <shm size in mb>
```
The shm size cannot be set per container for HomeAssistant 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
## Kubernetes
Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate).
## Virtualization
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.
### Proxmox
Some people have had success running Frigate in LXC directly with the following config:
```
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).

17
docs/docs/mdx.md Normal file
View File

@@ -0,0 +1,17 @@
---
id: mdx
title: Powered by MDX
---
You can write JSX and use React components within your Markdown thanks to [MDX](https://mdxjs.com/).
export const Highlight = ({children, color}) => ( <span style={{
backgroundColor: color,
borderRadius: '2px',
color: '#fff',
padding: '0.2rem',
}}>{children}</span> );
<Highlight color="#25c2a0">Docusaurus green</Highlight> and <Highlight color="#1877F2">Facebook blue</Highlight> are my favorite colors.
I can write **Markdown** alongside my _JSX_!

View File

@@ -0,0 +1,28 @@
---
id: troubleshooting
title: Troubleshooting and FAQ
---
### How can I get sound or audio in my clips and recordings?
By default, Frigate removes audio from clips and recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/frigate/configuration/index#ffmpeg).
### My mjpeg stream or snapshots look green and crazy
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
![mismatched-resolution](/img/mismatched-resolution.jpg)
### I have clips and snapshots in my clips folder, but I can't view them in the Web UI.
This is usually caused one of two things:
- The permissions on the parent folder don't have execute and nginx returns a 403 error you can see in the browser logs
- In this case, try mounting a volume to `/media/frigate` inside the container instead of `/media/frigate/clips`.
- Your cameras do not send h264 encoded video and the mp4 files are not playable in the browser
### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
These messages in the logs are expected in certain situations. Frigate checks the integrity of the video cache before assembling clips. Occasionally these cached files will be invalid and cleaned up automatically.
### "On connect called"
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.

208
docs/docs/usage/api.md Normal file
View File

@@ -0,0 +1,208 @@
---
id: api
title: HTTP API
---
A web server is available on port 5000 with the following endpoints.
### `/api/<camera_name>`
An mjpeg stream for debugging. Keep in mind the mjpeg endpoint is for debugging only and will put additional load on the system when in use.
Accepts the following query string parameters:
| param | Type | Description |
| ----------- | ---- | ------------------------------------------------------------------ |
| `fps` | int | Frame rate |
| `h` | int | Height in pixels |
| `bbox` | int | Show bounding boxes for detected objects (0 or 1) |
| `timestamp` | int | Print the timestamp in the upper left (0 or 1) |
| `zones` | int | Draw the zones on the image (0 or 1) |
| `mask` | int | Overlay the mask on the image (0 or 1) |
| `motion` | int | Draw blue boxes for areas with detected motion (0 or 1) |
| `regions` | int | Draw green boxes for areas where object detection was run (0 or 1) |
You can access a higher resolution mjpeg stream by appending `h=height-in-pixels` to the endpoint. For example `http://localhost:5000/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/back?fps=10` or both with `?fps=10&h=1000`.
### `/api/<camera_name>/<object_name>/best.jpg[?h=300&crop=1]`
The best snapshot for any object type. It is a full resolution image by default.
Example parameters:
- `h=300`: resizes the image to 300 pixes tall
- `crop=1`: crops the image to the region of the detection rather than returning the entire image
### `/api/<camera_name>/latest.jpg[?h=300]`
The most recent frame that frigate has finished processing. It is a full resolution image by default.
Accepts the following query string parameters:
| param | Type | Description |
| ----------- | ---- | ------------------------------------------------------------------ |
| `h` | int | Height in pixels |
| `bbox` | int | Show bounding boxes for detected objects (0 or 1) |
| `timestamp` | int | Print the timestamp in the upper left (0 or 1) |
| `zones` | int | Draw the zones on the image (0 or 1) |
| `mask` | int | Overlay the mask on the image (0 or 1) |
| `motion` | int | Draw blue boxes for areas with detected motion (0 or 1) |
| `regions` | int | Draw green boxes for areas where object detection was run (0 or 1) |
Example parameters:
- `h=300`: resizes the image to 300 pixes tall
### `/api/stats`
Contains some granular debug info that can be used for sensors in HomeAssistant.
Sample response:
```json
{
/* Per Camera Stats */
"back": {
/***************
* Frames per second being consumed from your camera. If this is higher
* than it is supposed to be, you should set -r FPS in your input_args.
* camera_fps = process_fps + skipped_fps
***************/
"camera_fps": 5.0,
/***************
* Number of times detection is run per second. This can be higher than
* your camera FPS because frigate often looks at the same frame multiple times
* or in multiple locations
***************/
"detection_fps": 1.5,
/***************
* PID for the ffmpeg process that consumes this camera
***************/
"capture_pid": 27,
/***************
* PID for the process that runs detection for this camera
***************/
"pid": 34,
/***************
* Frames per second being processed by frigate.
***************/
"process_fps": 5.1,
/***************
* Frames per second skip for processing by frigate.
***************/
"skipped_fps": 0.0
},
/***************
* Sum of detection_fps across all cameras and detectors.
* This should be the sum of all detection_fps values from cameras.
***************/
"detection_fps": 5.0,
/* Detectors Stats */
"detectors": {
"coral": {
/***************
* Timestamp when object detection started. If this value stays non-zero and constant
* for a long time, that means the detection process is stuck.
***************/
"detection_start": 0.0,
/***************
* Time spent running object detection in milliseconds.
***************/
"inference_speed": 10.48,
/***************
* PID for the shared process that runs object detection on the Coral.
***************/
"pid": 25321
}
},
"service": {
/* Uptime in seconds */
"uptime": 10,
"version": "0.8.0-8883709",
/* Storage data in MB for important locations */
"storage": {
"/media/frigate/clips": {
"total": 1000,
"used": 700,
"free": 300,
"mnt_type": "ext4",
},
"/media/frigate/recordings": {
"total": 1000,
"used": 700,
"free": 300,
"mnt_type": "ext4",
},
"/tmp/cache": {
"total": 256,
"used": 100,
"free": 156,
"mnt_type": "tmpfs",
},
"/dev/shm": {
"total": 256,
"used": 100,
"free": 156,
"mnt_type": "tmpfs",
},
}
}
}
```
### `/api/config`
A json representation of your configuration
### `/api/version`
Version info
### `/api/events`
Events from the database. Accepts the following query string parameters:
| param | Type | Description |
| -------------------- | ---- | --------------------------------------------- |
| `before` | int | Epoch time |
| `after` | int | Epoch time |
| `camera` | str | Camera name |
| `label` | str | Label name |
| `zone` | str | Zone name |
| `limit` | int | Limit the number of events returned |
| `has_snapshot` | int | Filter to events that have snapshots (0 or 1) |
| `has_clip` | int | Filter to events that have clips (0 or 1) |
| `include_thumbnails` | int | Include thumbnails in the response (0 or 1) |
### `/api/events/summary`
Returns summary data for events in the database. Used by the HomeAssistant integration.
### `/api/events/<id>`
Returns data for a single event.
### `/api/events/<id>/thumbnail.jpg`
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.
### `/api/events/<id>/snapshot.jpg`
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
Accepts the following query string parameters, but they are only applied when an event is in progress. After the event is completed, the saved snapshot is returned from disk without modification:
| param | Type | Description |
| ----------- | ---- | ------------------------------------------------- |
| `h` | int | Height in pixels |
| `bbox` | int | Show bounding boxes for detected objects (0 or 1) |
| `timestamp` | int | Print the timestamp in the upper left (0 or 1) |
| `crop` | int | Crop the snapshot to the (0 or 1) |
### `/clips/<camera>-<id>.mp4`
Video clip for the given camera and event id.
### `/clips/<camera>-<id>.jpg`
JPG snapshot for the given camera and event id.

View File

@@ -0,0 +1,132 @@
---
id: home-assistant
title: Integration with Home Assistant
sidebar_label: Home Assistant
---
The best way to integrate with HomeAssistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration). When configuring the integration, you will be asked for the `Host` of your frigate instance. This value should be the url you use to access Frigate in the browser and will look like `http://<host>:5000/`. If you are using HassOS with the addon, the host should be `http://ccab4aaf-frigate:5000` (or `http://ccab4aaf-frigate-beta:5000` if your are using the beta version of the addon). HomeAssistant needs access to port 5000 (api) and 1935 (rtmp) for all features. The integration will setup the following entities within HomeAssistant:
## Sensors:
- Stats to monitor frigate performance
- Object counts for all zones and cameras
## Cameras:
- Cameras for image of the last detected object for each camera
- Camera entities with stream support (requires RTMP)
## Media Browser:
- Rich UI with thumbnails for browsing event clips
- Rich UI for browsing 24/7 recordings by month, day, camera, time
## API:
- Notification API with public facing endpoints for images in notifications
### Notifications
Frigate publishes event information in the form of a change feed via MQTT. This allows lots of customization for notifications to meet your needs. Event changes are published with `before` and `after` information as shown [here](#frigateevents).
Note that some people may not want to expose frigate to the web, so you can leverage the HA API that frigate custom_integration ties into (which is exposed to the web, and thus can be used for mobile notifications etc):
To load an image taken by frigate from HomeAssistants API see below:
```
https://HA_URL/api/frigate/notifications/<event-id>/thumbnail.jpg
```
To load a video clip taken by frigate from HomeAssistants API :
```
https://HA_URL/api/frigate/notifications/<event-id>/<camera>/clip.mp4
```
Here is a simple example of a notification automation of events which will update the existing notification for each change. This means the image you see in the notification will update as frigate finds a "better" image.
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
image: 'https://your.public.hass.address.com/api/frigate/notifications/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg?format=android'
tag: '{{trigger.payload_json["after"]["id"]}}'
```
```yaml
automation:
- alias: When a person enters a zone named yard
trigger:
platform: mqtt
topic: frigate/events
condition:
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['after']['entered_zones'] }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: "A {{trigger.payload_json['after']['label']}} has entered the yard."
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
```
```yaml
- alias: When a person leaves a zone named yard
trigger:
platform: mqtt
topic: frigate/events
condition:
- "{{ trigger.payload_json['after']['label'] == 'person' }}"
- "{{ 'yard' in trigger.payload_json['before']['current_zones'] }}"
- "{{ not 'yard' in trigger.payload_json['after']['current_zones'] }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: "A {{trigger.payload_json['after']['label']}} has left the yard."
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
```
```yaml
- alias: Notify for dogs in the front with a high top score
trigger:
platform: mqtt
topic: frigate/events
condition:
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'High confidence dog detection.'
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
```
If you are using telegram, you can fetch the image directly from Frigate:
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.telegram_full
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
photo:
# this url should work for addon users
- url: 'http://ccab4aaf-frigate:5000/api/events/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg'
caption: 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
```

99
docs/docs/usage/mqtt.md Normal file
View File

@@ -0,0 +1,99 @@
---
id: mqtt
title: MQTT
---
These are the MQTT messages generated by Frigate. The default topic_prefix is `frigate`, but can be changed in the config file.
### `frigate/available`
Designed to be used as an availability topic with HomeAssistant. Possible message are:
"online": published when frigate is running (on startup)
"offline": published right before frigate stops
### `frigate/<camera_name>/<object_name>`
Publishes the count of objects for the camera for use as a sensor in HomeAssistant.
### `frigate/<zone_name>/<object_name>`
Publishes the count of objects for the zone for use as a sensor in HomeAssistant.
### `frigate/<camera_name>/<object_name>/snapshot`
Publishes a jpeg encoded frame of the detected object type. When the object is no longer detected, the highest confidence image is published or the original image
is published again.
The height and crop of snapshots can be configured in the config.
### `frigate/events`
Message published for each changed event. The first message is published when the tracked object is no longer marked as a false_positive. When frigate finds a better snapshot of the tracked object or when a zone change occurs, it will publish a message with the same id. When the event ends, a final message is published with `end_time` set.
```json
{
"type": "update", // new, update, or end
"before": {
"id": "1607123955.475377-mxklsc",
"camera": "front_door",
"frame_time": 1607123961.837752,
"label": "person",
"top_score": 0.958984375,
"false_positive": false,
"start_time": 1607123955.475377,
"end_time": null,
"score": 0.7890625,
"box": [424, 500, 536, 712],
"area": 23744,
"region": [264, 450, 667, 853],
"current_zones": ["driveway"],
"entered_zones": ["yard", "driveway"],
"thumbnail": null
},
"after": {
"id": "1607123955.475377-mxklsc",
"camera": "front_door",
"frame_time": 1607123962.082975,
"label": "person",
"top_score": 0.958984375,
"false_positive": false,
"start_time": 1607123955.475377,
"end_time": null,
"score": 0.87890625,
"box": [432, 496, 544, 854],
"area": 40096,
"region": [218, 440, 693, 915],
"current_zones": ["yard", "driveway"],
"entered_zones": ["yard", "driveway"],
"thumbnail": null
}
}
```
### `frigate/stats`
Same data available at `/api/stats` published at a configurable interval.
### `frigate/<camera_name>/detect/set`
Topic to turn detection for a camera on and off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/detect/state`
Topic with current state of detection for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/clips/set`
Topic to turn clips for a camera on and off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/clips/state`
Topic with current state of clips for a camera. Published values are `ON` and `OFF`.
### `frigate/<camera_name>/snapshots/set`
Topic to turn snapshots for a camera on and off. Expected values are `ON` and `OFF`.
### `frigate/<camera_name>/snapshots/state`
Topic with current state of snapshots for a camera. Published values are `ON` and `OFF`.

10
docs/docs/usage/web.md Normal file
View File

@@ -0,0 +1,10 @@
---
id: web
title: Web Interface
---
Frigate comes bundled with a simple web ui that supports the following:
- Show cameras
- Browse events
- Mask helper

76
docs/docusaurus.config.js Normal file
View File

@@ -0,0 +1,76 @@
module.exports = {
title: 'Frigate',
tagline: 'NVR With Realtime Object Detection for IP Cameras',
url: 'https://blakeblackshear.github.io',
baseUrl: '/frigate/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
organizationName: 'blakeblackshear',
projectName: 'frigate',
themeConfig: {
algolia: {
apiKey: '81ec882db78f7fed05c51daf973f0362',
indexName: 'frigate'
},
navbar: {
title: 'Frigate',
logo: {
alt: 'Frigate',
src: 'img/logo.svg',
srcDark: 'img/logo-dark.svg',
},
items: [
{
to: '/',
activeBasePath: 'docs',
label: 'Docs',
position: 'left',
},
{
href: 'https://github.com/blakeblackshear/frigate',
label: 'GitHub',
position: 'right',
},
],
},
sidebarCollapsible: false,
hideableSidebar: true,
footer: {
style: 'dark',
links: [
{
title: 'Community',
items: [
{
label: 'GitHub',
href: 'https://github.com/blakeblackshear/frigate',
},
{
label: 'Discussions',
href: 'https://github.com/blakeblackshear/frigate/discussions',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Blake Blackshear`,
},
},
presets: [
[
'@docusaurus/preset-classic',
{
docs: {
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
editUrl: 'https://github.com/blakeblackshear/frigate/edit/master/docs/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
},
],
],
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 MiB

View File

@@ -1,10 +0,0 @@
# How Frigate Works
Frigate is designed to minimize resource and maximize performance by only looking for objects when and where it is necessary
![Diagram](diagram.png)
## 1. Look for Motion
## 2. Calculate Detection Regions
## 3. Run Object Detection

View File

@@ -1,71 +0,0 @@
# Notification examples
Here are some examples of notifications for the HomeAssistant android companion app:
```yaml
automation:
- alias: When a person enters a zone named yard
trigger:
platform: mqtt
topic: frigate/events
conditions:
- "{{ 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']}}"
- alias: When a person leaves a zone named yard
trigger:
platform: mqtt
topic: frigate/events
conditions:
- "{{ 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']}}"
- alias: Notify for dogs in the front with a high top score
trigger:
platform: mqtt
topic: frigate/events
conditions:
- "{{ trigger.payload_json['after']['label'] == 'dog' }}"
- "{{ trigger.payload_json['after']['camera'] == 'front' }}"
- "{{ trigger.payload_json['after']['top_score'] > 0.98 }}"
action:
- service: notify.mobile_app_pixel_3
data_template:
message: 'High confidence dog detection.'
data:
image: "https://url.com/api/frigate/notifications/{{trigger.payload_json['after']['id']}}/thumbnail.jpg"
tag: "{{trigger.payload_json['after']['id']}}"
```
If you are using telegram, you can fetch the image directly from Frigate:
```yaml
automation:
- alias: Notify of events
trigger:
platform: mqtt
topic: frigate/events
action:
- service: notify.telegram_full
data_template:
message: 'A {{trigger.payload_json["after"]["label"]}} was detected.'
data:
photo:
# this url should work for addon users
- url: 'http://ccab4aaf-frigate:5000/api/events/{{trigger.payload_json["after"]["id"]}}/thumbnail.jpg'
caption : 'A {{trigger.payload_json["after"]["label"]}} was detected on {{ trigger.payload_json["after"]["camera"] }} camera'
```

14035
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
docs/package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"serve": "docusaurus serve",
"clear": "docusaurus clear"
},
"dependencies": {
"@docusaurus/core": "2.0.0-alpha.70",
"@docusaurus/preset-classic": "2.0.0-alpha.70",
"@mdx-js/react": "^1.6.21",
"clsx": "^1.1.1",
"react": "^16.8.4",
"react-dom": "^16.8.4"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

15
docs/sidebars.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
docs: {
Frigate: ['index', 'how-it-works', 'hardware', 'installation', 'troubleshooting'],
Configuration: [
'configuration/index',
'configuration/cameras',
'configuration/optimizing',
'configuration/detectors',
'configuration/false_positives',
'configuration/advanced',
],
Usage: ['usage/home-assistant', 'usage/web', 'usage/api', 'usage/mqtt'],
Development: ['contributing'],
},
};

25
docs/src/css/custom.css Normal file
View File

@@ -0,0 +1,25 @@
/* stylelint-disable docusaurus/copyright-header */
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #3b82f7;
--ifm-color-primary-dark: #1d4ed8;
--ifm-color-primary-darker: #1e40af;
--ifm-color-primary-darkest: #1e3a8a;
--ifm-color-primary-light: #60a5fa;
--ifm-color-primary-lighter: #93c5fd;
--ifm-color-primary-lightest: #dbeafe;
--ifm-code-font-size: 95%;
}
.docusaurus-highlight-code-line {
background-color: rgb(72, 77, 91);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}

0
docs/static/.nojekyll vendored Normal file
View File

BIN
docs/static/img/camera-ui.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 KiB

View File

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

BIN
docs/static/img/events-ui.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
docs/static/img/example-mask-poly.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
docs/static/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/static/img/home-ui.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

3
docs/static/img/logo-dark.svg vendored Normal file
View File

@@ -0,0 +1,3 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.4999 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 936 B

3
docs/static/img/logo.svg vendored Normal file
View File

@@ -0,0 +1,3 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130 446.5C131.6 459.3 145 468 137 470C129 472 94 406.5 86 378.5C78 350.5 73.5 319 75.5 301C77.4999 283 181 255 181 247.5C181 240 147.5 247 146 241C144.5 235 171.3 238.6 178.5 229C189.75 214 204 216.5 213 208.5C222 200.5 233 170 235 157C237 144 215 129 209 119C203 109 222 102 268 83C314 64 460 22 462 27C464 32 414 53 379 66C344 79 287 104 287 111C287 118 290 123.5 288 139.5C286 155.5 285.76 162.971 282 173.5C279.5 180.5 277 197 282 212C286 224 299 233 305 235C310 235.333 323.8 235.8 339 235C358 234 385 236 385 241C385 246 344 243 344 250C344 257 386 249 385 256C384 263 350 260 332 260C317.6 260 296.333 259.333 287 256L285 263C281.667 263 274.7 265 267.5 265C258.5 265 258 268 241.5 268C225 268 230 267 215 266C200 265 144 308 134 322C124 336 130 370 130 385.5C130 399.428 128 430.5 130 446.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 933 B

View File

Before

Width:  |  Height:  |  Size: 781 KiB

After

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -8,8 +8,11 @@ import sys
import signal
import yaml
from gevent import pywsgi
from geventwebsocket.handler import WebSocketHandler
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.sqliteq import SqliteQueueDatabase
from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
@@ -105,8 +108,8 @@ class FrigateApp():
for log, level in self.config.logger.logs.items():
logging.getLogger(log).setLevel(level)
if not 'werkzeug' in self.config.logger.logs:
logging.getLogger('werkzeug').setLevel('ERROR')
if not 'geventwebsocket.handler' in self.config.logger.logs:
logging.getLogger('geventwebsocket.handler').setLevel('ERROR')
def init_queues(self):
# Queues for clip processing
@@ -117,13 +120,16 @@ class FrigateApp():
self.detected_frames_queue = mp.Queue(maxsize=len(self.config.cameras.keys())*2)
def init_database(self):
self.db = SqliteExtDatabase(self.config.database.path)
migrate_db = SqliteExtDatabase(self.config.database.path)
# Run migrations
del(logging.getLogger('peewee_migrate').handlers[:])
router = Router(self.db)
router = Router(migrate_db)
router.run()
migrate_db.close()
self.db = SqliteQueueDatabase(self.config.database.path)
models = [Event]
self.db.bind(models)
@@ -131,7 +137,7 @@ class FrigateApp():
self.stats_tracking = stats_init(self.camera_metrics, self.detectors)
def init_web_server(self):
self.flask_app = create_app(self.config, self.db, self.stats_tracking, self.detected_frames_processor)
self.flask_app = create_app(self.config, self.db, self.stats_tracking, self.detected_frames_processor, self.mqtt_client)
def init_mqtt(self):
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
@@ -235,7 +241,9 @@ class FrigateApp():
signal.signal(signal.SIGTERM, receiveSignal)
self.flask_app.run(host='127.0.0.1', port=5001, debug=False)
server = pywsgi.WSGIServer(('127.0.0.1', 5001), self.flask_app, handler_class=WebSocketHandler)
server.serve_forever()
self.stop()
def stop(self):
@@ -248,6 +256,7 @@ class FrigateApp():
self.recording_maintainer.join()
self.stats_emitter.join()
self.frigate_watchdog.join()
self.db.stop()
for detector in self.detectors.values():
detector.stop()

View File

@@ -63,7 +63,7 @@ CLIPS_SCHEMA = vol.Schema(
}
)
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','warning']
FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
'-fflags', '+genpts+discardcorrupt',
'-rtsp_transport', 'tcp',
@@ -131,6 +131,7 @@ def filters_for_all_tracked_objects(object_config):
OBJECTS_SCHEMA = vol.Schema(vol.All(filters_for_all_tracked_objects,
{
'track': [str],
'mask': vol.Any(str, [str]),
vol.Optional('filters', default = {}): FILTER_SCHEMA.extend(
{
str: {
@@ -164,6 +165,9 @@ CAMERA_FFMPEG_SCHEMA = vol.Schema(
'input_args': vol.Any(str, [str]),
}], vol.Msg(each_role_used_once, msg="Each input role may only be used once"),
vol.Msg(detect_is_required, msg="The detect role is required")),
'global_args': vol.Any(str, [str]),
'hwaccel_args': vol.Any(str, [str]),
'input_args': vol.Any(str, [str]),
'output_args': {
vol.Optional('detect', default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
vol.Optional('record', default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT): vol.Any(str, [str]),
@@ -198,6 +202,7 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('enabled', default=False): bool,
vol.Optional('pre_capture', default=5): int,
vol.Optional('post_capture', default=5): int,
vol.Optional('required_zones', default=[]): [str],
'objects': [str],
vol.Optional('retain', default={}): RETAIN_SCHEMA,
},
@@ -213,6 +218,7 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('timestamp', default=False): bool,
vol.Optional('bounding_box', default=False): bool,
vol.Optional('crop', default=False): bool,
vol.Optional('required_zones', default=[]): [str],
'height': int,
vol.Optional('retain', default={}): RETAIN_SCHEMA,
},
@@ -221,7 +227,8 @@ CAMERAS_SCHEMA = vol.Schema(vol.All(
vol.Optional('timestamp', default=True): bool,
vol.Optional('bounding_box', default=True): bool,
vol.Optional('crop', default=True): bool,
vol.Optional('height', default=270): int
vol.Optional('height', default=270): int,
vol.Optional('required_zones', default=[]): [str],
},
vol.Optional('objects', default={}): OBJECTS_SCHEMA,
vol.Optional('motion', default={}): MOTION_SCHEMA,
@@ -389,12 +396,12 @@ class MqttConfig():
}
class CameraInput():
def __init__(self, global_config, ffmpeg_input):
def __init__(self, camera_config, global_config, ffmpeg_input):
self._path = ffmpeg_input['path']
self._roles = ffmpeg_input['roles']
self._global_args = ffmpeg_input.get('global_args', global_config['global_args'])
self._hwaccel_args = ffmpeg_input.get('hwaccel_args', global_config['hwaccel_args'])
self._input_args = ffmpeg_input.get('input_args', global_config['input_args'])
self._global_args = ffmpeg_input.get('global_args', camera_config.get('global_args', global_config['global_args']))
self._hwaccel_args = ffmpeg_input.get('hwaccel_args', camera_config.get('hwaccel_args', global_config['hwaccel_args']))
self._input_args = ffmpeg_input.get('input_args', camera_config.get('input_args', global_config['input_args']))
@property
def path(self):
@@ -418,7 +425,7 @@ class CameraInput():
class CameraFfmpegConfig():
def __init__(self, global_config, config):
self._inputs = [CameraInput(global_config, i) for i in config['inputs']]
self._inputs = [CameraInput(config, global_config, i) for i in config['inputs']]
self._output_args = config.get('output_args', global_config['output_args'])
@property
@@ -506,12 +513,25 @@ class RecordConfig():
}
class FilterConfig():
def __init__(self, global_config, config, frame_shape=None):
def __init__(self, global_config, config, global_mask=None, frame_shape=None):
self._min_area = config.get('min_area', global_config.get('min_area', 0))
self._max_area = config.get('max_area', global_config.get('max_area', 24000000))
self._threshold = config.get('threshold', global_config.get('threshold', 0.7))
self._min_score = config.get('min_score', global_config.get('min_score', 0.5))
self._raw_mask = config.get('mask')
self._raw_mask = []
if global_mask:
if isinstance(global_mask, list):
self._raw_mask += global_mask
elif isinstance(global_mask, str):
self._raw_mask += [global_mask]
mask = config.get('mask')
if mask:
if isinstance(mask, list):
self._raw_mask += mask
elif isinstance(mask, str):
self._raw_mask += [mask]
self._mask = create_mask(frame_shape, self._raw_mask) if self._raw_mask else None
@property
@@ -546,7 +566,8 @@ class FilterConfig():
class ObjectConfig():
def __init__(self, global_config, config, frame_shape):
self._track = config.get('track', global_config.get('track', DEFAULT_TRACKED_OBJECTS))
self._filters = { name: FilterConfig(global_config.get('filters').get(name, {}), config.get('filters').get(name, {}), frame_shape) for name in self._track }
self._raw_mask = config.get('mask')
self._filters = { name: FilterConfig(global_config['filters'].get(name, {}), config['filters'].get(name, {}), self._raw_mask, frame_shape) for name in self._track }
@property
def track(self):
@@ -559,6 +580,7 @@ class ObjectConfig():
def to_dict(self):
return {
'track': self.track,
'mask': self._raw_mask,
'filters': { k: f.to_dict() for k, f in self.filters.items() }
}
@@ -570,6 +592,7 @@ class CameraSnapshotsConfig():
self._crop = config['crop']
self._height = config.get('height')
self._retain = RetainConfig(global_config['snapshots']['retain'], config['retain'])
self._required_zones = config['required_zones']
@property
def enabled(self):
@@ -594,6 +617,10 @@ class CameraSnapshotsConfig():
@property
def retain(self):
return self._retain
@property
def required_zones(self):
return self._required_zones
def to_dict(self):
return {
@@ -602,7 +629,8 @@ class CameraSnapshotsConfig():
'bounding_box': self.bounding_box,
'crop': self.crop,
'height': self.height,
'retain': self.retain.to_dict()
'retain': self.retain.to_dict(),
'required_zones': self.required_zones
}
class CameraMqttConfig():
@@ -612,6 +640,7 @@ class CameraMqttConfig():
self._bounding_box = config['bounding_box']
self._crop = config['crop']
self._height = config.get('height')
self._required_zones = config['required_zones']
@property
def enabled(self):
@@ -633,13 +662,18 @@ class CameraMqttConfig():
def height(self):
return self._height
@property
def required_zones(self):
return self._required_zones
def to_dict(self):
return {
'enabled': self.enabled,
'timestamp': self.timestamp,
'bounding_box': self.bounding_box,
'crop': self.crop,
'height': self.height
'height': self.height,
'required_zones': self.required_zones
}
class CameraClipsConfig():
@@ -649,6 +683,7 @@ class CameraClipsConfig():
self._post_capture = config['post_capture']
self._objects = config.get('objects')
self._retain = RetainConfig(global_config['clips']['retain'], config['retain'])
self._required_zones = config['required_zones']
@property
def enabled(self):
@@ -670,13 +705,18 @@ class CameraClipsConfig():
def retain(self):
return self._retain
@property
def required_zones(self):
return self._required_zones
def to_dict(self):
return {
'enabled': self.enabled,
'pre_capture': self.pre_capture,
'post_capture': self.post_capture,
'objects': self.objects,
'retain': self.retain.to_dict()
'retain': self.retain.to_dict(),
'required_zones': self.required_zones
}
class CameraRtmpConfig():
@@ -746,7 +786,7 @@ class MotionConfig():
class DetectConfig():
def __init__(self, global_config, config, camera_fps):
self._enabled = config['enabled']
self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*2))
self._max_disappeared = config.get('max_disappeared', global_config.get('max_disappeared', camera_fps*5))
@property
def enabled(self):

View File

@@ -10,6 +10,7 @@ from collections import defaultdict
from pathlib import Path
import psutil
import shutil
from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
@@ -30,6 +31,18 @@ class EventProcessor(threading.Thread):
self.event_processed_queue = event_processed_queue
self.events_in_process = {}
self.stop_event = stop_event
def should_create_clip(self, camera, event_data):
if event_data['false_positive']:
return False
# if there are required zones and there is no overlap
required_zones = self.config.cameras[camera].clips.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
return True
def refresh_cache(self):
cached_files = os.listdir(CACHE_DIR)
@@ -95,18 +108,37 @@ class EventProcessor(threading.Thread):
for f, data in list(self.cached_clips.items()):
if earliest_event-90 > data['start_time']+data['duration']:
del self.cached_clips[f]
logger.debug(f"Cleaning up cached file {f}")
os.remove(os.path.join(CACHE_DIR,f))
# if we are still using more than 90% of the cache, proactively cleanup
cache_usage = shutil.disk_usage("/tmp/cache")
if cache_usage.used/cache_usage.total > .9 and cache_usage.free < 200000000 and len(self.cached_clips) > 0:
logger.warning("More than 90% of the cache is used.")
logger.warning("Consider increasing space available at /tmp/cache or reducing max_seconds in your clips config.")
logger.warning("Proactively cleaning up the cache...")
while cache_usage.used/cache_usage.total > .9:
oldest_clip = min(self.cached_clips.values(), key=lambda x:x['start_time'])
del self.cached_clips[oldest_clip['path']]
os.remove(os.path.join(CACHE_DIR,oldest_clip['path']))
cache_usage = shutil.disk_usage("/tmp/cache")
def create_clip(self, camera, event_data, pre_capture, post_capture):
# get all clips from the camera with the event sorted
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
# if there are no clips in the cache or we are still waiting on a needed file check every 5 seconds
wait_count = 0
while len(sorted_clips) == 0 or sorted_clips[-1]['start_time'] + sorted_clips[-1]['duration'] < event_data['end_time']+post_capture:
if wait_count > 4:
logger.warning(f"Unable to create clip for {camera} and event {event_data['id']}. There were no cache files for this event.")
return False
logger.debug(f"No cache clips for {camera}. Waiting...")
time.sleep(5)
self.refresh_cache()
# get all clips from the camera with the event sorted
sorted_clips = sorted([c for c in self.cached_clips.values() if c['camera'] == camera], key = lambda i: i['start_time'])
wait_count += 1
playlist_start = event_data['start_time']-pre_capture
playlist_end = event_data['end_time']+post_capture
@@ -173,11 +205,12 @@ class EventProcessor(threading.Thread):
if event_type == 'end':
clips_config = self.config.cameras[camera].clips
if not event_data['false_positive']:
clip_created = False
clip_created = False
if self.should_create_clip(camera, event_data):
if clips_config.enabled and (clips_config.objects is None or event_data['label'] in clips_config.objects):
clip_created = self.create_clip(camera, event_data, clips_config.pre_capture, clips_config.post_capture)
if clip_created or event_data['has_snapshot']:
Event.create(
id=event_data['id'],
label=event_data['label'],
@@ -279,6 +312,38 @@ class EventCleanup(threading.Thread):
Event.label == l.label)
)
update_query.execute()
def purge_duplicates(self):
duplicate_query = """with grouped_events as (
select id,
label,
camera,
has_snapshot,
has_clip,
row_number() over (
partition by label, camera, round(start_time/5,0)*5
order by end_time-start_time desc
) as copy_number
from event
)
select distinct id, camera, has_snapshot, has_clip from grouped_events
where copy_number > 1;"""
duplicate_events = Event.raw(duplicate_query)
for event in duplicate_events:
logger.debug(f"Removing duplicate: {event.id}")
media_name = f"{event.camera}-{event.id}"
if event.has_snapshot:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
media.unlink(missing_ok=True)
if event.has_clip:
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
media.unlink(missing_ok=True)
(Event.delete()
.where( Event.id << [event.id for event in duplicate_events] )
.execute())
def run(self):
counter = 0
@@ -287,15 +352,16 @@ class EventCleanup(threading.Thread):
logger.info(f"Exiting event cleanup...")
break
# only expire events every 10 minutes, but check for stop events every 10 seconds
# only expire events every 5 minutes, but check for stop events every 10 seconds
time.sleep(10)
counter = counter + 1
if counter < 60:
if counter < 30:
continue
counter = 0
self.expire('clips')
self.expire('snapshots')
self.purge_duplicates()
# drop events from db where has_clip and has_snapshot are false
delete_query = (

View File

@@ -1,17 +1,21 @@
import base64
import datetime
import json
import logging
import os
import time
from functools import reduce
import cv2
import gevent
import numpy as np
from flask import (Blueprint, Flask, Response, current_app, jsonify,
make_response, request)
from flask_sockets import Sockets
from peewee import SqliteDatabase, operator, fn, DoesNotExist
from playhouse.shortcuts import model_to_dict
from frigate.const import CLIPS_DIR
from frigate.models import Event
from frigate.stats import stats_snapshot
from frigate.util import calculate_region
@@ -20,9 +24,65 @@ from frigate.version import VERSION
logger = logging.getLogger(__name__)
bp = Blueprint('frigate', __name__)
ws = Blueprint('ws', __name__)
def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detected_frames_processor):
class MqttBackend():
"""Interface for registering and updating WebSocket clients."""
def __init__(self, mqtt_client, topic_prefix):
self.clients = list()
self.mqtt_client = mqtt_client
self.topic_prefix = topic_prefix
def register(self, client):
"""Register a WebSocket connection for Mqtt updates."""
self.clients.append(client)
def publish(self, message):
try:
json_message = json.loads(message)
json_message = {
'topic': f"{self.topic_prefix}/{json_message['topic']}",
'payload': json_message['payload'],
'retain': json_message.get('retain', False)
}
except:
logger.warning("Unable to parse websocket message as valid json.")
return
logger.debug(f"Publishing mqtt message from websockets at {json_message['topic']}.")
self.mqtt_client.publish(json_message['topic'], json_message['payload'], retain=json_message['retain'])
def run(self):
def send(client, userdata, message):
"""Sends mqtt messages to clients."""
try:
logger.debug(f"Received mqtt message on {message.topic}.")
ws_message = json.dumps({
'topic': message.topic.replace(f"{self.topic_prefix}/",""),
'payload': message.payload.decode()
})
except:
# if the payload can't be decoded don't relay to clients
logger.debug(f"MQTT payload for {message.topic} wasn't text. Skipping...")
return
for client in self.clients:
try:
client.send(ws_message)
except:
logger.debug("Removing websocket client due to a closed connection.")
self.clients.remove(client)
self.mqtt_client.message_callback_add(f"{self.topic_prefix}/#", send)
def start(self):
"""Maintains mqtt subscription in the background."""
gevent.spawn(self.run)
def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detected_frames_processor, mqtt_client):
app = Flask(__name__)
sockets = Sockets(app)
@app.before_request
def _db_connect():
@@ -38,6 +98,10 @@ def create_app(frigate_config, database: SqliteDatabase, stats_tracking, detecte
app.detected_frames_processor = detected_frames_processor
app.register_blueprint(bp)
sockets.register_blueprint(ws)
app.mqtt_backend = MqttBackend(mqtt_client, frigate_config.mqtt.topic_prefix)
app.mqtt_backend.start()
return app
@@ -54,7 +118,7 @@ def events_summary():
if not has_clip is None:
clauses.append((Event.has_clip == has_clip))
if not has_snapshot is None:
clauses.append((Event.has_snapshot == has_snapshot))
@@ -89,7 +153,7 @@ def event(id):
return "Event not found", 404
@bp.route('/events/<id>/thumbnail.jpg')
def event_snapshot(id):
def event_thumbnail(id):
format = request.args.get('format', 'ios')
thumbnail_bytes = None
try:
@@ -102,7 +166,7 @@ def event_snapshot(id):
if id in camera_state.tracked_objects:
tracked_obj = camera_state.tracked_objects.get(id)
if not tracked_obj is None:
thumbnail_bytes = tracked_obj.get_jpg_bytes()
thumbnail_bytes = tracked_obj.get_thumbnail()
except:
return "Event not found", 404
@@ -114,25 +178,59 @@ def event_snapshot(id):
jpg_as_np = np.frombuffer(thumbnail_bytes, dtype=np.uint8)
img = cv2.imdecode(jpg_as_np, flags=1)
thumbnail = cv2.copyMakeBorder(img, 0, 0, int(img.shape[1]*0.5), int(img.shape[1]*0.5), cv2.BORDER_CONSTANT, (0,0,0))
ret, jpg = cv2.imencode('.jpg', thumbnail)
ret, jpg = cv2.imencode('.jpg', thumbnail, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
thumbnail_bytes = jpg.tobytes()
response = make_response(thumbnail_bytes)
response.headers['Content-Type'] = 'image/jpg'
return response
@bp.route('/events/<id>/snapshot.jpg')
def event_snapshot(id):
jpg_bytes = None
try:
event = Event.get(Event.id == id)
if not event.has_snapshot:
return "Snapshot not available", 404
# read snapshot from disk
with open(os.path.join(CLIPS_DIR, f"{event.camera}-{id}.jpg"), 'rb') as image_file:
jpg_bytes = image_file.read()
except DoesNotExist:
# see if the object is currently being tracked
try:
for camera_state in current_app.detected_frames_processor.camera_states.values():
if id in camera_state.tracked_objects:
tracked_obj = camera_state.tracked_objects.get(id)
if not tracked_obj is None:
jpg_bytes = tracked_obj.get_jpg_bytes(
timestamp=request.args.get('timestamp', type=int),
bounding_box=request.args.get('bbox', type=int),
crop=request.args.get('crop', type=int),
height=request.args.get('h', type=int)
)
except:
return "Event not found", 404
except:
return "Event not found", 404
response = make_response(jpg_bytes)
response.headers['Content-Type'] = 'image/jpg'
return response
@bp.route('/events')
def events():
limit = request.args.get('limit', 100)
camera = request.args.get('camera')
label = request.args.get('label')
zone = request.args.get('zone')
after = request.args.get('after', type=int)
before = request.args.get('before', type=int)
after = request.args.get('after', type=float)
before = request.args.get('before', type=float)
has_clip = request.args.get('has_clip', type=int)
has_snapshot = request.args.get('has_snapshot', type=int)
include_thumbnails = request.args.get('include_thumbnails', default=1, type=int)
clauses = []
excluded_fields = []
if camera:
clauses.append((Event.camera == camera))
@@ -151,10 +249,13 @@ def events():
if not has_clip is None:
clauses.append((Event.has_clip == has_clip))
if not has_snapshot is None:
clauses.append((Event.has_snapshot == has_snapshot))
if not include_thumbnails:
excluded_fields.append(Event.thumbnail)
if len(clauses) == 0:
clauses.append((1 == 1))
@@ -163,7 +264,7 @@ def events():
.order_by(Event.start_time.desc())
.limit(limit))
return jsonify([model_to_dict(e) for e in events])
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
@bp.route('/config')
def config():
@@ -198,7 +299,7 @@ def best(camera_name, label):
width = int(height*best_frame.shape[1]/best_frame.shape[0])
best_frame = cv2.resize(best_frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
ret, jpg = cv2.imencode('.jpg', best_frame)
ret, jpg = cv2.imencode('.jpg', best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers['Content-Type'] = 'image/jpg'
return response
@@ -245,7 +346,7 @@ def latest_frame(camera_name):
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
ret, jpg = cv2.imencode('.jpg', frame)
ret, jpg = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
response = make_response(jpg.tobytes())
response.headers['Content-Type'] = 'image/jpg'
return response
@@ -255,7 +356,7 @@ def latest_frame(camera_name):
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
while True:
# max out at specified FPS
time.sleep(1/fps)
gevent.sleep(1/fps)
frame = detected_frames_processor.get_current_frame(camera_name, draw_options)
if frame is None:
frame = np.zeros((height,int(height*16/9),3), np.uint8)
@@ -263,6 +364,18 @@ def imagestream(detected_frames_processor, camera_name, fps, height, draw_option
width = int(height*frame.shape[1]/frame.shape[0])
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_LINEAR)
ret, jpg = cv2.imencode('.jpg', frame)
ret, jpg = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')
@ws.route('/ws')
def echo_socket(socket):
current_app.mqtt_backend.register(socket)
while not socket.closed:
# Sleep to prevent *constant* context-switches.
gevent.sleep(0.1)
message = socket.receive()
if message:
current_app.mqtt_backend.publish(message)

View File

@@ -7,6 +7,7 @@ import queue
import multiprocessing as mp
from logging import handlers
from setproctitle import setproctitle
from collections import deque
def listener_configurer():
@@ -54,6 +55,7 @@ class LogPipe(threading.Thread):
self.daemon = False
self.logger = logging.getLogger(log_name)
self.level = level
self.deque = deque(maxlen=100)
self.fdRead, self.fdWrite = os.pipe()
self.pipeReader = os.fdopen(self.fdRead)
self.start()
@@ -67,9 +69,13 @@ class LogPipe(threading.Thread):
"""Run the thread, logging everything.
"""
for line in iter(self.pipeReader.readline, ''):
self.logger.log(self.level, line.strip('\n'))
self.deque.append(line.strip('\n'))
self.pipeReader.close()
def dump(self):
while len(self.deque) > 0:
self.logger.log(self.level, self.deque.popleft())
def close(self):
"""Close the write end of the pipe.

View File

@@ -91,6 +91,7 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
logger.error("Unable to connect to MQTT: Connection refused. Error code: " + str(rc))
logger.info("MQTT connected")
client.subscribe(f"{mqtt_config.topic_prefix}/#")
client.publish(mqtt_config.topic_prefix+'/available', 'online', retain=True)
client = mqtt.Client(client_id=mqtt_config.client_id)
@@ -115,11 +116,9 @@ def create_mqtt_client(config: FrigateConfig, camera_metrics):
for name in config.cameras.keys():
client.publish(f"{mqtt_config.topic_prefix}/{name}/clips/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
client.publish(f"{mqtt_config.topic_prefix}/{name}/snapshots/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
client.publish(f"{mqtt_config.topic_prefix}/{name}/detect/state", 'ON' if config.cameras[name].clips.enabled else 'OFF', retain=True)
client.publish(f"{mqtt_config.topic_prefix}/{name}/snapshots/state", 'ON' if config.cameras[name].snapshots.enabled else 'OFF', retain=True)
client.publish(f"{mqtt_config.topic_prefix}/{name}/detect/state", 'ON' if config.cameras[name].detect.enabled else 'OFF', retain=True)
client.subscribe(f"{mqtt_config.topic_prefix}/+/clips/set")
client.subscribe(f"{mqtt_config.topic_prefix}/+/snapshots/set")
client.subscribe(f"{mqtt_config.topic_prefix}/+/detect/set")
client.subscribe(f"{mqtt_config.topic_prefix}/#")
return client

View File

@@ -72,6 +72,8 @@ class TrackedObject():
self.false_positive = True
self.top_score = self.computed_score = 0.0
self.thumbnail_data = None
self.last_updated = 0
self.last_published = 0
self.frame = None
self.previous = self.to_dict()
@@ -183,7 +185,11 @@ class TrackedObject():
if self.thumbnail_data is None:
return None
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
try:
best_frame = cv2.cvtColor(self.frame_cache[self.thumbnail_data['frame_time']], cv2.COLOR_YUV2BGR_I420)
except KeyError:
logger.warning(f"Unable to create jpg because frame {self.thumbnail_data['frame_time']} is not in the cache")
return None
if bounding_box:
thickness = 2
@@ -211,7 +217,7 @@ class TrackedObject():
cv2.putText(best_frame, time_to_show, (5, best_frame.shape[0]-7), cv2.FONT_HERSHEY_SIMPLEX,
fontScale=font_scale, color=(255, 255, 255), thickness=2)
ret, jpg = cv2.imencode('.jpg', best_frame)
ret, jpg = cv2.imencode('.jpg', best_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
if ret:
return jpg.tobytes()
else:
@@ -341,10 +347,16 @@ class CameraState():
# ensure this frame is stored in the cache
if updated_obj.thumbnail_data['frame_time'] == frame_time and frame_time not in self.frame_cache:
self.frame_cache[frame_time] = np.copy(current_frame)
updated_obj.last_updated = frame_time
# if it has been more than 5 seconds since the last publish
# and the last update is greater than the last publish
if frame_time - updated_obj.last_published > 5 and updated_obj.last_updated > updated_obj.last_published:
# call event handlers
for c in self.callbacks['update']:
c(self.name, updated_obj, frame_time)
updated_obj.last_published = frame_time
for id in removed_ids:
# publish events to mqtt
@@ -442,28 +454,35 @@ class TrackedObjectProcessor(threading.Thread):
message = { 'before': obj.previous, 'after': obj.to_dict(), 'type': 'end' }
self.client.publish(f"{self.topic_prefix}/events", json.dumps(message), retain=False)
# write snapshot to disk if enabled
if snapshot_config.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
)
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
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
self.event_queue.put(('end', camera, event_data))
def snapshot(camera, obj: TrackedObject, current_frame_time):
mqtt_config = self.config.cameras[camera].mqtt
if mqtt_config.enabled:
if mqtt_config.enabled and self.should_mqtt_snapshot(camera, obj):
jpg_bytes = obj.get_jpg_bytes(
timestamp=mqtt_config.timestamp,
bounding_box=mqtt_config.bounding_box,
crop=mqtt_config.crop,
height=mqtt_config.height
)
self.client.publish(f"{self.topic_prefix}/{camera}/{obj.obj_data['label']}/snapshot", jpg_bytes, retain=True)
if jpg_bytes is None:
logger.warning(f"Unable to send mqtt snapshot for {obj.obj_data['id']}.")
else:
self.client.publish(f"{self.topic_prefix}/{camera}/{obj.obj_data['label']}/snapshot", jpg_bytes, retain=True)
def object_status(camera, object_name, status):
self.client.publish(f"{self.topic_prefix}/{camera}/{object_name}", status, retain=False)
@@ -487,6 +506,24 @@ class TrackedObjectProcessor(threading.Thread):
# }
self.zone_data = defaultdict(lambda: defaultdict(lambda: {}))
def should_save_snapshot(self, camera, obj: TrackedObject):
# if there are required zones and there is no overlap
required_zones = self.config.cameras[camera].snapshots.required_zones
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
logger.debug(f"Not creating snapshot for {obj.obj_data['id']} because it did not enter required zones")
return False
return True
def should_mqtt_snapshot(self, camera, obj: TrackedObject):
# if there are required zones and there is no overlap
required_zones = self.config.cameras[camera].mqtt.required_zones
if len(required_zones) > 0 and not obj.entered_zones & set(required_zones):
logger.debug(f"Not sending mqtt for {obj.obj_data['id']} because it did not enter required zones")
return False
return True
def get_best(self, camera, label):
# TODO: need a lock here
camera_state = self.camera_states[camera]

View File

@@ -2,8 +2,11 @@ import json
import logging
import threading
import time
import psutil
import shutil
from frigate.config import FrigateConfig
from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR
from frigate.version import VERSION
logger = logging.getLogger(__name__)
@@ -16,6 +19,15 @@ def stats_init(camera_metrics, detectors):
}
return stats_tracking
def get_fs_type(path):
bestMatch = ""
fsType = ""
for part in psutil.disk_partitions(all=True):
if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint):
fsType = part.fstype
bestMatch = part.mountpoint
return fsType
def stats_snapshot(stats_tracking):
camera_metrics = stats_tracking['camera_metrics']
stats = {}
@@ -44,9 +56,19 @@ def stats_snapshot(stats_tracking):
stats['service'] = {
'uptime': (int(time.time()) - stats_tracking['started']),
'version': VERSION
'version': VERSION,
'storage': {}
}
for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]:
storage_stats = shutil.disk_usage(path)
stats['service']['storage'][path] = {
'total': round(storage_stats.total/1000000, 1),
'used': round(storage_stats.used/1000000, 1),
'free': round(storage_stats.free/1000000, 1),
'mount_type': get_fs_type(path)
}
return stats
class StatsEmitter(threading.Thread):

View File

@@ -160,7 +160,40 @@ class TestConfig(TestCase):
assert('dog' in frigate_config.cameras['back'].objects.filters)
assert(frigate_config.cameras['back'].objects.filters['dog'].threshold == 0.7)
def test_ffmpeg_params(self):
def test_global_object_mask(self):
config = {
'mqtt': {
'host': 'mqtt'
},
'objects': {
'track': ['person', 'dog']
},
'cameras': {
'back': {
'ffmpeg': {
'inputs': [
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
]
},
'height': 1080,
'width': 1920,
'objects': {
'mask': '0,0,1,1,0,1',
'filters': {
'dog': {
'mask': '1,1,1,1,1,1'
}
}
}
}
}
}
frigate_config = FrigateConfig(config=config)
assert('dog' in frigate_config.cameras['back'].objects.filters)
assert(len(frigate_config.cameras['back'].objects.filters['dog']._raw_mask) == 2)
assert(len(frigate_config.cameras['back'].objects.filters['person']._raw_mask) == 1)
def test_ffmpeg_params_global(self):
config = {
'ffmpeg': {
'input_args': ['-re']
@@ -190,6 +223,64 @@ class TestConfig(TestCase):
}
frigate_config = FrigateConfig(config=config)
assert('-re' in frigate_config.cameras['back'].ffmpeg_cmds[0]['cmd'])
def test_ffmpeg_params_camera(self):
config = {
'mqtt': {
'host': 'mqtt'
},
'cameras': {
'back': {
'ffmpeg': {
'inputs': [
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'] }
],
'input_args': ['-re']
},
'height': 1080,
'width': 1920,
'objects': {
'track': ['person', 'dog'],
'filters': {
'dog': {
'threshold': 0.7
}
}
}
}
}
}
frigate_config = FrigateConfig(config=config)
assert('-re' in frigate_config.cameras['back'].ffmpeg_cmds[0]['cmd'])
def test_ffmpeg_params_input(self):
config = {
'mqtt': {
'host': 'mqtt'
},
'cameras': {
'back': {
'ffmpeg': {
'inputs': [
{ 'path': 'rtsp://10.0.0.1:554/video', 'roles': ['detect'], 'input_args': ['-re'] }
]
},
'height': 1080,
'width': 1920,
'objects': {
'track': ['person', 'dog'],
'filters': {
'dog': {
'threshold': 0.7
}
}
}
}
}
}
frigate_config = FrigateConfig(config=config)
assert('-re' in frigate_config.cameras['back'].ffmpeg_cmds[0]['cmd'])
def test_inherit_clips_retention(self):
config = {

View File

@@ -181,6 +181,7 @@ class CameraWatchdog(threading.Thread):
now = datetime.datetime.now().timestamp()
if not self.capture_thread.is_alive():
self.logpipe.dump()
self.start_ffmpeg_detect()
elif now - self.capture_thread.current_frame.value > 20:
self.logger.info(f"No frames received from {self.camera_name} in 20 seconds. Exiting ffmpeg...")
@@ -197,6 +198,7 @@ class CameraWatchdog(threading.Thread):
poll = p['process'].poll()
if poll == None:
continue
p['logpipe'].dump()
p['process'] = start_or_restart_ffmpeg(p['cmd'], self.logger, p['logpipe'], ffmpeg_process=p['process'])
# wait a bit before checking again
@@ -279,6 +281,13 @@ def reduce_boxes(boxes):
reduced_boxes = cv2.groupRectangles([list(b) for b in itertools.chain(boxes, boxes)], 1, 0.2)[0]
return [tuple(b) for b in reduced_boxes]
# modified from https://stackoverflow.com/a/40795835
def intersects_any(box_a, boxes):
for box in boxes:
if box_a[2] < box[0] or box_a[0] > box[2] or box_a[1] > box[3] or box_a[3] < box[1]:
continue
return True
def detect(object_detector, frame, model_shape, region, objects_to_track, object_filters):
tensor_input = create_tensor_input(frame, model_shape, region)
@@ -348,7 +357,8 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape, model_s
# look for motion
motion_boxes = motion_detector.detect(frame)
tracked_object_boxes = [obj['box'] for obj in object_tracker.tracked_objects.values()]
# only get the tracked object boxes that intersect with motion
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
combined_boxes = reduce_boxes(motion_boxes + tracked_object_boxes)
@@ -414,8 +424,12 @@ def process_frames(camera_name: str, frame_queue: mp.Queue, frame_shape, model_s
if refining:
refine_count += 1
# Limit to the detections overlapping with motion areas
# to avoid picking up stationary background objects
detections_with_motion = [d for d in detections if intersects_any(d[2], motion_boxes)]
# now that we have refined our detections, we need to track objects
object_tracker.match_and_update(frame_time, detections)
object_tracker.match_and_update(frame_time, detections_with_motion)
# add to the queue if not full
if(detected_objects_queue.full()):

View File

@@ -2,6 +2,8 @@ import datetime
import logging
import threading
import time
import os
import signal
logger = logging.getLogger(__name__)
@@ -29,8 +31,8 @@ class FrigateWatchdog(threading.Thread):
detection_start = detector.detection_start.value
if (detection_start > 0.0 and
now - detection_start > 10):
logger.info("Detection appears to be stuck. Restarting detection process")
logger.info("Detection appears to be stuck. Restarting detection process...")
detector.start_or_restart()
elif not detector.detect_process.is_alive():
logger.info("Detection appears to have stopped. Restarting detection process")
detector.start_or_restart()
logger.info("Detection appears to have stopped. Exiting frigate...")
os.kill(os.getpid(), signal.SIGTERM)

View File

@@ -23,6 +23,12 @@ http {
keepalive_timeout 65;
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/x-javascript application/javascript text/javascript image/svg+xml image/x-icon image/bmp image/png image/gif image/jpeg image/jpg;
gzip_proxied no-cache no-store private expired auth;
gzip_vary on;
upstream frigate_api {
server localhost:5001;
keepalive 1024;
@@ -96,8 +102,17 @@ http {
root /media/frigate;
}
location /ws {
proxy_pass http://frigate_api/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /api/ {
add_header 'Access-Control-Allow-Origin' '*';
add_header Cache-Control "no-store";
proxy_pass http://frigate_api/;
proxy_pass_request_headers on;
proxy_set_header Host $host;
@@ -105,13 +120,24 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
add_header Cache-Control "no-cache";
location ~* \.(?:js|css|svg|ico|png)$ {
access_log off;
expires 1y;
add_header Cache-Control "public";
}
sub_filter 'href="/' 'href="$http_x_ingress_path/';
sub_filter 'url(/' 'url($http_x_ingress_path/';
sub_filter '"/dist/' '"$http_x_ingress_path/dist/';
sub_filter '"/js/' '"$http_x_ingress_path/js/';
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path";</script>';
sub_filter_types text/css application/javascript;
sub_filter_once off;
root /opt/frigate/web;
try_files $uri $uri/ /index.html;
}

2
web/.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
build/*
node_modules/*

140
web/.eslintrc.js Normal file
View File

@@ -0,0 +1,140 @@
module.exports = {
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
extends: [
'prettier',
'preact',
'plugin:import/react',
'plugin:testing-library/recommended',
'plugin:jest/recommended',
],
plugins: ['import', 'testing-library', 'jest'],
env: {
es6: true,
node: true,
browser: true,
},
rules: {
'constructor-super': 'error',
'default-case': ['error', { commentPattern: '^no default$' }],
'handle-callback-err': ['error', '^(err|error)$'],
'new-cap': ['error', { newIsCap: true, capIsNew: false }],
'no-alert': 'error',
'no-array-constructor': 'error',
'no-caller': 'error',
'no-case-declarations': 'error',
'no-class-assign': 'error',
'no-cond-assign': 'error',
'no-console': 'error',
'no-const-assign': 'error',
'no-control-regex': 'error',
'no-debugger': 'error',
'no-delete-var': 'error',
'no-dupe-args': 'error',
'no-dupe-class-members': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-duplicate-imports': 'error',
'no-empty-character-class': 'error',
'no-empty-pattern': 'error',
'no-eval': 'error',
'no-ex-assign': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-extra-boolean-cast': 'error',
'no-fallthrough': 'error',
'no-floating-decimal': 'error',
'no-func-assign': 'error',
'no-implied-eval': 'error',
'no-inner-declarations': ['error', 'functions'],
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-iterator': 'error',
'no-label-var': 'error',
'no-labels': ['error', { allowLoop: false, allowSwitch: false }],
'no-lone-blocks': 'error',
'no-loop-func': 'error',
'no-multi-str': 'error',
'no-native-reassign': 'error',
'no-negated-in-lhs': 'error',
'no-new': 'error',
'no-new-func': 'error',
'no-new-object': 'error',
'no-new-require': 'error',
'no-new-symbol': 'error',
'no-new-wrappers': 'error',
'no-obj-calls': 'error',
'no-octal': 'error',
'no-octal-escape': 'error',
'no-path-concat': 'error',
'no-proto': 'error',
'no-redeclare': 'error',
'no-regex-spaces': 'error',
'no-return-assign': ['error', 'except-parens'],
'no-script-url': 'error',
'no-self-assign': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-shadow-restricted-names': 'error',
'no-sparse-arrays': 'error',
'no-this-before-super': 'error',
'no-throw-literal': 'error',
'no-trailing-spaces': 'error',
'no-undef': 'error',
'no-undef-init': 'error',
'no-unexpected-multiline': 'error',
'no-unmodified-loop-condition': 'error',
'no-unneeded-ternary': ['error', { defaultAssignment: false }],
'no-unreachable': 'error',
'no-unsafe-finally': 'error',
'no-unused-vars': ['error', { vars: 'all', args: 'none', ignoreRestSiblings: true }],
'no-useless-call': 'error',
'no-useless-computed-key': 'error',
'no-useless-concat': 'error',
'no-useless-constructor': 'error',
'no-useless-escape': 'error',
'no-var': 'error',
'no-with': 'error',
'prefer-const': 'error',
'prefer-rest-params': 'error',
'use-isnan': 'error',
'valid-typeof': 'error',
camelcase: 'off',
eqeqeq: ['error', 'allow-null'],
indent: ['error', 2, { SwitchCase: 1 }],
quotes: ['error', 'single', 'avoid-escape'],
radix: 'error',
yoda: ['error', 'never'],
'import/no-unresolved': 'error',
'react-hooks/exhaustive-deps': 'error',
'jest/consistent-test-it': ['error', { fn: 'test' }],
'jest/no-test-prefixes': 'error',
'jest/no-restricted-matchers': [
'error',
{ toMatchSnapshot: 'Use `toMatchInlineSnapshot()` and ensure you only snapshot very small elements' },
],
'jest/valid-describe': 'error',
'jest/valid-expect-in-promise': 'error',
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx'],
},
},
},
};

View File

@@ -1,8 +1,3 @@
# Frigate Web UI
## Development
1. Build the docker images in the root of the repository `make amd64_all` (or appropriate for your system)
2. Create a config file in `config/`
3. Run the container: `docker run --rm --name frigate --privileged -v $PWD/config:/config:ro -v /etc/localtime:/etc/localtime:ro -p 5000:5000 frigate`
4. Run the dev ui: `cd web && npm run start`
For installation and contributing instructions, please follow the [Contributing Docs](https://blakeblackshear.github.io/frigate/contributing).

4
web/babel.config.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
presets: ['@babel/preset-env'],
plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'h' }]],
};

18
web/config/setupTests.js Normal file
View File

@@ -0,0 +1,18 @@
import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom/extend-expect';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query) => ({
matches: false,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}),
});
window.fetch = () => Promise.resolve();
jest.mock('../src/env');

9
web/jest.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx'],
name: 'react-component-benchmark',
resetMocks: true,
roots: ['<rootDir>'],
setupFilesAfterEnv: ['<rootDir>/config/setupTests.js'],
testEnvironment: 'jsdom',
timers: 'fake',
};

12294
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,22 +3,44 @@
"private": true,
"scripts": {
"start": "cross-env SNOWPACK_PUBLIC_API_HOST=http://localhost:5000 snowpack dev",
"start:custom": "snowpack dev",
"prebuild": "rimraf build",
"build": "snowpack build"
"build": "cross-env NODE_ENV=production SNOWPACK_MODE=production SNOWPACK_PUBLIC_API_HOST='' snowpack build",
"lint": "npm run lint:cmd -- --fix",
"lint:cmd": "eslint ./ --ext .jsx,.js",
"test": "jest"
},
"dependencies": {
"idb-keyval": "^5.0.2",
"immer": "^8.0.1",
"preact": "^10.5.9",
"preact-async-route": "^2.2.1",
"preact-router": "^3.2.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.12.13",
"@babel/plugin-transform-react-jsx": "^7.12.13",
"@babel/preset-env": "^7.12.13",
"@prefresh/snowpack": "^3.0.1",
"@snowpack/plugin-optimize": "^0.2.13",
"@snowpack/plugin-postcss": "^1.1.0",
"@snowpack/plugin-webpack": "^2.3.0",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/preact": "^2.0.1",
"@testing-library/user-event": "^12.7.1",
"autoprefixer": "^10.2.1",
"cross-env": "^7.0.3",
"eslint": "^7.19.0",
"eslint-config-preact": "^1.1.3",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.1.3",
"eslint-plugin-testing-library": "^3.10.1",
"jest": "^26.6.3",
"postcss": "^8.2.2",
"postcss-cli": "^8.3.1",
"preact": "^10.5.9",
"preact-router": "^3.2.1",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"snowpack": "^3.0.0",
"snowpack": "^3.0.11",
"snowpack-plugin-hash": "^0.14.2",
"tailwindcss": "^2.0.2"
}
}

View File

@@ -1,8 +1,3 @@
'use strict';
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
],
plugins: [require('tailwindcss'), require('autoprefixer')],
};

5
web/prettier.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
printWidth: 120,
singleQuote: true,
useTabs: false,
};

View File

@@ -14,7 +14,9 @@
<meta name="theme-color" content="#ff0000" />
</head>
<body>
<div id="root"></div>
<div id="root" class="z-0"></div>
<div id="menus" class="z-0"></div>
<div id="tooltips" class="z-0"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/dist/index.js"></script>
</body>

View File

@@ -1,31 +1,19 @@
'use strict';
module.exports = {
mount: {
public: { url: '/', static: true },
src: { url: '/dist' },
},
plugins: [
'@snowpack/plugin-postcss',
'@prefresh/snowpack',
[
'@snowpack/plugin-optimize',
{
preloadModules: true,
},
],
[
'@snowpack/plugin-webpack',
{
sourceMap: true,
},
],
],
plugins: ['@snowpack/plugin-postcss', '@prefresh/snowpack', 'snowpack-plugin-hash'],
routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
optimize: {
bundle: false,
minify: true,
treeshake: true,
},
packageOptions: {
sourcemap: false,
},
buildOptions: {
sourcemap: true,
sourcemap: false,
},
};

View File

@@ -1,43 +1,43 @@
import * as Routes from './routes';
import { h } from 'preact';
import Camera from './Camera';
import CameraMap from './CameraMap';
import Cameras from './Cameras';
import Debug from './Debug';
import Event from './Event';
import Events from './Events';
import ActivityIndicator from './components/ActivityIndicator';
import AsyncRoute from 'preact-async-route';
import AppBar from './AppBar';
import Cameras from './routes/Cameras';
import { Router } from 'preact-router';
import Sidebar from './Sidebar';
import { ApiHost, Config } from './context';
import { useContext, useEffect, useState } from 'preact/hooks';
import { DarkModeProvider, DrawerProvider } from './context';
import { FetchStatus, useConfig } from './api';
export default function App() {
const apiHost = useContext(ApiHost);
const [config, setConfig] = useState(null);
useEffect(async () => {
const response = await fetch(`${apiHost}/api/config`);
const data = response.ok ? await response.json() : {};
setConfig(data);
}, []);
return !config ? (
<div />
) : (
<Config.Provider value={config}>
<div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
<Sidebar />
<div className="p-4 min-w-0">
<Router>
<CameraMap path="/cameras/:camera/editor" />
<Camera path="/cameras/:camera" />
<Event path="/events/:eventId" />
<Events path="/events" />
<Debug path="/debug" />
<Cameras path="/" />
</Router>
const { status } = useConfig();
return (
<DarkModeProvider>
<DrawerProvider>
<div data-testid="app" className="w-full">
<AppBar />
{status !== FetchStatus.LOADED ? (
<div className="flex flex-grow-1 min-h-screen justify-center items-center">
<ActivityIndicator />
</div>
) : (
<div className="flex flex-row min-h-screen w-full bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<Sidebar />
<div className="w-full flex-auto p-2 mt-24 px-4 min-w-0">
<Router>
<AsyncRoute path="/cameras/:camera/editor" getComponent={Routes.getCameraMap} />
<AsyncRoute path="/cameras/:camera" getComponent={Routes.getCamera} />
<AsyncRoute path="/events/:eventId" getComponent={Routes.getEvent} />
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
<AsyncRoute path="/debug" getComponent={Routes.getDebug} />
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
<Cameras default path="/" />
</Router>
</div>
</div>
)}
</div>
</div>
</Config.Provider>
</DrawerProvider>
</DarkModeProvider>
);
return;
}

46
web/src/AppBar.jsx Normal file
View File

@@ -0,0 +1,46 @@
import { h, Fragment } from 'preact';
import BaseAppBar from './components/AppBar';
import LinkedLogo from './components/LinkedLogo';
import Menu, { MenuItem, MenuSeparator } from './components/Menu';
import AutoAwesomeIcon from './icons/AutoAwesome';
import LightModeIcon from './icons/LightMode';
import DarkModeIcon from './icons/DarkMode';
import { useDarkMode } from './context';
import { useCallback, useRef, useState } from 'preact/hooks';
export default function AppBar() {
const [showMoreMenu, setShowMoreMenu] = useState(false);
const { setDarkMode } = useDarkMode();
const handleSelectDarkMode = useCallback(
(value, label) => {
setDarkMode(value);
setShowMoreMenu(false);
},
[setDarkMode, setShowMoreMenu]
);
const moreRef = useRef(null);
const handleShowMenu = useCallback(() => {
setShowMoreMenu(true);
}, [setShowMoreMenu]);
const handleDismissMoreMenu = useCallback(() => {
setShowMoreMenu(false);
}, [setShowMoreMenu]);
return (
<Fragment>
<BaseAppBar title={LinkedLogo} overflowRef={moreRef} onOverflowClick={handleShowMenu} />
{showMoreMenu ? (
<Menu onDismiss={handleDismissMoreMenu} relativeTo={moreRef}>
<MenuItem icon={AutoAwesomeIcon} label="Auto dark mode" value="media" onSelect={handleSelectDarkMode} />
<MenuSeparator />
<MenuItem icon={LightModeIcon} label="Light" value="light" onSelect={handleSelectDarkMode} />
<MenuItem icon={DarkModeIcon} label="Dark" value="dark" onSelect={handleSelectDarkMode} />
</Menu>
) : null}
</Fragment>
);
}

View File

@@ -1,72 +0,0 @@
import { h } from 'preact';
import Box from './components/Box';
import Heading from './components/Heading';
import Link from './components/Link';
import Switch from './components/Switch';
import { route } from 'preact-router';
import { useCallback, useContext } from 'preact/hooks';
import { ApiHost, Config } from './context';
export default function Camera({ camera, url }) {
const config = useContext(Config);
const apiHost = useContext(ApiHost);
if (!(camera in config.cameras)) {
return <div>{`No camera named ${camera}`}</div>;
}
const cameraConfig = config.cameras[camera];
const { pathname, searchParams } = new URL(`${window.location.protocol}//${window.location.host}${url}`);
const searchParamsString = searchParams.toString();
const handleSetOption = useCallback(
(id, value) => {
searchParams.set(id, value ? 1 : 0);
route(`${pathname}?${searchParams.toString()}`, true);
},
[searchParams]
);
function getBoolean(id) {
return Boolean(parseInt(searchParams.get(id), 10));
}
return (
<div className="space-y-4">
<Heading size="2xl">{camera}</Heading>
<Box>
<img
width={cameraConfig.width}
height={cameraConfig.height}
key={searchParamsString}
src={`${apiHost}/api/${camera}?${searchParamsString}`}
/>
</Box>
<Box className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
<Switch checked={getBoolean('bbox')} id="bbox" label="Bounding box" onChange={handleSetOption} />
<Switch checked={getBoolean('timestamp')} id="timestamp" label="Timestamp" onChange={handleSetOption} />
<Switch checked={getBoolean('zones')} id="zones" label="Zones" onChange={handleSetOption} />
<Switch checked={getBoolean('mask')} id="mask" label="Masks" onChange={handleSetOption} />
<Switch checked={getBoolean('motion')} id="motion" label="Motion boxes" onChange={handleSetOption} />
<Switch checked={getBoolean('regions')} id="regions" label="Regions" onChange={handleSetOption} />
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
</Box>
<div className="space-y-4">
<Heading size="sm">Tracked objects</Heading>
<div className="grid grid-cols-3 md:grid-cols-4 gap-4">
{cameraConfig.objects.track.map((objectType) => {
return (
<Box key={objectType} hover href={`/events?camera=${camera}&label=${objectType}`}>
<Heading size="sm">{objectType}</Heading>
<img src={`${apiHost}/api/${camera}/${objectType}/best.jpg?crop=1&h=150`} />
</Box>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -1,38 +0,0 @@
import { h } from 'preact';
import Box from './components/Box';
import Events from './Events';
import Heading from './components/Heading';
import { route } from 'preact-router';
import { useContext } from 'preact/hooks';
import { ApiHost, Config } from './context';
export default function Cameras() {
const config = useContext(Config);
if (!config.cameras) {
return <p>loading</p>;
}
return (
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-4">
{Object.keys(config.cameras).map((camera) => (
<Camera name={camera} />
))}
</div>
);
}
function Camera({ name }) {
const apiHost = useContext(ApiHost);
const href = `/cameras/${name}`;
return (
<Box
className="bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900"
href={href}
>
<Heading size="base">{name}</Heading>
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
</Box>
);
}

View File

@@ -1,97 +0,0 @@
import { h } from 'preact';
import Heading from './components/Heading';
import Link from './components/Link';
import { ApiHost, Config } from './context';
import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
export default function Debug() {
const apiHost = useContext(ApiHost);
const config = useContext(Config);
const [stats, setStats] = useState({});
const [timeoutId, setTimeoutId] = useState(null);
const fetchStats = useCallback(async () => {
const statsResponse = await fetch(`${apiHost}/api/stats`);
const stats = statsResponse.ok ? await statsResponse.json() : {};
setStats(stats);
setTimeoutId(setTimeout(fetchStats, 1000));
}, [setStats]);
useEffect(() => {
fetchStats();
}, []);
useEffect(() => {
return () => {
clearTimeout(timeoutId);
};
}, [timeoutId]);
const { detectors, detection_fps, service, ...cameras } = stats;
if (!service) {
return 'loading…';
}
const detectorNames = Object.keys(detectors);
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
const cameraNames = Object.keys(cameras);
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
return (
<div>
<Heading>
Debug <span className="text-sm">{service.version}</span>
</Heading>
<Table className="w-full">
<Thead>
<Tr>
<Th>detector</Th>
{detectorDataKeys.map((name) => (
<Th>{name.replace('_', ' ')}</Th>
))}
</Tr>
</Thead>
<Tbody>
{detectorNames.map((detector, i) => (
<Tr index={i}>
<Td>{detector}</Td>
{detectorDataKeys.map((name) => (
<Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
<Table className="w-full">
<Thead>
<Tr>
<Th>camera</Th>
{cameraDataKeys.map((name) => (
<Th>{name.replace('_', ' ')}</Th>
))}
</Tr>
</Thead>
<Tbody>
{cameraNames.map((camera, i) => (
<Tr index={i}>
<Td>
<Link href={`/cameras/${camera}`}>{camera}</Link>
</Td>
{cameraDataKeys.map((name) => (
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
<Heading size="sm">Config</Heading>
<pre className="font-mono overflow-y-scroll overflow-x-scroll max-h-96 rounded bg-white dark:bg-gray-900">
{JSON.stringify(config, null, 2)}
</pre>
</div>
);
}

View File

@@ -1,90 +0,0 @@
import { h, Fragment } from 'preact';
import { ApiHost } from './context';
import Box from './components/Box';
import Heading from './components/Heading';
import Link from './components/Link';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
import { useContext, useEffect, useState } from 'preact/hooks';
export default function Event({ eventId }) {
const apiHost = useContext(ApiHost);
const [data, setData] = useState(null);
useEffect(async () => {
const response = await fetch(`${apiHost}/api/events/${eventId}`);
const data = response.ok ? await response.json() : null;
setData(data);
}, [apiHost, eventId]);
if (!data) {
return (
<div>
<Heading>{eventId}</Heading>
<p>loading</p>
</div>
);
}
const startime = new Date(data.start_time * 1000);
const endtime = new Date(data.end_time * 1000);
return (
<div className="space-y-4">
<Heading>
{data.camera} {data.label} <span className="text-sm">{startime.toLocaleString()}</span>
</Heading>
<Box>
{data.has_clip ? (
<Fragment>
<Heading size="sm">Clip</Heading>
<video className="w-100" src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} controls />
</Fragment>
) : (
<p>No clip available</p>
)}
</Box>
<Box>
<Heading size="sm">{data.has_snapshot ? 'Best image' : 'Thumbnail'}</Heading>
<img
src={
data.has_snapshot
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
: `data:image/jpeg;base64,${data.thumbnail}`
}
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
/>
</Box>
<Table>
<Thead>
<Th>Key</Th>
<Th>Value</Th>
</Thead>
<Tbody>
<Tr>
<Td>Camera</Td>
<Td>
<Link href={`/cameras/${data.camera}`}>{data.camera}</Link>
</Td>
</Tr>
<Tr index={1}>
<Td>Timeframe</Td>
<Td>
{startime.toLocaleString()} {endtime.toLocaleString()}
</Td>
</Tr>
<Tr>
<Td>Score</Td>
<Td>{(data.top_score * 100).toFixed(2)}%</Td>
</Tr>
<Tr index={1}>
<Td>Zones</Td>
<Td>{data.zones.join(', ')}</Td>
</Tr>
</Tbody>
</Table>
</div>
);
}

View File

@@ -1,120 +0,0 @@
import { h } from 'preact';
import { ApiHost } from './context';
import Box from './components/Box';
import Heading from './components/Heading';
import Link from './components/Link';
import { route } from 'preact-router';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
export default function Events({ url } = {}) {
const apiHost = useContext(ApiHost);
const [events, setEvents] = useState([]);
const searchParams = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`).searchParams;
const searchParamsString = searchParams.toString();
useEffect(async () => {
const response = await fetch(`${apiHost}/api/events?${searchParamsString}`);
const data = response.ok ? await response.json() : {};
setEvents(data);
}, [searchParamsString]);
const searchKeys = Array.from(searchParams.keys());
return (
<div className="space-y-4">
<Heading>Events</Heading>
{searchKeys.length ? (
<Box>
<Heading size="sm">Filters</Heading>
<div className="flex flex-wrap space-x-2">
{searchKeys.map((filterKey) => (
<UnFilterable
paramName={filterKey}
searchParams={searchParamsString}
name={`${filterKey}: ${searchParams.get(filterKey)}`}
/>
))}
</div>
</Box>
) : null}
<Box className="min-w-0 overflow-auto">
<Table>
<Thead>
<Tr>
<Th></Th>
<Th>Camera</Th>
<Th>Label</Th>
<Th>Score</Th>
<Th>Zones</Th>
<Th>Date</Th>
<Th>Start</Th>
<Th>End</Th>
</Tr>
</Thead>
<Tbody>
{events.map(
(
{ camera, id, label, start_time: startTime, end_time: endTime, thumbnail, top_score: score, zones },
i
) => {
const start = new Date(parseInt(startTime * 1000, 10));
const end = new Date(parseInt(endTime * 1000, 10));
return (
<Tr key={id} index={i}>
<Td>
<a href={`/events/${id}`}>
<img className="w-32 max-w-none" src={`data:image/jpeg;base64,${thumbnail}`} />
</a>
</Td>
<Td>
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
</Td>
<Td>
<Filterable searchParams={searchParamsString} paramName="label" name={label} />
</Td>
<Td>{(score * 100).toFixed(2)}%</Td>
<Td>
<ul>
{zones.map((zone) => (
<li>
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
</li>
))}
</ul>
</Td>
<Td>{start.toLocaleDateString()}</Td>
<Td>{start.toLocaleTimeString()}</Td>
<Td>{end.toLocaleTimeString()}</Td>
</Tr>
);
}
)}
</Tbody>
</Table>
</Box>
</div>
);
}
function Filterable({ searchParams, paramName, name }) {
const params = new URLSearchParams(searchParams);
params.set(paramName, name);
return <Link href={`?${params.toString()}`}>{name}</Link>;
}
function UnFilterable({ searchParams, paramName, name }) {
const params = new URLSearchParams(searchParams);
params.delete(paramName);
return (
<a
className="bg-gray-700 text-white px-3 py-1 rounded-md hover:bg-gray-300 hover:text-gray-900 dark:bg-gray-300 dark:text-gray-900 dark:hover:bg-gray-700 dark:hover:text-white"
href={`?${params.toString()}`}
>
{name}
</a>
);
}

View File

@@ -1,87 +1,52 @@
import { h } from 'preact';
import Link from './components/Link';
import { Link as RouterLink } from 'preact-router/match';
import { useCallback, useState } from 'preact/hooks';
function HamburgerIcon() {
return (
<svg fill="currentColor" viewBox="0 0 20 20" className="w-6 h-6">
<path
fill-rule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
clip-rule="evenodd"
></path>
</svg>
);
}
function CloseIcon() {
return (
<svg fill="currentColor" viewBox="0 0 20 20" className="w-6 h-6">
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
);
}
function NavLink({ className = '', href, text }) {
const external = href.startsWith('http');
const El = external ? Link : RouterLink;
const props = external ? { rel: 'noopener nofollow', target: '_blank' } : {};
return (
<El
activeClassName="bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:bg-gray-600 dark:focus:text-white dark:hover:text-white dark:text-gray-200"
className={`block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-transparent rounded-lg dark:bg-transparent dark:hover:bg-gray-600 dark:focus:bg-gray-600 dark:focus:text-white dark:hover:text-white dark:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline self-end ${className}`}
href={href}
{...props}
>
{text}
</El>
);
}
import { h, Fragment } from 'preact';
import LinkedLogo from './components/LinkedLogo';
import { Match } from 'preact-router/match';
import { memo } from 'preact/compat';
import { ENV } from './env';
import { useConfig } from './api';
import { useMemo } from 'preact/hooks';
import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer';
export default function Sidebar() {
const [open, setOpen] = useState(false);
const handleToggle = useCallback(() => {
setOpen(!open);
}, [open, setOpen]);
const { data: config } = useConfig();
const cameras = useMemo(() => Object.keys(config.cameras), [config]);
return (
<div className="flex flex-col w-full md:w-64 text-gray-700 bg-white dark:text-gray-200 dark:bg-gray-700 flex-shrink-0">
<div className="flex-shrink-0 px-8 py-4 flex flex-row items-center justify-between">
<a
href="#"
className="text-lg font-semibold tracking-widest text-gray-900 uppercase rounded-lg dark:text-white focus:outline-none focus:shadow-outline"
>
Frigate
</a>
<button
className="rounded-lg md:hidden rounded-lg focus:outline-none focus:shadow-outline"
onClick={handleToggle}
>
{open ? <CloseIcon /> : <HamburgerIcon />}
</button>
</div>
<nav
className={`flex-col flex-grow md:block overflow-hidden px-4 pb-4 md:pb-0 md:overflow-y-auto ${
!open ? 'md:h-0 hidden' : ''
}`}
>
<NavLink href="/" text="Cameras" />
<NavLink href="/events" text="Events" />
<NavLink href="/debug" text="Debug" />
<hr className="border-solid border-gray-500 mt-2" />
<NavLink
className="self-end"
href="https://github.com/blakeblackshear/frigate/blob/master/README.md"
text="Documentation"
/>
<NavLink className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
</nav>
</div>
<NavigationDrawer header={<Header />}>
<Destination href="/" text="Cameras" />
<Match path="/cameras/:camera/:other?">
{({ matches }) =>
matches ? (
<Fragment>
<Separator />
{cameras.map((camera) => (
<Destination href={`/cameras/${camera}`} text={camera} />
))}
<Separator />
</Fragment>
) : null
}
</Match>
<Destination href="/events" text="Events" />
<Destination href="/debug" text="Debug" />
<Separator />
<div className="flex flex-grow" />
{ENV !== 'production' ? (
<Fragment>
<Destination href="/styleguide" text="Style Guide" />
<Separator />
</Fragment>
) : null}
<Destination className="self-end" href="https://blakeblackshear.github.io/frigate" text="Documentation" />
<Destination className="self-end" href="https://github.com/blakeblackshear/frigate" text="GitHub" />
</NavigationDrawer>
);
}
const Header = memo(() => {
return (
<div className="text-gray-500">
<LinkedLogo />
</div>
);
});

2
web/src/__mocks__/env.js Normal file
View File

@@ -0,0 +1,2 @@
export const ENV = 'test';
export const API_HOST = 'http://base-url.local:5000';

View File

@@ -0,0 +1,27 @@
import { h } from 'preact';
import * as Api from '../api';
import * as IDB from 'idb-keyval';
import * as PreactRouter from 'preact-router';
import App from '../App';
import { render, screen } from '@testing-library/preact';
describe('App', () => {
let mockUseConfig;
beforeEach(() => {
jest.spyOn(IDB, 'get').mockImplementation(() => Promise.resolve(undefined));
jest.spyOn(IDB, 'set').mockImplementation(() => Promise.resolve(true));
mockUseConfig = jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
data: { cameras: { front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] } } } },
}));
jest.spyOn(Api, 'useApiHost').mockImplementation(() => 'http://base-url.local:5000');
jest.spyOn(PreactRouter, 'Router').mockImplementation(() => <div data-testid="router" />);
});
test('shows a loading indicator while loading', async () => {
mockUseConfig.mockReturnValue({ status: 'loading' });
render(<App />);
await screen.findByTestId('app');
expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,53 @@
import { h } from 'preact';
import * as Context from '../context';
import AppBar from '../AppBar';
import { fireEvent, render, screen } from '@testing-library/preact';
describe('AppBar', () => {
beforeEach(() => {
jest.spyOn(Context, 'useDarkMode').mockImplementation(() => ({
setDarkMode: jest.fn(),
}));
jest.spyOn(Context, 'DarkModeProvider').mockImplementation(({ children }) => {
return <div>{children}</div>;
});
});
test('shows a menu on overflow click', async () => {
render(
<Context.DarkModeProvider>
<Context.DrawerProvider>
<AppBar />
</Context.DrawerProvider>
</Context.DarkModeProvider>
);
const overflowButton = await screen.findByLabelText('More options');
fireEvent.click(overflowButton);
const menu = await screen.findByRole('listbox');
expect(menu).toBeInTheDocument();
});
test('sets dark mode on MenuItem select', async () => {
const setDarkModeSpy = jest.fn();
jest.spyOn(Context, 'useDarkMode').mockImplementation(() => ({
setDarkMode: setDarkModeSpy,
}));
render(
<Context.DarkModeProvider>
<Context.DrawerProvider>
<AppBar />
</Context.DrawerProvider>
</Context.DarkModeProvider>
);
const overflowButton = await screen.findByLabelText('More options');
fireEvent.click(overflowButton);
await screen.findByRole('listbox');
fireEvent.click(screen.getByText('Light'));
expect(setDarkModeSpy).toHaveBeenCalledWith('light');
});
});

View File

@@ -0,0 +1,33 @@
import { h } from 'preact';
import * as Api from '../api';
import * as Context from '../context';
import Sidebar from '../Sidebar';
import { render, screen } from '@testing-library/preact';
describe('Sidebar', () => {
beforeEach(() => {
jest.spyOn(Api, 'useConfig').mockImplementation(() => ({
data: {
cameras: {
front: { name: 'front', objects: { track: ['taco', 'cat', 'dog'] } },
side: { name: 'side', objects: { track: ['taco', 'cat', 'dog'] } },
},
},
}));
jest.spyOn(Context, 'useDrawer').mockImplementation(() => ({ showDrawer: true, setShowDrawer: () => {} }));
});
test('does not render cameras by default', async () => {
render(<Sidebar />);
expect(screen.queryByRole('link', { name: 'front' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'side' })).not.toBeInTheDocument();
});
test('render cameras if in camera route', async () => {
window.history.replaceState({}, 'Cameras', '/cameras/front');
render(<Sidebar />);
expect(screen.queryByRole('link', { name: 'front' })).toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'side' })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,121 @@
import { h } from 'preact';
import * as Mqtt from '../mqtt';
import { ApiProvider, useFetch, useApiHost } from '..';
import { render, screen } from '@testing-library/preact';
describe('useApiHost', () => {
beforeEach(() => {
jest.spyOn(Mqtt, 'MqttProvider').mockImplementation(({ children }) => children);
});
test('is set from the baseUrl', async () => {
function Test() {
const apiHost = useApiHost();
return <div>{apiHost}</div>;
}
render(
<ApiProvider>
<Test />
</ApiProvider>
);
expect(screen.queryByText('http://base-url.local:5000')).toBeInTheDocument();
});
});
function Test() {
const { data, status } = useFetch('/api/tacos');
return (
<div>
<span>{data ? data.returnData : ''}</span>
<span>{status}</span>
</div>
);
}
describe('useFetch', () => {
let fetchSpy;
beforeEach(() => {
jest.spyOn(Mqtt, 'MqttProvider').mockImplementation(({ children }) => children);
fetchSpy = jest.spyOn(window, 'fetch').mockImplementation(async (url, options) => {
if (url.endsWith('/api/config')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
}
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ok: true, json: () => Promise.resolve({ returnData: 'yep' }) });
}, 1);
});
});
});
test('loads data', async () => {
render(
<ApiProvider>
<Test />
</ApiProvider>
);
expect(screen.queryByText('loading')).toBeInTheDocument();
expect(screen.queryByText('yep')).not.toBeInTheDocument();
jest.runAllTimers();
await screen.findByText('loaded');
expect(fetchSpy).toHaveBeenCalledWith('http://base-url.local:5000/api/tacos');
expect(screen.queryByText('loaded')).toBeInTheDocument();
expect(screen.queryByText('yep')).toBeInTheDocument();
});
test('sets error if response is not okay', async () => {
jest.spyOn(window, 'fetch').mockImplementation((url) => {
if (url.includes('/config')) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
}
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ok: false });
}, 1);
});
});
render(
<ApiProvider>
<Test />
</ApiProvider>
);
expect(screen.queryByText('loading')).toBeInTheDocument();
jest.runAllTimers();
await screen.findByText('error');
});
test('does not re-fetch if the query has already been made', async () => {
const { rerender } = render(
<ApiProvider>
<Test key={0} />
</ApiProvider>
);
expect(screen.queryByText('loading')).toBeInTheDocument();
expect(screen.queryByText('yep')).not.toBeInTheDocument();
jest.runAllTimers();
await screen.findByText('loaded');
expect(fetchSpy).toHaveBeenCalledWith('http://base-url.local:5000/api/tacos');
rerender(
<ApiProvider>
<Test key={1} />
</ApiProvider>
);
expect(screen.queryByText('loaded')).toBeInTheDocument();
expect(screen.queryByText('yep')).toBeInTheDocument();
jest.runAllTimers();
// once for /api/config, once for /api/tacos
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
});

View File

@@ -0,0 +1,135 @@
import { h } from 'preact';
import { Mqtt, MqttProvider, useMqtt } from '../mqtt';
import { useCallback, useContext } from 'preact/hooks';
import { fireEvent, render, screen } from '@testing-library/preact';
function Test() {
const { state } = useContext(Mqtt);
return state.__connected ? (
<div data-testid="data">
{Object.keys(state).map((key) => (
<div data-testid={key}>{JSON.stringify(state[key])}</div>
))}
</div>
) : null;
}
const TEST_URL = 'ws://test-foo:1234/ws';
describe('MqttProvider', () => {
let createWebsocket, wsClient;
beforeEach(() => {
wsClient = {
close: jest.fn(),
send: jest.fn(),
};
createWebsocket = jest.fn((url) => {
wsClient.args = [url];
return new Proxy(
{},
{
get(target, prop, receiver) {
return wsClient[prop];
},
set(target, prop, value) {
wsClient[prop] = typeof value === 'function' ? jest.fn(value) : value;
if (prop === 'onopen') {
wsClient[prop]();
}
return true;
},
}
);
});
});
test('connects to the mqtt server', async () => {
render(
<MqttProvider config={mockConfig} createWebsocket={createWebsocket} mqttUrl={TEST_URL}>
<Test />
</MqttProvider>
);
await screen.findByTestId('data');
expect(wsClient.args).toEqual([TEST_URL]);
expect(screen.getByTestId('__connected')).toHaveTextContent('true');
});
test('receives data through useMqtt', async () => {
function Test() {
const {
value: { payload, retain },
connected,
} = useMqtt('tacos');
return connected ? (
<div>
<div data-testid="payload">{JSON.stringify(payload)}</div>
<div data-testid="retain">{JSON.stringify(retain)}</div>
</div>
) : null;
}
const { rerender } = render(
<MqttProvider config={mockConfig} createWebsocket={createWebsocket} mqttUrl={TEST_URL}>
<Test />
</MqttProvider>
);
await screen.findByTestId('payload');
wsClient.onmessage({
data: JSON.stringify({ topic: 'tacos', payload: JSON.stringify({ yes: true }), retain: false }),
});
rerender(
<MqttProvider config={mockConfig} createWebsocket={createWebsocket} mqttUrl={TEST_URL}>
<Test />
</MqttProvider>
);
expect(screen.getByTestId('payload')).toHaveTextContent('{"yes":true}');
expect(screen.getByTestId('retain')).toHaveTextContent('false');
});
test('can send values through useMqtt', async () => {
function Test() {
const { send, connected } = useMqtt('tacos');
const handleClick = useCallback(() => {
send({ yes: true });
}, [send]);
return connected ? <button onClick={handleClick}>click me</button> : null;
}
render(
<MqttProvider config={mockConfig} createWebsocket={createWebsocket} mqttUrl={TEST_URL}>
<Test />
</MqttProvider>
);
await screen.findByRole('button');
fireEvent.click(screen.getByRole('button'));
await expect(wsClient.send).toHaveBeenCalledWith(
JSON.stringify({ topic: 'tacos', payload: JSON.stringify({ yes: true }) })
);
});
test('prefills the clips/detect/snapshots state from config', async () => {
jest.spyOn(Date, 'now').mockReturnValue(123456);
const config = {
cameras: {
front: { name: 'front', detect: { enabled: true }, clips: { enabled: false }, snapshots: { enabled: true } },
side: { name: 'side', detect: { enabled: false }, clips: { enabled: false }, snapshots: { enabled: false } },
},
};
render(
<MqttProvider config={config} createWebsocket={createWebsocket} mqttUrl={TEST_URL}>
<Test />
</MqttProvider>
);
await screen.findByTestId('data');
expect(screen.getByTestId('front/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON"}');
expect(screen.getByTestId('front/clips/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}');
expect(screen.getByTestId('front/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"ON"}');
expect(screen.getByTestId('side/detect/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}');
expect(screen.getByTestId('side/clips/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}');
expect(screen.getByTestId('side/snapshots/state')).toHaveTextContent('{"lastUpdate":123456,"payload":"OFF"}');
});
});
const mockConfig = {
cameras: {},
};

2
web/src/api/baseUrl.js Normal file
View File

@@ -0,0 +1,2 @@
import { API_HOST } from '../env';
export const baseUrl = API_HOST || `${window.location.protocol}//${window.location.host}${window.baseUrl || ''}`;

121
web/src/api/index.jsx Normal file
View File

@@ -0,0 +1,121 @@
import { baseUrl } from './baseUrl';
import { h, createContext } from 'preact';
import { MqttProvider } from './mqtt';
import produce from 'immer';
import { useContext, useEffect, useReducer } from 'preact/hooks';
export const FetchStatus = {
NONE: 'none',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error',
};
const initialState = Object.freeze({
host: baseUrl,
queries: {},
});
const Api = createContext(initialState);
function reducer(state, { type, payload, meta }) {
switch (type) {
case 'REQUEST': {
const { url, fetchId } = payload;
const data = state.queries[url]?.data || null;
return produce(state, (draftState) => {
draftState.queries[url] = { status: FetchStatus.LOADING, data, fetchId };
});
}
case 'RESPONSE': {
const { url, ok, data, fetchId } = payload;
return produce(state, (draftState) => {
draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data, fetchId };
});
}
default:
return state;
}
}
export function ApiProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Api.Provider value={{ state, dispatch }}>
<MqttWithConfig>{children}</MqttWithConfig>
</Api.Provider>
);
}
function MqttWithConfig({ children }) {
const { data, status } = useConfig();
return status === FetchStatus.LOADED ? <MqttProvider config={data}>{children}</MqttProvider> : children;
}
function shouldFetch(state, url, fetchId = null) {
if ((fetchId && url in state.queries && state.queries[url].fetchId !== fetchId) || !(url in state.queries)) {
return true;
}
const { status } = state.queries[url];
return status !== FetchStatus.LOADING && status !== FetchStatus.LOADED;
}
export function useFetch(url, fetchId) {
const { state, dispatch } = useContext(Api);
useEffect(() => {
if (!shouldFetch(state, url, fetchId)) {
return;
}
async function fetchData() {
await dispatch({ type: 'REQUEST', payload: { url, fetchId } });
const response = await fetch(`${state.host}${url}`);
try {
const data = await response.json();
await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data, fetchId } });
} catch (e) {
await dispatch({ type: 'RESPONSE', payload: { url, ok: false, data: null, fetchId } });
}
}
fetchData();
}, [url, fetchId, state, dispatch]);
if (!(url in state.queries)) {
return { data: null, status: FetchStatus.NONE };
}
const data = state.queries[url].data || null;
const status = state.queries[url].status;
return { data, status };
}
export function useApiHost() {
const { state } = useContext(Api);
return state.host;
}
export function useEvents(searchParams, fetchId) {
const url = `/api/events${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);
}
export function useEvent(eventId, fetchId) {
const url = `/api/events/${eventId}`;
return useFetch(url, fetchId);
}
export function useConfig(searchParams, fetchId) {
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);
}
export function useStats(searchParams, fetchId) {
const url = `/api/stats${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, fetchId);
}

120
web/src/api/mqtt.jsx Normal file
View File

@@ -0,0 +1,120 @@
import { h, createContext } from 'preact';
import { baseUrl } from './baseUrl';
import produce from 'immer';
import { useCallback, useContext, useEffect, useRef, useReducer } from 'preact/hooks';
const initialState = Object.freeze({ __connected: false });
export const Mqtt = createContext({ state: initialState, connection: null });
const defaultCreateWebsocket = (url) => new WebSocket(url);
function reducer(state, { topic, payload, retain }) {
switch (topic) {
case '__CLIENT_CONNECTED':
return produce(state, (draftState) => {
draftState.__connected = true;
});
default:
return produce(state, (draftState) => {
let parsedPayload = payload;
try {
parsedPayload = payload && JSON.parse(payload);
} catch (e) {}
draftState[topic] = {
lastUpdate: Date.now(),
payload: parsedPayload,
retain,
};
});
}
}
export function MqttProvider({
config,
children,
createWebsocket = defaultCreateWebsocket,
mqttUrl = `${baseUrl.replace(/^http/, 'ws')}/ws`,
}) {
const [state, dispatch] = useReducer(reducer, initialState);
const wsRef = useRef();
useEffect(() => {
Object.keys(config.cameras).forEach((camera) => {
const { name, clips, detect, snapshots } = config.cameras[camera];
dispatch({ topic: `${name}/clips/state`, payload: clips.enabled ? 'ON' : 'OFF' });
dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF' });
dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF' });
});
}, [config]);
useEffect(
() => {
const ws = createWebsocket(mqttUrl);
ws.onopen = () => {
dispatch({ topic: '__CLIENT_CONNECTED' });
};
ws.onmessage = (event) => {
dispatch(JSON.parse(event.data));
};
wsRef.current = ws;
return () => {
ws.close(3000, 'Provider destroyed');
};
},
// Forces reconnecting
[state.__reconnectAttempts, mqttUrl] // eslint-disable-line react-hooks/exhaustive-deps
);
return <Mqtt.Provider value={{ state, ws: wsRef.current }}>{children}</Mqtt.Provider>;
}
export function useMqtt(watchTopic, publishTopic) {
const { state, ws } = useContext(Mqtt);
const value = state[watchTopic] || { payload: null };
const send = useCallback(
(payload) => {
ws.send(
JSON.stringify({
topic: publishTopic || watchTopic,
payload: typeof payload !== 'string' ? JSON.stringify(payload) : payload,
})
);
},
[ws, watchTopic, publishTopic]
);
return { value, send, connected: state.__connected };
}
export function useDetectState(camera) {
const {
value: { payload },
send,
connected,
} = useMqtt(`${camera}/detect/state`, `${camera}/detect/set`);
return { payload, send, connected };
}
export function useClipsState(camera) {
const {
value: { payload },
send,
connected,
} = useMqtt(`${camera}/clips/state`, `${camera}/clips/set`);
return { payload, send, connected };
}
export function useSnapshotsState(camera) {
const {
value: { payload },
send,
connected,
} = useMqtt(`${camera}/snapshots/state`, `${camera}/snapshots/set`);
return { payload, send, connected };
}

View File

@@ -0,0 +1,15 @@
import { h } from 'preact';
const sizes = {
sm: 'h-4 w-4 border-2 border-t-2',
md: 'h-8 w-8 border-4 border-t-4',
lg: 'h-16 w-16 border-8 border-t-8',
};
export default function ActivityIndicator({ size = 'md' }) {
return (
<div className="w-full flex items-center justify-center" aria-label="Loading…">
<div className={`activityindicator ease-in rounded-full border-gray-200 text-blue-500 ${sizes[size]}`} />
</div>
);
}

View File

@@ -0,0 +1,68 @@
import { h } from 'preact';
import Button from './Button';
import MenuIcon from '../icons/Menu';
import MoreIcon from '../icons/More';
import { useDrawer } from '../context';
import { useLayoutEffect, useCallback, useState } from 'preact/hooks';
// We would typically preserve these in component state
// But need to avoid too many re-renders
let lastScrollY = window.scrollY;
export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
const [show, setShow] = useState(true);
const [atZero, setAtZero] = useState(window.scrollY === 0);
const { setShowDrawer } = useDrawer();
const scrollListener = useCallback(() => {
const scrollY = window.scrollY;
window.requestAnimationFrame(() => {
setShow(scrollY <= 0 || lastScrollY > scrollY);
setAtZero(scrollY === 0);
lastScrollY = scrollY;
});
}, [setShow]);
useLayoutEffect(() => {
document.addEventListener('scroll', scrollListener);
return () => {
document.removeEventListener('scroll', scrollListener);
};
}, [scrollListener]);
const handleShowDrawer = useCallback(() => {
setShowDrawer(true);
}, [setShowDrawer]);
return (
<div
className={`w-full border-b border-gray-200 dark:border-gray-700 flex items-center align-middle p-4 space-x-2 fixed left-0 right-0 z-10 bg-white dark:bg-gray-900 transform transition-all duration-200 ${
!show ? '-translate-y-full' : 'translate-y-0'
} ${!atZero ? 'shadow-sm' : ''}`}
data-testid="appbar"
>
<div className="lg:hidden">
<Button color="black" className="rounded-full w-12 h-12" onClick={handleShowDrawer} type="text">
<MenuIcon className="w-10 h-10" />
</Button>
</div>
<Title />
<div className="flex-grow-1 flex justify-end w-full">
{overflowRef && onOverflowClick ? (
<div className="w-auto" ref={overflowRef}>
<Button
aria-label="More options"
color="black"
className="rounded-full w-12 h-12"
onClick={onOverflowClick}
type="text"
>
<MoreIcon className="w-10 h-10" />
</Button>
</div>
) : null}
</div>
</div>
);
}

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