Compare commits

..

205 Commits

Author SHA1 Message Date
Blake Blackshear
714d76887d use sqlitequeuedb 2021-01-24 06:53:37 -06:00
James Carlos
0c0e1416ff Update documentation link in sidebar to new docs 2021-01-23 07:00:51 -06:00
Blake Blackshear
c6044ba9a1 add debug log when cache is cleaned up 2021-01-22 17:54:30 -06:00
Blake Blackshear
a7739a0a62 if detection stopped, assume the container needs a restart 2021-01-22 16:58:14 -06:00
Blake Blackshear
84ed126db6 fix table in docs 2021-01-22 08:02:09 -06:00
Blake Blackshear
a76f54c326 readme update 2021-01-22 07:48:40 -06:00
Paul Armstrong
b93d354c60 docs: move docs to docusaurus 2021-01-22 07:33:27 -06:00
Blake Blackshear
14d218af46 rate limit tracked object updates to every 5 seconds 2021-01-22 06:40:01 -06:00
Blake Blackshear
bd4973e3f7 add snapshot endpoint that works during the event fixes #575 2021-01-21 18:49:20 -06:00
Blake Blackshear
d94f81969b get the thumbnail instead of the full frame 2021-01-21 17:28:49 -06:00
Blake Blackshear
d32fed2c01 dont wait forever for the cache 2021-01-21 17:26:53 -06:00
Blake Blackshear
7b4e510b95 fix initial switch state 2021-01-20 21:56:43 -06:00
Blake Blackshear
bb4f79cdfe handle exception when frame isnt in cache 2021-01-20 21:56:43 -06:00
Paul Armstrong
e32e69c2d0 feat(web): AutoUpdatingCameraImage to replace MJPEG feed 2021-01-20 21:15:25 -06:00
Paul Armstrong
a71ae053e4 fix(web): set default path to cameras view 2021-01-20 06:46:25 -06:00
Blake Blackshear
fcc9cd56cc update index.js to use baseUrl 2021-01-19 21:31:17 -06:00
Blake Blackshear
b981a3110b first pass at subfilter for ingress support 2021-01-19 19:58:42 -06:00
Paul Armstrong
2da50cc538 fix(web): dark mode text color fixes
fixes #544
2021-01-19 18:02:08 -06:00
Blake Blackshear
cb4a0aa594 ensure error message with missing config is printed 2021-01-19 18:00:26 -06:00
Blake Blackshear
52da1fddc7 update notification example 2021-01-19 07:41:45 -06:00
Blake Blackshear
14645ce4f8 fix mqtt switch handling 2021-01-19 07:41:17 -06:00
Blake Blackshear
97ce7f3028 initialize detection correctly from config 2021-01-19 07:40:51 -06:00
Blake Blackshear
3b5302f6ea update wheels version 2021-01-19 06:19:28 -06:00
Blake Blackshear
74eb16f213 pin numpy 2021-01-19 06:16:44 -06:00
Paul Armstrong
a3d6bf214c feat(web): layout & auto-update debug page 2021-01-18 12:57:09 -06:00
Paul Armstrong
16121ffd00 fix(web): ensure button bg colors show in prod builds 2021-01-18 11:39:42 -06:00
Blake Blackshear
91628bd5d8 fix zone config 2021-01-18 06:38:26 -06:00
Blake Blackshear
b10b64bf57 no longer need special aarch64 wheels build 2021-01-17 08:18:54 -06:00
Blake Blackshear
749c34be9f versioning wheels image 2021-01-16 20:03:42 -06:00
Blake Blackshear
8cfdfab985 move wheels to build container 2021-01-16 19:56:21 -06:00
Paul Armstrong
ef25f8a31e 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-16 19:09:18 -06:00
Paul Armstrong
2a0551a08a feat(web): hash build files to avoid cache issues 2021-01-16 19:09:18 -06:00
Paul Armstrong
0b80419f15 fix(web): ensure mask editing works in firefox 2021-01-16 19:09:18 -06:00
Blake Blackshear
0dc81117aa docs updates for notification changes 2021-01-16 19:09:18 -06:00
Blake Blackshear
49b29d72a7 rename snapshot endpoint to thumbnail 2021-01-16 19:09:18 -06:00
Blake Blackshear
21ece238ff mqtt tweaks for switches 2021-01-16 19:09:18 -06:00
Blake Blackshear
f6ba3f2daa allow summary data to be filtered 2021-01-16 19:09:18 -06:00
Blake Blackshear
bb0d3cb59a update readme 2021-01-16 19:09:18 -06:00
Blake Blackshear
ca9b6d6c5c snapshots config typo 2021-01-16 19:09:18 -06:00
Blake Blackshear
3103ad2bfe update object filters to inherit like motion settings 2021-01-16 19:09:18 -06:00
Blake Blackshear
eab3998ad0 remove support for image masks 2021-01-16 19:09:18 -06:00
Blake Blackshear
a3dfd3a8e0 don't fallback to the CPU
fixes #381
2021-01-16 19:09:18 -06:00
Blake Blackshear
f1c3087775 add change type to events topic
#476
2021-01-16 19:09:18 -06:00
Blake Blackshear
1be91ed3f2 ensure each camera has a detect role set 2021-01-16 19:09:18 -06:00
Blake Blackshear
fd83c4f229 add detection enable to config
fixes #482
2021-01-16 19:09:18 -06:00
Blake Blackshear
de99221ad5 add env vars to config
fixes #509
2021-01-16 19:09:18 -06:00
Blake Blackshear
6892ce56ac enable and disable detection via mqtt 2021-01-16 19:09:18 -06:00
Blake Blackshear
41cea6f62e move setproctitle to prebuilt wheel location 2021-01-16 19:09:18 -06:00
Blake Blackshear
4bbffa97df switch to docker based web builds 2021-01-16 19:09:18 -06:00
Blake Blackshear
614f8abfef handle null thumbnail data 2021-01-16 19:09:18 -06:00
Blake Blackshear
14289b5fd1 add mask as object filter 2021-01-16 19:09:18 -06:00
Blake Blackshear
4164beff1c add object masks and move moton mask 2021-01-16 19:09:18 -06:00
Blake Blackshear
9b3ab486de add missing global shapshots config 2021-01-16 19:09:18 -06:00
Patrick Decat
232a49814a Add missing migrations in docker images 2021-01-16 19:09:18 -06:00
Paul Armstrong
6c61f0b135 fix(web): ensure postcss and postcss-cli are marked as deps 2021-01-16 19:09:18 -06:00
Patrick Decat
c572cec253 Fix Makefile to ignore gpg signatures in commits 2021-01-16 19:09:18 -06:00
Paul Armstrong
d4941f2a5f feat!: web user interface 2021-01-16 19:09:18 -06:00
Blake Blackshear
bf5ec2f65f try to cleanup some migration logging 2021-01-16 19:09:18 -06:00
Blake Blackshear
f8e21584b6 add retention settings for snapshots 2021-01-16 19:09:18 -06:00
Blake Blackshear
3cba83f84b init variables on camera state 2021-01-16 19:09:18 -06:00
Blake Blackshear
dcb4255d7e handle process exit exceptions 2021-01-16 19:09:18 -06:00
Blake Blackshear
9fc3c0dc2f store has_clip and has_snapshot on events 2021-01-16 19:09:18 -06:00
Blake Blackshear
a78830b48e add database migrations 2021-01-16 19:09:18 -06:00
Nat Morris
949fbadcdc Set titles for forked processes 2021-01-16 19:09:18 -06:00
Nat Morris
12c9e63b13 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-16 19:09:18 -06:00
Blake Blackshear
157b230702 turn off snapshots via mqtt 2021-01-16 19:09:18 -06:00
Blake Blackshear
c69299d659 enable turning clips on and off via mqtt 2021-01-16 19:09:18 -06:00
Blake Blackshear
285d630770 cleanup save_Clips/clips inconsistency 2021-01-16 19:09:18 -06:00
Blake Blackshear
b9318092f4 add jpg snapshots to disk and clean up config 2021-01-16 19:09:18 -06:00
Paul Armstrong
905c361d52 fix: ensure timestamp is drawn above mask 2021-01-13 06:55:10 -06:00
Leonardo Merza
4443abbc49 add notes for Blue Iris RTSP support 2020-12-31 08:36:03 -06:00
yllar
dabb36ad93 Update README.md
change tmpfs size from 100MB to 1GB
2020-12-31 08:33:31 -06:00
kluszczyn
2bc8736fd9 Recordings - fix expire_file 2020-12-22 09:58:26 -05:00
Blake Blackshear
e9b3b09cc2 add clips endpoint to readme 2020-12-22 09:58:26 -05:00
Blake Blackshear
ca337c32b4 better mask error handling 2020-12-22 09:58:26 -05:00
Blake Blackshear
24b8bd7c85 fix tmpfs 2020-12-22 09:58:26 -05:00
Blake Blackshear
3ad75a441d remove redundant error output 2020-12-20 08:04:54 -06:00
Blake Blackshear
f006e9be8d use CACHE_DIR constant 2020-12-20 08:04:54 -06:00
Blake Blackshear
03f3ba8008 enable mounting tmpfs volume on start 2020-12-20 08:04:54 -06:00
Blake Blackshear
96a44eb7bf docs and issue template 2020-12-20 07:37:44 -06:00
Blake Blackshear
006782fe3d update process clip for latest changes 2020-12-20 07:37:44 -06:00
Blake Blackshear
ff3e95bbf7 publish event updates on zone change 2020-12-20 07:37:44 -06:00
Blake Blackshear
4b95a37e65 readme updates 2020-12-20 07:37:44 -06:00
Blake Blackshear
38c661b3a8 handle scenario with empty cache 2020-12-20 07:37:44 -06:00
Blake Blackshear
0d6e4f6a66 add qsv support to amd64 image 2020-12-20 07:37:44 -06:00
Blake Blackshear
1ad2219f1c add num_threads fixes #322 2020-12-20 07:37:44 -06:00
Blake Blackshear
dfcdd289c3 optimize clips fixes #299 2020-12-20 07:37:44 -06:00
Blake Blackshear
32f5f2cca9 add post_capture option 2020-12-20 07:37:44 -06:00
Blake Blackshear
24bfe9f3e8 re-crop to the object rather than the region 2020-12-20 07:37:44 -06:00
Blake Blackshear
004667dc99 allow runtime drawing settings for mjpeg and latest 2020-12-20 07:37:44 -06:00
Blake Blackshear
9d785dc781 allow the mask to be a list of masks 2020-12-20 07:37:44 -06:00
Blake Blackshear
cbba5a7af0 adding version endpoint 2020-12-20 07:37:44 -06:00
Blake Blackshear
29b29ee349 configurable motion and detect settings 2020-12-20 07:37:44 -06:00
Blake Blackshear
9ad53e09af update gitignore 2020-12-20 07:37:44 -06:00
Blake Blackshear
c9278991c9 fix test 2020-12-20 07:37:44 -06:00
Blake Blackshear
729de48934 switch default threshold to .7 2020-12-20 07:37:44 -06:00
Blake Blackshear
7476bff5fb allow process clips to output a csv of scores 2020-12-20 07:37:44 -06:00
Blake Blackshear
1e9eae8d9a allow db path to be customized 2020-12-20 07:37:44 -06:00
Blake Blackshear
8113a53381 add telegram example 2020-12-20 07:37:44 -06:00
Blake Blackshear
72833686f1 fix process clip 2020-12-20 07:37:44 -06:00
Blake Blackshear
096c21f105 handle empty string args 2020-12-20 07:37:44 -06:00
Blake Blackshear
181f66357b allow region to extend beyond the frame 2020-12-20 07:37:44 -06:00
tubalainen
a54fbc483c Updated file
ref: https://github.com/blakeblackshear/frigate/issues/373
2020-12-12 10:38:02 -06:00
Blake Blackshear
92d5a002d3 swap width and height to reduce confusion 2020-12-10 19:22:03 -06:00
Blake Blackshear
f9184903d7 updating compose example to reduce confusion 2020-12-10 19:02:08 -06:00
Blake Blackshear
91cde6ce7b allow defining model shape and switch to mobiledet as default model 2020-12-09 07:22:26 -06:00
Blake Blackshear
186a4587c7 add model dimensions to config 2020-12-09 07:22:26 -06:00
Patrick Decat
6049acb1f3 Document beta addon host 2020-12-08 07:25:13 -06:00
Blake Blackshear
2d2ebf313c make shm consistent with compose 2020-12-08 07:24:37 -06:00
tubalainen
3d329dcb52 Updated docker command line...
...to correspond with 0.8.0 feature set.
2020-12-08 07:24:37 -06:00
Blake Blackshear
06854fc34f readme cleanup fixes #332 2020-12-07 18:00:12 -06:00
Blake Blackshear
e01e14d866 handle and warn if roles dont match enabled features 2020-12-07 08:07:35 -06:00
Blake Blackshear
3dfd251ebb camera recommendations 2020-12-07 07:36:29 -06:00
Blake Blackshear
dcea807f77 catch all psutil errors 2020-12-07 07:16:48 -06:00
Blake Blackshear
87d83ff33a clarify height width and fps 2020-12-07 07:16:28 -06:00
Blake Blackshear
1d31cbdf0d readme updates 2020-12-06 14:25:28 -06:00
Blake Blackshear
e05b27b8dc tweak screenshots 2020-12-06 08:27:03 -06:00
Blake Blackshear
7111bd208e readme updates 2020-12-06 08:25:25 -06:00
Blake Blackshear
04a80280da set ffmpeg image versions 2020-12-06 07:09:14 -06:00
Blake Blackshear
3bda092140 comment you zeroconf 2020-12-06 07:05:45 -06:00
Blake Blackshear
9086820479 fix flask logger config 2020-12-05 19:05:03 -06:00
Blake Blackshear
d1da57aedc fix graceful exits 2020-12-05 12:06:07 -06:00
Blake Blackshear
6ded12c566 better exception handling 2020-12-05 12:06:07 -06:00
Blake Blackshear
70352566a7 fix default args 2020-12-05 12:06:07 -06:00
Blake Blackshear
cf5cc86588 fix fontconfig issue 2020-12-05 08:48:46 -06:00
Blake Blackshear
e41db49ab8 doc updates 2020-12-05 08:48:46 -06:00
Blake Blackshear
1b7effafee update some default config values 2020-12-05 08:48:46 -06:00
Blake Blackshear
69e9e0b0bf log level configuration 2020-12-05 08:48:46 -06:00
Blake Blackshear
89624df411 no need to write jpg disk 2020-12-05 08:48:46 -06:00
Blake Blackshear
d1a7405211 dont delete the recordings directory 2020-12-05 08:48:46 -06:00
Blake Blackshear
040f8c7c20 default save_clips objects 2020-12-05 08:48:46 -06:00
Blake Blackshear
6d7acabf4c add logging for directory creation 2020-12-05 08:48:46 -06:00
Blake Blackshear
45a8b42157 exit on config errors 2020-12-05 08:48:46 -06:00
Blake Blackshear
8785be24b7 add zeroconf discovery 2020-12-05 08:48:46 -06:00
Blake Blackshear
cc0812540c optional android notification aspect ratio 2020-12-05 08:48:46 -06:00
Blake Blackshear
5cf38ca4f7 reduce min timestamp size 2020-12-05 08:48:46 -06:00
Blake Blackshear
7e4395c30e publish object counts rather than on/off 2020-12-05 08:48:46 -06:00
Blake Blackshear
598d3aeda2 make directories constants 2020-12-05 08:48:46 -06:00
Blake Blackshear
012dbf81f7 cleanup empty directories 2020-12-05 08:48:46 -06:00
Blake Blackshear
f869def12e serve up recordings with nginx 2020-12-05 08:48:46 -06:00
Blake Blackshear
31f7666337 add recording maintenance 2020-12-05 08:48:46 -06:00
Blake Blackshear
9e339acbca add record settings to config 2020-12-05 08:48:46 -06:00
Blake Blackshear
8f8054a299 fix log timeout 2020-12-05 08:48:46 -06:00
Blake Blackshear
f7021eec4c ensure zones dont have the same name as a camera 2020-12-05 08:48:46 -06:00
Blake Blackshear
c124153da4 graceful exit of subprocesses 2020-12-05 08:48:46 -06:00
Blake Blackshear
706c2f921e add multiple streams per camera 2020-12-05 08:48:46 -06:00
Blake Blackshear
de1d66bcb9 fix fontconfig error 2020-12-05 08:48:46 -06:00
Blake Blackshear
4502ca8e80 add support for rebroadcasting as rtmp 2020-12-05 08:48:46 -06:00
Blake Blackshear
32a66fe5e8 avoid null error 2020-12-05 08:48:46 -06:00
Blake Blackshear
e1251aafdb minimize logging 2020-12-05 08:48:46 -06:00
Blake Blackshear
587494068c oops 2020-12-05 08:48:46 -06:00
Blake Blackshear
7a4d90a47a only publish end events for true positives 2020-12-05 08:48:46 -06:00
Blake Blackshear
d06b587d33 ensure all events are cleaned up 2020-12-05 08:48:46 -06:00
Blake Blackshear
eef70e434b publish events like a change feed 2020-12-05 08:48:46 -06:00
Blake Blackshear
b39da3ee01 pull from memory if event in progress 2020-12-05 08:48:46 -06:00
Blake Blackshear
e07c4e0d8c add endpoint for event thumbnail 2020-12-05 08:48:46 -06:00
Blake Blackshear
2f41ba6f77 add service to get by id 2020-12-05 08:48:46 -06:00
Blake Blackshear
bf95af0f22 add zones to summary data 2020-12-05 08:48:46 -06:00
Blake Blackshear
2e15847f86 sleep in the right place 2020-12-05 08:48:46 -06:00
Blake Blackshear
5992e85dc8 manage events for unlisted cameras 2020-12-05 08:48:46 -06:00
Blake Blackshear
24d416b869 add event cleanup thread 2020-12-05 08:48:46 -06:00
Blake Blackshear
5dbf368c4b add clip retention to config 2020-12-05 08:48:46 -06:00
Blake Blackshear
7d56fe105f use localtime in group by 2020-12-05 08:48:46 -06:00
Blake Blackshear
e9327aa18c new http endpoints 2020-12-05 08:48:46 -06:00
Blake Blackshear
df56e079de add parameters to event query 2020-12-05 08:48:46 -06:00
Blake Blackshear
8c5bfbd187 only save events when a clip is created 2020-12-05 08:48:46 -06:00
Blake Blackshear
2613e74f97 add bas64 encoded thumbnail to the database 2020-12-05 08:48:46 -06:00
Blake Blackshear
9a7fb96357 check for None value thumbnail_data 2020-12-05 08:48:46 -06:00
Blake Blackshear
37f9dfed92 only set thumbnail data if object is a true positive 2020-12-05 08:48:46 -06:00
Blake Blackshear
68c1544808 add some debug logging to frame cache 2020-12-05 08:48:46 -06:00
Blake Blackshear
2b3d3c5824 dont use a property 2020-12-05 08:48:46 -06:00
Blake Blackshear
efea87a3ea attempt to fix missing thumbs 2020-12-05 08:48:46 -06:00
Blake Blackshear
977785fb10 better frame handling for best images 2020-12-05 08:48:46 -06:00
Blake Blackshear
4e113e62c0 cleanup false_positive attribute 2020-12-05 08:48:46 -06:00
Blake Blackshear
5080b2d781 ensure some valid thumbnail is available 2020-12-05 08:48:46 -06:00
Blake Blackshear
5cfd6d1edb don't save thumbnails for false positives 2020-12-05 08:48:46 -06:00
Blake Blackshear
27ae4d8ab0 cleanup 2020-12-05 08:48:46 -06:00
Blake Blackshear
3db33302ec reduce logging 2020-12-05 08:48:46 -06:00
Blake Blackshear
f2910d48e0 fixes 2020-12-05 08:48:46 -06:00
Blake Blackshear
cf0f8892e2 update nginx config 2020-12-05 08:48:46 -06:00
Blake Blackshear
4d22e172ff stop writing json file to disk 2020-12-05 08:48:46 -06:00
Blake Blackshear
8874a55b0f create tracked object class and save thumbnails 2020-12-05 08:48:46 -06:00
Blake Blackshear
24b703a875 maintain thumbnail frames for tracked objects 2020-12-05 08:48:46 -06:00
Blake Blackshear
8b8f5b5c40 sort imports 2020-12-05 08:48:46 -06:00
Blake Blackshear
eac81136d2 naming threads and processes for logs 2020-12-05 08:48:46 -06:00
Blake Blackshear
d1e27b43ea use a queue for logging 2020-12-05 08:48:46 -06:00
Blake Blackshear
105dcb7094 create typed config classes 2020-12-05 08:48:46 -06:00
Blake Blackshear
c0a16efdc1 add nginx and change default file locations 2020-12-05 08:48:46 -06:00
Blake Blackshear
2800c54743 config setup 2020-12-05 08:48:46 -06:00
Blake Blackshear
2a24e8abcb add watchdog 2020-12-05 08:48:46 -06:00
Blake Blackshear
37ee746ebb add back all endpoints 2020-12-05 08:48:46 -06:00
Blake Blackshear
7ee6bfe855 add event processor 2020-12-05 08:48:46 -06:00
Blake Blackshear
40f57a8754 add capture processes 2020-12-05 08:48:46 -06:00
Blake Blackshear
e0da462223 add camera processors 2020-12-05 08:48:46 -06:00
Blake Blackshear
47a9fc4292 add detected_frames_processor 2020-12-05 08:48:46 -06:00
Blake Blackshear
03fe5158db add detector processes 2020-12-05 08:48:46 -06:00
Blake Blackshear
72be6b480d init db/http/mqtt 2020-12-05 08:48:46 -06:00
Blake Blackshear
a8964dcc1f app container and config schema 2020-12-05 08:48:46 -06:00
Blake Blackshear
732e91ee42 move primary script into the module 2020-12-05 08:48:46 -06:00
Blake Blackshear
27da080ce6 saving events and simple endpoint 2020-12-05 08:48:46 -06:00
Blake Blackshear
075d06b108 basic database model and api endpoint 2020-12-05 08:48:46 -06:00
Blake Blackshear
95dc17ffcd store events in tinydb 2020-12-05 08:48:46 -06:00
Blake Blackshear
408b53f8b4 update events model 2020-12-05 08:48:46 -06:00
Marc Seeger
3ef68a297a 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
2020-12-05 07:00:07 -06:00
Michael Wei
3e9b3711dc Use cv2.bitwise_and instead of numpy.where 2020-12-05 06:59:28 -06:00
36 changed files with 522 additions and 875 deletions

View File

@@ -3,7 +3,7 @@ default_target: amd64_frigate
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
version:
echo "VERSION='0.8.1-$(COMMIT_HASH)'" > frigate/version.py
echo "VERSION='0.8.0-$(COMMIT_HASH)'" > frigate/version.py
web:
docker build --tag frigate-web --file docker/Dockerfile.web web/

View File

@@ -14,28 +14,9 @@ Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but
- Uses a very low overhead motion detection to determine where to run object detection
- Object detection with TensorFlow runs in separate processes for maximum FPS
- Communicates over MQTT for easy integration into other systems
- Records video clips of detected objects
- 24/7 recording
- Re-streaming via RTMP to reduce the number of connections to your camera
## Documentation
View the documentation at https://blakeblackshear.github.io/frigate
## Donations
If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear).
## Screenshots
Integration into HomeAssistant
<div>
<a href="docs/static/img/media_browser.png"><img src="docs/static/img/media_browser.png" height=400></a>
<a href="docs/static/img/notification.png"><img src="docs/static/img/notification.png" height=400></a>
</div>
Also comes with a builtin UI:
<div>
<a href="docs/static/img/home-ui.png"><img src="docs/static/img/home-ui.png" height=400></a>
<a href="docs/static/img/camera-ui.png"><img src="docs/static/img/camera-ui.png" height=400></a>
</div>
![Events](docs/static/img/events-ui.png)

View File

@@ -386,29 +386,6 @@ ffmpeg:
- '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

View File

@@ -3,7 +3,7 @@ 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).
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](/configuration/cameras.md).
It is recommended to start with a minimal configuration and add to it:
@@ -51,7 +51,7 @@ mqtt:
## `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.
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) for a complete list of options.
```yaml
cameras:
@@ -80,8 +80,7 @@ clips:
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.
# Notice: 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:
@@ -97,7 +96,7 @@ clips:
```yaml
ffmpeg:
# Optional: global ffmpeg args (default: shown below)
global_args: -hide_banner -loglevel warning
global_args: -hide_banner -loglevel fatal
# Optional: global hwaccel args (default: shown below)
# NOTE: See hardware acceleration docs for your specific device
hwaccel_args: []

View File

@@ -17,12 +17,10 @@ HassOS users can install via the addon repository. Frigate requires an MQTT serv
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|
@@ -75,27 +73,15 @@ docker run --rm \
blakeblackshear/frigate:0.8.0-beta2-amd64
```
### 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).
Use the [helm chart](https://github.com/k8s-at-home/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
## Proxmox
Some people have had success running Frigate in LXC directly with the following config:
@@ -117,6 +103,12 @@ 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).
### 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>
```

View File

@@ -1,20 +1,27 @@
---
id: troubleshooting
title: Troubleshooting and FAQ
title: Troubleshooting
---
### 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 default ffmpeg args are shown [here](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.
Example:
![mismatched-resolution](/img/mismatched-resolution.jpg)
## "[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.
## "ffmpeg didnt return a frame. something is wrong"
Turn on logging for the ffmpeg process by overriding the global_args and setting the log level to `info` (the default is `fatal`). Note that all ffmpeg logs show up in the Frigate logs as `ERROR` level. This does not mean they are actually errors.
```yaml
ffmpeg:
global_args: -hide_banner -loglevel info
```
## "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.

View File

@@ -135,17 +135,16 @@ Version info
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) |
| 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) |
### `/api/events/summary`
@@ -160,17 +159,16 @@ Returns data for a single event.
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) |
|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`

View File

@@ -9,10 +9,6 @@ module.exports = {
organizationName: 'blakeblackshear',
projectName: 'frigate',
themeConfig: {
algolia: {
apiKey: '81ec882db78f7fed05c51daf973f0362',
indexName: 'frigate'
},
navbar: {
title: 'Frigate',
logo: {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -63,7 +63,7 @@ CLIPS_SCHEMA = vol.Schema(
}
)
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','warning']
FFMPEG_GLOBAL_ARGS_DEFAULT = ['-hide_banner','-loglevel','fatal']
FFMPEG_INPUT_ARGS_DEFAULT = ['-avoid_negative_ts', 'make_zero',
'-fflags', '+genpts+discardcorrupt',
'-rtsp_transport', 'tcp',

View File

@@ -55,7 +55,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))
@@ -160,14 +160,12 @@ def events():
camera = request.args.get('camera')
label = request.args.get('label')
zone = request.args.get('zone')
after = request.args.get('after', type=float)
before = request.args.get('before', type=float)
after = request.args.get('after', type=int)
before = request.args.get('before', type=int)
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))
@@ -186,13 +184,10 @@ 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))
@@ -201,7 +196,7 @@ def events():
.order_by(Event.start_time.desc())
.limit(limit))
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
return jsonify([model_to_dict(e) for e in events])
@bp.route('/config')
def config():

View File

@@ -7,7 +7,6 @@ import queue
import multiprocessing as mp
from logging import handlers
from setproctitle import setproctitle
from collections import deque
def listener_configurer():
@@ -55,7 +54,6 @@ 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()
@@ -69,13 +67,9 @@ class LogPipe(threading.Thread):
"""Run the thread, logging everything.
"""
for line in iter(self.pipeReader.readline, ''):
self.deque.append(line.strip('\n'))
self.logger.log(self.level, 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

@@ -181,7 +181,6 @@ 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...")
@@ -198,7 +197,6 @@ 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

View File

@@ -23,12 +23,6 @@ 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;
@@ -104,7 +98,6 @@ http {
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;
@@ -112,23 +105,13 @@ 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 '"/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;
}

289
web/package-lock.json generated
View File

@@ -93,6 +93,13 @@
"@babel/helper-validator-option": "^7.12.1",
"browserslist": "^4.14.5",
"semver": "^5.5.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"@babel/helper-create-class-features-plugin": {
@@ -864,6 +871,13 @@
"@babel/types": "^7.12.11",
"core-js-compat": "^3.8.0",
"semver": "^5.5.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"@babel/preset-modules": {
@@ -954,12 +968,22 @@
}
},
"@npmcli/move-file": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.1.tgz",
"integrity": "sha512-LtWTicuF2wp7PNTuyCwABx7nNG+DnzSE8gN0iWxkC6mpgm/iOPu0ZMTkXuCxmJxtWFsDxUaixM9COSNJEMUfuQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.0.tgz",
"integrity": "sha512-Iv2iq0JuyYjKeFkSR4LPaCdDZwlGK9X2cP/01nJcp3yMJ1FjNd9vpiEYvLUgzBxKPg2SFmaOhizoQsPc0LWeOQ==",
"requires": {
"mkdirp": "^1.0.4",
"rimraf": "^3.0.2"
"rimraf": "^2.7.1"
},
"dependencies": {
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"@prefresh/babel-plugin": {
@@ -989,6 +1013,24 @@
"resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.1.1.tgz",
"integrity": "sha512-MUhT5m2XNN5NsZl4GnpuvlzLo6VSTa/+wBfBd3fiWUvHGhv0GF9hnA1pd//v0uJaKwUnVRQ1hYElxCV7DtYsCQ=="
},
"@snowpack/plugin-optimize": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/@snowpack/plugin-optimize/-/plugin-optimize-0.2.13.tgz",
"integrity": "sha512-DW0+iaSc80vWZKDIKbdR5pqDkNDsv3ZbH8eVB8yppK/JlBmOqmsUGdKlZ2eq8XDxE23/V5+gmA9RXLWcTMJ1eQ==",
"requires": {
"csso": "^4.1.0",
"es-module-lexer": "^0.3.25",
"esbuild": "^0.8.0",
"glob": "^7.1.6",
"html-minifier": "^4.0.0",
"hypertag": "^0.0.3",
"kleur": "^4.1.3",
"mkdirp": "^1.0.4",
"node-inject-html": "^0.0.5",
"object.fromentries": "^2.0.2",
"p-queue": "^6.6.1"
}
},
"@snowpack/plugin-postcss": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@snowpack/plugin-postcss/-/plugin-postcss-1.1.0.tgz",
@@ -1019,14 +1061,14 @@
}
},
"@types/json-schema": {
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw=="
},
"@types/node": {
"version": "14.14.22",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
"integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw=="
"version": "14.14.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
"integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
},
"@types/parse-json": {
"version": "4.0.0",
@@ -2062,16 +2104,16 @@
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
},
"core-js": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz",
"integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q=="
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz",
"integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A=="
},
"core-js-compat": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.3.tgz",
"integrity": "sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.2.tgz",
"integrity": "sha512-LO8uL9lOIyRRrQmZxHZFl1RV+ZbcsAkFWTktn5SmH40WgLtSNYN4m4W2v9ONT147PxBY/XrRhrWq8TlvObyUjQ==",
"requires": {
"browserslist": "^4.16.1",
"browserslist": "^4.16.0",
"semver": "7.0.0"
},
"dependencies": {
@@ -2782,9 +2824,9 @@
}
},
"entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
},
"errno": {
"version": "0.1.8",
@@ -2821,6 +2863,11 @@
"string.prototype.trimstart": "^1.0.1"
}
},
"es-module-lexer": {
"version": "0.3.26",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz",
"integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA=="
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
@@ -2897,6 +2944,11 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"events": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
@@ -3523,6 +3575,11 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
},
"hypertag": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/hypertag/-/hypertag-0.0.3.tgz",
"integrity": "sha512-Z/cAsLCihKj+QJGQt5X19L/YY8PW5l8/A9Ju7s4g1ty4kwDdSzGp2QdRoKb266kFglJw6+CSsR/zNpkrOEo54A=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3574,11 +3631,6 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
},
"immer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
"integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
},
"import-cwd": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
@@ -3876,9 +3928,12 @@
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
},
"is-wsl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"requires": {
"is-docker": "^2.0.0"
}
},
"isarray": {
"version": "1.0.0",
@@ -4047,6 +4102,11 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"kleur": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.3.tgz",
"integrity": "sha512-H1tr8QP2PxFTNwAFM74Mui2b6ovcY9FoxJefgrwxY+OCJcq01k5nvhf4M/KnizzrJvLRap5STUy7dgDV35iUBw=="
},
"last-call-webpack-plugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz",
@@ -4167,13 +4227,6 @@
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"requires": {
"semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"map-cache": {
@@ -4267,15 +4320,23 @@
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"mini-css-extract-plugin": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz",
"integrity": "sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.4.tgz",
"integrity": "sha512-dNjqyeogUd8ucUgw5sxm1ahvSfSUgef7smbmATRSbDm4EmNx5kQA6VdUEhEeCKSjX6CTYjb5vxgMUvRjqP3uHg==",
"requires": {
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0",
"webpack-sources": "^1.1.0"
},
"dependencies": {
"json5": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
"integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==",
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
@@ -4408,6 +4469,11 @@
"resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.0.0.tgz",
"integrity": "sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw=="
},
"moo": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
"integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w=="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -4494,6 +4560,14 @@
"lodash.toarray": "^4.4.0"
}
},
"node-inject-html": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/node-inject-html/-/node-inject-html-0.0.5.tgz",
"integrity": "sha512-YUjHKq6sjD5rru/lqxQ0BHLvgJP33T54/ehsxgi1sShUkA5cU+Kb/eZ8UnbjaZeKxueA5D75vwAmyc+B5Kof1Q==",
"requires": {
"moo": "^0.5.1"
}
},
"node-libs-browser": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@@ -4655,6 +4729,17 @@
"has": "^1.0.3"
}
},
"object.fromentries": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz",
"integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==",
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1",
"has": "^1.0.3"
}
},
"object.getownpropertydescriptors": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
@@ -4707,16 +4792,6 @@
"requires": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"dependencies": {
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"requires": {
"is-docker": "^2.0.0"
}
}
}
},
"optimize-css-assets-webpack-plugin": {
@@ -4746,6 +4821,11 @@
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@@ -4770,6 +4850,23 @@
"aggregate-error": "^3.0.0"
}
},
"p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"requires": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
}
},
"p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"requires": {
"p-finally": "^1.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
@@ -6574,9 +6671,9 @@
}
},
"rollup": {
"version": "2.38.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.1.tgz",
"integrity": "sha512-q07T6vU/V1kqM8rGRRyCgEvIQcIAXoKIE5CpkYAlHhfiWM1Iuh4dIPWpIbqFngCK6lwAB2aYHiUVhIbSWHQWhw==",
"version": "2.36.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.36.2.tgz",
"integrity": "sha512-qjjiuJKb+/8n0EZyQYVW+gFU4bNRBcZaXVzUgSVrGw0HlQBlK2aWyaOMMs1Ufic1jV69b9kW3u3i9B+hISDm3A==",
"requires": {
"fsevents": "~2.1.2"
},
@@ -6644,14 +6741,14 @@
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"serialize-javascript": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"requires": {
"randombytes": "^2.1.0"
}
@@ -6916,9 +7013,9 @@
}
},
"ssri": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
"integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz",
"integrity": "sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA==",
"requires": {
"minipass": "^3.1.1"
}
@@ -7233,6 +7330,11 @@
"source-map-support": "~0.5.19"
},
"dependencies": {
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
@@ -7273,6 +7375,14 @@
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
}
},
"serialize-javascript": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
"integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
"requires": {
"randombytes": "^2.1.0"
}
}
}
},
@@ -7814,6 +7924,26 @@
"binary-extensions": "^1.0.0"
}
},
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"optional": true,
"requires": {
"kind-of": "^3.0.2"
},
"dependencies": {
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"optional": true,
"requires": {
"is-buffer": "^1.1.5"
}
}
}
},
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@@ -7952,6 +8082,11 @@
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -7991,6 +8126,29 @@
"locate-path": "^3.0.0"
}
},
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"requires": {
"kind-of": "^3.0.2"
},
"dependencies": {
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"requires": {
"is-buffer": "^1.1.5"
}
}
}
},
"is-wsl": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
"integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
@@ -8089,13 +8247,10 @@
"ajv-keywords": "^3.1.0"
}
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"requires": {
"randombytes": "^2.1.0"
}
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"ssri": {
"version": "6.0.1",

View File

@@ -8,17 +8,17 @@
},
"dependencies": {
"@prefresh/snowpack": "^3.0.1",
"@snowpack/plugin-optimize": "^0.2.13",
"@snowpack/plugin-postcss": "^1.1.0",
"@snowpack/plugin-webpack": "^2.3.0",
"autoprefixer": "^10.2.1",
"cross-env": "^7.0.3",
"immer": "^8.0.1",
"postcss": "^8.2.2",
"postcss-cli": "^8.3.1",
"preact": "^10.5.9",
"preact-router": "^3.2.1",
"rimraf": "^3.0.2",
"snowpack": "^3.0.11",
"snowpack": "^3.0.0",
"tailwindcss": "^2.0.2"
}
}

View File

@@ -8,6 +8,12 @@ module.exports = {
plugins: [
'@snowpack/plugin-postcss',
'@prefresh/snowpack',
[
'@snowpack/plugin-optimize',
{
preloadModules: true,
},
],
[
'@snowpack/plugin-webpack',
{

View File

@@ -1,5 +1,4 @@
import { h } from 'preact';
import ActivityIndicator from './components/ActivityIndicator';
import Camera from './Camera';
import CameraMap from './CameraMap';
import Cameras from './Cameras';
@@ -8,27 +7,37 @@ import Event from './Event';
import Events from './Events';
import { Router } from 'preact-router';
import Sidebar from './Sidebar';
import Api, { FetchStatus, useConfig } from './api';
import { ApiHost, Config } from './context';
import { useContext, useEffect, useState } from 'preact/hooks';
export default function App() {
const { data, status } = useConfig();
return status !== FetchStatus.LOADED ? (
<div className="flex flex-grow-1 min-h-screen justify-center items-center">
<ActivityIndicator />
</div>
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 />
) : (
<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="flex-auto p-2 md:p-4 lg:pl-8 lg:pr-8 min-w-0">
<Router>
<CameraMap path="/cameras/:camera/editor" />
<Camera path="/cameras/:camera" />
<Event path="/events/:eventId" />
<Events path="/events" />
<Debug path="/debug" />
<Cameras default path="/" />
</Router>
<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 default path="/" />
</Router>
</div>
</div>
</div>
</Config.Provider>
);
return;
}

View File

@@ -6,13 +6,13 @@ import Link from './components/Link';
import Switch from './components/Switch';
import { route } from 'preact-router';
import { useCallback, useContext } from 'preact/hooks';
import { useApiHost, useConfig } from './api';
import { ApiHost, Config } from './context';
export default function Camera({ camera, url }) {
const { data: config } = useConfig();
const apiHost = useApiHost();
const config = useContext(Config);
const apiHost = useContext(ApiHost);
if (!config) {
if (!(camera in config.cameras)) {
return <div>{`No camera named ${camera}`}</div>;
}

View File

@@ -5,11 +5,11 @@ import Heading from './components/Heading';
import Switch from './components/Switch';
import { route } from 'preact-router';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useApiHost, useConfig } from './api';
import { ApiHost, Config } from './context';
export default function CameraMasks({ camera, url }) {
const { data: config } = useConfig();
const apiHost = useApiHost();
const config = useContext(Config);
const apiHost = useContext(ApiHost);
const imageRef = useRef(null);
const [imageScale, setImageScale] = useState(1);
const [snap, setSnap] = useState(true);
@@ -27,26 +27,14 @@ export default function CameraMasks({ camera, url }) {
zones,
} = cameraConfig;
const resizeObserver = useMemo(
() =>
new ResizeObserver((entries) => {
window.requestAnimationFrame(() => {
if (Array.isArray(entries) && entries.length) {
const scaledWidth = entries[0].contentRect.width;
const scale = scaledWidth / width;
setImageScale(scale);
}
});
}),
[camera, width, setImageScale]
);
useEffect(() => {
if (!imageRef.current) {
return;
}
resizeObserver.observe(imageRef.current);
}, [resizeObserver, imageRef.current]);
const scaledWidth = imageRef.current.width;
const scale = scaledWidth / width;
setImageScale(scale);
}, [imageRef.current, setImageScale]);
const [motionMaskPoints, setMotionMaskPoints] = useState(
Array.isArray(motionMask)
@@ -189,7 +177,7 @@ ${Object.keys(zonePoints)
const handleAddObjectMask = useCallback(() => {
const n = Object.keys(objectMaskPoints).filter((name) => name.startsWith('object_')).length;
const newObjectName = `object_${n}`;
const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [[]] };
const newObjectMaskPoints = { ...objectMaskPoints, [newObjectName]: [] };
setObjectMaskPoints(newObjectMaskPoints);
setEditing({ set: newObjectMaskPoints, key: newObjectName, subkey: 0, fn: setObjectMaskPoints });
}, [objectMaskPoints, setObjectMaskPoints, setEditing]);
@@ -197,7 +185,7 @@ ${Object.keys(zonePoints)
const handleRemoveObjectMask = useCallback(
(key, subkey) => {
const newObjectMaskPoints = { ...objectMaskPoints };
delete newObjectMaskPoints[key][subkey];
delete newObjectMaskPoints[key];
setObjectMaskPoints(newObjectMaskPoints);
},
[objectMaskPoints, setObjectMaskPoints]
@@ -217,20 +205,6 @@ ${Object.keys(objectMaskPoints)
.join('\n')}`);
}, [objectMaskPoints]);
const handleAddToObjectMask = useCallback(
(key) => {
const newObjectMaskPoints = { ...objectMaskPoints, [key]: [...objectMaskPoints[key], []] };
setObjectMaskPoints(newObjectMaskPoints);
setEditing({
set: newObjectMaskPoints,
key,
subkey: newObjectMaskPoints[key].length - 1,
fn: setObjectMaskPoints,
});
},
[objectMaskPoints, setObjectMaskPoints, setEditing]
);
const handleChangeSnap = useCallback(
(id, value) => {
setSnap(value);
@@ -252,10 +226,10 @@ ${Object.keys(objectMaskPoints)
<Box className="space-y-4">
<div className="relative">
<img ref={imageRef} src={`${apiHost}/api/${camera}/latest.jpg`} />
<img ref={imageRef} className="w-full" src={`${apiHost}/api/${camera}/latest.jpg`} />
<EditableMask
onChange={handleUpdateEditable}
points={'subkey' in editing ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
points={editing.subkey ? editing.set[editing.key][editing.subkey] : editing.set[editing.key]}
scale={imageScale}
snap={snap}
width={width}
@@ -294,7 +268,6 @@ ${Object.keys(objectMaskPoints)
isMulti
editing={editing}
title="Object masks"
onAdd={handleAddToObjectMask}
onCopy={handleCopyObjectMasks}
onCreate={handleAddObjectMask}
onEdit={handleEditObjectMask}
@@ -424,7 +397,6 @@ function MaskValues({
isMulti = false,
editing,
title,
onAdd,
onCopy,
onCreate,
onEdit,
@@ -466,14 +438,6 @@ function MaskValues({
[onRemove]
);
const handleAdd = useCallback(
(event) => {
const { key } = event.target.dataset;
onAdd(key);
},
[onAdd]
);
return (
<Box className="overflow-hidden" onmouseover={handleMousein} onmouseout={handleMouseout}>
<div class="flex space-x-4">
@@ -490,20 +454,15 @@ function MaskValues({
return (
<div>
{` ${mainkey}:\n mask:\n`}
{onAdd && showButtons ? (
<Button className="absolute -mt-12 right-0 font-sans" data-key={mainkey} onClick={handleAdd}>
{`Add to ${mainkey}`}
</Button>
) : null}
{points[mainkey].map((item, subkey) => (
<Item
mainkey={mainkey}
subkey={subkey}
editing={editing}
handleEdit={handleEdit}
handleRemove={handleRemove}
points={item}
showButtons={showButtons}
handleRemove={handleRemove}
yamlKeyPrefix={yamlKeyPrefix}
/>
))}
@@ -514,11 +473,10 @@ function MaskValues({
<Item
mainkey={mainkey}
editing={editing}
handleAdd={onAdd ? handleAdd : undefined}
handleEdit={handleEdit}
handleRemove={handleRemove}
points={points[mainkey]}
showButtons={showButtons}
handleRemove={handleRemove}
yamlKeyPrefix={yamlKeyPrefix}
/>
);
@@ -529,7 +487,7 @@ function MaskValues({
);
}
function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handleAdd, handleRemove, yamlKeyPrefix }) {
function Item({ mainkey, subkey, editing, handleEdit, points, showButtons, handleRemove, yamlKeyPrefix }) {
return (
<span
data-key={mainkey}

View File

@@ -1,16 +1,15 @@
import { h } from 'preact';
import ActivityIndicator from './components/ActivityIndicator';
import Box from './components/Box';
import CameraImage from './components/CameraImage';
import Events from './Events';
import Heading from './components/Heading';
import { route } from 'preact-router';
import { useConfig } from './api';
import { useContext } from 'preact/hooks';
import { ApiHost, Config } from './context';
export default function Cameras() {
const { data: config, status } = useConfig();
const config = useContext(Config);
if (!config) {
if (!config.cameras) {
return <p>loading</p>;
}
@@ -24,6 +23,7 @@ export default function Cameras() {
}
function Camera({ name }) {
const apiHost = useContext(ApiHost);
const href = `/cameras/${name}`;
return (
@@ -32,7 +32,7 @@ function Camera({ name }) {
href={href}
>
<Heading size="base">{name}</Heading>
<CameraImage camera={name} />
<img className="w-full" src={`${apiHost}/api/${name}/latest.jpg`} />
</Box>
);
}

View File

@@ -1,24 +1,25 @@
import { h } from 'preact';
import ActivityIndicator from './components/ActivityIndicator';
import Box from './components/Box';
import Button from './components/Button';
import Heading from './components/Heading';
import Link from './components/Link';
import { FetchStatus, useConfig, useStats } from './api';
import { ApiHost, Config } from './context';
import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
export default function Debug() {
const config = useConfig();
const apiHost = useContext(ApiHost);
const config = useContext(Config);
const [stats, setStats] = useState({});
const [timeoutId, setTimeoutId] = useState(null);
const forceUpdate = useCallback(async () => {
setTimeoutId(setTimeout(forceUpdate, 1000));
}, []);
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(() => {
forceUpdate();
fetchStats();
}, []);
useEffect(() => {
@@ -26,13 +27,11 @@ export default function Debug() {
clearTimeout(timeoutId);
};
}, [timeoutId]);
const { data: stats, status } = useStats(null, timeoutId);
if (stats === null && (status === FetchStatus.LOADING || status === FetchStatus.NONE)) {
return <ActivityIndicator />;
}
const { detectors, detection_fps, service, ...cameras } = stats;
if (!service) {
return 'loading…';
}
const detectorNames = Object.keys(detectors);
const detectorDataKeys = Object.keys(detectors[detectorNames[0]]);
@@ -40,73 +39,59 @@ export default function Debug() {
const cameraNames = Object.keys(cameras);
const cameraDataKeys = Object.keys(cameras[cameraNames[0]]);
const handleCopyConfig = useCallback(async () => {
await window.navigator.clipboard.writeText(JSON.stringify(config, null, 2));
}, [config]);
return (
<div class="space-y-4">
<div>
<Heading>
Debug <span className="text-sm">{service.version}</span>
</Heading>
<Box>
<Table className="w-full">
<Thead>
<Tr>
<Th>detector</Th>
<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) => (
<Th>{name.replace('_', ' ')}</Th>
<Td key={`${name}-${detector}`}>{detectors[detector][name]}</Td>
))}
</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>
</Box>
))}
</Tbody>
</Table>
<Box>
<Table className="w-full">
<Thead>
<Tr>
<Th>camera</Th>
<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) => (
<Th>{name.replace('_', ' ')}</Th>
<Td key={`${name}-${camera}`}>{cameras[camera][name]}</Td>
))}
</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>
</Box>
))}
</Tbody>
</Table>
<Box className="relative">
<Heading size="sm">Config</Heading>
<Button className="absolute top-4 right-8" onClick={handleCopyConfig}>
Copy to Clipboard
</Button>
<pre className="overflow-auto font-mono text-gray-900 dark:text-gray-100 rounded bg-gray-100 dark:bg-gray-800 p-2 max-h-96">
{JSON.stringify(config, null, 2)}
</pre>
</Box>
<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,17 +1,28 @@
import { h, Fragment } from 'preact';
import ActivityIndicator from './components/ActivityIndicator';
import { ApiHost } from './context';
import Box from './components/Box';
import Heading from './components/Heading';
import Link from './components/Link';
import { FetchStatus, useApiHost, useEvent } from './api';
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 = useApiHost();
const { data, status } = useEvent(eventId);
const apiHost = useContext(ApiHost);
const [data, setData] = useState(null);
if (status !== FetchStatus.LOADED) {
return <ActivityIndicator />;
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);
@@ -46,36 +57,34 @@ export default function Event({ eventId }) {
/>
</Box>
<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>
</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,123 +1,48 @@
import { h } from 'preact';
import ActivityIndicator from './components/ActivityIndicator';
import { ApiHost } from './context';
import Box from './components/Box';
import Heading from './components/Heading';
import Link from './components/Link';
import produce from 'immer';
import { route } from 'preact-router';
import { FetchStatus, useApiHost, useConfig, useEvents } from './api';
import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
import { useCallback, useContext, useEffect, useState } from 'preact/hooks';
const API_LIMIT = 25;
export default function Events({ url } = {}) {
const apiHost = useContext(ApiHost);
const [events, setEvents] = useState([]);
const initialState = Object.freeze({ events: [], reachedEnd: false, searchStrings: {} });
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'APPEND_EVENTS': {
const {
meta: { searchString },
payload,
} = action;
return produce(state, (draftState) => {
draftState.searchStrings[searchString] = true;
draftState.events.push(...payload);
});
}
const searchParams = new URL(`${window.location.protocol}//${window.location.host}${url || '/events'}`).searchParams;
const searchParamsString = searchParams.toString();
case 'REACHED_END': {
const {
meta: { searchString },
} = action;
return produce(state, (draftState) => {
draftState.reachedEnd = true;
draftState.searchStrings[searchString] = true;
});
}
useEffect(async () => {
const response = await fetch(`${apiHost}/api/events?${searchParamsString}`);
const data = response.ok ? await response.json() : {};
setEvents(data);
}, [searchParamsString]);
case 'RESET':
return initialState;
default:
return state;
}
};
const defaultSearchString = `include_thumbnails=0&limit=${API_LIMIT}`;
function removeDefaultSearchKeys(searchParams) {
searchParams.delete('limit');
searchParams.delete('include_thumbnails');
searchParams.delete('before');
}
export default function Events({ path: pathname } = {}) {
const apiHost = useApiHost();
const [{ events, reachedEnd, searchStrings }, dispatch] = useReducer(reducer, initialState);
const { searchParams: initialSearchParams } = new URL(window.location);
const [searchString, setSearchString] = useState(`${defaultSearchString}&${initialSearchParams.toString()}`);
const { data, status } = useEvents(searchString);
useEffect(() => {
if (data && !(searchString in searchStrings)) {
dispatch({ type: 'APPEND_EVENTS', payload: data, meta: { searchString } });
}
if (Array.isArray(data) && data.length < API_LIMIT) {
dispatch({ type: 'REACHED_END', meta: { searchString } });
}
}, [data]);
const observer = useRef(
new IntersectionObserver((entries, observer) => {
window.requestAnimationFrame(() => {
if (entries.length === 0) {
return;
}
// under certain edge cases, a ref may be applied / in memory twice
// avoid fetching twice by grabbing the last observed entry only
const entry = entries[entries.length - 1];
if (entry.isIntersecting) {
const { startTime } = entry.target.dataset;
const { searchParams } = new URL(window.location);
searchParams.set('before', parseFloat(startTime) - 0.0001);
setSearchString(`${defaultSearchString}&${searchParams.toString()}`);
}
});
})
);
const lastCellRef = useCallback(
(node) => {
if (node !== null) {
observer.current.disconnect();
if (!reachedEnd) {
observer.current.observe(node);
}
}
},
[observer.current, reachedEnd]
);
const handleFilter = useCallback(
(searchParams) => {
dispatch({ type: 'RESET' });
removeDefaultSearchKeys(searchParams);
setSearchString(`${defaultSearchString}&${searchParams.toString()}`);
route(`${pathname}?${searchParams.toString()}`);
},
[pathname, setSearchString]
);
const searchParams = useMemo(() => new URLSearchParams(searchString), [searchString]);
const searchKeys = Array.from(searchParams.keys());
return (
<div className="space-y-4 w-full">
<div className="space-y-4">
<Heading>Events</Heading>
<Filters onChange={handleFilter} searchParams={searchParams} />
{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 className="min-w-full table-fixed">
<Table>
<Thead>
<Tr>
<Th></Th>
@@ -138,49 +63,25 @@ export default function Events({ path: pathname } = {}) {
) => {
const start = new Date(parseInt(startTime * 1000, 10));
const end = new Date(parseInt(endTime * 1000, 10));
const ref = i === events.length - 1 ? lastCellRef : undefined;
return (
<Tr key={id} index={i}>
<Td className="w-40">
<a href={`/events/${id}`} ref={ref} data-start-time={startTime} data-reached-end={reachedEnd}>
<img
width="150"
height="150"
style="min-height: 48px; min-width: 48px;"
src={`${apiHost}/api/events/${id}/thumbnail.jpg`}
/>
<Td>
<a href={`/events/${id}`}>
<img className="w-32 max-w-none" src={`data:image/jpeg;base64,${thumbnail}`} />
</a>
</Td>
<Td>
<Filterable
onFilter={handleFilter}
pathname={pathname}
searchParams={searchParams}
paramName="camera"
name={camera}
/>
<Filterable searchParams={searchParamsString} paramName="camera" name={camera} />
</Td>
<Td>
<Filterable
onFilter={handleFilter}
pathname={pathname}
searchParams={searchParams}
paramName="label"
name={label}
/>
<Filterable searchParams={searchParamsString} paramName="label" name={label} />
</Td>
<Td>{(score * 100).toFixed(2)}%</Td>
<Td>
<ul>
{zones.map((zone) => (
<li>
<Filterable
onFilter={handleFilter}
pathname={pathname}
searchParams={searchString}
paramName="zone"
name={zone}
/>
<Filterable searchParams={searchParamsString} paramName="zone" name={zone} />
</li>
))}
</ul>
@@ -193,108 +94,27 @@ export default function Events({ path: pathname } = {}) {
}
)}
</Tbody>
<Tfoot>
<Tr>
<Td className="text-center p-4" colspan="8">
{status === FetchStatus.LOADING ? <ActivityIndicator /> : reachedEnd ? 'No more events' : null}
</Td>
</Tr>
</Tfoot>
</Table>
</Box>
</div>
);
}
function Filterable({ onFilter, pathname, searchParams, paramName, name }) {
const href = useMemo(() => {
const params = new URLSearchParams(searchParams.toString());
params.set(paramName, name);
removeDefaultSearchKeys(params);
return `${pathname}?${params.toString()}`;
}, [searchParams]);
const handleClick = useCallback(
(event) => {
event.preventDefault();
route(href, true);
const params = new URLSearchParams(searchParams.toString());
params.set(paramName, name);
onFilter(params);
},
[href, searchParams]
);
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 (
<Link href={href} onclick={handleClick}>
<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}
</Link>
);
}
function Filters({ onChange, searchParams }) {
const { data } = useConfig();
const cameras = useMemo(() => Object.keys(data.cameras), [data]);
const zones = useMemo(
() =>
Object.values(data.cameras)
.reduce((memo, camera) => {
memo = memo.concat(Object.keys(camera.zones));
return memo;
}, [])
.filter((value, i, self) => self.indexOf(value) === i),
[data]
);
const labels = useMemo(() => {
return Object.values(data.cameras)
.reduce((memo, camera) => {
memo = memo.concat(camera.objects?.track || []);
return memo;
}, data.objects?.track || [])
.filter((value, i, self) => self.indexOf(value) === i);
}, [data]);
return (
<Box className="flex space-y-0 space-x-8 flex-wrap">
<Filter onChange={onChange} options={cameras} paramName="camera" searchParams={searchParams} />
<Filter onChange={onChange} options={zones} paramName="zone" searchParams={searchParams} />
<Filter onChange={onChange} options={labels} paramName="label" searchParams={searchParams} />
</Box>
);
}
function Filter({ onChange, searchParams, paramName, options }) {
const handleSelect = useCallback(
(event) => {
const newParams = new URLSearchParams(searchParams.toString());
const value = event.target.value;
if (value) {
newParams.set(paramName, event.target.value);
} else {
newParams.delete(paramName);
}
onChange(newParams);
},
[searchParams, paramName, onChange]
);
return (
<label>
<span className="block uppercase text-sm">{paramName}</span>
<select className="border-solid border border-gray-500 rounded dark:text-gray-900" onChange={handleSelect}>
<option>All</option>
{options.map((opt) => {
return (
<option value={opt} selected={searchParams.get(paramName) === opt}>
{opt}
</option>
);
})}
</select>
</label>
</a>
);
}

View File

@@ -1,108 +0,0 @@
import { h, createContext } from 'preact';
import produce from 'immer';
import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');
export const FetchStatus = {
NONE: 'none',
LOADING: 'loading',
LOADED: 'loaded',
ERROR: 'error',
};
const initialState = Object.freeze({
host: import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '',
queries: {},
});
export const Api = createContext(initialState);
export default Api;
function reducer(state, { type, payload, meta }) {
switch (type) {
case 'REQUEST': {
const { url, request } = payload;
const data = state.queries[url]?.data || null;
return produce(state, (draftState) => {
draftState.queries[url] = { status: FetchStatus.LOADING, data };
});
}
case 'RESPONSE': {
const { url, ok, data } = payload;
return produce(state, (draftState) => {
draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data };
});
}
default:
return state;
}
}
export const ApiProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <Api.Provider value={{ state, dispatch }}>{children}</Api.Provider>;
};
function shouldFetch(state, url, forceRefetch = false) {
if (forceRefetch || !(url in state.queries)) {
return true;
}
const { status } = state.queries[url];
return status !== FetchStatus.LOADING && status !== FetchStatus.LOADED;
}
export function useFetch(url, forceRefetch) {
const { state, dispatch } = useContext(Api);
useEffect(() => {
if (!shouldFetch(state, url, forceRefetch)) {
return;
}
async function fetchConfig() {
await dispatch({ type: 'REQUEST', payload: { url } });
const response = await fetch(`${state.host}${url}`);
const data = await response.json();
await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data } });
}
fetchConfig();
}, [url, forceRefetch]);
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, dispatch } = useContext(Api);
return state.host;
}
export function useEvents(searchParams, forceRefetch) {
const url = `/api/events${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, forceRefetch);
}
export function useEvent(eventId, forceRefetch) {
const url = `/api/events/${eventId}`;
return useFetch(url, forceRefetch);
}
export function useConfig(searchParams, forceRefetch) {
const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, forceRefetch);
}
export function useStats(searchParams, forceRefetch) {
const url = `/api/stats${searchParams ? `?${searchParams.toString()}` : ''}`;
return useFetch(url, forceRefetch);
}

View File

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

@@ -1,28 +1,27 @@
import { h } from 'preact';
import CameraImage from './CameraImage';
import { useCallback, useState } from 'preact/hooks';
import { ApiHost, Config } from '../context';
import { useCallback, useEffect, useContext, useState } from 'preact/hooks';
const MIN_LOAD_TIMEOUT_MS = 200;
export default function AutoUpdatingCameraImage({ camera, searchParams }) {
const config = useContext(Config);
const apiHost = useContext(ApiHost);
const cameraConfig = config.cameras[camera];
export default function AutoUpdatingCameraImage({ camera, searchParams, showFps = true }) {
const [key, setKey] = useState(Date.now());
const [fps, setFps] = useState(0);
const handleLoad = useCallback(() => {
const loadTime = Date.now() - key;
setFps((1000 / Math.max(loadTime, MIN_LOAD_TIMEOUT_MS)).toFixed(1));
setTimeout(
() => {
setKey(Date.now());
},
loadTime > MIN_LOAD_TIMEOUT_MS ? 1 : MIN_LOAD_TIMEOUT_MS
);
}, [key, searchParams, setFps]);
useEffect(() => {
const timeoutId = setTimeout(() => {
setKey(Date.now());
}, 500);
return () => {
clearTimeout(timeoutId);
};
}, [key, searchParams]);
return (
<div>
<CameraImage camera={camera} onload={handleLoad} searchParams={`cache=${key}&${searchParams}`} />
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
</div>
<img
className="w-full"
src={`${apiHost}/api/${camera}/latest.jpg?cache=${key}&${searchParams}`}
alt={`Auto-updating ${camera} image`}
/>
);
}

View File

@@ -4,7 +4,7 @@ export default function Box({ children, className = '', hover = false, href, ...
const Element = href ? 'a' : 'div';
return (
<Element
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-2 lg:p-4 ${
className={`bg-white dark:bg-gray-700 shadow-lg rounded-lg p-4 ${
hover ? 'hover:bg-gray-300 hover:dark:bg-gray-500 dark:hover:text-gray-900 dark:hover:text-gray-900' : ''
} ${className}`}
href={href}

View File

@@ -1,65 +0,0 @@
import { h } from 'preact';
import ActivityIndicator from './ActivityIndicator';
import { useApiHost, useConfig } from '../api';
import { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'preact/hooks';
export default function CameraImage({ camera, onload, searchParams = '' }) {
const { data: config } = useConfig();
const apiHost = useApiHost();
const [availableWidth, setAvailableWidth] = useState(0);
const [loadedSrc, setLoadedSrc] = useState(null);
const containerRef = useRef(null);
const { name, width, height } = config.cameras[camera];
const aspectRatio = width / height;
const resizeObserver = useMemo(() => {
return new ResizeObserver((entries) => {
window.requestAnimationFrame(() => {
if (Array.isArray(entries) && entries.length) {
setAvailableWidth(entries[0].contentRect.width);
}
});
});
}, [setAvailableWidth, width]);
useEffect(() => {
if (!containerRef.current) {
return;
}
resizeObserver.observe(containerRef.current);
}, [resizeObserver, containerRef.current]);
const scaledHeight = useMemo(() => Math.min(Math.ceil(availableWidth / aspectRatio), height), [
availableWidth,
aspectRatio,
height,
]);
const img = useMemo(() => new Image(), [camera]);
img.onload = useCallback(
(event) => {
const src = event.srcElement.currentSrc;
setLoadedSrc(src);
onload && onload(event);
},
[searchParams, onload]
);
useEffect(() => {
if (!scaledHeight) {
return;
}
img.src = `${apiHost}/api/${name}/latest.jpg?h=${scaledHeight}${searchParams ? `&${searchParams}` : ''}`;
}, [apiHost, name, img, searchParams, scaledHeight]);
return (
<div ref={containerRef}>
{loadedSrc ? (
<img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} />
) : (
<ActivityIndicator />
)}
</div>
);
}

View File

@@ -2,9 +2,13 @@ import { h } from 'preact';
import { useCallback, useState } from 'preact/hooks';
export default function Switch({ checked, label, id, onChange }) {
const handleChange = useCallback(() => {
onChange(id, !checked);
}, [id, onChange, checked]);
const handleChange = useCallback(
(event) => {
console.log(event.target.checked, !checked);
onChange(id, !checked);
},
[id, onChange, checked]
);
return (
<label for={id} className="flex items-center cursor-pointer">

View File

@@ -6,12 +6,12 @@ export function Table({ children, className = '' }) {
);
}
export function Thead({ children, className }) {
return <thead className={className}>{children}</thead>;
export function Thead({ children, className = '' }) {
return <thead className={`${className}`}>{children}</thead>;
}
export function Tbody({ children, className }) {
return <tbody className={className}>{children}</tbody>;
export function Tbody({ children, className = '' }) {
return <tbody className={`${className}`}>{children}</tbody>;
}
export function Tfoot({ children, className = '' }) {
@@ -19,21 +19,13 @@ export function Tfoot({ children, className = '' }) {
}
export function Tr({ children, className = '', index }) {
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-600' : ''} ${className}`}>{children}</tr>;
return <tr className={`${index % 2 ? 'bg-gray-200 dark:bg-gray-700' : ''} ${className}`}>{children}</tr>;
}
export function Th({ children, className = '', colspan }) {
return (
<th className={`border-b-2 border-gray-400 p-1 md:p-2 text-left ${className}`} colspan={colspan}>
{children}
</th>
);
export function Th({ children, className = '' }) {
return <th className={`border-b-2 border-gray-400 p-4 text-left ${className}`}>{children}</th>;
}
export function Td({ children, className = '', colspan }) {
return (
<td className={`p-1 md:p-2 ${className}`} colspan={colspan}>
{children}
</td>
);
export function Td({ children, className = '' }) {
return <td className={`p-4 ${className}`}>{children}</td>;
}

5
web/src/context/index.js Normal file
View File

@@ -0,0 +1,5 @@
import { createContext } from 'preact';
export const Config = createContext({});
export const ApiHost = createContext(import.meta.env.SNOWPACK_PUBLIC_API_HOST || window.baseUrl || '');

View File

@@ -1,27 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.activityindicator {
border-top-color: currentColor;
-webkit-animation: spinner 0.75s linear infinite;
animation: spinner 0.75s linear infinite;
}
@-webkit-keyframes spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -1,12 +1,9 @@
import App from './App';
import { ApiProvider } from './api';
import { h, render } from 'preact';
import 'preact/devtools';
import './index.css';
render(
<ApiProvider>
<App />
</ApiProvider>,
<App />,
document.getElementById('root')
);