Compare commits
1218 Commits
v0.1.1
...
v0.11.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed1897db71 | ||
|
|
dfbebb63ff | ||
|
|
a67a768e89 | ||
|
|
43f05c18d6 | ||
|
|
3b076c28c2 | ||
|
|
cbf12e3f90 | ||
|
|
17b745434c | ||
|
|
37011c2fda | ||
|
|
fa95a041dd | ||
|
|
0879d7a2d1 | ||
|
|
061fb15a80 | ||
|
|
3246fcce22 | ||
|
|
f2a3797b46 | ||
|
|
b80080ac52 | ||
|
|
b36b63599b | ||
|
|
5d8c0e43c2 | ||
|
|
7845995dfd | ||
|
|
afe88d6e3a | ||
|
|
560ee0104d | ||
|
|
dc8b625d55 | ||
|
|
162c0147d2 | ||
|
|
ef54cd6fb3 | ||
|
|
c2465a46a8 | ||
|
|
24d3a9cdd5 | ||
|
|
5e82eaed88 | ||
|
|
1d45b0b351 | ||
|
|
ba119e4f96 | ||
|
|
75c2feb387 | ||
|
|
da637d3c8f | ||
|
|
bc078fcc88 | ||
|
|
5f9d477863 | ||
|
|
ca693240b1 | ||
|
|
468febc434 | ||
|
|
4b81c88794 | ||
|
|
2ac28b93f3 | ||
|
|
3e7ed982d4 | ||
|
|
d8d410802f | ||
|
|
ca7bad8909 | ||
|
|
7b2b5bfa71 | ||
|
|
d2c3cdcf04 | ||
|
|
7c8142174e | ||
|
|
efe3f96223 | ||
|
|
de244d6873 | ||
|
|
90bff605fa | ||
|
|
f512df44c1 | ||
|
|
4e8ce28948 | ||
|
|
16057cdef7 | ||
|
|
da36f25fd0 | ||
|
|
0e9c8a4ddd | ||
|
|
ccc9b90625 | ||
|
|
d992a959f2 | ||
|
|
358d0521a1 | ||
|
|
691ed6a4c7 | ||
|
|
78e1782084 | ||
|
|
0d5ae9a399 | ||
|
|
d343a658ec | ||
|
|
0bd3cff13b | ||
|
|
5b71271b3c | ||
|
|
0a4d658c7f | ||
|
|
b75929a846 | ||
|
|
e33cd442cd | ||
|
|
91539de3ff | ||
|
|
bc0206de9d | ||
|
|
f536494a38 | ||
|
|
f2030d301f | ||
|
|
d890217447 | ||
|
|
cafe0917c7 | ||
|
|
ebf4e43ced | ||
|
|
a1afade9ba | ||
|
|
b86b2d6602 | ||
|
|
12abbc59d6 | ||
|
|
ec91466fe4 | ||
|
|
0044f73d7a | ||
|
|
65e0ec7826 | ||
|
|
a5016afdd4 | ||
|
|
6808ba1b3b | ||
|
|
41f58c7692 | ||
|
|
c6234bf548 | ||
|
|
d995761419 | ||
|
|
d749cf2e6b | ||
|
|
164e9b7eb8 | ||
|
|
d43f9189a6 | ||
|
|
a292f272e9 | ||
|
|
58c32857d3 | ||
|
|
35bd1de5ba | ||
|
|
51fd18f56d | ||
|
|
162e275ef3 | ||
|
|
44a2b54773 | ||
|
|
d17828931b | ||
|
|
cef77fba01 | ||
|
|
e724fe3da6 | ||
|
|
045aac8933 | ||
|
|
2e5d082ef3 | ||
|
|
923d07b1a4 | ||
|
|
c424c4b7ef | ||
|
|
b1cc64d4fa | ||
|
|
0abd0627df | ||
|
|
dccfc3b84f | ||
|
|
deb3536cb2 | ||
|
|
1d8f1b24a9 | ||
|
|
7cabf8e5f5 | ||
|
|
ffee9c8065 | ||
|
|
00112eb7bc | ||
|
|
9bbe75d64e | ||
|
|
95fe62e141 | ||
|
|
1c9ba11e07 | ||
|
|
4bae3993da | ||
|
|
d03865d1a5 | ||
|
|
83481afee1 | ||
|
|
b1a2b0cda2 | ||
|
|
0dfba6e8d9 | ||
|
|
e4afe50509 | ||
|
|
006569391f | ||
|
|
f4c3bb0617 | ||
|
|
3e07d4eddb | ||
|
|
4004048add | ||
|
|
cbf26e09a4 | ||
|
|
20b4b503f0 | ||
|
|
08f573aaa5 | ||
|
|
3dd3786055 | ||
|
|
bfecee9650 | ||
|
|
395c16300d | ||
|
|
ff19cdb773 | ||
|
|
5627b66a6e | ||
|
|
ebdfbfe96c | ||
|
|
1a009c7fd1 | ||
|
|
c14f986fae | ||
|
|
ee5b9986ad | ||
|
|
190f217b13 | ||
|
|
e78662b924 | ||
|
|
5a2e395352 | ||
|
|
8de15af7b4 | ||
|
|
21178613de | ||
|
|
340be7f86d | ||
|
|
0ff4acd59c | ||
|
|
28dd43f8ae | ||
|
|
56d24cbf6d | ||
|
|
e433bec17f | ||
|
|
3189467a36 | ||
|
|
63536d249f | ||
|
|
3e90f3032c | ||
|
|
5cff849e59 | ||
|
|
06cc7527a9 | ||
|
|
d78dc2388c | ||
|
|
583912db9c | ||
|
|
5792cf042e | ||
|
|
4524dca3ed | ||
|
|
304a569c7e | ||
|
|
10c200dc24 | ||
|
|
0b8617d09f | ||
|
|
e7026dfd6e | ||
|
|
b82d75b79e | ||
|
|
ac30091258 | ||
|
|
de58bdcc9f | ||
|
|
329e5f8f91 | ||
|
|
4a16171f96 | ||
|
|
f0212c2aa4 | ||
|
|
7401cf2399 | ||
|
|
493d16519a | ||
|
|
a87675d3cc | ||
|
|
0bef16bb17 | ||
|
|
e0078f388e | ||
|
|
cf6e66c453 | ||
|
|
34fa53afcc | ||
|
|
663bf05fd7 | ||
|
|
f512af2563 | ||
|
|
7e7d70aa5b | ||
|
|
dd1cf4d2ce | ||
|
|
e627f4e935 | ||
|
|
c6445898ce | ||
|
|
f1ddd0e6f7 | ||
|
|
db369a5b7f | ||
|
|
87cd618998 | ||
|
|
338e4004d4 | ||
|
|
675f21e23a | ||
|
|
4d2d11193f | ||
|
|
69aaf1f8e6 | ||
|
|
a10970d7c9 | ||
|
|
6eecb6780e | ||
|
|
80627e4989 | ||
|
|
9e987fdebc | ||
|
|
e6292c719d | ||
|
|
7c74bf2566 | ||
|
|
2c91e7853c | ||
|
|
1e7f196e5c | ||
|
|
f91f4f0053 | ||
|
|
8b2622a234 | ||
|
|
a2ddb12eb3 | ||
|
|
985bd6d9bd | ||
|
|
ec3c15e4a7 | ||
|
|
188b202836 | ||
|
|
01e607a14e | ||
|
|
5b164b72dc | ||
|
|
dcf65febba | ||
|
|
56a2d4e64d | ||
|
|
ef214fb80a | ||
|
|
93f418ac0b | ||
|
|
689af4ff87 | ||
|
|
4ab0927de8 | ||
|
|
014e6fc909 | ||
|
|
6832575643 | ||
|
|
07ad2d97b1 | ||
|
|
039f1a522e | ||
|
|
24e2f84231 | ||
|
|
e0c0033852 | ||
|
|
c50e9d48bf | ||
|
|
173eaabddf | ||
|
|
a748b70da1 | ||
|
|
8eabe5dd41 | ||
|
|
114415b5e1 | ||
|
|
ba55b5a6db | ||
|
|
7533f2a8ab | ||
|
|
543a8a1712 | ||
|
|
9b23ff597c | ||
|
|
b2ce1edd5a | ||
|
|
a0235b7da4 | ||
|
|
87e2300855 | ||
|
|
34bc6a6457 | ||
|
|
273076e7f4 | ||
|
|
b29b311e92 | ||
|
|
5a9e82c4b0 | ||
|
|
6218791708 | ||
|
|
0e43f452d2 | ||
|
|
0695bb097d | ||
|
|
294c79a271 | ||
|
|
e351e132f5 | ||
|
|
258215a3ae | ||
|
|
08ddfc100f | ||
|
|
8ab6cba521 | ||
|
|
eb16de7395 | ||
|
|
dde0498ed3 | ||
|
|
75c8570913 | ||
|
|
e36099a342 | ||
|
|
2f2329ba44 | ||
|
|
6c8b184d2c | ||
|
|
32878bd016 | ||
|
|
12d13988c4 | ||
|
|
9e4d921488 | ||
|
|
edc1884c4e | ||
|
|
a2d1bd2c67 | ||
|
|
bb68a2405b | ||
|
|
42ac4172ff | ||
|
|
998921ae63 | ||
|
|
26ae6084ea | ||
|
|
76142e9699 | ||
|
|
5e692acfbb | ||
|
|
a67b8ab84d | ||
|
|
4cf55ad8e2 | ||
|
|
c1132e6897 | ||
|
|
d6104f2eb2 | ||
|
|
b0e0abe385 | ||
|
|
4916e1cd1d | ||
|
|
cd87f3e6f4 | ||
|
|
18f4ab2644 | ||
|
|
0bd3be94ec | ||
|
|
25bb515afc | ||
|
|
7ab6961ee1 | ||
|
|
ae24cf3bb2 | ||
|
|
2e494477a6 | ||
|
|
80b72c75d9 | ||
|
|
9494bb7f5f | ||
|
|
86a741b6e6 | ||
|
|
f738275d21 | ||
|
|
e297e02800 | ||
|
|
b2e05afff2 | ||
|
|
05fc35fc3d | ||
|
|
c809494c98 | ||
|
|
ef82c5c691 | ||
|
|
c0e2a75715 | ||
|
|
01ddd00bc5 | ||
|
|
d150f01a2c | ||
|
|
f9e159deaf | ||
|
|
381b00157e | ||
|
|
800f33e7be | ||
|
|
b8218876be | ||
|
|
5669f4c161 | ||
|
|
c492b30adb | ||
|
|
eb48722126 | ||
|
|
8e881b60f0 | ||
|
|
0260d824a6 | ||
|
|
0877a7dec7 | ||
|
|
4c7919ad69 | ||
|
|
4e997124b3 | ||
|
|
8b040f5c95 | ||
|
|
96156805ed | ||
|
|
b8d48d7e62 | ||
|
|
8ca12806ca | ||
|
|
de811b7018 | ||
|
|
7bf7365f6c | ||
|
|
1daffd92fd | ||
|
|
74986982a0 | ||
|
|
aa807d25ed | ||
|
|
cd28869649 | ||
|
|
ae97692883 | ||
|
|
e8e778c6d4 | ||
|
|
5c552a0d71 | ||
|
|
0f5dfea9de | ||
|
|
e6cdb6a7a2 | ||
|
|
1d25936f31 | ||
|
|
1049673413 | ||
|
|
c3109f808c | ||
|
|
a943ac1308 | ||
|
|
96319e795c | ||
|
|
5a8016de87 | ||
|
|
bc350644bd | ||
|
|
c793500ad2 | ||
|
|
1b2134c49e | ||
|
|
86a5b46c68 | ||
|
|
f83d4a58dd | ||
|
|
a5f241d5bd | ||
|
|
661f7baa21 | ||
|
|
7b063a19dc | ||
|
|
0320d94ea6 | ||
|
|
a7b7a45b23 | ||
|
|
89e317a6bb | ||
|
|
5a209caed3 | ||
|
|
288b1a0562 | ||
|
|
d35b09b18f | ||
|
|
e8eb3125a5 | ||
|
|
8109445fdd | ||
|
|
f63a7cb6c0 | ||
|
|
7fc5297f60 | ||
|
|
00ff76a0b9 | ||
|
|
b8df419bad | ||
|
|
faf103152a | ||
|
|
65855e23d9 | ||
|
|
6c28613def | ||
|
|
56480dc1ef | ||
|
|
8e1c15291d | ||
|
|
a1e52c51b1 | ||
|
|
8cc834633e | ||
|
|
7d65c05994 | ||
|
|
d74021af47 | ||
|
|
46fe06e779 | ||
|
|
fbea51372f | ||
|
|
fa5ec8d019 | ||
|
|
11c425a7eb | ||
|
|
0d352f3d8a | ||
|
|
6ccff71408 | ||
|
|
41fea2a531 | ||
|
|
3d6dad7e7e | ||
|
|
4efc584816 | ||
|
|
10ab70080a | ||
|
|
bddde74c06 | ||
|
|
29de723267 | ||
|
|
354a9240f0 | ||
|
|
5ae4f47e96 | ||
|
|
26424488a5 | ||
|
|
334095252c | ||
|
|
1c85f774eb | ||
|
|
bbf0fc8324 | ||
|
|
b143e11e0e | ||
|
|
927f56ab9f | ||
|
|
2181379475 | ||
|
|
45798d6d14 | ||
|
|
f6d5e96dbf | ||
|
|
e18aa56427 | ||
|
|
f3a1c1de0a | ||
|
|
0ccf543ec1 | ||
|
|
1f1a708388 | ||
|
|
58c0d97b5f | ||
|
|
abef002af8 | ||
|
|
adf2bc078c | ||
|
|
b33f4e2dc6 | ||
|
|
9cab5da74c | ||
|
|
189b9c6648 | ||
|
|
1823bd0305 | ||
|
|
8fd12f001b | ||
|
|
fc40567794 | ||
|
|
de121008c1 | ||
|
|
2fbfbf614b | ||
|
|
1181620f33 | ||
|
|
c527b1ca5d | ||
|
|
181a504a14 | ||
|
|
5d940bcb86 | ||
|
|
dcfe26c9c0 | ||
|
|
5d94c68d66 | ||
|
|
a476bc9885 | ||
|
|
dc759a3e56 | ||
|
|
4818c08fe2 | ||
|
|
3bc75ae931 | ||
|
|
a94b61ef91 | ||
|
|
e51021c573 | ||
|
|
19115fb828 | ||
|
|
16ef432228 | ||
|
|
4449bc5292 | ||
|
|
50f0c05e69 | ||
|
|
6b26fc753e | ||
|
|
ebee204ca0 | ||
|
|
1a3567c5d0 | ||
|
|
4fa6fa2001 | ||
|
|
92aa16c556 | ||
|
|
a3853af47a | ||
|
|
f26f7b8d3f | ||
|
|
f4aa02cc19 | ||
|
|
da2492413f | ||
|
|
3876b00088 | ||
|
|
b1dfee2cf5 | ||
|
|
13c2c582ad | ||
|
|
f892a821f5 | ||
|
|
bb3ba2d572 | ||
|
|
9ee2e776fa | ||
|
|
fd61ca20b0 | ||
|
|
f9add57ed4 | ||
|
|
e1b341788d | ||
|
|
04c8b089a5 | ||
|
|
9971482506 | ||
|
|
955d9e6ec2 | ||
|
|
fe2e32c84f | ||
|
|
c6e53d0ea8 | ||
|
|
436b10a87b | ||
|
|
a2d5754e4b | ||
|
|
3d28d8cee2 | ||
|
|
11d8095cdd | ||
|
|
a4052a9fe5 | ||
|
|
6e6ee93282 | ||
|
|
85de881181 | ||
|
|
3712a8ab80 | ||
|
|
4ee7240ef3 | ||
|
|
df8126de73 | ||
|
|
2eddbe3d03 | ||
|
|
46dad7b58d | ||
|
|
eb5e6d19fa | ||
|
|
5afeb6e081 | ||
|
|
17944882f4 | ||
|
|
b24bb3eb18 | ||
|
|
98d8118fb2 | ||
|
|
3e1b680e4c | ||
|
|
b4e7e51e63 | ||
|
|
2a41a9d3ff | ||
|
|
09b0068d16 | ||
|
|
f9457be109 | ||
|
|
e9291fe9d1 | ||
|
|
20912c12e7 | ||
|
|
7ddba4fc59 | ||
|
|
7ad8b8298d | ||
|
|
3fb24b4bf5 | ||
|
|
ab93cae4c0 | ||
|
|
8d01cc4807 | ||
|
|
1e21a62851 | ||
|
|
c664bd63f6 | ||
|
|
dada764d2c | ||
|
|
762afb8f43 | ||
|
|
f009897eca | ||
|
|
fa61e9da29 | ||
|
|
0ff037997f | ||
|
|
e8c2cfa5b5 | ||
|
|
fb214b64ca | ||
|
|
222dc2f7c9 | ||
|
|
f94a946418 | ||
|
|
b3ee2de079 | ||
|
|
515d8c04ad | ||
|
|
49c65cf9c6 | ||
|
|
211fcd64c7 | ||
|
|
5075e4eee1 | ||
|
|
7bfe8e4f5b | ||
|
|
8e0c2b256e | ||
|
|
d66f5f6bad | ||
|
|
fd9c8c1f0d | ||
|
|
3a3b788c65 | ||
|
|
b134db48b3 | ||
|
|
f110a261b9 | ||
|
|
b72b66781a | ||
|
|
9b3a649f17 | ||
|
|
d83ffd8984 | ||
|
|
175c85d69a | ||
|
|
3aa7f753b3 | ||
|
|
861ee0485d | ||
|
|
03e756dd27 | ||
|
|
5d0984998d | ||
|
|
3b695040d1 | ||
|
|
40bf7a0f41 | ||
|
|
cbdf2c2c71 | ||
|
|
bbe3f07ec6 | ||
|
|
35281c037c | ||
|
|
e0eb247028 | ||
|
|
a7ecc2af4b | ||
|
|
f2d1f7b00f | ||
|
|
89c2ae2208 | ||
|
|
0df35379ef | ||
|
|
138fa45820 | ||
|
|
58cfb8bba4 | ||
|
|
ed761d1767 | ||
|
|
babe1dd1eb | ||
|
|
cbd418b8de | ||
|
|
9c5b0d4138 | ||
|
|
94c21a6363 | ||
|
|
d1a5e2e8e0 | ||
|
|
17179abf5a | ||
|
|
61f717daa3 | ||
|
|
c70419bd0b | ||
|
|
4eed27e178 | ||
|
|
8e2ba4a8ea | ||
|
|
700f25abc3 | ||
|
|
4596ada801 | ||
|
|
a8df97dc1a | ||
|
|
4e0203ca68 | ||
|
|
8413e10091 | ||
|
|
7aaf5bd29f | ||
|
|
5afda72b84 | ||
|
|
7a6ffb1032 | ||
|
|
fd51c7a955 | ||
|
|
b91b0d39dd | ||
|
|
7fc9026ca6 | ||
|
|
f4a0ec43a6 | ||
|
|
46e5a042ae | ||
|
|
7309c06be8 | ||
|
|
c71b717a54 | ||
|
|
da8e54ed51 | ||
|
|
b70c11e7a7 | ||
|
|
e8c342e162 | ||
|
|
f7975cbbc4 | ||
|
|
098e293c82 | ||
|
|
eb3f50c9f0 | ||
|
|
78a0b5e6c9 | ||
|
|
055bd22138 | ||
|
|
8dfff83447 | ||
|
|
de3a19c4f0 | ||
|
|
2a33c05378 | ||
|
|
bbb4d10718 | ||
|
|
68dfaaf767 | ||
|
|
0bb998c465 | ||
|
|
ec9b525a3c | ||
|
|
88ed7501a7 | ||
|
|
c73aebadcb | ||
|
|
40c4ca305e | ||
|
|
7b3abe330e | ||
|
|
0d96c3529d | ||
|
|
62452b1d21 | ||
|
|
c5f005afef | ||
|
|
4a45b6e76d | ||
|
|
a3cfbb6722 | ||
|
|
dc8ba5239d | ||
|
|
b53a50cd54 | ||
|
|
9822d614e2 | ||
|
|
ca20c735f7 | ||
|
|
d3dc018260 | ||
|
|
28a2a3816a | ||
|
|
5461308d30 | ||
|
|
b2f469ad76 | ||
|
|
8ea75e6748 | ||
|
|
09a4d6d030 | ||
|
|
abbc608ee4 | ||
|
|
9634ec8e31 | ||
|
|
84a0827aee | ||
|
|
1fbcf4d9b9 | ||
|
|
80f8256422 | ||
|
|
57864f2be6 | ||
|
|
f4bc68d396 | ||
|
|
926eec3c65 | ||
|
|
abc6bbe7e0 | ||
|
|
acdfd1ccc4 | ||
|
|
315a13e1ce | ||
|
|
aab6a00e4c | ||
|
|
a4e6d9ed9a | ||
|
|
e16dbcb671 | ||
|
|
e89697fab0 | ||
|
|
c52782aa3f | ||
|
|
7060f22024 | ||
|
|
d1931f249c | ||
|
|
bf1a542afb | ||
|
|
ebb6d348a3 | ||
|
|
f667bd9066 | ||
|
|
ff2eba5b9f | ||
|
|
3eb0021207 | ||
|
|
6379f5cd5e | ||
|
|
e3a8448a23 | ||
|
|
00d6036788 | ||
|
|
53bb69621c | ||
|
|
ad85beea91 | ||
|
|
d7a237677a | ||
|
|
16effa3938 | ||
|
|
33a04c425b | ||
|
|
20a52bc4e6 | ||
|
|
482399d82f | ||
|
|
3a171d19e3 | ||
|
|
480970c799 | ||
|
|
ceb3329f9e | ||
|
|
1c0162c181 | ||
|
|
cf62dccef7 | ||
|
|
42410a260c | ||
|
|
00c6a8f577 | ||
|
|
9d2469549f | ||
|
|
d2bc2c20c1 | ||
|
|
10c37a3036 | ||
|
|
00d14fa49f | ||
|
|
0f1bc40f00 | ||
|
|
4252857e19 | ||
|
|
6d12a34c40 | ||
|
|
9e126a4b91 | ||
|
|
040ffda687 | ||
|
|
39ff49e054 | ||
|
|
b8f72a5bcb | ||
|
|
43db704b74 | ||
|
|
5043040530 | ||
|
|
3c60aeeef9 | ||
|
|
0344d61b26 | ||
|
|
0e8467782b | ||
|
|
423ea26266 | ||
|
|
2f3339ba85 | ||
|
|
9433b50785 | ||
|
|
1e7b53dc0e | ||
|
|
bc94748f2a | ||
|
|
2395f93ed1 | ||
|
|
d771726c2a | ||
|
|
b2a2fe898c | ||
|
|
31d408a746 | ||
|
|
4a74f295e7 | ||
|
|
b6ba6459fb | ||
|
|
e399790442 | ||
|
|
20c65b9a31 | ||
|
|
1a7853a47e | ||
|
|
683c3a4c90 | ||
|
|
4a8d998afe | ||
|
|
fe59d90c51 | ||
|
|
f87813805a | ||
|
|
a7e5b9978f | ||
|
|
0a3959af86 | ||
|
|
9ba6054140 | ||
|
|
3348f04889 | ||
|
|
c12aec7c8f | ||
|
|
05f66b8f24 | ||
|
|
d8b80f0fe9 | ||
|
|
7314572d97 | ||
|
|
52a29ed00a | ||
|
|
5eaf8a5448 | ||
|
|
f70fb12c3d | ||
|
|
ece6c1203c | ||
|
|
9c7e3177a2 | ||
|
|
058c0affaf | ||
|
|
5ee7146884 | ||
|
|
1aa9a7a093 | ||
|
|
a202c44a0f | ||
|
|
85776cc7d0 | ||
|
|
6d133ef724 | ||
|
|
53288d361c | ||
|
|
e729bd52aa | ||
|
|
ddb6127519 | ||
|
|
50b42eb6fe | ||
|
|
b6572b7272 | ||
|
|
57ced2c284 | ||
|
|
26a3491466 | ||
|
|
eed8463832 | ||
|
|
718b4f3fd7 | ||
|
|
22461d1728 | ||
|
|
69cab1e6bb | ||
|
|
a661fddaf3 | ||
|
|
1b85e561b9 | ||
|
|
a803ab8577 | ||
|
|
daa759cc55 | ||
|
|
513a099c24 | ||
|
|
e299e73a68 | ||
|
|
9550ac7422 | ||
|
|
07bd376649 | ||
|
|
ec6a1ed9d1 | ||
|
|
7aee28d080 | ||
|
|
24ec13e36d | ||
|
|
d2e7c360b9 | ||
|
|
f00628f4e5 | ||
|
|
19bd5ace7d | ||
|
|
3e2506136c | ||
|
|
4e03acc944 | ||
|
|
188eb6b9ea | ||
|
|
c89e1a5735 | ||
|
|
e50cc59f0d | ||
|
|
96f87caff0 | ||
|
|
b422a83b57 | ||
|
|
15ae3bee55 | ||
|
|
0cac2fec2a | ||
|
|
5965da88c3 | ||
|
|
ba0338e9d5 | ||
|
|
ff62338359 | ||
|
|
9867f4eeee | ||
|
|
ba278dfc3d | ||
|
|
063030bcf3 | ||
|
|
276ce8710c | ||
|
|
5ed7a17f46 | ||
|
|
01c3b4fa6e | ||
|
|
165ca8fbc7 | ||
|
|
ce90ae343c | ||
|
|
a99f360a64 | ||
|
|
d51e9446ff | ||
|
|
d3524ee46f | ||
|
|
121ea37825 | ||
|
|
9592d95599 | ||
|
|
d6faa18adb | ||
|
|
1cbe6f77ee | ||
|
|
4f5d4e36b7 | ||
|
|
163025c1f2 | ||
|
|
880178d62e | ||
|
|
d285ff7e54 | ||
|
|
54671fc522 | ||
|
|
53e3e6545d | ||
|
|
91cb49c4a3 | ||
|
|
c065cb48f2 | ||
|
|
d376f6b1d2 | ||
|
|
45526a7652 | ||
|
|
cc7929932b | ||
|
|
e6516235fa | ||
|
|
40d5a9f890 | ||
|
|
ee3e744cc6 | ||
|
|
b55bd1e027 | ||
|
|
9a96df0319 | ||
|
|
e9b1618364 | ||
|
|
6dc6ed1e94 | ||
|
|
1943a49274 | ||
|
|
a8c00edc94 | ||
|
|
faa8abb2b9 | ||
|
|
f6cd2fc68e | ||
|
|
6482000d6b | ||
|
|
dcf7209706 | ||
|
|
2ec921593e | ||
|
|
75a01f657e | ||
|
|
d4e512c1fc | ||
|
|
26e7d34f18 | ||
|
|
15b5ffddd4 | ||
|
|
f0f3764992 | ||
|
|
2beb44b591 | ||
|
|
27b659dde1 | ||
|
|
630c2ee6f6 | ||
|
|
600477c487 | ||
|
|
d31c295598 | ||
|
|
a7bb0931c4 | ||
|
|
ff99a01423 | ||
|
|
ea6e311318 | ||
|
|
6790467bbc | ||
|
|
d315dbea22 | ||
|
|
8db7ab6724 | ||
|
|
9a2c034ae8 | ||
|
|
2885b80a13 | ||
|
|
4a85156e87 | ||
|
|
1785c69e1b | ||
|
|
a862ba8348 | ||
|
|
633d45d02f | ||
|
|
7f4e042dfa | ||
|
|
507ec13848 | ||
|
|
2132352639 | ||
|
|
11016b8486 | ||
|
|
8615f14407 | ||
|
|
1e84f08018 | ||
|
|
7f663328dc | ||
|
|
ea53068432 | ||
|
|
144aff9b4e | ||
|
|
18db6daf0a | ||
|
|
26ba29b538 | ||
|
|
70167a34b6 | ||
|
|
ccb668a1b6 | ||
|
|
0989c64eab | ||
|
|
c082fc5cb2 | ||
|
|
d39111a294 | ||
|
|
3c072f94b0 | ||
|
|
7f8ae2ce5c | ||
|
|
d84b75168c | ||
|
|
eb0a5e1c55 | ||
|
|
47ac77dbb0 | ||
|
|
ec84847be7 | ||
|
|
e7839bfd40 | ||
|
|
8762da627b | ||
|
|
3fab321045 | ||
|
|
9451048574 | ||
|
|
46c002038b | ||
|
|
d1d833ea9a | ||
|
|
c1f0750526 | ||
|
|
89e02b6956 | ||
|
|
97e8258288 | ||
|
|
39040c1874 | ||
|
|
c709851888 | ||
|
|
b022bec1fa | ||
|
|
bca0531963 | ||
|
|
b2c7fc8f5b | ||
|
|
96ac2c29d6 | ||
|
|
14a5118b4d | ||
|
|
232fa1ffe8 | ||
|
|
d2e91754e9 | ||
|
|
4d9066a58d | ||
|
|
c618867941 | ||
|
|
5ad4017510 | ||
|
|
63e14a98f9 | ||
|
|
25e3fe8eab | ||
|
|
840f046572 | ||
|
|
89e3c2e4b1 | ||
|
|
c770470b58 | ||
|
|
4619836122 | ||
|
|
76403bba8e | ||
|
|
a9afa303a2 | ||
|
|
e5399ae07a | ||
|
|
80a5a7b129 | ||
|
|
9dc97d4b6b | ||
|
|
d8c9169af2 | ||
|
|
ec256f7130 | ||
|
|
19bbfce4ed | ||
|
|
b0b2d9d972 | ||
|
|
6f5f5c9461 | ||
|
|
fc04bc6046 | ||
|
|
9f504253fb | ||
|
|
961997e078 | ||
|
|
363594a9a2 | ||
|
|
247e2677f3 | ||
|
|
5b5159f4dd | ||
|
|
bc8b85860c | ||
|
|
44d45c5880 | ||
|
|
a6d8e4fc3f | ||
|
|
2cc9a15f6a | ||
|
|
151f9fb2ee | ||
|
|
32fb76b3d1 | ||
|
|
8d52e2635a | ||
|
|
f20e1f20a6 | ||
|
|
af8594c5c6 | ||
|
|
899d41f361 | ||
|
|
7dc6382c90 | ||
|
|
e8009c2d26 | ||
|
|
3bc7cdaab6 | ||
|
|
724d8187c6 | ||
|
|
8f68df60c7 | ||
|
|
6af3cb6134 | ||
|
|
2ff0c3907f | ||
|
|
dd102ff01d | ||
|
|
f20b1d75e6 | ||
|
|
a4b88ac4a7 | ||
|
|
93b9d586d2 | ||
|
|
41dd4447cc | ||
|
|
8b9c8a2e80 | ||
|
|
a63ff1bb99 | ||
|
|
d5e3b59245 | ||
|
|
d0470fffcc | ||
|
|
5053305e17 | ||
|
|
9c79392060 | ||
|
|
708c3278bf | ||
|
|
c0249f6e59 | ||
|
|
afd8aefac2 | ||
|
|
3c07767138 | ||
|
|
953c442f13 | ||
|
|
e147852878 | ||
|
|
db7ee6cfb3 | ||
|
|
3b41b6cc33 | ||
|
|
5e79888370 | ||
|
|
a6aa9bdd59 | ||
|
|
d78b7cc110 | ||
|
|
e7cdace0ab | ||
|
|
f60eb4e977 | ||
|
|
7aecf6c6de | ||
|
|
75d62096a6 | ||
|
|
7c44994070 | ||
|
|
f49f3fd9c3 | ||
|
|
5ea86d636c | ||
|
|
4c6e90717a | ||
|
|
d60ca9d783 | ||
|
|
d304718ea0 | ||
|
|
c787c8948e | ||
|
|
62728ef7fb | ||
|
|
47e256f03d | ||
|
|
527db52d5e | ||
|
|
f78b2c48a7 | ||
|
|
90c965a32a | ||
|
|
d4afcde6c9 | ||
|
|
257de89ce4 | ||
|
|
735cc3962b | ||
|
|
feb42181de | ||
|
|
f5c4bfa7b4 | ||
|
|
65ddd91855 | ||
|
|
6d7d838613 | ||
|
|
5edf7b7f00 | ||
|
|
117569830d | ||
|
|
d62aec7287 | ||
|
|
4e0cf3681e | ||
|
|
d98751102a | ||
|
|
1acbeb813e | ||
|
|
b87ec752cf | ||
|
|
753df31fa6 | ||
|
|
bd77b74689 | ||
|
|
810c23d8ee | ||
|
|
34d9b2983e | ||
|
|
63c5c8412a | ||
|
|
60207723d1 | ||
|
|
f4117ad096 | ||
|
|
8bed4e9970 | ||
|
|
f72eaf781c | ||
|
|
e9ecc20a36 | ||
|
|
addfa2a32d | ||
|
|
0dad9bc393 | ||
|
|
5155875a72 | ||
|
|
4ed1217366 | ||
|
|
50e898a684 | ||
|
|
251c7fa982 | ||
|
|
00c75e9f98 | ||
|
|
1b5b02d286 | ||
|
|
946d655cee | ||
|
|
d56710b0b5 | ||
|
|
0cf78277b5 | ||
|
|
ce2a583ff9 | ||
|
|
84bddad30e | ||
|
|
0ff682504a | ||
|
|
5d5984166f | ||
|
|
b825eb44fe | ||
|
|
7015eb66f2 | ||
|
|
494eeb16a5 | ||
|
|
692fdc8d5d | ||
|
|
ec1a8ebd4a | ||
|
|
59daa6597b | ||
|
|
3941ce4ad1 | ||
|
|
aff87d4372 | ||
|
|
373ca87887 | ||
|
|
03c855ecbe | ||
|
|
3a3cb24631 | ||
|
|
4c3fea25a5 | ||
|
|
af303cbf2a | ||
|
|
b7c09a9b38 | ||
|
|
eced06eea8 | ||
|
|
15d989255c | ||
|
|
095566b9c2 | ||
|
|
b77a65d446 | ||
|
|
9778a748fc | ||
|
|
a89dddcafa | ||
|
|
75973fd4c0 | ||
|
|
514036f9d1 | ||
|
|
36fbedab20 | ||
|
|
180baeba50 | ||
|
|
cce82fe2a5 | ||
|
|
5512bb2e06 | ||
|
|
be1fcbbdf8 | ||
|
|
422cd52049 | ||
|
|
d67a56d37e | ||
|
|
070c9721b6 | ||
|
|
0219834dd1 | ||
|
|
a1cc9ad1f0 | ||
|
|
29e8aa4020 | ||
|
|
777aff403f | ||
|
|
4b3b702459 | ||
|
|
893e6b40a7 | ||
|
|
a85d780020 | ||
|
|
34439699ae | ||
|
|
64b63142b1 | ||
|
|
cee1ab000b | ||
|
|
3ff98770c1 | ||
|
|
244203463d | ||
|
|
b6f7940b10 | ||
|
|
75312602aa | ||
|
|
75977128f0 | ||
|
|
eafde6c677 | ||
|
|
da0598baef | ||
|
|
35ba5e2f7c | ||
|
|
49258d6dbe | ||
|
|
5a081e4f00 | ||
|
|
4feae472e9 | ||
|
|
4e83239258 | ||
|
|
c4cccf44a5 | ||
|
|
64e7cbcc62 | ||
|
|
dd86e4f317 | ||
|
|
4db285a875 | ||
|
|
939d1ba091 | ||
|
|
0fe8d486d9 | ||
|
|
a3cb02af5c | ||
|
|
45a6b8452c | ||
|
|
9d594cc640 | ||
|
|
59e41ae1ac | ||
|
|
c6ed16465b | ||
|
|
8f14b36f5a | ||
|
|
b6c2491e3b | ||
|
|
8e31d04d90 | ||
|
|
bf93fbb357 | ||
|
|
c064b244db | ||
|
|
0280610e96 | ||
|
|
4363623c45 | ||
|
|
c960914ec3 | ||
|
|
9ecc80b443 | ||
|
|
3e146de0a2 | ||
|
|
bee54c39dc | ||
|
|
623d138d60 | ||
|
|
76befc1249 | ||
|
|
51251b9fb0 | ||
|
|
8c45076bb6 | ||
|
|
7d683ef399 | ||
|
|
e4da3822b1 | ||
|
|
12c4cd77c5 | ||
|
|
a611cbb942 | ||
|
|
f946813ccb | ||
|
|
49fca1b839 | ||
|
|
54cb4a2180 | ||
|
|
9954e3b11e | ||
|
|
82692b0ddc | ||
|
|
9d4fdec12f | ||
|
|
ed72c995ef | ||
|
|
66c77d1157 | ||
|
|
40c322ad47 | ||
|
|
83f1e0d713 | ||
|
|
2d89044bd3 | ||
|
|
dc4d24c2b9 | ||
|
|
d5fb20c524 | ||
|
|
7e92e8bfe8 | ||
|
|
efdcfcef97 | ||
|
|
574ee2a46f | ||
|
|
ec4d048905 | ||
|
|
b063099b2a | ||
|
|
2937dac4c3 | ||
|
|
7c283a1805 | ||
|
|
309c0dcda3 | ||
|
|
b35cc01035 | ||
|
|
6e79a5402e | ||
|
|
a989f8daaf | ||
|
|
7880d24b29 | ||
|
|
fdc8bbf72d | ||
|
|
005e188d38 | ||
|
|
adcc3e9b98 | ||
|
|
5fe201da25 | ||
|
|
974f7bd0df | ||
|
|
780ae7cd4f | ||
|
|
50e568b84c | ||
|
|
1ce993051e | ||
|
|
69406343ee | ||
|
|
1c33b8acb2 | ||
|
|
5e77436d39 | ||
|
|
e26308a05b | ||
|
|
c16ee3186f | ||
|
|
fedeeab561 | ||
|
|
bfcaabecfa | ||
|
|
606fa6f6d5 | ||
|
|
6a8d8bf53d | ||
|
|
1f81cba706 | ||
|
|
5db7b242aa | ||
|
|
0b7f65e227 | ||
|
|
2f758af097 | ||
|
|
f64320a464 | ||
|
|
3e87ef6426 | ||
|
|
acb75fa02d | ||
|
|
ea4ecae27c | ||
|
|
a8556a729b | ||
|
|
068df3ef2d | ||
|
|
b304139db2 | ||
|
|
df2aae5169 | ||
|
|
351ac4ec7d | ||
|
|
12e40291c0 | ||
|
|
8af7d51159 | ||
|
|
84ada716ac | ||
|
|
cbcc89be9c | ||
|
|
73a5e11b9b | ||
|
|
194baaeb56 | ||
|
|
469259d663 | ||
|
|
f3db69d975 | ||
|
|
0914cb71ad | ||
|
|
0ae2806eb4 | ||
|
|
adcfe699c2 | ||
|
|
e5048f98b6 | ||
|
|
e6c6338266 | ||
|
|
1f03c8cb8c | ||
|
|
69f5249788 | ||
|
|
3a1f1c946b | ||
|
|
d88745af6e | ||
|
|
709d917f0c | ||
|
|
918386bdc1 | ||
|
|
a8c0fadf95 | ||
|
|
6dc7b8f246 | ||
|
|
71f6f0bee4 | ||
|
|
a00afb61c0 | ||
|
|
5dbe6c5f36 | ||
|
|
16732aa5b3 | ||
|
|
3d2f1437e4 | ||
|
|
fbe721c860 | ||
|
|
7383db60b0 | ||
|
|
53ccc903da | ||
|
|
9d1f9f35e5 | ||
|
|
c1f522ff54 | ||
|
|
b345571a63 | ||
|
|
f29ee6165f | ||
|
|
ec6432cc5f | ||
|
|
8c917667b6 | ||
|
|
941434b8d8 | ||
|
|
2d0632adf8 | ||
|
|
f1afaf641a | ||
|
|
743116a733 | ||
|
|
8e77cf25d9 | ||
|
|
7d33e03943 | ||
|
|
0c44666c89 | ||
|
|
ddaa746807 | ||
|
|
760e1ffe1d | ||
|
|
15b4024715 | ||
|
|
918112a793 | ||
|
|
4ee200a81c | ||
|
|
e37eba49ff | ||
|
|
6de8e3bd1f | ||
|
|
3a9781c4f8 | ||
|
|
a60b9211d2 | ||
|
|
777fb1d5d1 | ||
|
|
8e9110f42e | ||
|
|
c80137e059 | ||
|
|
2768e1dadb | ||
|
|
2fbba01577 | ||
|
|
e7c536ea31 | ||
|
|
1734c0569a | ||
|
|
a5bef89123 | ||
|
|
d8aa73d26e | ||
|
|
791409d5e5 | ||
|
|
01bf89907d | ||
|
|
8e73c7e95e | ||
|
|
088bd18adb | ||
|
|
2e8c7ec225 | ||
|
|
9340a74371 | ||
|
|
5998de610b | ||
|
|
dfabff3846 | ||
|
|
76a7a3bad5 | ||
|
|
a3fa97dd52 | ||
|
|
1d2a41129c | ||
|
|
956298128d | ||
|
|
e6892d66b8 | ||
|
|
6ef22cf578 | ||
|
|
3e6f6edf7e | ||
|
|
81c5b96ed7 | ||
|
|
6f6d202c99 | ||
|
|
2fc389c3ad | ||
|
|
05951aa7da | ||
|
|
bb8e4621f5 | ||
|
|
04e9ab5ce4 | ||
|
|
1089a40943 | ||
|
|
68c3a069ba | ||
|
|
80b9652f7a | ||
|
|
569e07949f | ||
|
|
ffa9534549 | ||
|
|
c539993387 | ||
|
|
8a572f96d5 | ||
|
|
24cb3508e8 | ||
|
|
3f34c57e31 | ||
|
|
4c618daa90 | ||
|
|
cd057370e1 | ||
|
|
6263912655 | ||
|
|
af247275cf | ||
|
|
1198c29dac | ||
|
|
169603d3ff | ||
|
|
dc7eecebc6 | ||
|
|
0dd4087d5d | ||
|
|
6ecf87fc60 | ||
|
|
ebcf1482f8 | ||
|
|
50bcf60893 | ||
|
|
38efbd63ea | ||
|
|
50bcad8b77 | ||
|
|
cfffb219ae | ||
|
|
382d7be50a | ||
|
|
f43dc36a37 | ||
|
|
38e7fa07d2 | ||
|
|
e261c20819 | ||
|
|
3a66e672d3 | ||
|
|
2aada930e3 | ||
|
|
d87f4407a0 | ||
|
|
be5a114f6a | ||
|
|
32b212c7b6 | ||
|
|
76c8e3a12f | ||
|
|
16f7a361c3 | ||
|
|
634b87307f | ||
|
|
1d4fbbdba3 | ||
|
|
65579e9cbf | ||
|
|
49dc029c43 | ||
|
|
08174d8db2 | ||
|
|
5199242a68 | ||
|
|
725dd3220c | ||
|
|
10dc56f6ea | ||
|
|
cc2abe93a6 | ||
|
|
0c6717090c | ||
|
|
f5a2252b29 | ||
|
|
02efb6f415 | ||
|
|
5b4c6e50bc | ||
|
|
9cc46a71cb | ||
|
|
be1673b00a | ||
|
|
b6130e77ff | ||
|
|
4180c710cd | ||
|
|
ab3e70b4db | ||
|
|
d90e408d50 | ||
|
|
6c87ce0879 | ||
|
|
b7b4e38f62 | ||
|
|
480175d70f | ||
|
|
bee99ca6ff | ||
|
|
5c01720567 | ||
|
|
262f45c8bc | ||
|
|
22bb17b2fd | ||
|
|
3a3afe14bf | ||
|
|
01f058a482 | ||
|
|
d899ef158e | ||
|
|
39d64f7ba7 | ||
|
|
f148eb5a7b | ||
|
|
297e2f1c0c | ||
|
|
e818744d81 | ||
|
|
ceedfae993 | ||
|
|
e13563770d | ||
|
|
a659019d1a | ||
|
|
ba71927d53 | ||
|
|
04fed31eac | ||
|
|
ebaa8fac01 | ||
|
|
2ec45cd1b6 | ||
|
|
700bd1e3ef | ||
|
|
c9e9f7a735 | ||
|
|
aea4dc8724 | ||
|
|
12d5007b90 | ||
|
|
8970e73f75 | ||
|
|
1ba006b24f | ||
|
|
4a58f16637 | ||
|
|
436b876b24 | ||
|
|
a770ab7f69 | ||
|
|
806acaf445 | ||
|
|
c653567cc1 | ||
|
|
8fee8f86a2 | ||
|
|
59a4b0e650 | ||
|
|
834a3df0bc | ||
|
|
c41b104997 | ||
|
|
7028b05856 | ||
|
|
2d22a04391 | ||
|
|
baa587028b | ||
|
|
2b51dc3e5b | ||
|
|
9f8278ea8f | ||
|
|
56b9c754f5 | ||
|
|
5c4f5ef3f0 | ||
|
|
8c924896c5 | ||
|
|
2c2f0044b9 | ||
|
|
874e9085a7 | ||
|
|
e791d6646b | ||
|
|
3019b0218c | ||
|
|
6900e140d5 | ||
|
|
911c1b2bfa | ||
|
|
f4587462cf | ||
|
|
cac1faa8ac | ||
|
|
9525bae5a3 | ||
|
|
dbcfd109f6 |
40
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "Frigate Dev",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "dev",
|
||||
"workspaceFolder": "/lab/frigate",
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"mhutchie.git-graph",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-python.vscode-pylance",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"mikestead.dotenv",
|
||||
"csstools.postcss",
|
||||
"blanu.vscode-styled-jsx",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
],
|
||||
"settings": {
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.languageServer": "Pylance",
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnType": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"eslint.workingDirectories": ["./web"],
|
||||
"[json][jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsx][js][tsx][ts]": {
|
||||
"editor.codeActionsOnSave": ["source.addMissingImports", "source.fixAll"],
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"cSpell.ignoreWords": ["rtmp"],
|
||||
"cSpell.words": ["preact"]
|
||||
}
|
||||
}
|
||||
@@ -1 +1,12 @@
|
||||
README.md
|
||||
README.md
|
||||
docs/
|
||||
.gitignore
|
||||
debug
|
||||
config/
|
||||
*.pyc
|
||||
.git
|
||||
core
|
||||
*.mp4
|
||||
*.jpg
|
||||
*.db
|
||||
*.ts
|
||||
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github:
|
||||
- blakeblackshear
|
||||
- paularmstrong
|
||||
107
.github/ISSUE_TEMPLATE/camera_support_request.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Camera Support Request
|
||||
description: Support for setting up cameras in Frigate
|
||||
title: "[Camera Support]: "
|
||||
labels: ["support", "triage"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the problem you are having
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Visible on the Debug page in the Web UI
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Frigate config file
|
||||
description: This will be automatically formatted into code, so no need for backticks.
|
||||
render: yaml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: ffprobe
|
||||
attributes:
|
||||
label: FFprobe output from your camera
|
||||
description: Run `ffprobe <camera_url>` and provide output below
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: stats
|
||||
attributes:
|
||||
label: Frigate stats
|
||||
description: Output from frigate's /api/stats endpoint
|
||||
render: json
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- HassOS
|
||||
- Debian
|
||||
- Other Linux
|
||||
- Proxmox
|
||||
- UNRAID
|
||||
- Windows
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- HassOS Addon
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: coral
|
||||
attributes:
|
||||
label: Coral version
|
||||
options:
|
||||
- USB
|
||||
- PCIe
|
||||
- M.2
|
||||
- Dev Board
|
||||
- Other
|
||||
- CPU (no coral)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: network
|
||||
attributes:
|
||||
label: Network connection
|
||||
options:
|
||||
- Wired
|
||||
- Wireless
|
||||
- Mixed
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: camera
|
||||
attributes:
|
||||
label: Camera make and model
|
||||
description: Dahua, hikvision, amcrest, reolink, etc and model number
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: Any other information that may be helpful
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
82
.github/ISSUE_TEMPLATE/config_support_request.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Config Support Request
|
||||
description: Support for Frigate configuration
|
||||
title: "[Config Support]: "
|
||||
labels: ["support", "triage"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the problem you are having
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Visible on the Debug page in the Web UI
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Frigate config file
|
||||
description: This will be automatically formatted into code, so no need for backticks.
|
||||
render: yaml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: stats
|
||||
attributes:
|
||||
label: Frigate stats
|
||||
description: Output from frigate's /api/stats endpoint
|
||||
render: json
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- HassOS
|
||||
- Debian
|
||||
- Other Linux
|
||||
- Proxmox
|
||||
- UNRAID
|
||||
- Windows
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- HassOS Addon
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: coral
|
||||
attributes:
|
||||
label: Coral version
|
||||
options:
|
||||
- USB
|
||||
- PCIe
|
||||
- M.2
|
||||
- Dev Board
|
||||
- Other
|
||||
- CPU (no coral)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: Any other information that may be helpful
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe what you are trying to accomplish and why in non technical terms**
|
||||
I want to be able to ... so that I can ...
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
107
.github/ISSUE_TEMPLATE/general_support_request.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: General Support Request
|
||||
description: General support request for Frigate
|
||||
title: "[Support]: "
|
||||
labels: ["support", "triage"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the problem you are having
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Visible on the Debug page in the Web UI
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Frigate config file
|
||||
description: This will be automatically formatted into code, so no need for backticks.
|
||||
render: yaml
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: ffprobe
|
||||
attributes:
|
||||
label: FFprobe output from your camera
|
||||
description: Run `ffprobe <camera_url>` and provide output below
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: stats
|
||||
attributes:
|
||||
label: Frigate stats
|
||||
description: Output from frigate's /api/stats endpoint
|
||||
render: json
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system
|
||||
options:
|
||||
- HassOS
|
||||
- Debian
|
||||
- Other Linux
|
||||
- Proxmox
|
||||
- UNRAID
|
||||
- Windows
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: install-method
|
||||
attributes:
|
||||
label: Install method
|
||||
options:
|
||||
- HassOS Addon
|
||||
- Docker Compose
|
||||
- Docker CLI
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: coral
|
||||
attributes:
|
||||
label: Coral version
|
||||
options:
|
||||
- USB
|
||||
- PCIe
|
||||
- M.2
|
||||
- Dev Board
|
||||
- Other
|
||||
- CPU (no coral)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: network
|
||||
attributes:
|
||||
label: Network connection
|
||||
options:
|
||||
- Wired
|
||||
- Wireless
|
||||
- Mixed
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: camera
|
||||
attributes:
|
||||
label: Camera make and model
|
||||
description: Dahua, hikvision, amcrest, reolink, etc and model number
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: Any other information that may be helpful
|
||||
17
.github/stale.yml
vendored
Normal 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
|
||||
80
.github/workflows/pull_request.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: On pull request
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: 3.9
|
||||
|
||||
jobs:
|
||||
web_lint:
|
||||
name: Web - Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: npm install
|
||||
working-directory: ./web
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
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: 16.x
|
||||
- run: npm install
|
||||
working-directory: ./web
|
||||
- name: Test
|
||||
run: npm run test
|
||||
working-directory: ./web
|
||||
|
||||
python_checks:
|
||||
runs-on: ubuntu-latest
|
||||
name: Python checks
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Install requirements
|
||||
run: |
|
||||
pip install pip
|
||||
pip install -r requirements-dev.txt
|
||||
- name: Lint
|
||||
run: |
|
||||
python3 -m black frigate --check
|
||||
|
||||
python_tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: Python Tests
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: npm install
|
||||
working-directory: ./web
|
||||
- name: Build web
|
||||
run: npm run build
|
||||
working-directory: ./web
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Create Version Module
|
||||
run: make version
|
||||
- name: Build
|
||||
run: make
|
||||
- name: Run mypy
|
||||
run: docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
|
||||
- name: Run tests
|
||||
run: docker run --rm --entrypoint=python3 frigate:latest -u -m unittest
|
||||
17
.gitignore
vendored
@@ -1,2 +1,17 @@
|
||||
*.pyc
|
||||
.DS_Store
|
||||
*.pyc
|
||||
*.swp
|
||||
debug
|
||||
.vscode
|
||||
config/config.yml
|
||||
models
|
||||
*.mp4
|
||||
*.ts
|
||||
*.db
|
||||
*.csv
|
||||
frigate/version.py
|
||||
web/build
|
||||
web/node_modules
|
||||
web/coverage
|
||||
core
|
||||
!/web/**/*.ts
|
||||
|
||||
588
.pylintrc
Normal file
@@ -0,0 +1,588 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Specify a score threshold to be exceeded before program exits with error.
|
||||
fail-under=10.0
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
|
||||
# which contain the number of messages in each category, as well as 'statement'
|
||||
# which is the total number of statements analyzed. This score is used by the
|
||||
# global evaluation report (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
#notes-rgx=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=fstr
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
||||
107
Dockerfile
@@ -1,107 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
# Install system packages
|
||||
RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y python3 \
|
||||
python3-dev \
|
||||
python-pil \
|
||||
python-lxml \
|
||||
python-tk \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
libgtk2.0-dev \
|
||||
pkg-config \
|
||||
libavcodec-dev \
|
||||
libavformat-dev \
|
||||
libswscale-dev \
|
||||
libtbb2 \
|
||||
libtbb-dev \
|
||||
libjpeg-dev \
|
||||
libpng-dev \
|
||||
libtiff-dev \
|
||||
libjasper-dev \
|
||||
libdc1394-22-dev \
|
||||
x11-apps \
|
||||
wget \
|
||||
vim \
|
||||
ffmpeg \
|
||||
unzip \
|
||||
libusb-1.0-0-dev \
|
||||
python3-setuptools \
|
||||
python3-numpy \
|
||||
zlib1g-dev \
|
||||
libgoogle-glog-dev \
|
||||
swig \
|
||||
libunwind-dev \
|
||||
libc++-dev \
|
||||
libc++abi-dev \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install core packages
|
||||
RUN wget -q -O /tmp/get-pip.py --no-check-certificate https://bootstrap.pypa.io/get-pip.py && python3 /tmp/get-pip.py
|
||||
RUN pip install -U pip \
|
||||
numpy \
|
||||
pillow \
|
||||
matplotlib \
|
||||
notebook \
|
||||
Flask \
|
||||
imutils \
|
||||
paho-mqtt \
|
||||
PyYAML
|
||||
|
||||
# Install tensorflow models object detection
|
||||
RUN GIT_SSL_NO_VERIFY=true git clone -q https://github.com/tensorflow/models /usr/local/lib/python3.5/dist-packages/tensorflow/models
|
||||
RUN wget -q -P /usr/local/src/ --no-check-certificate https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-python-3.5.1.tar.gz
|
||||
|
||||
# Download & build protobuf-python
|
||||
RUN cd /usr/local/src/ \
|
||||
&& tar xf protobuf-python-3.5.1.tar.gz \
|
||||
&& rm protobuf-python-3.5.1.tar.gz \
|
||||
&& cd /usr/local/src/protobuf-3.5.1/ \
|
||||
&& ./configure \
|
||||
&& make \
|
||||
&& make install \
|
||||
&& ldconfig \
|
||||
&& rm -rf /usr/local/src/protobuf-3.5.1/
|
||||
|
||||
# Download & build OpenCV
|
||||
RUN wget -q -P /usr/local/src/ --no-check-certificate https://github.com/opencv/opencv/archive/4.0.1.zip
|
||||
RUN cd /usr/local/src/ \
|
||||
&& unzip 4.0.1.zip \
|
||||
&& rm 4.0.1.zip \
|
||||
&& cd /usr/local/src/opencv-4.0.1/ \
|
||||
&& mkdir build \
|
||||
&& cd /usr/local/src/opencv-4.0.1/build \
|
||||
&& cmake -D CMAKE_INSTALL_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr/local/ .. \
|
||||
&& make -j4 \
|
||||
&& make install \
|
||||
&& rm -rf /usr/local/src/opencv-4.0.1
|
||||
|
||||
# Download and install EdgeTPU libraries
|
||||
RUN wget -q -O edgetpu_api.tar.gz --no-check-certificate http://storage.googleapis.com/cloud-iot-edge-pretrained-models/edgetpu_api.tar.gz
|
||||
|
||||
RUN tar xzf edgetpu_api.tar.gz \
|
||||
&& cd python-tflite-source \
|
||||
&& cp -p libedgetpu/libedgetpu_x86_64.so /lib/x86_64-linux-gnu/libedgetpu.so \
|
||||
&& cp edgetpu/swig/compiled_so/_edgetpu_cpp_wrapper_x86_64.so edgetpu/swig/_edgetpu_cpp_wrapper.so \
|
||||
&& cp edgetpu/swig/compiled_so/edgetpu_cpp_wrapper.py edgetpu/swig/ \
|
||||
&& python3 setup.py develop --user
|
||||
|
||||
# Minimize image size
|
||||
RUN (apt-get autoremove -y; \
|
||||
apt-get autoclean -y)
|
||||
|
||||
# symlink the model and labels
|
||||
RUN ln -s /python-tflite-source/edgetpu/test_data/mobilenet_ssd_v2_coco_quant_postprocess_edgetpu.tflite /frozen_inference_graph.pb
|
||||
RUN ln -s /python-tflite-source/edgetpu/test_data/coco_labels.txt /label_map.pbtext
|
||||
|
||||
# Set TF object detection available
|
||||
ENV PYTHONPATH "$PYTHONPATH:/usr/local/lib/python3.5/dist-packages/tensorflow/models/research:/usr/local/lib/python3.5/dist-packages/tensorflow/models/research/slim"
|
||||
RUN cd /usr/local/lib/python3.5/dist-packages/tensorflow/models/research && protoc object_detection/protos/*.proto --python_out=.
|
||||
|
||||
WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
COPY detect_objects.py .
|
||||
|
||||
CMD ["python3", "-u", "detect_objects.py"]
|
||||
682
LICENSE
@@ -1,661 +1,21 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2020 Blake Blackshear
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
default_target: local
|
||||
|
||||
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
|
||||
VERSION = 0.11.0
|
||||
CURRENT_UID := $(shell id -u)
|
||||
CURRENT_GID := $(shell id -g)
|
||||
|
||||
version:
|
||||
echo "VERSION=\"$(VERSION)-$(COMMIT_HASH)\"" > frigate/version.py
|
||||
|
||||
build_web:
|
||||
docker run --volume ${PWD}/web:/web -w /web --volume /etc/passwd:/etc/passwd:ro --volume /etc/group:/etc/group:ro -u $(CURRENT_UID):$(CURRENT_GID) node:16 /bin/bash -c "npm install && npm run build"
|
||||
|
||||
nginx_frigate:
|
||||
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate-nginx:1.0.2 --file docker/Dockerfile.nginx .
|
||||
|
||||
local:
|
||||
DOCKER_BUILDKIT=1 docker build -t frigate -f docker/Dockerfile .
|
||||
|
||||
amd64:
|
||||
docker buildx build --platform linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
|
||||
|
||||
arm64:
|
||||
docker buildx build --platform linux/arm64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
|
||||
|
||||
armv7:
|
||||
docker buildx build --platform linux/arm/v7 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
|
||||
|
||||
build: version amd64 arm64 armv7
|
||||
docker buildx build --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
|
||||
|
||||
push: build
|
||||
docker buildx build --push --platform linux/arm/v7,linux/arm64/v8,linux/amd64 --tag blakeblackshear/frigate:$(VERSION)-$(COMMIT_HASH) --file docker/Dockerfile .
|
||||
|
||||
run_tests: frigate
|
||||
docker run --rm --entrypoint=python3 frigate:latest -u -m unittest
|
||||
docker run --rm --entrypoint=python3 frigate:latest -u -m mypy --config-file frigate/mypy.ini frigate
|
||||
|
||||
.PHONY: run_tests
|
||||
115
README.md
@@ -1,98 +1,45 @@
|
||||
# Frigate - Realtime Object Detection for RTSP Cameras
|
||||
**Note:** This version requires the use of a [Google Coral USB Accelerator](https://coral.withgoogle.com/products/accelerator/)
|
||||
<p align="center">
|
||||
<img align="center" alt="logo" src="docs/static/img/frigate.png">
|
||||
</p>
|
||||
|
||||
Uses OpenCV and Tensorflow to perform realtime object detection locally for RTSP cameras. Designed for integration with HomeAssistant or others via MQTT.
|
||||
# Frigate - NVR With Realtime Object Detection for IP Cameras
|
||||
|
||||
- Leverages multiprocessing and threads heavily with an emphasis on realtime over processing every frame
|
||||
- Allows you to define specific regions (squares) in the image to look for objects
|
||||
- No motion detection (for now)
|
||||
- Object detection with Tensorflow runs in a separate thread
|
||||
- Object info is published over MQTT for integration into HomeAssistant as a binary sensor
|
||||
- An endpoint is available to view an MJPEG stream for debugging
|
||||
A complete and local NVR designed for [Home Assistant](https://www.home-assistant.io) 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.
|
||||
|
||||
## Example video (from older version)
|
||||
You see multiple bounding boxes because it draws bounding boxes from all frames in the past 1 second where a person was detected. Not all of the bounding boxes were from the current frame.
|
||||
[](http://www.youtube.com/watch?v=nqHbCtyo4dY "Frigate")
|
||||
- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration)
|
||||
- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
|
||||
- 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
|
||||
- Records video with retention settings based on detected objects
|
||||
- 24/7 recording
|
||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||
|
||||
## Getting Started
|
||||
Build the container with
|
||||
```
|
||||
docker build -t frigate .
|
||||
```
|
||||
## Documentation
|
||||
|
||||
The `mobilenet_ssd_v2_coco_quant_postprocess_edgetpu.tflite` model is included and used by default. You can use your own model and labels by mounting files in the container at `/frozen_inference_graph.pb` and `/label_map.pbtext`. Models must be compatible with the Coral according to [this](https://coral.withgoogle.com/models/).
|
||||
View the documentation at https://docs.frigate.video
|
||||
|
||||
Run the container with
|
||||
```
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
-v /dev/bus/usb:/dev/bus/usb \
|
||||
-v <path_to_config_dir>:/config:ro \
|
||||
-p 5000:5000 \
|
||||
-e RTSP_PASSWORD='password' \
|
||||
frigate:latest
|
||||
```
|
||||
## Donations
|
||||
|
||||
Example docker-compose:
|
||||
```
|
||||
frigate:
|
||||
container_name: frigate
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
image: frigate:latest
|
||||
volumes:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- <path_to_config>:/config
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
RTSP_PASSWORD: "password"
|
||||
```
|
||||
If you would like to make a donation to support development, please use [Github Sponsors](https://github.com/sponsors/blakeblackshear).
|
||||
|
||||
A `config.yml` file must exist in the `config` directory. See example [here](config/config.yml).
|
||||
## Screenshots
|
||||
|
||||
Access the mjpeg stream at `http://localhost:5000/<camera_name>` and the best person snapshot at `http://localhost:5000/<camera_name>/best_person.jpg`
|
||||
Integration into Home Assistant
|
||||
|
||||
## Integration with HomeAssistant
|
||||
```
|
||||
camera:
|
||||
- name: Camera Last Person
|
||||
platform: generic
|
||||
still_image_url: http://<ip>:5000/<camera_name>/best_person.jpg
|
||||
<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>
|
||||
|
||||
sensor:
|
||||
- name: Camera Person
|
||||
platform: mqtt
|
||||
state_topic: "frigate/<camera_name>/objects"
|
||||
value_template: '{{ value_json.person }}'
|
||||
device_class: moving
|
||||
availability_topic: "frigate/available"
|
||||
```
|
||||
Also comes with a builtin UI:
|
||||
|
||||
## Tips
|
||||
- Lower the framerate of the RTSP feed on the camera to reduce the CPU usage for capturing the feed
|
||||
<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>
|
||||
|
||||
## Future improvements
|
||||
- [x] Remove motion detection for now
|
||||
- [x] Try running object detection in a thread rather than a process
|
||||
- [x] Implement min person size again
|
||||
- [x] Switch to a config file
|
||||
- [x] Handle multiple cameras in the same container
|
||||
- [ ] Attempt to figure out coral symlinking
|
||||
- [ ] Add object list to config with min scores for mqtt
|
||||
- [ ] Move mjpeg encoding to a separate process
|
||||
- [ ] Simplify motion detection (check entire image against mask, resize instead of gaussian blur)
|
||||
- [ ] See if motion detection is even worth running
|
||||
- [ ] Scan for people across entire image rather than specfic regions
|
||||
- [ ] Dynamically resize detection area and follow people
|
||||
- [ ] Add ability to turn detection on and off via MQTT
|
||||
- [ ] Output movie clips of people for notifications, etc.
|
||||
- [ ] Integrate with homeassistant push camera
|
||||
- [ ] Merge bounding boxes that span multiple regions
|
||||
- [ ] Implement mode to save labeled objects for training
|
||||
- [ ] Try and reduce CPU usage by simplifying the tensorflow model to just include the objects we care about
|
||||
- [ ] Look into GPU accelerated decoding of RTSP stream
|
||||
- [ ] Send video over a socket and use JSMPEG
|
||||
- [x] Look into neural compute stick
|
||||

|
||||
|
||||
93
benchmark.py
Executable file
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
from statistics import mean
|
||||
import multiprocessing as mp
|
||||
import numpy as np
|
||||
import datetime
|
||||
from frigate.edgetpu import LocalObjectDetector, EdgeTPUProcess, RemoteObjectDetector, load_labels
|
||||
|
||||
my_frame = np.expand_dims(np.full((300,300,3), 1, np.uint8), axis=0)
|
||||
labels = load_labels('/labelmap.txt')
|
||||
|
||||
######
|
||||
# Minimal same process runner
|
||||
######
|
||||
# object_detector = LocalObjectDetector()
|
||||
# tensor_input = np.expand_dims(np.full((300,300,3), 0, np.uint8), axis=0)
|
||||
|
||||
# start = datetime.datetime.now().timestamp()
|
||||
|
||||
# frame_times = []
|
||||
# for x in range(0, 1000):
|
||||
# start_frame = datetime.datetime.now().timestamp()
|
||||
|
||||
# tensor_input[:] = my_frame
|
||||
# detections = object_detector.detect_raw(tensor_input)
|
||||
# parsed_detections = []
|
||||
# for d in detections:
|
||||
# if d[1] < 0.4:
|
||||
# break
|
||||
# parsed_detections.append((
|
||||
# labels[int(d[0])],
|
||||
# float(d[1]),
|
||||
# (d[2], d[3], d[4], d[5])
|
||||
# ))
|
||||
# frame_times.append(datetime.datetime.now().timestamp()-start_frame)
|
||||
|
||||
# duration = datetime.datetime.now().timestamp()-start
|
||||
# print(f"Processed for {duration:.2f} seconds.")
|
||||
# print(f"Average frame processing time: {mean(frame_times)*1000:.2f}ms")
|
||||
|
||||
|
||||
def start(id, num_detections, detection_queue, event):
|
||||
object_detector = RemoteObjectDetector(str(id), '/labelmap.txt', detection_queue, event)
|
||||
start = datetime.datetime.now().timestamp()
|
||||
|
||||
frame_times = []
|
||||
for x in range(0, num_detections):
|
||||
start_frame = datetime.datetime.now().timestamp()
|
||||
detections = object_detector.detect(my_frame)
|
||||
frame_times.append(datetime.datetime.now().timestamp()-start_frame)
|
||||
|
||||
duration = datetime.datetime.now().timestamp()-start
|
||||
object_detector.cleanup()
|
||||
print(f"{id} - Processed for {duration:.2f} seconds.")
|
||||
print(f"{id} - FPS: {object_detector.fps.eps():.2f}")
|
||||
print(f"{id} - Average frame processing time: {mean(frame_times)*1000:.2f}ms")
|
||||
|
||||
######
|
||||
# Separate process runner
|
||||
######
|
||||
# event = mp.Event()
|
||||
# detection_queue = mp.Queue()
|
||||
# edgetpu_process = EdgeTPUProcess(detection_queue, {'1': event}, 'usb:0')
|
||||
|
||||
# start(1, 1000, edgetpu_process.detection_queue, event)
|
||||
# print(f"Average raw inference speed: {edgetpu_process.avg_inference_speed.value*1000:.2f}ms")
|
||||
|
||||
####
|
||||
# Multiple camera processes
|
||||
####
|
||||
camera_processes = []
|
||||
|
||||
events = {}
|
||||
for x in range(0, 10):
|
||||
events[str(x)] = mp.Event()
|
||||
detection_queue = mp.Queue()
|
||||
edgetpu_process_1 = EdgeTPUProcess(detection_queue, events, 'usb:0')
|
||||
edgetpu_process_2 = EdgeTPUProcess(detection_queue, events, 'usb:1')
|
||||
|
||||
for x in range(0, 10):
|
||||
camera_process = mp.Process(target=start, args=(x, 300, detection_queue, events[str(x)]))
|
||||
camera_process.daemon = True
|
||||
camera_processes.append(camera_process)
|
||||
|
||||
start_time = datetime.datetime.now().timestamp()
|
||||
|
||||
for p in camera_processes:
|
||||
p.start()
|
||||
|
||||
for p in camera_processes:
|
||||
p.join()
|
||||
|
||||
duration = datetime.datetime.now().timestamp()-start_time
|
||||
print(f"Total - Processed for {duration:.2f} seconds.")
|
||||
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -1,29 +0,0 @@
|
||||
web_port: 5000
|
||||
|
||||
mqtt:
|
||||
host: mqtt.server.com
|
||||
topic_prefix: frigate
|
||||
|
||||
cameras:
|
||||
back:
|
||||
rtsp:
|
||||
user: viewer
|
||||
host: 10.0.10.10
|
||||
port: 554
|
||||
# values that begin with a "$" will be replaced with environment variable
|
||||
password: $RTSP_PASSWORD
|
||||
path: /cam/realmonitor?channel=1&subtype=2
|
||||
mask: back-mask.bmp
|
||||
regions:
|
||||
- size: 350
|
||||
x_offset: 0
|
||||
y_offset: 300
|
||||
min_person_area: 5000
|
||||
- size: 400
|
||||
x_offset: 350
|
||||
y_offset: 250
|
||||
min_person_area: 2000
|
||||
- size: 400
|
||||
x_offset: 750
|
||||
y_offset: 250
|
||||
min_person_area: 2000
|
||||
@@ -1,90 +0,0 @@
|
||||
import cv2
|
||||
import time
|
||||
import queue
|
||||
import yaml
|
||||
import numpy as np
|
||||
from flask import Flask, Response, make_response
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from frigate.video import Camera
|
||||
from frigate.object_detection import PreppedQueueProcessor
|
||||
|
||||
with open('/config/config.yml') as f:
|
||||
CONFIG = yaml.safe_load(f)
|
||||
|
||||
MQTT_HOST = CONFIG['mqtt']['host']
|
||||
MQTT_PORT = CONFIG.get('mqtt', {}).get('port', 1883)
|
||||
MQTT_TOPIC_PREFIX = CONFIG.get('mqtt', {}).get('topic_prefix', 'frigate')
|
||||
MQTT_USER = CONFIG.get('mqtt', {}).get('user')
|
||||
MQTT_PASS = CONFIG.get('mqtt', {}).get('password')
|
||||
|
||||
WEB_PORT = CONFIG.get('web_port', 5000)
|
||||
DEBUG = (CONFIG.get('debug', '0') == '1')
|
||||
|
||||
def main():
|
||||
# connect to mqtt and setup last will
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
print("On connect called")
|
||||
# publish a message to signal that the service is running
|
||||
client.publish(MQTT_TOPIC_PREFIX+'/available', 'online', retain=True)
|
||||
client = mqtt.Client()
|
||||
client.on_connect = on_connect
|
||||
client.will_set(MQTT_TOPIC_PREFIX+'/available', payload='offline', qos=1, retain=True)
|
||||
if not MQTT_USER is None:
|
||||
client.username_pw_set(MQTT_USER, password=MQTT_PASS)
|
||||
client.connect(MQTT_HOST, MQTT_PORT, 60)
|
||||
client.loop_start()
|
||||
|
||||
# Queue for prepped frames, max size set to (number of cameras * 5)
|
||||
max_queue_size = len(CONFIG['cameras'].items())*5
|
||||
prepped_frame_queue = queue.Queue(max_queue_size)
|
||||
|
||||
cameras = {}
|
||||
for name, config in CONFIG['cameras'].items():
|
||||
cameras[name] = Camera(name, config, prepped_frame_queue, client, MQTT_TOPIC_PREFIX)
|
||||
|
||||
prepped_queue_processor = PreppedQueueProcessor(
|
||||
cameras,
|
||||
prepped_frame_queue
|
||||
)
|
||||
prepped_queue_processor.start()
|
||||
|
||||
for name, camera in cameras.items():
|
||||
camera.start()
|
||||
print("Capture process for {}: {}".format(name, camera.get_capture_pid()))
|
||||
|
||||
# create a flask app that encodes frames a mjpeg on demand
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/<camera_name>/best_person.jpg')
|
||||
def best_person(camera_name):
|
||||
best_person_frame = cameras[camera_name].get_best_person()
|
||||
if best_person_frame is None:
|
||||
best_person_frame = np.zeros((720,1280,3), np.uint8)
|
||||
ret, jpg = cv2.imencode('.jpg', best_person_frame)
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers['Content-Type'] = 'image/jpg'
|
||||
return response
|
||||
|
||||
@app.route('/<camera_name>')
|
||||
def mjpeg_feed(camera_name):
|
||||
# return a multipart response
|
||||
return Response(imagestream(camera_name),
|
||||
mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
def imagestream(camera_name):
|
||||
while True:
|
||||
# max out at 5 FPS
|
||||
time.sleep(0.2)
|
||||
frame = cameras[camera_name].get_current_frame_with_objects()
|
||||
# encode the image into a jpg
|
||||
ret, jpg = cv2.imencode('.jpg', frame)
|
||||
yield (b'--frame\r\n'
|
||||
b'Content-Type: image/jpeg\r\n\r\n' + jpg.tobytes() + b'\r\n\r\n')
|
||||
|
||||
app.run(host='0.0.0.0', port=WEB_PORT, debug=False)
|
||||
|
||||
camera.join()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
diagram.png
|
Before Width: | Height: | Size: 283 KiB |
37
docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
||||
version: "3"
|
||||
services:
|
||||
dev:
|
||||
container_name: frigate-dev
|
||||
user: vscode
|
||||
# add groups from host for render, plugdev, video
|
||||
group_add:
|
||||
- "109" # render
|
||||
- "110" # render
|
||||
- "44" # video
|
||||
- "46" # plugdev
|
||||
shm_size: "256mb"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.dev
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- /dev/dri:/dev/dri # for intel hwaccel, needs to be updated for your hardware
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- .:/lab/frigate:cached
|
||||
- ./config/config.yml:/config/config.yml:ro
|
||||
- ./debug:/media/frigate
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
ports:
|
||||
- "1935:1935"
|
||||
- "3000:3000"
|
||||
- "5000:5000"
|
||||
- "5001:5001"
|
||||
- "8080:8080"
|
||||
entrypoint: ["sudo", "/init"]
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
mqtt:
|
||||
container_name: mqtt
|
||||
image: eclipse-mosquitto:1.6
|
||||
ports:
|
||||
- "1883:1883"
|
||||
138
docker/Dockerfile
Normal file
@@ -0,0 +1,138 @@
|
||||
FROM blakeblackshear/frigate-nginx:1.0.2 as nginx
|
||||
|
||||
FROM debian:11 as wheels
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Use a separate container to build wheels to prevent build dependencies in final image
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
apt-transport-https \
|
||||
gnupg \
|
||||
wget \
|
||||
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
|
||||
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
|
||||
&& apt-get -qq update \
|
||||
&& apt-get -qq install -y \
|
||||
python3 \
|
||||
python3-dev \
|
||||
wget \
|
||||
# opencv dependencies
|
||||
build-essential cmake git pkg-config libgtk-3-dev \
|
||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||
gfortran openexr libatlas-base-dev libssl-dev\
|
||||
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev \
|
||||
# scipy dependencies
|
||||
gcc gfortran libopenblas-dev liblapack-dev
|
||||
|
||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||
&& python3 get-pip.py "pip"
|
||||
|
||||
RUN if [ "${TARGETARCH}" = "arm" ]; \
|
||||
then echo "[global]" > /etc/pip.conf \
|
||||
&& echo "extra-index-url=https://www.piwheels.org/simple" >> /etc/pip.conf; \
|
||||
fi
|
||||
|
||||
COPY requirements.txt /requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
COPY requirements-wheels.txt /requirements-wheels.txt
|
||||
RUN pip3 wheel --wheel-dir=/wheels -r requirements-wheels.txt
|
||||
|
||||
# Frigate Container
|
||||
FROM debian:11-slim
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG JELLYFIN_FFMPEG_VERSION=5.0.1-7
|
||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
# http://stackoverflow.com/questions/48162574/ddg#49462622
|
||||
ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
|
||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
|
||||
|
||||
ENV FLASK_ENV=development
|
||||
|
||||
COPY --from=wheels /wheels /wheels
|
||||
|
||||
# Install ffmpeg
|
||||
RUN apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends -y \
|
||||
apt-transport-https \
|
||||
gnupg \
|
||||
wget \
|
||||
unzip tzdata libxml2 xz-utils \
|
||||
python3-pip \
|
||||
# add raspberry pi repo
|
||||
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 9165938D90FDDD2E \
|
||||
&& echo "deb http://raspbian.raspberrypi.org/raspbian/ bullseye main contrib non-free rpi" | tee /etc/apt/sources.list.d/raspi.list \
|
||||
# add coral repo
|
||||
&& apt-key adv --fetch-keys https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
||||
&& echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" > /etc/apt/sources.list.d/coral-edgetpu.list \
|
||||
&& echo "libedgetpu1-max libedgetpu/accepted-eula select true" | debconf-set-selections \
|
||||
# enable non-free repo
|
||||
&& sed -i -e's/ main/ main contrib non-free/g' /etc/apt/sources.list \
|
||||
&& apt-get -qq update \
|
||||
&& apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
# coral drivers
|
||||
libedgetpu1-max python3-tflite-runtime python3-pycoral \
|
||||
&& pip3 install -U /wheels/*.whl \
|
||||
# jellyfin-ffmpeg
|
||||
&& wget -O jellyfin.deb "https://repo.jellyfin.org/releases/server/debian/versions/jellyfin-ffmpeg/${JELLYFIN_FFMPEG_VERSION}/jellyfin-ffmpeg5_${JELLYFIN_FFMPEG_VERSION}-$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )_$( dpkg --print-architecture ).deb" \
|
||||
&& apt-get -qq install --no-install-recommends --no-install-suggests -y ./jellyfin.deb \
|
||||
&& rm jellyfin.deb \
|
||||
# arch specific packages
|
||||
&& if [ "${TARGETARCH}" = "amd64" ]; then \
|
||||
apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
mesa-va-drivers intel-media-va-driver-non-free; \
|
||||
fi \
|
||||
# not sure why 32bit arm requires all these
|
||||
&& if [ "${TARGETARCH}" = "arm" ]; then \
|
||||
apt-get -qq install --no-install-recommends --no-install-suggests -y \
|
||||
libgtk-3-dev \
|
||||
libavcodec-dev libavformat-dev libswscale-dev libv4l-dev \
|
||||
libxvidcore-dev libx264-dev libjpeg-dev libpng-dev libtiff-dev \
|
||||
gfortran openexr libatlas-base-dev libssl-dev\
|
||||
libtbb2 libtbb-dev libdc1394-22-dev libopenexr-dev \
|
||||
libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev; \
|
||||
fi \
|
||||
&& rm -rf /wheels \
|
||||
&& apt-get remove gnupg apt-transport-https -y \
|
||||
&& apt-get clean autoclean -y \
|
||||
&& apt-get autoremove -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PATH=$PATH:/usr/lib/jellyfin-ffmpeg
|
||||
|
||||
COPY --from=nginx /usr/local/nginx/ /usr/local/nginx/
|
||||
|
||||
# get model and labels
|
||||
COPY labelmap.txt /labelmap.txt
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite -O /edgetpu_model.tflite
|
||||
RUN wget -q https://github.com/google-coral/test_data/raw/release-frogfish/ssdlite_mobiledet_coco_qat_postprocess.tflite -O /cpu_model.tflite
|
||||
|
||||
WORKDIR /opt/frigate/
|
||||
ADD frigate frigate/
|
||||
ADD migrations migrations/
|
||||
|
||||
COPY web/dist web/
|
||||
|
||||
COPY docker/rootfs/ /
|
||||
|
||||
# s6-overlay
|
||||
RUN S6_ARCH="${TARGETARCH}" \
|
||||
&& if [ "${TARGETARCH}" = "amd64" ]; then S6_ARCH="amd64"; fi \
|
||||
&& if [ "${TARGETARCH}" = "arm" ]; then S6_ARCH="armhf"; fi \
|
||||
&& if [ "${TARGETARCH}" = "arm64" ]; then S6_ARCH="aarch64"; fi \
|
||||
&& wget -O /tmp/s6-overlay-installer "https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-${S6_ARCH}-installer" \
|
||||
&& chmod +x /tmp/s6-overlay-installer && /tmp/s6-overlay-installer /
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 1935
|
||||
|
||||
ENTRYPOINT ["/init"]
|
||||
|
||||
CMD ["python3", "-u", "-m", "frigate"]
|
||||
27
docker/Dockerfile.dev
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM frigate:latest
|
||||
|
||||
ARG USERNAME=vscode
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=$USER_UID
|
||||
|
||||
# Create the user
|
||||
RUN groupadd --gid $USER_GID $USERNAME \
|
||||
&& useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash \
|
||||
#
|
||||
# [Optional] Add sudo support. Omit if you don't need to install software after connecting.
|
||||
&& apt-get update \
|
||||
&& apt-get install -y sudo \
|
||||
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
|
||||
&& chmod 0440 /etc/sudoers.d/$USERNAME
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git curl vim htop
|
||||
|
||||
COPY requirements-dev.txt /opt/frigate/requirements-dev.txt
|
||||
RUN pip3 install -r requirements-dev.txt
|
||||
|
||||
# Install Node 16
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN npm install -g npm@latest
|
||||
5
docker/rootfs/etc/services.d/nginx/finish
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/execlineb -S1
|
||||
if { s6-test ${1} -ne 0 }
|
||||
if { s6-test ${1} -ne 256 }
|
||||
|
||||
s6-svscanctl -t /var/run/s6/services
|
||||
2
docker/rootfs/etc/services.d/nginx/run
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/execlineb -P
|
||||
/usr/local/nginx/sbin/nginx
|
||||
237
docker/rootfs/usr/local/nginx/conf/nginx.conf
Normal file
@@ -0,0 +1,237 @@
|
||||
daemon off;
|
||||
user root;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /usr/local/nginx/logs/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /usr/local/nginx/logs/access.log main;
|
||||
|
||||
sendfile on;
|
||||
|
||||
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 127.0.0.1:5001;
|
||||
keepalive 1024;
|
||||
}
|
||||
|
||||
upstream mqtt_ws {
|
||||
server 127.0.0.1:5002;
|
||||
keepalive 1024;
|
||||
}
|
||||
|
||||
upstream jsmpeg {
|
||||
server 127.0.0.1:8082;
|
||||
keepalive 1024;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 5000;
|
||||
|
||||
# vod settings
|
||||
vod_base_url '';
|
||||
vod_segments_base_url '';
|
||||
vod_mode mapped;
|
||||
vod_max_mapping_response_size 1m;
|
||||
vod_upstream_location /api;
|
||||
vod_align_segments_to_key_frames on;
|
||||
vod_manifest_segment_durations_mode accurate;
|
||||
|
||||
# vod caches
|
||||
vod_metadata_cache metadata_cache 512m;
|
||||
vod_mapping_cache mapping_cache 5m 10m;
|
||||
|
||||
# gzip manifests
|
||||
gzip on;
|
||||
gzip_types application/vnd.apple.mpegurl;
|
||||
|
||||
# file handle caching / aio
|
||||
open_file_cache max=1000 inactive=5m;
|
||||
open_file_cache_valid 2m;
|
||||
open_file_cache_min_uses 1;
|
||||
open_file_cache_errors on;
|
||||
aio on;
|
||||
|
||||
location /vod/ {
|
||||
vod hls;
|
||||
|
||||
secure_token $args;
|
||||
secure_token_types application/vnd.apple.mpegurl;
|
||||
|
||||
add_header Access-Control-Allow-Headers '*';
|
||||
add_header Access-Control-Expose-Headers 'Server,range,Content-Length,Content-Range';
|
||||
add_header Access-Control-Allow-Methods 'GET, HEAD, OPTIONS';
|
||||
add_header Access-Control-Allow-Origin '*';
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
}
|
||||
|
||||
location /stream/ {
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
types {
|
||||
application/dash+xml mpd;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
image/jpeg jpg;
|
||||
}
|
||||
|
||||
root /tmp;
|
||||
}
|
||||
|
||||
location /clips/ {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
image/jpeg jpg;
|
||||
}
|
||||
|
||||
autoindex on;
|
||||
root /media/frigate;
|
||||
}
|
||||
|
||||
location /cache/ {
|
||||
internal; # This tells nginx it's not accessible from the outside
|
||||
alias /tmp/cache/;
|
||||
}
|
||||
|
||||
location /recordings/ {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length';
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
}
|
||||
|
||||
autoindex on;
|
||||
autoindex_format json;
|
||||
root /media/frigate;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://mqtt_ws/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location /live/ {
|
||||
proxy_pass http://jsmpeg/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location ~* /api/(.*\.(jpg|jpeg|png)$) {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
proxy_pass http://frigate_api/$1$is_args$args;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
|
||||
proxy_pass http://frigate_api/;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
add_header Cache-Control "no-store";
|
||||
expires off;
|
||||
|
||||
location /assets/ {
|
||||
access_log off;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
sub_filter 'href="/BASE_PATH/' 'href="$http_x_ingress_path/';
|
||||
sub_filter 'url(/BASE_PATH/' 'url($http_x_ingress_path/';
|
||||
sub_filter '"/BASE_PATH/dist/' '"$http_x_ingress_path/dist/';
|
||||
sub_filter '"/BASE_PATH/js/' '"$http_x_ingress_path/js/';
|
||||
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
|
||||
sub_filter '="/BASE_PATH/"' '=window.baseUrl';
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rtmp {
|
||||
server {
|
||||
listen 1935;
|
||||
chunk_size 4096;
|
||||
allow publish 127.0.0.1;
|
||||
deny publish all;
|
||||
allow play all;
|
||||
application live {
|
||||
live on;
|
||||
record off;
|
||||
meta copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
docs/.gitignore
vendored
Normal 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
@@ -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
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
69
docs/docs/configuration/advanced.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
id: advanced
|
||||
title: Advanced Options
|
||||
sidebar_label: Advanced Options
|
||||
---
|
||||
|
||||
## Advanced configuration
|
||||
|
||||
### `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 HassOS)
|
||||
|
||||
### `database`
|
||||
|
||||
Event and recording information is managed in a sqlite database at `/media/frigate/frigate.db`. If that database is deleted, recordings will be orphaned and will need to be cleaned up manually. They also won't show up in the Media Browser within Home Assistant.
|
||||
|
||||
If you are storing your database on a network share (SMB, NFS, etc), you may get a `database is locked` error message on startup. You can customize the location of the database in the config if necessary.
|
||||
|
||||
This may need to be in a custom location if network storage is used for the media folder.
|
||||
|
||||
```yaml
|
||||
database:
|
||||
path: /path/to/frigate.db
|
||||
```
|
||||
|
||||
### `model`
|
||||
|
||||
If using a custom model, the width and height will need to be specified.
|
||||
|
||||
The labelmap can be customized to your needs. A common reason to do this is to combine multiple object types that are easily confused when you don't need to be as granular such as car/truck. By default, truck is renamed to car because they are often confused. You cannot add new object types, but you can change the names of existing objects in the model.
|
||||
|
||||
```yaml
|
||||
model:
|
||||
labelmap:
|
||||
2: vehicle
|
||||
3: vehicle
|
||||
5: vehicle
|
||||
7: vehicle
|
||||
15: animal
|
||||
16: animal
|
||||
17: animal
|
||||
```
|
||||
|
||||
Note that if you rename objects in the labelmap, you will also need to update your `objects -> track` list as well.
|
||||
14
docs/docs/configuration/birdseye.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Birdseye
|
||||
|
||||
Birdseye allows a heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
|
||||
|
||||
### Birdseye Modes
|
||||
|
||||
Birdseye offers different modes to customize which cameras show under which circumstances.
|
||||
- **continuous:** All cameras are always included
|
||||
- **motion:** Cameras that have detected motion within the last 30 seconds are included
|
||||
- **objects:** Cameras that have tracked an active object within the last 30 seconds are included
|
||||
|
||||
### Custom Birdseye Icon
|
||||
|
||||
A custom icon can be added to the birdseye background by provided a file `custom.png` inside of the Frigate `media` folder. The file must be a png with the icon as transparent, any non-transparent pixels will be white when displayed in the birdseye view.
|
||||
115
docs/docs/configuration/camera_specific.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
id: camera_specific
|
||||
title: Camera Specific Configurations
|
||||
---
|
||||
|
||||
### MJPEG Cameras
|
||||
|
||||
The input and output parameters need to be adjusted for MJPEG cameras
|
||||
|
||||
```yaml
|
||||
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -use_wallclock_as_timestamps 1
|
||||
```
|
||||
|
||||
Note that mjpeg cameras require encoding the video into h264 for recording, and rtmp roles. This will use significantly more CPU than if the cameras supported h264 feeds directly.
|
||||
|
||||
```yaml
|
||||
output_args:
|
||||
record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v libx264 -an
|
||||
rtmp: -c:v libx264 -an -f flv
|
||||
```
|
||||
|
||||
### JPEG Stream Cameras
|
||||
|
||||
Cameras using a live changing jpeg image will need input parameters as below
|
||||
|
||||
```yaml
|
||||
input_args:
|
||||
- -r
|
||||
- 5 # << enter FPS here
|
||||
- -stream_loop
|
||||
- -1
|
||||
- -f
|
||||
- image2
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -fflags
|
||||
- +genpts+discardcorrupt
|
||||
- -use_wallclock_as_timestamps
|
||||
- 1
|
||||
```
|
||||
|
||||
Outputting the stream will have the same args and caveats as per [MJPEG Cameras](#mjpeg-cameras)
|
||||
|
||||
### RTMP Cameras
|
||||
|
||||
The input parameters need to be adjusted for RTMP cameras
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args: -avoid_negative_ts make_zero -fflags nobuffer -flags low_delay -strict experimental -fflags +genpts+discardcorrupt -rw_timeout 5000000 -use_wallclock_as_timestamps 1 -f live_flv
|
||||
```
|
||||
|
||||
### Reolink 410/520 (possibly others)
|
||||
|
||||
According to [this discussion](https://github.com/blakeblackshear/frigate/issues/1713#issuecomment-932976305), the http video streams seem to be the most reliable for Reolink.
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
reolink:
|
||||
ffmpeg:
|
||||
hwaccel_args:
|
||||
input_args:
|
||||
- -avoid_negative_ts
|
||||
- make_zero
|
||||
- -fflags
|
||||
- nobuffer+genpts+discardcorrupt
|
||||
- -flags
|
||||
- low_delay
|
||||
- -strict
|
||||
- experimental
|
||||
- -analyzeduration
|
||||
- 1000M
|
||||
- -probesize
|
||||
- 1000M
|
||||
- -rw_timeout
|
||||
- "5000000"
|
||||
inputs:
|
||||
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password
|
||||
roles:
|
||||
- record
|
||||
- rtmp
|
||||
- path: http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password
|
||||
roles:
|
||||
- detect
|
||||
detect:
|
||||
width: 896
|
||||
height: 672
|
||||
fps: 7
|
||||
```
|
||||
|
||||

|
||||
|
||||
### 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 -timeout 5000000 -use_wallclock_as_timestamps 1
|
||||
```
|
||||
|
||||
### UDP Only Cameras
|
||||
|
||||
If your cameras do not support TCP connections for RTSP, you can use UDP.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport udp -timeout 5000000 -use_wallclock_as_timestamps 1
|
||||
```
|
||||
45
docs/docs/configuration/cameras.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
id: cameras
|
||||
title: Cameras
|
||||
---
|
||||
|
||||
## Setting Up Camera Inputs
|
||||
|
||||
Several inputs can be configured for each camera and the role of each input can be mixed and matched based on your needs. This allows you to use a lower resolution stream for object detection, but create recordings from a higher resolution stream, or vice versa.
|
||||
|
||||
Each role can only be assigned to one input per camera. The options for roles are as follows:
|
||||
|
||||
| Role | Description |
|
||||
| -------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `detect` | Main feed for object detection |
|
||||
| `record` | Saves segments of the video feed based on configuration settings. [docs](/configuration/record) |
|
||||
| `rtmp` | Broadcast as an RTMP feed for other services to consume. [docs](/configuration/rtmp) |
|
||||
|
||||
```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:
|
||||
- record
|
||||
detect:
|
||||
width: 1280
|
||||
height: 720
|
||||
```
|
||||
|
||||
Additional cameras are simply added to the config under the `cameras` entry.
|
||||
|
||||
```yaml
|
||||
mqtt: ...
|
||||
cameras:
|
||||
back: ...
|
||||
front: ...
|
||||
side: ...
|
||||
```
|
||||
79
docs/docs/configuration/detectors.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
id: detectors
|
||||
title: Detectors
|
||||
---
|
||||
|
||||
By default, Frigate will use a single CPU detector. If you have a Coral, you will need to configure your detector devices in the config file. When using multiple detectors, they run in dedicated processes, but pull from a common queue of requested detections across all cameras.
|
||||
|
||||
Frigate supports `edgetpu` and `cpu` as detector types. The device value should be specified according to the [Documentation for the TensorFlow Lite Python API](https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api).
|
||||
|
||||
**Note**: There is no support for Nvidia GPUs to perform object detection with tensorflow. It can be used for ffmpeg decoding, but not object detection.
|
||||
|
||||
### Single USB Coral
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: usb
|
||||
```
|
||||
|
||||
### Multiple USB Corals
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral1:
|
||||
type: edgetpu
|
||||
device: usb:0
|
||||
coral2:
|
||||
type: edgetpu
|
||||
device: usb:1
|
||||
```
|
||||
|
||||
### Native Coral (Dev Board)
|
||||
_warning: may have [compatibility issues](https://github.com/blakeblackshear/frigate/issues/1706) after `v0.9.x`_
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: ""
|
||||
```
|
||||
|
||||
### Multiple PCIE/M.2 Corals
|
||||
|
||||
```yaml
|
||||
detectors:
|
||||
coral1:
|
||||
type: edgetpu
|
||||
device: pci:0
|
||||
coral2:
|
||||
type: edgetpu
|
||||
device: pci: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
|
||||
num_threads: 3
|
||||
cpu2:
|
||||
type: cpu
|
||||
num_threads: 3
|
||||
```
|
||||
|
||||
When using CPU detectors, you can add a CPU detector per camera. Adding more detectors than the number of cameras should not improve performance.
|
||||
116
docs/docs/configuration/hardware_acceleration.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
id: hardware_acceleration
|
||||
title: Hardware Acceleration
|
||||
---
|
||||
|
||||
It is recommended to update your configuration to enable hardware accelerated decoding in ffmpeg. Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
|
||||
|
||||
### Raspberry Pi 3/4
|
||||
|
||||
Ensure you increase the allocated RAM for your GPU to at least 128 (raspi-config > Performance Options > GPU Memory).
|
||||
**NOTICE**: If you are using the addon, you may need to turn off `Protection mode` for hardware acceleration.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args: -c:v h264_v4l2m2m
|
||||
```
|
||||
|
||||
### Intel-based CPUs (<10th Generation) via Quicksync
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
|
||||
```
|
||||
**NOTICE**: With some of the processors, like the J4125, the default driver `iHD` doesn't seem to work correctly for hardware acceleration. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME_JELLYFIN=i965` to your docker-compose file.
|
||||
|
||||
### Intel-based CPUs (>=10th Generation) via Quicksync
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args: -c:v h264_qsv
|
||||
```
|
||||
|
||||
### AMD/ATI GPUs (Radeon HD 2000 and newer GPUs) via libva-mesa-driver
|
||||
|
||||
**Note:** You also need to set `LIBVA_DRIVER_NAME=radeonsi` as an environment variable on the container.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args: -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p
|
||||
```
|
||||
|
||||
### NVIDIA GPU
|
||||
|
||||
These instructions are based on the [jellyfin documentation](https://jellyfin.org/docs/general/administration/hardware-acceleration.html#nvidia-hardware-acceleration-on-docker-linux)
|
||||
|
||||
Add `--gpus all` to your docker run command or update your compose file.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
frigate:
|
||||
...
|
||||
image: blakeblackshear/frigate:stable
|
||||
deploy: # <------------- Add this section
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
```
|
||||
|
||||
The decoder you need to pass in the `hwaccel_args` will depend on the input video.
|
||||
|
||||
A list of supported codecs (you can use `ffmpeg -decoders | grep cuvid` in the container to get a list)
|
||||
|
||||
```shell
|
||||
V..... h263_cuvid Nvidia CUVID H263 decoder (codec h263)
|
||||
V..... h264_cuvid Nvidia CUVID H264 decoder (codec h264)
|
||||
V..... hevc_cuvid Nvidia CUVID HEVC decoder (codec hevc)
|
||||
V..... mjpeg_cuvid Nvidia CUVID MJPEG decoder (codec mjpeg)
|
||||
V..... mpeg1_cuvid Nvidia CUVID MPEG1VIDEO decoder (codec mpeg1video)
|
||||
V..... mpeg2_cuvid Nvidia CUVID MPEG2VIDEO decoder (codec mpeg2video)
|
||||
V..... mpeg4_cuvid Nvidia CUVID MPEG4 decoder (codec mpeg4)
|
||||
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 H264 video, you'll select `h264_cuvid`.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
hwaccel_args: -c:v h264_cuvid
|
||||
```
|
||||
|
||||
If everything is working correctly, you should see a significant improvement in performance.
|
||||
Verify that hardware decoding is working by running `nvidia-smi`, which should show the ffmpeg
|
||||
processes:
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------------+
|
||||
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|
||||
|-------------------------------+----------------------+----------------------+
|
||||
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
|
||||
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|
||||
| | | MIG M. |
|
||||
|===============================+======================+======================|
|
||||
| 0 GeForce GTX 166... Off | 00000000:03:00.0 Off | N/A |
|
||||
| 38% 41C P2 36W / 125W | 2082MiB / 5942MiB | 5% Default |
|
||||
| | | N/A |
|
||||
+-------------------------------+----------------------+----------------------+
|
||||
|
||||
+-----------------------------------------------------------------------------+
|
||||
| Processes: |
|
||||
| GPU GI CI PID Type Process name GPU Memory |
|
||||
| ID ID Usage |
|
||||
|=============================================================================|
|
||||
| 0 N/A N/A 12737 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12751 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12772 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12775 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12800 C ffmpeg 249MiB |
|
||||
| 0 N/A N/A 12811 C ffmpeg 417MiB |
|
||||
| 0 N/A N/A 12827 C ffmpeg 417MiB |
|
||||
+-----------------------------------------------------------------------------+
|
||||
```
|
||||
455
docs/docs/configuration/index.md
Normal file
@@ -0,0 +1,455 @@
|
||||
---
|
||||
id: index
|
||||
title: Configuration File
|
||||
---
|
||||
|
||||
For Home Assistant Addon installations, the config file needs to be in the root of your Home Assistant config directory (same location as `configuration.yaml`) and named `frigate.yml`.
|
||||
|
||||
For all other installation types, the config file should be mapped to `/config/config.yml` inside the container.
|
||||
|
||||
It is recommended to start with a minimal configuration and add to it as described in [this guide](/guides/getting_started):
|
||||
|
||||
```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
|
||||
detect:
|
||||
width: 1280
|
||||
height: 720
|
||||
```
|
||||
|
||||
### Full configuration reference:
|
||||
|
||||
:::caution
|
||||
|
||||
It is not recommended to copy this full configuration file. Only specify values that are different from the defaults. Configuration options and default values may change in future versions.
|
||||
|
||||
:::
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
# Required: host name
|
||||
host: mqtt.server.com
|
||||
# Optional: port (default: shown below)
|
||||
port: 1883
|
||||
# Optional: topic prefix (default: shown below)
|
||||
# NOTE: must be unique if you are running multiple instances
|
||||
topic_prefix: frigate
|
||||
# Optional: client id (default: shown below)
|
||||
# NOTE: must be unique if you are running multiple instances
|
||||
client_id: frigate
|
||||
# Optional: user
|
||||
user: mqtt_user
|
||||
# Optional: password
|
||||
# NOTE: MQTT password can be specified with an environment variables that must begin with 'FRIGATE_'.
|
||||
# e.g. password: '{FRIGATE_MQTT_PASSWORD}'
|
||||
password: password
|
||||
# Optional: tls_ca_certs for enabling TLS using self-signed certs (default: None)
|
||||
tls_ca_certs: /path/to/ca.crt
|
||||
# Optional: tls_client_cert and tls_client key in order to use self-signed client
|
||||
# certificates (default: None)
|
||||
# NOTE: certificate must not be password-protected
|
||||
# do not set user and password when using a client certificate
|
||||
tls_client_cert: /path/to/client.crt
|
||||
tls_client_key: /path/to/client.key
|
||||
# Optional: tls_insecure (true/false) for enabling TLS verification of
|
||||
# the server hostname in the server certificate (default: None)
|
||||
tls_insecure: false
|
||||
# Optional: interval in seconds for publishing stats (default: shown below)
|
||||
stats_interval: 60
|
||||
|
||||
# Optional: Detectors configuration. Defaults to a single CPU detector
|
||||
detectors:
|
||||
# Required: name of the detector
|
||||
coral:
|
||||
# Required: type of the detector
|
||||
# Valid values are 'edgetpu' (requires device property below) and 'cpu'.
|
||||
type: edgetpu
|
||||
# Optional: device name as defined here: https://coral.ai/docs/edgetpu/multiple-edgetpu/#using-the-tensorflow-lite-python-api
|
||||
device: usb
|
||||
# Optional: num_threads value passed to the tflite.Interpreter (default: shown below)
|
||||
# This value is only used for CPU types
|
||||
num_threads: 3
|
||||
|
||||
# Optional: Database configuration
|
||||
database:
|
||||
# The path to store the SQLite DB (default: shown below)
|
||||
path: /media/frigate/frigate.db
|
||||
|
||||
# Optional: model modifications
|
||||
model:
|
||||
# Optional: path to the model (default: automatic based on detector)
|
||||
path: /edgetpu_model.tflite
|
||||
# Optional: path to the labelmap (default: shown below)
|
||||
labelmap_path: /labelmap.txt
|
||||
# Required: Object detection model input width (default: shown below)
|
||||
width: 320
|
||||
# Required: Object detection model input height (default: shown below)
|
||||
height: 320
|
||||
# Optional: Label name modifications. These are merged into the standard labelmap.
|
||||
labelmap:
|
||||
2: vehicle
|
||||
|
||||
# Optional: logger verbosity settings
|
||||
logger:
|
||||
# Optional: Default log verbosity (default: shown below)
|
||||
default: info
|
||||
# Optional: Component specific logger overrides
|
||||
logs:
|
||||
frigate.event: debug
|
||||
|
||||
# Optional: set environment variables
|
||||
environment_vars:
|
||||
EXAMPLE_VAR: value
|
||||
|
||||
# Optional: birdseye configuration
|
||||
# NOTE: Can (enabled, mode) be overridden at the camera level
|
||||
birdseye:
|
||||
# Optional: Enable birdseye view (default: shown below)
|
||||
enabled: True
|
||||
# Optional: Width of the output resolution (default: shown below)
|
||||
width: 1280
|
||||
# Optional: Height of the output resolution (default: shown below)
|
||||
height: 720
|
||||
# Optional: Encoding quality of the mpeg1 feed (default: shown below)
|
||||
# 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources.
|
||||
quality: 8
|
||||
# Optional: Mode of the view. Available options are: objects, motion, and continuous
|
||||
# objects - cameras are included if they have had a tracked object within the last 30 seconds
|
||||
# motion - cameras are included if motion was detected in the last 30 seconds
|
||||
# continuous - all cameras are included always
|
||||
mode: objects
|
||||
|
||||
# Optional: ffmpeg configuration
|
||||
ffmpeg:
|
||||
# Optional: global ffmpeg args (default: shown below)
|
||||
global_args: -hide_banner -loglevel warning
|
||||
# Optional: global hwaccel args (default: shown below)
|
||||
# NOTE: See hardware acceleration docs for your specific device
|
||||
hwaccel_args: []
|
||||
# Optional: global input args (default: shown below)
|
||||
input_args: -avoid_negative_ts make_zero -fflags +genpts+discardcorrupt -rtsp_transport tcp -timeout 5000000 -use_wallclock_as_timestamps 1
|
||||
# Optional: global output args
|
||||
output_args:
|
||||
# Optional: output args for detect streams (default: shown below)
|
||||
detect: -f rawvideo -pix_fmt yuv420p
|
||||
# Optional: output args for record streams (default: shown below)
|
||||
record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
# Optional: output args for rtmp streams (default: shown below)
|
||||
rtmp: -c copy -f flv
|
||||
|
||||
# Optional: Detect configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
detect:
|
||||
# Optional: width of the frame for the input with the detect role (default: shown below)
|
||||
width: 1280
|
||||
# Optional: height of the frame for the input with the detect role (default: shown below)
|
||||
height: 720
|
||||
# Optional: desired fps for your camera for the input with the detect role (default: shown below)
|
||||
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
|
||||
fps: 5
|
||||
# Optional: enables detection for the camera (default: True)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: True
|
||||
# Optional: Number of frames without a detection before frigate considers an object to be gone. (default: 5x the frame rate)
|
||||
max_disappeared: 25
|
||||
# Optional: Configuration for stationary object tracking
|
||||
stationary:
|
||||
# Optional: Frequency for confirming stationary objects (default: shown below)
|
||||
# When set to 0, object detection will not confirm stationary objects until movement is detected.
|
||||
# If set to 10, object detection will run to confirm the object still exists on every 10th frame.
|
||||
interval: 0
|
||||
# Optional: Number of frames without a position change for an object to be considered stationary (default: 10x the frame rate or 10s)
|
||||
threshold: 50
|
||||
# Optional: Define a maximum number of frames for tracking a stationary object (default: not set, track forever)
|
||||
# This can help with false positives for objects that should only be stationary for a limited amount of time.
|
||||
# It can also be used to disable stationary object tracking. For example, you may want to set a value for person, but leave
|
||||
# car at the default.
|
||||
# WARNING: Setting these values overrides default behavior and disables stationary object tracking.
|
||||
# There are very few situations where you would want it disabled. It is NOT recommended to
|
||||
# copy these values from the example config into your config unless you know they are needed.
|
||||
max_frames:
|
||||
# Optional: Default for all object types (default: not set, track forever)
|
||||
default: 3000
|
||||
# Optional: Object specific values
|
||||
objects:
|
||||
person: 1000
|
||||
|
||||
# Optional: Object configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
objects:
|
||||
# Optional: list of objects to track from labelmap.txt (default: shown below)
|
||||
track:
|
||||
- person
|
||||
# Optional: mask to prevent all object types from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object.
|
||||
# NOTE: This mask is COMBINED with the object type specific mask below
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
# Optional: filters to reduce false positives for specific object types
|
||||
filters:
|
||||
person:
|
||||
# Optional: minimum width*height of the bounding box for the detected object (default: 0)
|
||||
min_area: 5000
|
||||
# Optional: maximum width*height of the bounding box for the detected object (default: 24000000)
|
||||
max_area: 100000
|
||||
# Optional: minimum width/height of the bounding box for the detected object (default: 0)
|
||||
min_ratio: 0.5
|
||||
# Optional: maximum width/height of the bounding box for the detected object (default: 24000000)
|
||||
max_ratio: 2.0
|
||||
# Optional: minimum score for the object to initiate tracking (default: shown below)
|
||||
min_score: 0.5
|
||||
# Optional: minimum decimal percentage for tracked object's computed score to be considered a true positive (default: shown below)
|
||||
threshold: 0.7
|
||||
# Optional: mask to prevent this object type from being detected in certain areas (default: no mask)
|
||||
# Checks based on the bottom center of the bounding box of the object
|
||||
mask: 0,0,1000,0,1000,200,0,200
|
||||
|
||||
# Optional: Motion configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
motion:
|
||||
# Optional: The threshold passed to cv2.threshold to determine if a pixel is different enough to be counted as motion. (default: shown below)
|
||||
# Increasing this value will make motion detection less sensitive and decreasing it will make motion detection more sensitive.
|
||||
# The value should be between 1 and 255.
|
||||
threshold: 25
|
||||
# Optional: Minimum size in pixels in the resized motion image that counts as motion (default: 30)
|
||||
# Increasing this value will prevent smaller areas of motion from being detected. Decreasing will
|
||||
# make motion detection more sensitive to smaller moving objects.
|
||||
# As a rule of thumb:
|
||||
# - 15 - high sensitivity
|
||||
# - 30 - medium sensitivity
|
||||
# - 50 - low sensitivity
|
||||
contour_area: 30
|
||||
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging the motion delta across multiple frames (default: shown below)
|
||||
# Higher values mean the current frame impacts the delta a lot, and a single raindrop may register as motion.
|
||||
# Too low and a fast moving person wont be detected as motion.
|
||||
delta_alpha: 0.2
|
||||
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
|
||||
# Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
|
||||
# Low values will cause things like moving shadows to be detected as motion for longer.
|
||||
# https://www.geeksforgeeks.org/background-subtraction-in-an-image-using-concept-of-running-average/
|
||||
frame_alpha: 0.2
|
||||
# Optional: Height of the resized motion frame (default: 50)
|
||||
# This operates as an efficient blur alternative. Higher values will result in more granular motion detection at the expense
|
||||
# of higher CPU usage. Lower values result in less CPU, but small changes may not register as motion.
|
||||
frame_height: 50
|
||||
# Optional: motion mask
|
||||
# NOTE: see docs for more detailed info on creating masks
|
||||
mask: 0,900,1080,900,1080,1920,0,1920
|
||||
# Optional: improve contrast (default: shown below)
|
||||
# Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive
|
||||
# for daytime.
|
||||
improve_contrast: False
|
||||
# Optional: Delay when updating camera motion through MQTT from ON -> OFF (default: shown below).
|
||||
mqtt_off_delay: 30
|
||||
|
||||
# Optional: Record configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
record:
|
||||
# Optional: Enable recording (default: shown below)
|
||||
# WARNING: If recording is disabled in the config, turning it on via
|
||||
# the UI or MQTT later will have no effect.
|
||||
# WARNING: Frigate does not currently support limiting recordings based
|
||||
# on available disk space automatically. If using recordings,
|
||||
# you must specify retention settings for a number of days that
|
||||
# will fit within the available disk space of your drive or Frigate
|
||||
# will crash.
|
||||
enabled: False
|
||||
# Optional: Number of minutes to wait between cleanup runs (default: shown below)
|
||||
# This can be used to reduce the frequency of deleting recording segments from disk if you want to minimize i/o
|
||||
expire_interval: 60
|
||||
# Optional: Retention settings for recording
|
||||
retain:
|
||||
# Optional: Number of days to retain recordings regardless of events (default: shown below)
|
||||
# NOTE: This should be set to 0 and retention should be defined in events section below
|
||||
# if you only want to retain recordings of events.
|
||||
days: 0
|
||||
# Optional: Mode for retention. Available options are: all, motion, and active_objects
|
||||
# all - save all recording segments regardless of activity
|
||||
# motion - save all recordings segments with any detected motion
|
||||
# active_objects - save all recording segments with active/moving objects
|
||||
# NOTE: this mode only applies when the days setting above is greater than 0
|
||||
mode: all
|
||||
# Optional: Event recording settings
|
||||
events:
|
||||
# Optional: Number of seconds before the event to include (default: shown below)
|
||||
pre_capture: 5
|
||||
# Optional: Number of seconds after the event to include (default: shown below)
|
||||
post_capture: 5
|
||||
# Optional: Objects to save recordings for. (default: all tracked objects)
|
||||
objects:
|
||||
- person
|
||||
# Optional: Restrict recordings to objects that entered any of the listed zones (default: no required zones)
|
||||
required_zones: []
|
||||
# Optional: Retention settings for recordings of events
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Mode for retention. (default: shown below)
|
||||
# all - save all recording segments for events regardless of activity
|
||||
# motion - save all recordings segments for events with any detected motion
|
||||
# active_objects - save all recording segments for event with active/moving objects
|
||||
#
|
||||
# NOTE: If the retain mode for the camera is more restrictive than the mode configured
|
||||
# here, the segments will already be gone by the time this mode is applied.
|
||||
# For example, if the camera retain mode is "motion", the segments without motion are
|
||||
# never stored, so setting the mode to "all" here won't bring them back.
|
||||
mode: motion
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: Configuration for the jpg snapshots written to the clips directory for each event
|
||||
# NOTE: Can be overridden at the camera level
|
||||
snapshots:
|
||||
# Optional: Enable writing jpg snapshot to /media/frigate/clips (default: shown below)
|
||||
# This value can be set via MQTT and will be updated in startup based on retained value
|
||||
enabled: False
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: False
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: False
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: False
|
||||
# Optional: height to resize the snapshot to (default: original size)
|
||||
height: 175
|
||||
# Optional: Restrict snapshots to objects that entered any of the listed zones (default: no required zones)
|
||||
required_zones: []
|
||||
# Optional: Camera override for retention settings (default: global values)
|
||||
retain:
|
||||
# Required: Default retention days (default: shown below)
|
||||
default: 10
|
||||
# Optional: Per object retention days
|
||||
objects:
|
||||
person: 15
|
||||
|
||||
# Optional: RTMP configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
rtmp:
|
||||
# Optional: Enable the RTMP stream (default: True)
|
||||
enabled: True
|
||||
|
||||
# Optional: Live stream configuration for WebUI
|
||||
# NOTE: Can be overridden at the camera level
|
||||
live:
|
||||
# Optional: Set the height of the live stream. (default: 720)
|
||||
# This must be less than or equal to the height of the detect stream. Lower resolutions
|
||||
# reduce bandwidth required for viewing the live stream. Width is computed to match known aspect ratio.
|
||||
height: 720
|
||||
# Optional: Set the encode quality of the live stream (default: shown below)
|
||||
# 1 is the highest quality, and 31 is the lowest. Lower quality feeds utilize less CPU resources.
|
||||
quality: 8
|
||||
|
||||
# Optional: in-feed timestamp style configuration
|
||||
# NOTE: Can be overridden at the camera level
|
||||
timestamp_style:
|
||||
# Optional: Position of the timestamp (default: shown below)
|
||||
# "tl" (top left), "tr" (top right), "bl" (bottom left), "br" (bottom right)
|
||||
position: "tl"
|
||||
# Optional: Format specifier conform to the Python package "datetime" (default: shown below)
|
||||
# Additional Examples:
|
||||
# german: "%d.%m.%Y %H:%M:%S"
|
||||
format: "%m/%d/%Y %H:%M:%S"
|
||||
# Optional: Color of font
|
||||
color:
|
||||
# All Required when color is specified (default: shown below)
|
||||
red: 255
|
||||
green: 255
|
||||
blue: 255
|
||||
# Optional: Line thickness of font (default: shown below)
|
||||
thickness: 2
|
||||
# Optional: Effect of lettering (default: shown below)
|
||||
# None (No effect),
|
||||
# "solid" (solid background in inverse color of font)
|
||||
# "shadow" (shadow for font)
|
||||
effect: None
|
||||
|
||||
# Required
|
||||
cameras:
|
||||
# Required: name of the camera
|
||||
back:
|
||||
# Required: ffmpeg settings for the camera
|
||||
ffmpeg:
|
||||
# Required: A list of input streams for the camera. See documentation for more information.
|
||||
inputs:
|
||||
# Required: the path to the stream
|
||||
# NOTE: path may include environment variables, which must begin with 'FRIGATE_' and be referenced in {}
|
||||
- path: rtsp://viewer:{FRIGATE_RTSP_PASSWORD}@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
|
||||
# Required: list of roles for this stream. valid values are: detect,record,rtmp
|
||||
# NOTICE: In addition to assigning the record, and rtmp roles,
|
||||
# they must also be enabled in the camera config.
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
# Optional: stream specific global args (default: inherit)
|
||||
# global_args:
|
||||
# Optional: stream specific hwaccel args (default: inherit)
|
||||
# hwaccel_args:
|
||||
# Optional: stream specific input args (default: inherit)
|
||||
# input_args:
|
||||
# Optional: camera specific global args (default: inherit)
|
||||
# global_args:
|
||||
# Optional: camera specific hwaccel args (default: inherit)
|
||||
# hwaccel_args:
|
||||
# Optional: camera specific input args (default: inherit)
|
||||
# input_args:
|
||||
# Optional: camera specific output args (default: inherit)
|
||||
# output_args:
|
||||
|
||||
# Optional: timeout for highest scoring image before allowing it
|
||||
# to be replaced by a newer image. (default: shown below)
|
||||
best_image_timeout: 60
|
||||
|
||||
# Optional: zones for this camera
|
||||
zones:
|
||||
# Required: name of the zone
|
||||
# NOTE: This must be different than any camera names, but can match with another zone on another
|
||||
# camera.
|
||||
front_steps:
|
||||
# Required: List of x,y coordinates to define the polygon of the zone.
|
||||
# NOTE: Presence in a zone is evaluated only based on the bottom center of the objects bounding box.
|
||||
coordinates: 545,1077,747,939,788,805
|
||||
# Optional: List of objects that can trigger this zone (default: all tracked objects)
|
||||
objects:
|
||||
- person
|
||||
# Optional: Zone level object filters.
|
||||
# NOTE: The global and camera filters are applied upstream.
|
||||
filters:
|
||||
person:
|
||||
min_area: 5000
|
||||
max_area: 100000
|
||||
threshold: 0.7
|
||||
|
||||
# Optional: Configuration for the jpg snapshots published via MQTT
|
||||
mqtt:
|
||||
# Optional: Enable publishing snapshot via mqtt for camera (default: shown below)
|
||||
# NOTE: Only applies to publishing image data to MQTT via 'frigate/<camera_name>/<object_name>/snapshot'.
|
||||
# All other messages will still be published.
|
||||
enabled: True
|
||||
# Optional: print a timestamp on the snapshots (default: shown below)
|
||||
timestamp: True
|
||||
# Optional: draw bounding box on the snapshots (default: shown below)
|
||||
bounding_box: True
|
||||
# Optional: crop the snapshot (default: shown below)
|
||||
crop: True
|
||||
# Optional: height to resize the snapshot to (default: shown below)
|
||||
height: 270
|
||||
# Optional: jpeg encode quality (default: shown below)
|
||||
quality: 70
|
||||
# Optional: Restrict mqtt messages to objects that entered any of the listed zones (default: no required zones)
|
||||
required_zones: []
|
||||
|
||||
# Optional: Configuration for how camera is handled in the GUI.
|
||||
ui:
|
||||
# Optional: Adjust sort order of cameras in the UI. Larger numbers come later (default: shown below)
|
||||
# By default the cameras are sorted alphabetically.
|
||||
order: 0
|
||||
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
|
||||
dashboard: True
|
||||
```
|
||||
77
docs/docs/configuration/masks.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
id: masks
|
||||
title: Masks
|
||||
---
|
||||
|
||||
There are two types of masks available:
|
||||
|
||||
**Motion masks**: Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the debug feed with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. Over masking will make it more difficult for objects to be tracked. To see this effect, create a mask, and then watch the video feed with `Motion Boxes` enabled again.
|
||||
|
||||
**Object filter masks**: Object filter masks are used to filter out false positives for a given object type based on location. These should be used to filter any areas where it is not possible for an object of that type to be. The bottom center of the detected object's bounding box is evaluated against the mask. If it is in a masked area, it is assumed to be a false positive. For example, you may want to mask out rooftops, walls, the sky, treetops for people. For cars, masking locations other than the street or your driveway will tell frigate that anything in your yard is a false positive.
|
||||
|
||||
To create a poly mask:
|
||||
|
||||
1. Visit the Web UI
|
||||
1. Click the camera you wish to create a mask for
|
||||
1. Select "Debug" at the top
|
||||
1. Expand the "Options" below the video feed
|
||||
1. Click "Mask & Zone creator"
|
||||
1. Click "Add" on the type of mask or zone you would like to create
|
||||
1. Click on the camera's latest image to create a masked area. The yaml representation will be updated in real-time
|
||||
1. When you've finished creating your mask, click "Copy" and paste the contents into your config file and restart Frigate
|
||||
|
||||
Example of a finished row corresponding to the below example image:
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
mask: "0,461,3,0,1919,0,1919,843,1699,492,1344,458,1346,336,973,317,869,375,866,432"
|
||||
```
|
||||
|
||||
Multiple masks can be listed.
|
||||
|
||||
```yaml
|
||||
motion:
|
||||
mask:
|
||||
- 458,1346,336,973,317,869,375,866,432
|
||||
- 0,461,3,0,1919,0,1919,843,1699,492,1344
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Further Clarification
|
||||
|
||||
This is a response to a [question posed on reddit](https://www.reddit.com/r/homeautomation/comments/ppxdve/replacing_my_doorbell_with_a_security_camera_a_6/hd876w4?utm_source=share&utm_medium=web2x&context=3):
|
||||
|
||||
It is helpful to understand a bit about how Frigate uses motion detection and object detection together.
|
||||
|
||||
First, Frigate uses motion detection as a first line check to see if there is anything happening in the frame worth checking with object detection.
|
||||
|
||||
Once motion is detected, it tries to group up nearby areas of motion together in hopes of identifying a rectangle in the image that will capture the area worth inspecting. These are the red "motion boxes" you see in the debug viewer.
|
||||
|
||||
After the area with motion is identified, Frigate creates a "region" (the green boxes in the debug viewer) to run object detection on. The models are trained on square images, so these regions are always squares. It adds a margin around the motion area in hopes of capturing a cropped view of the object moving that fills most of the image passed to object detection, but doesn't cut anything off. It also takes into consideration the location of the bounding box from the previous frame if it is tracking an object.
|
||||
|
||||
After object detection runs, if there are detected objects that seem to be cut off, Frigate reframes the region and runs object detection again on the same frame to get a better look.
|
||||
|
||||
All of this happens for each area of motion and tracked object.
|
||||
|
||||
> Are you simply saying that INITIAL triggering of any kind of detection will only happen in un-masked areas, but that once this triggering happens, the masks become irrelevant and object detection takes precedence?
|
||||
|
||||
Essentially, yes. I wouldn't describe it as object detection taking precedence though. The motion masks just prevent those areas from being counted as motion. Those masks do not modify the regions passed to object detection in any way, so you can absolutely detect objects in areas masked for motion.
|
||||
|
||||
> If so, this is completely expected and intuitive behavior for me. Because obviously if a "foot" starts motion detection the camera should be able to check if it's an entire person before it fully crosses into the zone. The docs imply this is the behavior, so I also don't understand why this would be detrimental to object detection on the whole.
|
||||
|
||||
When just a foot is triggering motion, Frigate will zoom in and look only at the foot. If that even qualifies as a person, it will determine the object is being cut off and look again and again until it zooms back out enough to find the whole person.
|
||||
|
||||
It is also detrimental to how Frigate tracks a moving object. Motion nearby the bounding box from the previous frame is used to intelligently determine where the region should be in the next frame. With too much masking, tracking is hampered and if an object walks from an unmasked area into a fully masked area, they essentially disappear and will be picked up as a "new" object if they leave the masked area. This is important because Frigate uses the history of scores while tracking an object to determine if it is a false positive or not. It takes a minimum of 3 frames for Frigate to determine is the object type it thinks it is, and the median score must be greater than the threshold. If a person meets this threshold while on the sidewalk before they walk into your stoop, you will get an alert the instant they step a single foot into a zone.
|
||||
|
||||
> I thought the main point of this feature was to cut down on CPU use when motion is happening in unnecessary areas.
|
||||
|
||||
It is, but the definition of "unnecessary" varies. I want to ignore areas of motion that I know are definitely not being triggered by objects of interest. Timestamps, trees, sky, rooftops. I don't want to ignore motion from objects that I want to track and know where they go.
|
||||
|
||||
> For me, giving my masks ANY padding results in a lot of people detection I'm not interested in. I live in the city and catch a lot of the sidewalk on my camera. People walk by my front door all the time and the margin between the sidewalk and actually walking onto my stoop is very thin, so I basically have everything but the exact contours of my stoop masked out. This results in very tidy detections but this info keeps throwing me off. Am I just overthinking it?
|
||||
|
||||
This is what `required_zones` are for. You should define a zone (remember this is evaluated based on the bottom center of the bounding box) and make it required to save snapshots and clips (now events in 0.9.0). You can also use this in your conditions for a notification.
|
||||
|
||||
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
|
||||
|
||||
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.
|
||||
28
docs/docs/configuration/objects.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
id: objects
|
||||
title: Objects
|
||||
---
|
||||
|
||||
import labels from "../../../labelmap.txt";
|
||||
|
||||
Frigate includes the object models listed below from the Google Coral test data.
|
||||
|
||||
Please note:
|
||||
- `car` is listed twice because `truck` has been renamed to `car` by default. These object types are frequently confused.
|
||||
- `person` is the only tracked object by default. See the [full configuration reference](https://docs.frigate.video/configuration/index#full-configuration-reference) for an example of expanding the list of tracked objects.
|
||||
|
||||
<ul>
|
||||
{labels.split("\n").map((label) => (
|
||||
<li>{label.replace(/^\d+\s+/, "")}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
## 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 config](/configuration/advanced#model) if they differ from the defaults.
|
||||
44
docs/docs/configuration/record.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
id: record
|
||||
title: Recording
|
||||
---
|
||||
|
||||
Recordings can be enabled and are stored at `/media/frigate/recordings`. The folder structure for the recordings is `YYYY-MM/DD/HH/<camera_name>/MM.SS.mp4`. These recordings are written directly from your camera stream without re-encoding. Each camera supports a configurable retention policy in the config. Frigate chooses the largest matching retention value between the recording retention and the event retention when determining if a recording should be removed.
|
||||
|
||||
H265 recordings can be viewed in Edge and Safari only. All other browsers require recordings to be encoded with H264.
|
||||
|
||||
## What if I don't want 24/7 recordings?
|
||||
|
||||
If you only used clips in previous versions with recordings disabled, you can use the following config to get the same behavior. This is also the default behavior when recordings are enabled.
|
||||
|
||||
```yaml
|
||||
record:
|
||||
enabled: True
|
||||
events:
|
||||
retain:
|
||||
default: 10
|
||||
```
|
||||
|
||||
This configuration will retain recording segments that overlap with events and have active tracked objects for 10 days. Because multiple events can reference the same recording segments, this avoids storing duplicate footage for overlapping events and reduces overall storage needs.
|
||||
|
||||
When `retain -> days` is set to `0`, segments will be deleted from the cache if no events are in progress.
|
||||
|
||||
## Can I have "24/7" recordings, but only at certain times?
|
||||
|
||||
Using Frigate UI, HomeAssistant, or MQTT, cameras can be automated to only record in certain situations or at certain times.
|
||||
|
||||
**WARNING**: Recordings still must be enabled in the config. If a camera has recordings disabled in the config, enabling via the methods listed above will have no effect.
|
||||
|
||||
## What do the different retain modes mean?
|
||||
|
||||
Frigate saves from the stream with the `record` role in 10 second segments. These options determine which recording segments are kept for 24/7 recording (but can also affect events).
|
||||
|
||||
Let's say you have frigate configured so that your doorbell camera would retain the last **2** days of 24/7 recording.
|
||||
- With the `all` option all 48 hours of those two days would be kept and viewable.
|
||||
- With the `motion` option the only parts of those 48 hours would be segments that frigate detected motion. This is the middle ground option that won't keep all 48 hours, but will likely keep all segments of interest along with the potential for some extra segments.
|
||||
- With the `active_objects` option the only segments that would be kept are those where there was a true positive object that was not considered stationary.
|
||||
|
||||
The same options are available with events. Let's consider a scenario where you drive up and park in your driveway, go inside, then come back out 4 hours later.
|
||||
- With the `all` option all segments for the duration of the event would be saved for the event. This event would have 4 hours of footage.
|
||||
- With the `motion` option all segments for the duration of the event with motion would be saved. This means any segment where a car drove by in the street, person walked by, lighting changed, etc. would be saved.
|
||||
- With the `active_objects` it would only keep segments where the object was active. In this case the only segments that would be saved would be the ones where the car was driving up, you going inside, you coming outside, and the car driving away. Essentially reducing the 4 hours to a minute or two of event footage.
|
||||
8
docs/docs/configuration/rtmp.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
id: rtmp
|
||||
title: RTMP
|
||||
---
|
||||
|
||||
Frigate can re-stream your video feed as a RTMP feed for other applications such as Home Assistant to utilize it at `rtmp://<frigate_host>/live/<camera_name>`. Port 1935 must be open. This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera. The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.
|
||||
|
||||
Some video feeds are not compatible with RTMP. If you are experiencing issues, check to make sure your camera feed is h264 with AAC audio. If your camera doesn't support a compatible format for RTMP, you can use the ffmpeg args to re-encode it on the fly at the expense of increased CPU utilization. Some more information about it can be found [here](../faqs#audio-in-recordings).
|
||||
6
docs/docs/configuration/snapshots.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
id: snapshots
|
||||
title: Snapshots
|
||||
---
|
||||
|
||||
Frigate can save a snapshot image to `/media/frigate/clips` for each event named as `<camera>-<id>.jpg`.
|
||||
28
docs/docs/configuration/stationary_objects.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Stationary Objects
|
||||
|
||||
An object is considered stationary when it is being tracked and has been in a very similar position for a certain number of frames. This number is defined in the configuration under `detect -> stationary -> threshold`, and is 10x the frame rate (or 10 seconds) by default. Once an object is considered stationary, it will remain stationary until motion occurs near the object at which point object detection will start running again. If the object changes location, it will be considered active.
|
||||
|
||||
## Why does it matter if an object is stationary?
|
||||
|
||||
Once an object becomes stationary, object detection will not be continually run on that object. This serves to reduce resource usage and redundant detections when there has been no motion near the tracked object. This also means that Frigate is contextually aware, and can for example [filter out recording segments](record.md#what-do-the-different-retain-modes-mean) to only when the object is considered active. Motion alone does not determine if an object is "active" for active_objects segment retention. Lighting changes for a parked car won't make an object active.
|
||||
|
||||
## Tuning stationary behavior
|
||||
|
||||
The default config is:
|
||||
|
||||
```yaml
|
||||
detect:
|
||||
stationary:
|
||||
interval: 0
|
||||
threshold: 50
|
||||
```
|
||||
|
||||
`interval` is defined as the frequency for running detection on stationary objects. This means that by default once an object is considered stationary, detection will not be run on it until motion is detected. With `interval > 0`, every nth frames detection will be run to make sure the object is still there.
|
||||
|
||||
NOTE: There is no way to disable stationary object tracking with this value.
|
||||
|
||||
`threshold` is the number of frames an object needs to remain relatively still before it is considered stationary.
|
||||
|
||||
## Avoiding stationary objects
|
||||
|
||||
In some cases, like a driveway, you may prefer to only have an event when a car is coming & going vs a constant event of it stationary in the driveway. [This docs sections](../guides/stationary_objects.md) explains how to approach that scenario.
|
||||
15
docs/docs/configuration/user_interface.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: user_interface
|
||||
title: User Interface Configurations
|
||||
---
|
||||
|
||||
### Experimental UI
|
||||
|
||||
While developing and testing new components, users may decide to opt-in to test potential new features on the front-end.
|
||||
|
||||
```yaml
|
||||
ui:
|
||||
use_experimental: true
|
||||
```
|
||||
|
||||
Note that experimental changes may contain bugs or may be removed at any time in future releases of the software. Use of these features are presented as-is and with no functional guarantee.
|
||||
40
docs/docs/configuration/zones.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: zones
|
||||
title: Zones
|
||||
---
|
||||
|
||||
Zones allow you to define a specific area of the frame and apply additional filters for object types so you can determine whether or not an object is within a particular area. Presence in a zone is evaluated based on the bottom center of the bounding box for the object. It does not matter how much of the bounding box overlaps with the zone.
|
||||
|
||||
Zones cannot have the same name as a camera. If desired, a single zone can include multiple cameras if you have multiple cameras covering the same area by configuring zones with the same name for each camera.
|
||||
|
||||
During testing, enable the Zones option for the debug feed so you can adjust as needed. The zone line will increase in thickness when any object enters the zone.
|
||||
|
||||
To create a zone, follow [the steps for a "Motion mask"](/configuration/masks), but use the section of the web UI for creating a zone instead.
|
||||
|
||||
### Restricting zones to specific objects
|
||||
|
||||
Sometimes you want to limit a zone to specific object types to have more granular control of when events/snapshots are saved. The following example will limit one zone to person objects and the other to cars.
|
||||
|
||||
```yaml
|
||||
camera:
|
||||
record:
|
||||
events:
|
||||
required_zones:
|
||||
- entire_yard
|
||||
- front_yard_street
|
||||
snapshots:
|
||||
required_zones:
|
||||
- entire_yard
|
||||
- front_yard_street
|
||||
zones:
|
||||
entire_yard:
|
||||
coordinates: ... (everywhere you want a person)
|
||||
objects:
|
||||
- person
|
||||
front_yard_street:
|
||||
coordinates: ... (just the street)
|
||||
objects:
|
||||
- car
|
||||
```
|
||||
|
||||
Only car objects can trigger the `front_yard_street` zone and only person can trigger the `entire_yard`. You will get events for person objects that enter anywhere in the yard, and events for cars only if they enter the street.
|
||||
223
docs/docs/contributing.md
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
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
|
||||
- Extra Coral device (optional, but very helpful to simulate real world performance)
|
||||
|
||||
### Setup
|
||||
|
||||
#### 1. Build the version information and docker container locally by running `make`
|
||||
|
||||
#### 2. Create a local config file for testing
|
||||
|
||||
Place the file at `config/config.yml` in the root of the repo.
|
||||
|
||||
Here is an example, but modify for your needs:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
host: mqtt
|
||||
|
||||
cameras:
|
||||
test:
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: /media/frigate/car-stopping.mp4
|
||||
input_args: -re -stream_loop -1 -fflags +genpts
|
||||
roles:
|
||||
- detect
|
||||
- rtmp
|
||||
detect:
|
||||
height: 1080
|
||||
width: 1920
|
||||
fps: 5
|
||||
```
|
||||
|
||||
These input args tell ffmpeg to read the mp4 file in an infinite loop. You can use any valid ffmpeg input here.
|
||||
|
||||
#### 3. Gather some mp4 files for testing
|
||||
|
||||
Create and place these files in a `debug` folder in the root of the repo. This is also where recordings will be created if you enable them in your test config. Update your config from step 2 above to point at the right file. You can check the `docker-compose.yml` file in the repo to see how the volumes are mapped.
|
||||
|
||||
#### 4. Open the repo with Visual Studio Code
|
||||
|
||||
Upon opening, you should be prompted to open the project in a remote container. This will build a container on top of the base frigate container with all the development dependencies installed. This ensures everyone uses a consistent development environment without the need to install any dependencies on your host machine.
|
||||
|
||||
#### 5. Run frigate from the command line
|
||||
|
||||
VSCode will start the docker compose file for you and open a terminal window connected to `frigate-dev`.
|
||||
|
||||
- Run `python3 -m frigate` to start the backend.
|
||||
- In a separate terminal window inside VS Code, change into the `web` directory and run `npm install && npm start` to start the frontend.
|
||||
|
||||
#### 6. Teardown
|
||||
|
||||
After closing VSCode, you may still have containers running. To close everything down, just run `docker-compose down -v` to cleanup all containers.
|
||||
|
||||
### Testing
|
||||
|
||||
#### FFMPEG Hardware Acceleration
|
||||
|
||||
The following commands are used inside the container to ensure hardware acceleration is working properly.
|
||||
|
||||
**Raspberry Pi (64bit)**
|
||||
|
||||
This should show <50% CPU in top, and ~80% CPU without `-c:v h264_v4l2m2m`.
|
||||
|
||||
```shell
|
||||
ffmpeg -c:v h264_v4l2m2m -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
**NVIDIA**
|
||||
|
||||
```shell
|
||||
ffmpeg -c:v h264_cuvid -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
**VAAPI**
|
||||
|
||||
```shell
|
||||
ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format yuv420p -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
**QSV**
|
||||
|
||||
```shell
|
||||
ffmpeg -c:v h264_qsv -re -stream_loop -1 -i https://streams.videolan.org/ffmpeg/incoming/720p60.mp4 -f rawvideo -pix_fmt yuv420p pipe: > /dev/null
|
||||
```
|
||||
|
||||
## 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 dev
|
||||
```
|
||||
|
||||
#### 3a. Run the development server against a non-local instance
|
||||
|
||||
To run the development server against a non-local instance, you will need to modify the API_HOST default return in `web/src/env.js`.
|
||||
|
||||
#### 4. Making changes
|
||||
|
||||
The Web UI is built using [Vite](https://vitejs.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 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.
|
||||
|
||||
## Official builds
|
||||
|
||||
Setup buildx for multiarch
|
||||
|
||||
```
|
||||
docker buildx stop builder && docker buildx rm builder # <---- if existing
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
docker buildx create --name builder --driver docker-container --driver-opt network=host --use
|
||||
docker buildx inspect builder --bootstrap
|
||||
make build_web
|
||||
make push
|
||||
```
|
||||
49
docs/docs/faqs.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: faqs
|
||||
title: Frequently Asked Questions
|
||||
---
|
||||
|
||||
### Fatal Python error: Bus error
|
||||
|
||||
This error message is due to a shm-size that is too small. Try updating your shm-size according to [this guide](/installation#calculating-required-shm-size).
|
||||
|
||||
### I am seeing a solid green image for my camera.
|
||||
|
||||
A solid green image means that frigate has not received any frames from ffmpeg. Check the logs to see why ffmpeg is exiting and adjust your ffmpeg args accordingly.
|
||||
|
||||
### How can I get sound or audio in my recordings? {#audio-in-recordings}
|
||||
|
||||
By default, Frigate removes audio from recordings to reduce the likelihood of failing for invalid data. If you would like to include audio, you need to override the output args to remove `-an` for where you want to include audio. The recommended audio codec is `aac`. Not all audio codecs are supported by RTMP, so you may need to re-encode your audio with `-c:a aac`. The default ffmpeg args are shown [here](/configuration/index/#full-configuration-reference).
|
||||
|
||||
:::tip
|
||||
|
||||
When using `-c:a aac`, do not forget to replace `-c copy` with `-c:v copy`. Example:
|
||||
|
||||
```diff title="frigate.yml"
|
||||
ffmpeg:
|
||||
output_args:
|
||||
- record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c copy -an
|
||||
+ record: -f segment -segment_time 10 -segment_format mp4 -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac
|
||||
```
|
||||
|
||||
This is needed because the `-c` flag (without `:a` or `:v`) applies for both audio and video, thus making it conflicting with `-c:a aac`.
|
||||
|
||||
:::
|
||||
|
||||
### My mjpeg stream or snapshots look green and crazy
|
||||
|
||||
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with vlc or another player. Also make sure you don't have the width and height values backwards.
|
||||
|
||||

|
||||
|
||||
### I can't view events or recordings in the Web UI.
|
||||
|
||||
Ensure your cameras send h264 encoded video
|
||||
|
||||
### "[mov,mp4,m4a,3gp,3g2,mj2 @ 0x5639eeb6e140] moov atom not found"
|
||||
|
||||
These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically.
|
||||
|
||||
### "On connect called"
|
||||
|
||||
If you see repeated "On connect called" messages in your config, check for another instance of frigate. This happens when multiple frigate containers are trying to connect to mqtt with the same client_id.
|
||||
47
docs/docs/guides/camera_setup.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
id: camera_setup
|
||||
title: Camera setup
|
||||
---
|
||||
|
||||
Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but far less compatibility. Safari and Edge are the only browsers able to play H.265. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around.
|
||||
|
||||
- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The recommended frame rate is 5fps, but may need to be higher for very fast moving objects. Higher resolutions and frame rates will drive higher CPU usage on your server.
|
||||
|
||||
- **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed to 15 fps.
|
||||
|
||||
- **Stream Viewing**: This stream will be rebroadcast as is to Home Assistant for viewing with the stream component. Setting this resolution too high will use significant bandwidth when viewing streams in Home Assistant, and they may not load reliably over slower connections.
|
||||
|
||||
### Choosing a detect resolution
|
||||
|
||||
The ideal resolution for detection is one where the objects you want to detect fit inside the dimensions of the model used by Frigate (320x320). Frigate does not pass the entire camera frame to object detection. It will crop an area of motion from the full frame and look in that portion of the frame. If the area being inspected is larger than 320x320, Frigate must resize it before running object detection. Higher resolutions do not improve the detection accuracy because the additional detail is lost in the resize. Below you can see a reference for how large a 320x320 area is against common resolutions.
|
||||
|
||||
Larger resolutions **do** improve performance if the objects are very small in the frame.
|
||||
|
||||

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

|
||||
|
||||
To limit snapshots and events, you can list the zone for the entrance of your driveway under `required_zones` in your configuration file. Example below.
|
||||
|
||||
```yaml
|
||||
camera:
|
||||
record:
|
||||
events:
|
||||
required_zones:
|
||||
- zone_2
|
||||
zones:
|
||||
zone_1:
|
||||
coordinates: ... (parking area)
|
||||
zone_2:
|
||||
coordinates: ... (entrance to driveway)
|
||||
```
|
||||
66
docs/docs/hardware.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
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 Home Assistant. It is also helpful if your camera supports multiple substreams to allow different resolutions to be used for detection, streaming, and recordings without re-encoding.
|
||||
|
||||
I recommend Dahua, Hikvision, and Amcrest in that order. Dahua edges out Hikvision because they are easier to find and order, not because they are better cameras. I personally use Dahua cameras because they are easier to purchase directly. In my experience Dahua and Hikvision both have multiple streams with configurable resolutions and frame rates and rock solid streams. They also both have models with large sensors well known for excellent image quality at night. Not all the models are equal. Larger sensors are better than higher resolutions; especially at night. Amcrest is the fallback recommendation because they are rebranded Dahuas. They are rebranding the lower end models with smaller sensors or less configuration options.
|
||||
|
||||
Many users have reported various issues with Reolink cameras, so I do not recommend them. If you are using Reolink, I suggest the [Reolink specific configuration](configuration/camera_specific#reolink-410520-possibly-others). Wifi cameras are also not recommended. Their streams are less reliable and cause connection loss and/or lost video data.
|
||||
|
||||
Here are some of the camera's I recommend:
|
||||
|
||||
- <a href="https://amzn.to/3uFLtxB" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) T5442TM-AS-LED</a> (affiliate link)
|
||||
- <a href="https://amzn.to/3isJ3gU" target="_blank" rel="nofollow noopener sponsored">Loryta(Dahua) IPC-T5442TM-AS</a> (affiliate link)
|
||||
- <a href="https://amzn.to/2ZWNWIA" target="_blank" rel="nofollow noopener sponsored">Amcrest IP5M-T1179EW-28MM</a> (affiliate link)
|
||||
|
||||
I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
|
||||
|
||||
## Server
|
||||
|
||||
My current favorite is the Minisforum GK41 because of the dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral. I may earn a small commission for my endorsement, recommendation, testimonial, or link to any products or services from this website.
|
||||
|
||||
| Name | Inference Speed | Coral Compatibility | Notes |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <a href="https://amzn.to/3oH4BKi" target="_blank" rel="nofollow noopener sponsored">Odyssey X86 Blue J4125</a> (affiliate link) | 9-10ms | M.2 B+M | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
|
||||
| <a href="https://amzn.to/3ptnb8D" target="_blank" rel="nofollow noopener sponsored">Minisforum GK41</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
|
||||
| <a href="https://amzn.to/35E79BC" target="_blank" rel="nofollow noopener sponsored">Beelink GK55</a> (affiliate link) | 9-10ms | USB | Dual gigabit NICs for easy isolated camera network. Easily handles several 1080p cameras. |
|
||||
| <a href="https://amzn.to/3psFlHi" target="_blank" rel="nofollow noopener sponsored">Intel NUC</a> (affiliate link) | 8-10ms | USB | Overkill for most, but great performance. Can handle many cameras at 5fps depending on typical amounts of motion. Requires extra parts. |
|
||||
| <a href="https://amzn.to/3a6TBh8" target="_blank" rel="nofollow noopener sponsored">BMAX B2 Plus</a> (affiliate link) | 10-12ms | USB | Good balance of performance and cost. Also capable of running many other services at the same time as frigate. |
|
||||
| <a href="https://amzn.to/2YjpY9m" target="_blank" rel="nofollow noopener sponsored">Atomic Pi</a> (affiliate link) | 16ms | USB | Good option for a dedicated low power board with a small number of cameras. Can leverage Intel QuickSync for stream decoding. |
|
||||
| <a href="https://amzn.to/2YhSGHH" target="_blank" rel="nofollow noopener sponsored">Raspberry Pi 4 (64bit)</a> (affiliate link) | 10-15ms | USB | Can handle a small number of cameras. |
|
||||
|
||||
## Google Coral TPU
|
||||
|
||||
It is strongly recommended to use a Google Coral. Frigate is designed around the expectation that a Coral is used to achieve very low inference speeds. Offloading TensorFlow to the Google Coral is an order of magnitude faster and will reduce your CPU load dramatically. A $60 device will outperform $2000 CPU. Frigate should work with any supported Coral device from https://coral.ai
|
||||
|
||||
The USB version is compatible with the widest variety of hardware and does not require a driver on the host machine. However, it does lack the automatic throttling features of the other versions.
|
||||
|
||||
The PCIe and M.2 versions require installation of a driver on the host. Follow the instructions for your version from https://coral.ai
|
||||
|
||||
A single Coral can handle many cameras and will be sufficient for the majority of users. You can calculate the maximum performance of your Coral based on the inference speed reported by Frigate. With an inference speed of 10, your Coral will top out at `1000/10=100`, or 100 frames per second. If your detection fps is regularly getting close to that, you should first consider tuning motion masks. If those are already properly configured, a second Coral may be needed.
|
||||
|
||||
### What does Frigate use the CPU for and what does it use the Coral for? (ELI5 Version)
|
||||
|
||||
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
|
||||
|
||||
CPU Usage: I am a CPU, Mendel is a Google Coral
|
||||
|
||||
My buddy Mendel and I have been tasked with keeping the neighbor's red footed booby off my parent's yard. Now I'm really bad at identifying birds. It takes me forever, but my buddy Mendel is incredible at it.
|
||||
|
||||
Mendel however, struggles at pretty much anything else. So we make an agreement. I wait till I see something that moves, and snap a picture of it for Mendel. I then show him the picture and he tells me what it is. Most of the time it isn't anything. But eventually I see some movement and Mendel tells me it is the Booby. Score!
|
||||
|
||||
_What happens when I increase the resolution of my camera?_
|
||||
|
||||
However we realize that there is a problem. There is still booby poop all over the yard. How could we miss that! I've been watching all day! My parents check the window and realize its dirty and a bit small to see the entire yard so they clean it and put a bigger one in there. Now there is so much more to see! However I now have a much bigger area to scan for movement and have to work a lot harder! Even my buddy Mendel has to work harder, as now the pictures have a lot more detail in them that he has to look at to see if it is our sneaky booby.
|
||||
|
||||
Basically - When you increase the resolution and/or the frame rate of the stream there is now significantly more data for the CPU to parse. That takes additional computing power. The Google Coral is really good at doing object detection, but it doesn't have time to look everywhere all the time (especially when there are many windows to check). To balance it, Frigate uses the CPU to look for movement, then sends those frames to the Coral to do object detection. This allows the Coral to be available to a large number of cameras and not overload it.
|
||||
|
||||
### Do hwaccel args help if I am using a Coral?
|
||||
|
||||
YES! The Coral does not help with decoding video streams.
|
||||
|
||||
Decompressing video streams takes a significant amount of CPU power. Video compression uses key frames (also known as I-frames) to send a full frame in the video stream. The following frames only include the difference from the key frame, and the CPU has to compile each frame by merging the differences with the key frame. [More detailed explanation](https://blog.video.ibm.com/streaming-video-tips/keyframes-interframe-video-compression/). Higher resolutions and frame rates mean more processing power is needed to decode the video stream, so try and set them on the camera to avoid unnecessary decoding work.
|
||||
25
docs/docs/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
id: index
|
||||
title: Introduction
|
||||
slug: /
|
||||
---
|
||||
|
||||
A complete and local NVR designed for Home Assistant with AI object detection. Uses OpenCV and Tensorflow to perform realtime object detection locally for IP cameras.
|
||||
|
||||
Use of a [Google Coral Accelerator](https://coral.ai/products/) is optional, but strongly recommended. CPU detection should only be used for testing purposes. The Coral will outperform even the best CPUs and can process 100+ FPS with very little overhead.
|
||||
|
||||
- Tight integration with Home Assistant via a [custom component](https://github.com/blakeblackshear/frigate-hass-integration)
|
||||
- Designed to minimize resource use and maximize performance by only looking for objects when and where it is necessary
|
||||
- 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
|
||||
- Recording with retention based on detected objects
|
||||
- Re-streaming via RTMP to reduce the number of connections to your camera
|
||||
- A dynamic combined camera view of all tracked cameras.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
230
docs/docs/installation.md
Normal file
@@ -0,0 +1,230 @@
|
||||
---
|
||||
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/). Note that a Home Assistant Addon is **not** the same thing as the integration. The [integration](integrations/home-assistant) is required to integrate Frigate into Home Assistant.
|
||||
|
||||
## Dependencies
|
||||
|
||||
**MQTT broker** - Frigate requires an MQTT broker. If using Home Assistant, Frigate and Home Assistant must be connected to the same MQTT broker.
|
||||
|
||||
## Preparing your hardware
|
||||
|
||||
### Operating System
|
||||
|
||||
Frigate runs best with docker installed on bare metal debian-based distributions. For ideal performance, Frigate needs access to underlying hardware for the Coral and GPU devices. Running Frigate in a VM on top of Proxmox, ESXi, Virtualbox, etc. is not recommended. The virtualization layer often introduces a sizable amount of overhead for communication with Coral devices, but [not in all circumstances](https://github.com/blakeblackshear/frigate/discussions/1837).
|
||||
|
||||
Windows is not officially supported, but some users have had success getting it to run under WSL or Virtualbox. Getting the GPU and/or Coral devices properly passed to Frigate may be difficult or impossible. Search previous discussions or issues for help.
|
||||
|
||||
### Storage
|
||||
|
||||
Frigate uses the following locations for read/write operations in the container. Docker volume mappings can be used to map these to any location on your host machine.
|
||||
|
||||
:::caution
|
||||
|
||||
Note that Frigate does not currently support limiting recordings based on available disk space automatically. If using recordings, you must specify retention settings for a number of days that will fit within the available disk space of your drive or Frigate will crash.
|
||||
|
||||
:::
|
||||
|
||||
- `/media/frigate/clips`: Used for snapshot storage. In the future, it will likely be renamed from `clips` to `snapshots`. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||
- `/media/frigate/recordings`: Internal system storage for recording segments. The file structure here cannot be modified and isn't intended to be browsed or managed manually.
|
||||
- `/media/frigate/frigate.db`: Default location for the sqlite database. You will also see several files alongside this file while frigate is running. If moving the database location (often needed when using a network drive at `/media/frigate`), it is recommended to mount a volume with docker at `/db` and change the storage location of the database to `/db/frigate.db` in the config file.
|
||||
- `/tmp/cache`: Cache location for recording segments. Initial recordings are written here before being checked and converted to mp4 and moved to the recordings folder.
|
||||
- `/dev/shm`: It is not recommended to modify this directory or map it with docker. This is the location for raw decoded frames in shared memory and it's size is impacted by the `shm-size` calculations below.
|
||||
- `/config/config.yml`: Default location of the config file.
|
||||
|
||||
#### Common docker compose storage configurations
|
||||
|
||||
Writing to a local disk or external USB drive:
|
||||
|
||||
```yaml
|
||||
version: "3.9"
|
||||
services:
|
||||
frigate:
|
||||
...
|
||||
volumes:
|
||||
- /path/to/your/config.yml:/config/config.yml:ro
|
||||
- /path/to/your/storage:/media/frigate
|
||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
...
|
||||
```
|
||||
|
||||
Writing to a network drive with database on a local drive:
|
||||
|
||||
```yaml
|
||||
version: "3.9"
|
||||
services:
|
||||
frigate:
|
||||
...
|
||||
volumes:
|
||||
- /path/to/your/config.yml:/config/config.yml:ro
|
||||
- /path/to/network/storage:/media/frigate
|
||||
- /path/to/local/disk:/db
|
||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
...
|
||||
```
|
||||
|
||||
frigate.yml
|
||||
|
||||
```yaml
|
||||
database:
|
||||
path: /db/frigate.db
|
||||
```
|
||||
|
||||
### Calculating required shm-size
|
||||
|
||||
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is 64m.
|
||||
|
||||
The default shm-size of 64m is fine for setups with 2 or less 1080p cameras. If frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size.
|
||||
|
||||
You can calculate the necessary shm-size for each camera with the following formula using the resolution specified for detect:
|
||||
|
||||
```
|
||||
(width * height * 1.5 * 9 + 270480)/1048576 = <shm size in mb>
|
||||
```
|
||||
|
||||
The shm size cannot be set per container for Home Assistant Addons. You must set `default-shm-size` in `/etc/docker/daemon.json` to increase the default shm size. This will increase the shm size for all of your docker containers. This may or may not cause issues with your setup. https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file
|
||||
|
||||
### Raspberry Pi 3/4
|
||||
|
||||
By default, the Raspberry Pi limits the amount of memory available to the GPU. In order to use ffmpeg hardware acceleration, you must increase the available memory by setting `gpu_mem` to the maximum recommended value in `config.txt` as described in the [official docs](https://www.raspberrypi.org/documentation/computers/config_txt.html#memory-options).
|
||||
|
||||
Additionally, the USB Coral draws a considerable amount of power. If using any other USB devices such as an SSD, you will experience instability due to the Pi not providing enough power to USB devices. You will need to purchase an external USB hub with it's own power supply. Some have reported success with <a href="https://amzn.to/3a2mH0P" target="_blank" rel="nofollow noopener sponsored">this</a> (affiliate link).
|
||||
|
||||
## Docker
|
||||
|
||||
Running in Docker directly is the recommended install method.
|
||||
|
||||
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>
|
||||
shm_size: "64mb" # update for your cameras based on calculation above
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb # passes the USB Coral, needs to be modified for other versions
|
||||
- /dev/apex_0:/dev/apex_0 # passes a PCIe Coral, follow driver instructions here https://coral.ai/docs/m2/get-started/#2a-on-linux
|
||||
- /dev/dri/renderD128 # for intel hwaccel, needs to be updated for your hardware
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /path/to/your/config.yml:/config/config.yml:ro
|
||||
- /path/to/your/storage:/media/frigate
|
||||
- type: tmpfs # Optional: 1GB of memory, reduces SSD/SD Card wear
|
||||
target: /tmp/cache
|
||||
tmpfs:
|
||||
size: 1000000000
|
||||
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 \
|
||||
--shm-size=64m \
|
||||
-v /path/to/your/storage:/media/frigate \
|
||||
-v /path/to/your/config.yml:/config/config.yml:ro \
|
||||
-v /etc/localtime:/etc/localtime:ro \
|
||||
-e FRIGATE_RTSP_PASSWORD='password' \
|
||||
-p 5000:5000 \
|
||||
-p 1935:1935 \
|
||||
blakeblackshear/frigate:<specify_version_tag>
|
||||
```
|
||||
|
||||
## Home Assistant Operating System (HassOS)
|
||||
|
||||
:::caution
|
||||
|
||||
Due to limitations in Home Assistant Operating System, utilizing external storage for recordings or snapshots requires [modifying udev rules manually](https://community.home-assistant.io/t/solved-mount-usb-drive-in-hassio-to-be-used-on-the-media-folder-with-udev-customization/258406/46).
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
|
||||
If possible, it is recommended to run Frigate standalone in Docker and use [Frigate's Proxy Addon](https://github.com/blakeblackshear/frigate-hass-addons/blob/main/frigate_proxy/README.md).
|
||||
|
||||
:::
|
||||
|
||||
HassOS users can install via the addon repository.
|
||||
|
||||
1. Navigate to Supervisor > Add-on Store > Repositories
|
||||
2. Add https://github.com/blakeblackshear/frigate-hass-addons
|
||||
3. Install your desired Frigate NVR Addon and navigate to it's page
|
||||
4. Setup your network configuration in the `Configuration` tab
|
||||
5. (not for proxy addon) Create the file `frigate.yml` in your `config` directory with your detailed Frigate configuration
|
||||
6. Start the addon container
|
||||
7. (not for proxy addon) If you are using hardware acceleration for ffmpeg, you may need to disable "Protection mode"
|
||||
|
||||
There are several versions of the addon available:
|
||||
|
||||
| Addon Version | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------- |
|
||||
| Frigate NVR | Current release with protection mode on |
|
||||
| Frigate NVR (Full Access) | Current release with the option to disable protection mode |
|
||||
| Frigate NVR Beta | Beta release with protection mode on |
|
||||
| Frigate NVR Beta (Full Access) | Beta release with the option to disable protection mode |
|
||||
|
||||
## Home Assistant Supervised
|
||||
|
||||
:::tip
|
||||
|
||||
If possible, it is recommended to run Frigate standalone in Docker and use [Frigate's Proxy Addon](https://github.com/blakeblackshear/frigate-hass-addons/blob/main/frigate_proxy/README.md).
|
||||
|
||||
:::
|
||||
|
||||
When running Home Assistant with the [Supervised install method](https://github.com/home-assistant/supervised-installer), you can get the benefit of running the Addon along with the ability to customize the storage used by Frigate.
|
||||
|
||||
In order to customize the storage location for Frigate, simply use `fstab` to mount the drive you want at `/usr/share/hassio/media`. Here is an example fstab entry:
|
||||
|
||||
```shell
|
||||
UUID=1a65fec6-c25f-404a-b3d2-1f2fcf6095c8 /media/data ext4 defaults 0 0
|
||||
/media/data/homeassistant/media /usr/share/hassio/media none bind 0 0
|
||||
```
|
||||
|
||||
Then follow the instructions listed for [Home Assistant Operating System](#home-assistant-operating-system-hassos).
|
||||
|
||||
## Kubernetes
|
||||
|
||||
Use the [helm chart](https://github.com/blakeblackshear/blakeshome-charts/tree/master/charts/frigate).
|
||||
|
||||
## 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)
|
||||
|
||||
## Proxmox
|
||||
|
||||
It is recommended to run Frigate in LXC for maximum performance. See [this discussion](https://github.com/blakeblackshear/frigate/discussions/1111) for more information.
|
||||
|
||||
## ESX
|
||||
|
||||
For details on running Frigate under ESX, see details [here](https://github.com/blakeblackshear/frigate/issues/305).
|
||||
266
docs/docs/integrations/api.md
Normal file
@@ -0,0 +1,266 @@
|
||||
---
|
||||
id: api
|
||||
title: HTTP API
|
||||
---
|
||||
|
||||
A web server is available on port 5000 with the following endpoints.
|
||||
|
||||
### `GET /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/api/back?h=1080`. You can also increase the FPS by appending `fps=frame-rate` to the URL such as `http://localhost:5000/api/back?fps=10` or both with `?fps=10&h=1000`.
|
||||
|
||||
### `GET /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) |
|
||||
| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
|
||||
|
||||
Example parameters:
|
||||
|
||||
- `h=300`: resizes the image to 300 pixes tall
|
||||
|
||||
### `GET /api/stats`
|
||||
|
||||
Contains some granular debug info that can be used for sensors in Home Assistant.
|
||||
|
||||
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.10.1-8883709",
|
||||
"latest_version": "0.10.1",
|
||||
/* 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/config`
|
||||
|
||||
A json representation of your configuration
|
||||
|
||||
### `GET /api/version`
|
||||
|
||||
Version info
|
||||
|
||||
### `GET /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) |
|
||||
|
||||
### `GET /api/events/summary`
|
||||
|
||||
Returns summary data for events in the database. Used by the Home Assistant integration.
|
||||
|
||||
### `GET /api/events/<id>`
|
||||
|
||||
Returns data for a single event.
|
||||
|
||||
### `DELETE /api/events/<id>`
|
||||
|
||||
Permanently deletes the event along with any clips/snapshots.
|
||||
|
||||
### `POST /api/events/<id>/retain`
|
||||
|
||||
Sets retain to true for the event id.
|
||||
|
||||
### `POST /api/events/<id>/plus`
|
||||
|
||||
Submits the snapshot of the event to Frigate+ for labeling.
|
||||
|
||||
### `DELETE /api/events/<id>/retain`
|
||||
|
||||
Sets retain to false for the event id (event may be deleted quickly after removing).
|
||||
|
||||
### `POST /api/events/<id>/sub_label`
|
||||
|
||||
Set a sub label for an event. For example to update `person` -> `person's name` if they were recognized with facial recognition.
|
||||
Sub labels must be 20 characters or shorter.
|
||||
|
||||
```json
|
||||
{
|
||||
"subLabel": "some_string"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /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.
|
||||
|
||||
### `GET /api/<camera_name>/<label>/thumbnail.jpg`
|
||||
|
||||
Returns the thumbnail from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
|
||||
|
||||
### `GET /api/events/<id>/clip.mp4`
|
||||
|
||||
Returns the clip for the event id. Works after the event has ended.
|
||||
|
||||
### `GET /api/events/<id>/snapshot.jpg`
|
||||
|
||||
Returns the snapshot image for the event id. Works while the event is in progress and after completion.
|
||||
|
||||
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) |
|
||||
| `quality` | int | Jpeg encoding quality (0-100). Defaults to 70. |
|
||||
|
||||
### `GET /api/<camera_name>/<label>/snapshot.jpg`
|
||||
|
||||
Returns the snapshot image from the latest event for the given camera and label combo. Using `any` as the label will return the latest thumbnail regardless of type.
|
||||
|
||||
### `GET /clips/<camera>-<id>.jpg`
|
||||
|
||||
JPG snapshot for the given camera and event id.
|
||||
|
||||
### `GET /vod/<year>-<month>/<day>/<hour>/<camera>/master.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the specified hour and camera. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /vod/event/<event-id>/index.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /vod/event/<event-id>/index.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the specified event. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /vod/<camera>/start/<start-timestamp>/end/<end-timestamp>/index.m3u8`
|
||||
|
||||
HTTP Live Streaming Video on Demand URL for the camera with the specified time range. Can be viewed in an application like VLC.
|
||||
|
||||
### `GET /api/<camera_name>/recordings/summary`
|
||||
|
||||
Hourly summary of recordings data for a camera.
|
||||
|
||||
### `GET /api/<camera_name>/recordings`
|
||||
|
||||
Get recording segment details for the given timestamp range.
|
||||
|
||||
| param | Type | Description |
|
||||
| -------- | ---- | ------------------------------------- |
|
||||
| `after` | int | Unix timestamp for beginning of range |
|
||||
| `before` | int | Unix timestamp for end of range |
|
||||
192
docs/docs/integrations/home-assistant.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
id: home-assistant
|
||||
title: Home Assistant Integration
|
||||
---
|
||||
|
||||
The best way to integrate with Home Assistant is to use the [official integration](https://github.com/blakeblackshear/frigate-hass-integration).
|
||||
|
||||
## Installation
|
||||
|
||||
### Preparation
|
||||
|
||||
The Frigate integration requires the `mqtt` integration to be installed and
|
||||
manually configured first.
|
||||
|
||||
See the [MQTT integration
|
||||
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
|
||||
details.
|
||||
|
||||
### Integration installation
|
||||
|
||||
Available via HACS as a default repository. To install:
|
||||
|
||||
- Use [HACS](https://hacs.xyz/) to install the integration:
|
||||
|
||||
```
|
||||
Home Assistant > HACS > Integrations > "Explore & Add Integrations" > Frigate
|
||||
```
|
||||
|
||||
- Restart Home Assistant.
|
||||
- Then add/configure the integration:
|
||||
|
||||
```
|
||||
Home Assistant > Configuration > Integrations > Add Integration > Frigate
|
||||
```
|
||||
|
||||
Note: You will also need
|
||||
[media_source](https://www.home-assistant.io/integrations/media_source/) enabled
|
||||
in your Home Assistant configuration for the Media Browser to appear.
|
||||
|
||||
### (Optional) Lovelace Card Installation
|
||||
|
||||
To install the optional companion Lovelace card, please see the [separate
|
||||
installation instructions](https://github.com/dermotduffy/frigate-hass-card) for
|
||||
that card.
|
||||
|
||||
## Configuration
|
||||
|
||||
When configuring the integration, you will be asked for the `URL` of your frigate instance which is the URL you use to access Frigate in the browser. This may look like `http://<host>:5000/`. If you are using HassOS with the addon, the URL should be one of the following depending on which addon version you are using. Note that if you are using the Proxy Addon, you do NOT point the integration at the proxy URL. Just enter the URL used to access frigate directly from your network.
|
||||
|
||||
| Addon Version | URL |
|
||||
| ------------------------------ | -------------------------------------- |
|
||||
| Frigate NVR | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate NVR (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate NVR Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate NVR Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
|
||||
<a name="options"></a>
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
Home Assistant > Configuration > Integrations > Frigate > Options
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| RTMP URL Template | A [jinja2](https://jinja.palletsprojects.com/) template that is used to override the standard RTMP stream URL (e.g. for use with reverse proxies). This option is only shown to users who have [advanced mode](https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode) enabled. See [RTMP streams](#streams) below. |
|
||||
|
||||
## Entities Provided
|
||||
|
||||
| Platform | Description |
|
||||
| --------------- | --------------------------------------------------------------------------------- |
|
||||
| `camera` | Live camera stream (requires RTMP), camera for image of the last detected object. |
|
||||
| `sensor` | States to monitor Frigate performance, object counts for all zones and cameras. |
|
||||
| `switch` | Switch entities to toggle detection, recordings and snapshots. |
|
||||
| `binary_sensor` | A "motion" binary sensor entity per camera/zone/object. |
|
||||
|
||||
## Media Browser Support
|
||||
|
||||
The integration provides:
|
||||
|
||||
- Browsing event recordings with thumbnails
|
||||
- Browsing snapshots
|
||||
- Browsing recordings by month, day, camera, time
|
||||
|
||||
This is accessible via "Media Browser" on the left menu panel in Home Assistant.
|
||||
|
||||
<a name="api"></a>
|
||||
|
||||
## Notification API
|
||||
|
||||
Many people do not want to expose Frigate to the web, so the integration creates some public API endpoints that can be used for notifications.
|
||||
|
||||
To load a thumbnail for an event:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/thumbnail.jpg
|
||||
```
|
||||
|
||||
To load a snapshot for an event:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/snapshot.jpg
|
||||
```
|
||||
|
||||
To load a video clip of an event:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/notifications/<event-id>/clip.mp4
|
||||
```
|
||||
|
||||
<a name="streams"></a>
|
||||
|
||||
## RTMP stream
|
||||
|
||||
In order for the live streams to function they need to be accessible on the RTMP
|
||||
port (default: `1935`) at `<frigatehost>:1935`. Home Assistant will directly
|
||||
connect to that streaming port when the live camera is viewed.
|
||||
|
||||
#### RTMP URL Template
|
||||
|
||||
For advanced usecases, this behavior can be changed with the [RTMP URL
|
||||
template](#options) option. When set, this string will override the default stream
|
||||
address that is derived from the default behavior described above. This option supports
|
||||
[jinja2 templates](https://jinja.palletsprojects.com/) and has the `camera` dict
|
||||
variables from [Frigate API](https://blakeblackshear.github.io/frigate/usage/api#apiconfig)
|
||||
available for the template. Note that no Home Assistant state is available to the
|
||||
template, only the camera dict from Frigate.
|
||||
|
||||
This is potentially useful when Frigate is behind a reverse proxy, and/or when
|
||||
the default stream port is otherwise not accessible to Home Assistant (e.g.
|
||||
firewall rules).
|
||||
|
||||
###### RTMP URL Template Examples
|
||||
|
||||
Use a different port number:
|
||||
|
||||
```
|
||||
rtmp://<frigate_host>:2000/live/front_door
|
||||
```
|
||||
|
||||
Use the camera name in the stream URL:
|
||||
|
||||
```
|
||||
rtmp://<frigate_host>:2000/live/{{ name }}
|
||||
```
|
||||
|
||||
Use the camera name in the stream URL, converting it to lowercase first:
|
||||
|
||||
```
|
||||
rtmp://<frigate_host>:2000/live/{{ name|lower }}
|
||||
```
|
||||
|
||||
## Multiple Instance Support
|
||||
|
||||
The Frigate integration seamlessly supports the use of multiple Frigate servers.
|
||||
|
||||
### Requirements for Multiple Instances
|
||||
|
||||
In order for multiple Frigate instances to function correctly, the
|
||||
`topic_prefix` and `client_id` parameters must be set differently per server.
|
||||
See [MQTT
|
||||
configuration](https://blakeblackshear.github.io/frigate/configuration/index#mqtt)
|
||||
for how to set these.
|
||||
|
||||
#### API URLs
|
||||
|
||||
When multiple Frigate instances are configured, [API](#api) URLs should include an
|
||||
identifier to tell Home Assistant which Frigate instance to refer to. The
|
||||
identifier used is the MQTT `client_id` paremeter included in the configuration,
|
||||
and is used like so:
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/<client-id>/notifications/<event-id>/thumbnail.jpg
|
||||
```
|
||||
|
||||
```
|
||||
https://HA_URL/api/frigate/<client-id>/clips/front_door-1624599978.427826-976jaa.mp4
|
||||
```
|
||||
|
||||
#### Default Treatment
|
||||
|
||||
When a single Frigate instance is configured, the `client-id` parameter need not
|
||||
be specified in URLs/identifiers -- that single instance is assumed. When
|
||||
multiple Frigate instances are configured, the user **must** explicitly specify
|
||||
which server they are referring to.
|
||||
|
||||
## FAQ
|
||||
|
||||
#### If I am detecting multiple objects, how do I assign the correct `binary_sensor` to the camera in HomeKit?
|
||||
|
||||
The [HomeKit integration](https://www.home-assistant.io/integrations/homekit/) randomly links one of the binary sensors (motion sensor entities) grouped with the camera device in Home Assistant. You can specify a `linked_motion_sensor` in the Home Assistant [HomeKit configuration](https://www.home-assistant.io/integrations/homekit/#linked_motion_sensor) for each camera.
|
||||
158
docs/docs/integrations/mqtt.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
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 Home Assistant. Possible message are:
|
||||
"online": published when frigate is running (on startup)
|
||||
"offline": published right before frigate stops
|
||||
|
||||
### `frigate/restart`
|
||||
|
||||
Causes frigate to exit. Docker should be configured to automatically restart the container on exit.
|
||||
|
||||
### `frigate/<camera_name>/<object_name>`
|
||||
|
||||
Publishes the count of objects for the camera for use as a sensor in Home Assistant.
|
||||
`all` can be used as the object_name for the count of all objects for the camera.
|
||||
|
||||
### `frigate/<zone_name>/<object_name>`
|
||||
|
||||
Publishes the count of objects for the zone for use as a sensor in Home Assistant.
|
||||
`all` can be used as the object_name for the count of all objects for the zone.
|
||||
|
||||
### `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, end
|
||||
"before": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
"frame_time": 1607123961.837752,
|
||||
"snapshot_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,
|
||||
"ratio": 2.113207,
|
||||
"region": [264, 450, 667, 853],
|
||||
"current_zones": ["driveway"],
|
||||
"entered_zones": ["yard", "driveway"],
|
||||
"thumbnail": null,
|
||||
"has_snapshot": false,
|
||||
"has_clip": false,
|
||||
"stationary": false, // whether or not the object is considered stationary
|
||||
"motionless_count": 0, // number of frames the object has been motionless
|
||||
"position_changes": 2 // number of times the object has moved from a stationary position
|
||||
},
|
||||
"after": {
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"camera": "front_door",
|
||||
"frame_time": 1607123962.082975,
|
||||
"snapshot_time": 1607123961.837752,
|
||||
"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,
|
||||
"ratio": 1.251397,
|
||||
"region": [218, 440, 693, 915],
|
||||
"current_zones": ["yard", "driveway"],
|
||||
"entered_zones": ["yard", "driveway"],
|
||||
"thumbnail": null,
|
||||
"has_snapshot": false,
|
||||
"has_clip": false,
|
||||
"stationary": false, // whether or not the object is considered stationary
|
||||
"motionless_count": 0, // number of frames the object has been motionless
|
||||
"position_changes": 2 // number of times the object has changed position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `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>/recordings/set`
|
||||
|
||||
Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/recordings/state`
|
||||
|
||||
Topic with current state of recordings 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`.
|
||||
|
||||
### `frigate/<camera_name>/motion/set`
|
||||
|
||||
Topic to turn motion detection for a camera on and off. Expected values are `ON` and `OFF`.
|
||||
NOTE: Turning off motion detection will fail if detection is not disabled.
|
||||
|
||||
### `frigate/<camera_name>/motion`
|
||||
|
||||
Whether camera_name is currently detecting motion. Expected values are `ON` and `OFF`.
|
||||
NOTE: After motion is initially detected, `ON` will be set until no motion has
|
||||
been detected for `mqtt_off_delay` seconds (30 by default).
|
||||
|
||||
### `frigate/<camera_name>/motion/state`
|
||||
|
||||
Topic with current state of motion detection for a camera. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/improve_contrast/set`
|
||||
|
||||
Topic to turn improve_contrast for a camera on and off. Expected values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/improve_contrast/state`
|
||||
|
||||
Topic with current state of improve_contrast for a camera. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/motion_threshold/set`
|
||||
|
||||
Topic to adjust motion threshold for a camera. Expected value is an integer.
|
||||
|
||||
### `frigate/<camera_name>/motion_threshold/state`
|
||||
|
||||
Topic with current motion threshold for a camera. Published value is an integer.
|
||||
|
||||
### `frigate/<camera_name>/motion_contour_area/set`
|
||||
|
||||
Topic to adjust motion contour area for a camera. Expected value is an integer.
|
||||
|
||||
### `frigate/<camera_name>/motion_contour_area/state`
|
||||
|
||||
Topic with current motion contour area for a camera. Published value is an integer.
|
||||
17
docs/docs/mdx.md
Normal 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_!
|
||||
89
docs/docusaurus.config.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
title: 'Frigate',
|
||||
tagline: 'NVR With Realtime Object Detection for IP Cameras',
|
||||
url: 'https://docs.frigate.video',
|
||||
baseUrl: '/',
|
||||
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://frigate.video',
|
||||
label: 'Website',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://demo.frigate.video',
|
||||
label: 'Demo',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
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`,
|
||||
},
|
||||
},
|
||||
plugins: [path.resolve(__dirname, 'plugins', 'raw-loader')],
|
||||
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'),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
25529
docs/package-lock.json
generated
Normal file
38
docs/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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-beta.20",
|
||||
"@docusaurus/preset-classic": "^2.0.0-beta.20",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.1.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.14.0"
|
||||
}
|
||||
}
|
||||
12
docs/plugins/raw-loader.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = function (context, options) {
|
||||
return {
|
||||
name: 'labelmap',
|
||||
configureWebpack(config, isServer, utils) {
|
||||
return {
|
||||
module: {
|
||||
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
35
docs/sidebars.js
Normal file
@@ -0,0 +1,35 @@
|
||||
module.exports = {
|
||||
docs: {
|
||||
Frigate: ["index", "hardware", "installation"],
|
||||
Guides: [
|
||||
"guides/camera_setup",
|
||||
"guides/getting_started",
|
||||
"guides/false_positives",
|
||||
"guides/ha_notifications",
|
||||
"guides/stationary_objects",
|
||||
],
|
||||
Configuration: [
|
||||
"configuration/index",
|
||||
"configuration/detectors",
|
||||
"configuration/cameras",
|
||||
"configuration/masks",
|
||||
"configuration/record",
|
||||
"configuration/snapshots",
|
||||
"configuration/objects",
|
||||
"configuration/rtmp",
|
||||
"configuration/zones",
|
||||
"configuration/birdseye",
|
||||
"configuration/stationary_objects",
|
||||
"configuration/advanced",
|
||||
"configuration/hardware_acceleration",
|
||||
"configuration/camera_specific",
|
||||
],
|
||||
Integrations: [
|
||||
"integrations/home-assistant",
|
||||
"integrations/api",
|
||||
"integrations/mqtt",
|
||||
],
|
||||
Troubleshooting: ["faqs"],
|
||||
Development: ["contributing"],
|
||||
},
|
||||
};
|
||||
25
docs/src/css/custom.css
Normal 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
BIN
docs/static/img/camera-ui.png
vendored
Normal file
|
After Width: | Height: | Size: 944 KiB |
BIN
docs/static/img/diagram.png
vendored
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/driveway_zones-min.png
vendored
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/static/img/driveway_zones.png
vendored
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/static/img/events-ui.png
vendored
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/static/img/example-mask-poly-min.png
vendored
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/static/img/example-mask-poly.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/frigate.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/static/img/home-ui.png
vendored
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
3
docs/static/img/logo-dark.svg
vendored
Normal 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
@@ -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 |
BIN
docs/static/img/media_browser-min.png
vendored
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/static/img/media_browser.png
vendored
Normal file
|
After Width: | Height: | Size: 781 KiB |
BIN
docs/static/img/mismatched-resolution-min.jpg
vendored
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/static/img/mismatched-resolution.jpg
vendored
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/static/img/notification-min.png
vendored
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/static/img/notification.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/static/img/reolink-settings.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/static/img/resolutions-min.jpg
vendored
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
docs/static/img/resolutions.png
vendored
Normal file
|
After Width: | Height: | Size: 65 KiB |
0
frigate/__init__.py
Normal file
16
frigate/__main__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import faulthandler
|
||||
from flask import cli
|
||||
|
||||
faulthandler.enable()
|
||||
import threading
|
||||
|
||||
threading.current_thread().name = "frigate"
|
||||
|
||||
from frigate.app import FrigateApp
|
||||
|
||||
cli.show_server_banner = lambda *x: None
|
||||
|
||||
if __name__ == "__main__":
|
||||
frigate_app = FrigateApp()
|
||||
|
||||
frigate_app.start()
|
||||
409
frigate/app.py
Normal file
@@ -0,0 +1,409 @@
|
||||
import json
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
from multiprocessing.queues import Queue
|
||||
from multiprocessing.synchronize import Event
|
||||
from multiprocessing.context import Process
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
from logging.handlers import QueueHandler
|
||||
from typing import Optional
|
||||
from types import FrameType
|
||||
|
||||
import traceback
|
||||
import yaml
|
||||
from peewee_migrate import Router
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
from pydantic import ValidationError
|
||||
|
||||
from frigate.config import DetectorTypeEnum, FrigateConfig
|
||||
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
|
||||
from frigate.edgetpu import EdgeTPUProcess
|
||||
from frigate.events import EventCleanup, EventProcessor
|
||||
from frigate.http import create_app
|
||||
from frigate.log import log_process, root_configurer
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.mqtt import MqttSocketRelay, create_mqtt_client
|
||||
from frigate.object_processing import TrackedObjectProcessor
|
||||
from frigate.output import output_frames
|
||||
from frigate.plus import PlusApi
|
||||
from frigate.record import RecordingCleanup, RecordingMaintainer
|
||||
from frigate.stats import StatsEmitter, stats_init
|
||||
from frigate.version import VERSION
|
||||
from frigate.video import capture_camera, track_camera
|
||||
from frigate.watchdog import FrigateWatchdog
|
||||
from frigate.types import CameraMetricsTypes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FrigateApp:
|
||||
def __init__(self) -> None:
|
||||
self.stop_event: Event = mp.Event()
|
||||
self.detection_queue: Queue = mp.Queue()
|
||||
self.detectors: dict[str, EdgeTPUProcess] = {}
|
||||
self.detection_out_events: dict[str, Event] = {}
|
||||
self.detection_shms: list[mp.shared_memory.SharedMemory] = []
|
||||
self.log_queue: Queue = mp.Queue()
|
||||
self.plus_api = PlusApi()
|
||||
self.camera_metrics: dict[str, CameraMetricsTypes] = {}
|
||||
|
||||
def set_environment_vars(self) -> None:
|
||||
for key, value in self.config.environment_vars.items():
|
||||
os.environ[key] = value
|
||||
|
||||
def ensure_dirs(self) -> None:
|
||||
for d in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
|
||||
if not os.path.exists(d) and not os.path.islink(d):
|
||||
logger.info(f"Creating directory: {d}")
|
||||
os.makedirs(d)
|
||||
else:
|
||||
logger.debug(f"Skipping directory: {d}")
|
||||
|
||||
def init_logger(self) -> None:
|
||||
self.log_process = mp.Process(
|
||||
target=log_process, args=(self.log_queue,), name="log_process"
|
||||
)
|
||||
self.log_process.daemon = True
|
||||
self.log_process.start()
|
||||
root_configurer(self.log_queue)
|
||||
|
||||
def init_config(self) -> None:
|
||||
config_file = os.environ.get("CONFIG_FILE", "/config/config.yml")
|
||||
|
||||
# Check if we can use .yaml instead of .yml
|
||||
config_file_yaml = config_file.replace(".yml", ".yaml")
|
||||
if os.path.isfile(config_file_yaml):
|
||||
config_file = config_file_yaml
|
||||
|
||||
user_config = FrigateConfig.parse_file(config_file)
|
||||
self.config = user_config.runtime_config
|
||||
|
||||
for camera_name in self.config.cameras.keys():
|
||||
# create camera_metrics
|
||||
self.camera_metrics[camera_name] = {
|
||||
"camera_fps": mp.Value("d", 0.0),
|
||||
"skipped_fps": mp.Value("d", 0.0),
|
||||
"process_fps": mp.Value("d", 0.0),
|
||||
"detection_enabled": mp.Value(
|
||||
"i", self.config.cameras[camera_name].detect.enabled
|
||||
),
|
||||
"motion_enabled": mp.Value("i", True),
|
||||
"improve_contrast_enabled": mp.Value(
|
||||
"i", self.config.cameras[camera_name].motion.improve_contrast
|
||||
),
|
||||
"motion_threshold": mp.Value(
|
||||
"i", self.config.cameras[camera_name].motion.threshold
|
||||
),
|
||||
"motion_contour_area": mp.Value(
|
||||
"i", self.config.cameras[camera_name].motion.contour_area
|
||||
),
|
||||
"detection_fps": mp.Value("d", 0.0),
|
||||
"detection_frame": mp.Value("d", 0.0),
|
||||
"read_start": mp.Value("d", 0.0),
|
||||
"ffmpeg_pid": mp.Value("i", 0),
|
||||
"frame_queue": mp.Queue(maxsize=2),
|
||||
"capture_process": None,
|
||||
"process": None,
|
||||
}
|
||||
|
||||
def set_log_levels(self) -> None:
|
||||
logging.getLogger().setLevel(self.config.logger.default.value.upper())
|
||||
for log, level in self.config.logger.logs.items():
|
||||
logging.getLogger(log).setLevel(level.value.upper())
|
||||
|
||||
if not "werkzeug" in self.config.logger.logs:
|
||||
logging.getLogger("werkzeug").setLevel("ERROR")
|
||||
|
||||
def init_queues(self) -> None:
|
||||
# Queues for clip processing
|
||||
self.event_queue: Queue = mp.Queue()
|
||||
self.event_processed_queue: Queue = mp.Queue()
|
||||
self.video_output_queue: Queue = mp.Queue(
|
||||
maxsize=len(self.config.cameras.keys()) * 2
|
||||
)
|
||||
|
||||
# Queue for cameras to push tracked objects to
|
||||
self.detected_frames_queue: Queue = mp.Queue(
|
||||
maxsize=len(self.config.cameras.keys()) * 2
|
||||
)
|
||||
|
||||
# Queue for recordings info
|
||||
self.recordings_info_queue: Queue = mp.Queue()
|
||||
|
||||
def init_database(self) -> None:
|
||||
# Migrate DB location
|
||||
old_db_path = os.path.join(CLIPS_DIR, "frigate.db")
|
||||
if not os.path.isfile(self.config.database.path) and os.path.isfile(
|
||||
old_db_path
|
||||
):
|
||||
os.rename(old_db_path, self.config.database.path)
|
||||
|
||||
# Migrate DB schema
|
||||
migrate_db = SqliteExtDatabase(self.config.database.path)
|
||||
|
||||
# Run migrations
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
router = Router(migrate_db)
|
||||
router.run()
|
||||
|
||||
migrate_db.close()
|
||||
|
||||
self.db = SqliteQueueDatabase(self.config.database.path)
|
||||
models = [Event, Recordings]
|
||||
self.db.bind(models)
|
||||
|
||||
def init_stats(self) -> None:
|
||||
self.stats_tracking = stats_init(self.camera_metrics, self.detectors)
|
||||
|
||||
def init_web_server(self) -> None:
|
||||
self.flask_app = create_app(
|
||||
self.config,
|
||||
self.db,
|
||||
self.stats_tracking,
|
||||
self.detected_frames_processor,
|
||||
self.plus_api,
|
||||
)
|
||||
|
||||
def init_mqtt(self) -> None:
|
||||
self.mqtt_client = create_mqtt_client(self.config, self.camera_metrics)
|
||||
|
||||
def start_mqtt_relay(self) -> None:
|
||||
self.mqtt_relay = MqttSocketRelay(
|
||||
self.mqtt_client, self.config.mqtt.topic_prefix
|
||||
)
|
||||
self.mqtt_relay.start()
|
||||
|
||||
def start_detectors(self) -> None:
|
||||
model_path = self.config.model.path
|
||||
model_shape = (self.config.model.height, self.config.model.width)
|
||||
for name in self.config.cameras.keys():
|
||||
self.detection_out_events[name] = mp.Event()
|
||||
|
||||
try:
|
||||
shm_in = mp.shared_memory.SharedMemory(
|
||||
name=name,
|
||||
create=True,
|
||||
size=self.config.model.height * self.config.model.width * 3,
|
||||
)
|
||||
except FileExistsError:
|
||||
shm_in = mp.shared_memory.SharedMemory(name=name)
|
||||
|
||||
try:
|
||||
shm_out = mp.shared_memory.SharedMemory(
|
||||
name=f"out-{name}", create=True, size=20 * 6 * 4
|
||||
)
|
||||
except FileExistsError:
|
||||
shm_out = mp.shared_memory.SharedMemory(name=f"out-{name}")
|
||||
|
||||
self.detection_shms.append(shm_in)
|
||||
self.detection_shms.append(shm_out)
|
||||
|
||||
for name, detector in self.config.detectors.items():
|
||||
if detector.type == DetectorTypeEnum.cpu:
|
||||
self.detectors[name] = EdgeTPUProcess(
|
||||
name,
|
||||
self.detection_queue,
|
||||
self.detection_out_events,
|
||||
model_path,
|
||||
model_shape,
|
||||
"cpu",
|
||||
detector.num_threads,
|
||||
)
|
||||
if detector.type == DetectorTypeEnum.edgetpu:
|
||||
self.detectors[name] = EdgeTPUProcess(
|
||||
name,
|
||||
self.detection_queue,
|
||||
self.detection_out_events,
|
||||
model_path,
|
||||
model_shape,
|
||||
detector.device,
|
||||
detector.num_threads,
|
||||
)
|
||||
|
||||
def start_detected_frames_processor(self) -> None:
|
||||
self.detected_frames_processor = TrackedObjectProcessor(
|
||||
self.config,
|
||||
self.mqtt_client,
|
||||
self.config.mqtt.topic_prefix,
|
||||
self.detected_frames_queue,
|
||||
self.event_queue,
|
||||
self.event_processed_queue,
|
||||
self.video_output_queue,
|
||||
self.recordings_info_queue,
|
||||
self.stop_event,
|
||||
)
|
||||
self.detected_frames_processor.start()
|
||||
|
||||
def start_video_output_processor(self) -> None:
|
||||
output_processor = mp.Process(
|
||||
target=output_frames,
|
||||
name=f"output_processor",
|
||||
args=(
|
||||
self.config,
|
||||
self.video_output_queue,
|
||||
),
|
||||
)
|
||||
output_processor.daemon = True
|
||||
self.output_processor = output_processor
|
||||
output_processor.start()
|
||||
logger.info(f"Output process started: {output_processor.pid}")
|
||||
|
||||
def start_camera_processors(self) -> None:
|
||||
model_shape = (self.config.model.height, self.config.model.width)
|
||||
for name, config in self.config.cameras.items():
|
||||
camera_process = mp.Process(
|
||||
target=track_camera,
|
||||
name=f"camera_processor:{name}",
|
||||
args=(
|
||||
name,
|
||||
config,
|
||||
model_shape,
|
||||
self.config.model.merged_labelmap,
|
||||
self.detection_queue,
|
||||
self.detection_out_events[name],
|
||||
self.detected_frames_queue,
|
||||
self.camera_metrics[name],
|
||||
),
|
||||
)
|
||||
camera_process.daemon = True
|
||||
self.camera_metrics[name]["process"] = camera_process
|
||||
camera_process.start()
|
||||
logger.info(f"Camera processor started for {name}: {camera_process.pid}")
|
||||
|
||||
def start_camera_capture_processes(self) -> None:
|
||||
for name, config in self.config.cameras.items():
|
||||
capture_process = mp.Process(
|
||||
target=capture_camera,
|
||||
name=f"camera_capture:{name}",
|
||||
args=(name, config, self.camera_metrics[name]),
|
||||
)
|
||||
capture_process.daemon = True
|
||||
self.camera_metrics[name]["capture_process"] = capture_process
|
||||
capture_process.start()
|
||||
logger.info(f"Capture process started for {name}: {capture_process.pid}")
|
||||
|
||||
def start_event_processor(self) -> None:
|
||||
self.event_processor = EventProcessor(
|
||||
self.config,
|
||||
self.camera_metrics,
|
||||
self.event_queue,
|
||||
self.event_processed_queue,
|
||||
self.stop_event,
|
||||
)
|
||||
self.event_processor.start()
|
||||
|
||||
def start_event_cleanup(self) -> None:
|
||||
self.event_cleanup = EventCleanup(self.config, self.stop_event)
|
||||
self.event_cleanup.start()
|
||||
|
||||
def start_recording_maintainer(self) -> None:
|
||||
self.recording_maintainer = RecordingMaintainer(
|
||||
self.config, self.recordings_info_queue, self.stop_event
|
||||
)
|
||||
self.recording_maintainer.start()
|
||||
|
||||
def start_recording_cleanup(self) -> None:
|
||||
self.recording_cleanup = RecordingCleanup(self.config, self.stop_event)
|
||||
self.recording_cleanup.start()
|
||||
|
||||
def start_stats_emitter(self) -> None:
|
||||
self.stats_emitter = StatsEmitter(
|
||||
self.config,
|
||||
self.stats_tracking,
|
||||
self.mqtt_client,
|
||||
self.config.mqtt.topic_prefix,
|
||||
self.stop_event,
|
||||
)
|
||||
self.stats_emitter.start()
|
||||
|
||||
def start_watchdog(self) -> None:
|
||||
self.frigate_watchdog = FrigateWatchdog(self.detectors, self.stop_event)
|
||||
self.frigate_watchdog.start()
|
||||
|
||||
def start(self) -> None:
|
||||
self.init_logger()
|
||||
logger.info(f"Starting Frigate ({VERSION})")
|
||||
try:
|
||||
try:
|
||||
self.init_config()
|
||||
except Exception as e:
|
||||
print("*************************************************************")
|
||||
print("*************************************************************")
|
||||
print("*** Your config file is not valid! ***")
|
||||
print("*** Please check the docs at ***")
|
||||
print("*** https://docs.frigate.video/configuration/index ***")
|
||||
print("*************************************************************")
|
||||
print("*************************************************************")
|
||||
print("*** Config Validation Errors ***")
|
||||
print("*************************************************************")
|
||||
print(e)
|
||||
print(traceback.format_exc())
|
||||
print("*************************************************************")
|
||||
print("*** End Config Validation Errors ***")
|
||||
print("*************************************************************")
|
||||
self.log_process.terminate()
|
||||
sys.exit(1)
|
||||
self.set_environment_vars()
|
||||
self.ensure_dirs()
|
||||
self.set_log_levels()
|
||||
self.init_queues()
|
||||
self.init_database()
|
||||
self.init_mqtt()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.log_process.terminate()
|
||||
sys.exit(1)
|
||||
self.start_detectors()
|
||||
self.start_video_output_processor()
|
||||
self.start_detected_frames_processor()
|
||||
self.start_camera_processors()
|
||||
self.start_camera_capture_processes()
|
||||
self.init_stats()
|
||||
self.init_web_server()
|
||||
self.start_mqtt_relay()
|
||||
self.start_event_processor()
|
||||
self.start_event_cleanup()
|
||||
self.start_recording_maintainer()
|
||||
self.start_recording_cleanup()
|
||||
self.start_stats_emitter()
|
||||
self.start_watchdog()
|
||||
# self.zeroconf = broadcast_zeroconf(self.config.mqtt.client_id)
|
||||
|
||||
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
|
||||
self.stop()
|
||||
sys.exit()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
|
||||
try:
|
||||
self.flask_app.run(host="127.0.0.1", port=5001, debug=False)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
self.stop()
|
||||
|
||||
def stop(self) -> None:
|
||||
logger.info(f"Stopping...")
|
||||
self.stop_event.set()
|
||||
|
||||
self.mqtt_relay.stop()
|
||||
self.detected_frames_processor.join()
|
||||
self.event_processor.join()
|
||||
self.event_cleanup.join()
|
||||
self.recording_maintainer.join()
|
||||
self.recording_cleanup.join()
|
||||
self.stats_emitter.join()
|
||||
self.frigate_watchdog.join()
|
||||
self.db.stop()
|
||||
|
||||
for detector in self.detectors.values():
|
||||
detector.stop()
|
||||
|
||||
while len(self.detection_shms) > 0:
|
||||
shm = self.detection_shms.pop()
|
||||
shm.close()
|
||||
shm.unlink()
|
||||
BIN
frigate/birdseye.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
942
frigate/config.py
Normal file
@@ -0,0 +1,942 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import yaml
|
||||
from pydantic import BaseModel, Extra, Field, validator
|
||||
from pydantic.fields import PrivateAttr
|
||||
|
||||
from frigate.const import BASE_DIR, CACHE_DIR, YAML_EXT
|
||||
from frigate.util import create_mask, deep_merge, load_labels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: Identify what the default format to display timestamps is
|
||||
DEFAULT_TIME_FORMAT = "%m/%d/%Y %H:%M:%S"
|
||||
# German Style:
|
||||
# DEFAULT_TIME_FORMAT = "%d.%m.%Y %H:%M:%S"
|
||||
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
|
||||
DEFAULT_TRACKED_OBJECTS = ["person"]
|
||||
DEFAULT_DETECTORS = {"cpu": {"type": "cpu"}}
|
||||
|
||||
|
||||
class FrigateBaseModel(BaseModel):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
class DetectorTypeEnum(str, Enum):
|
||||
edgetpu = "edgetpu"
|
||||
cpu = "cpu"
|
||||
|
||||
|
||||
class DetectorConfig(FrigateBaseModel):
|
||||
type: DetectorTypeEnum = Field(default=DetectorTypeEnum.cpu, title="Detector Type")
|
||||
device: str = Field(default="usb", title="Device Type")
|
||||
num_threads: int = Field(default=3, title="Number of detection threads")
|
||||
|
||||
|
||||
class UIConfig(FrigateBaseModel):
|
||||
use_experimental: bool = Field(default=False, title="Experimental UI")
|
||||
|
||||
|
||||
class MqttConfig(FrigateBaseModel):
|
||||
host: str = Field(title="MQTT Host")
|
||||
port: int = Field(default=1883, title="MQTT Port")
|
||||
topic_prefix: str = Field(default="frigate", title="MQTT Topic Prefix")
|
||||
client_id: str = Field(default="frigate", title="MQTT Client ID")
|
||||
stats_interval: int = Field(default=60, title="MQTT Camera Stats Interval")
|
||||
user: Optional[str] = Field(title="MQTT Username")
|
||||
password: Optional[str] = Field(title="MQTT Password")
|
||||
tls_ca_certs: Optional[str] = Field(title="MQTT TLS CA Certificates")
|
||||
tls_client_cert: Optional[str] = Field(title="MQTT TLS Client Certificate")
|
||||
tls_client_key: Optional[str] = Field(title="MQTT TLS Client Key")
|
||||
tls_insecure: Optional[bool] = Field(title="MQTT TLS Insecure")
|
||||
|
||||
@validator("password", pre=True, always=True)
|
||||
def validate_password(cls, v, values):
|
||||
if (v is None) != (values["user"] is None):
|
||||
raise ValueError("Password must be provided with username.")
|
||||
return v
|
||||
|
||||
|
||||
class RetainModeEnum(str, Enum):
|
||||
all = "all"
|
||||
motion = "motion"
|
||||
active_objects = "active_objects"
|
||||
|
||||
|
||||
class RetainConfig(FrigateBaseModel):
|
||||
default: float = Field(default=10, title="Default retention period.")
|
||||
mode: RetainModeEnum = Field(default=RetainModeEnum.motion, title="Retain mode.")
|
||||
objects: Dict[str, float] = Field(
|
||||
default_factory=dict, title="Object retention period."
|
||||
)
|
||||
|
||||
|
||||
class EventsConfig(FrigateBaseModel):
|
||||
pre_capture: int = Field(default=5, title="Seconds to retain before event starts.")
|
||||
post_capture: int = Field(default=5, title="Seconds to retain after event ends.")
|
||||
required_zones: List[str] = Field(
|
||||
default_factory=list,
|
||||
title="List of required zones to be entered in order to save the event.",
|
||||
)
|
||||
objects: Optional[List[str]] = Field(
|
||||
title="List of objects to be detected in order to save the event.",
|
||||
)
|
||||
retain: RetainConfig = Field(
|
||||
default_factory=RetainConfig, title="Event retention settings."
|
||||
)
|
||||
|
||||
|
||||
class RecordRetainConfig(FrigateBaseModel):
|
||||
days: float = Field(default=0, title="Default retention period.")
|
||||
mode: RetainModeEnum = Field(default=RetainModeEnum.all, title="Retain mode.")
|
||||
|
||||
|
||||
class RecordConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=False, title="Enable record on all cameras.")
|
||||
expire_interval: int = Field(
|
||||
default=60,
|
||||
title="Number of minutes to wait between cleanup runs.",
|
||||
)
|
||||
# deprecated - to be removed in a future version
|
||||
retain_days: Optional[float] = Field(title="Recording retention period in days.")
|
||||
retain: RecordRetainConfig = Field(
|
||||
default_factory=RecordRetainConfig, title="Record retention settings."
|
||||
)
|
||||
events: EventsConfig = Field(
|
||||
default_factory=EventsConfig, title="Event specific settings."
|
||||
)
|
||||
|
||||
|
||||
class MotionConfig(FrigateBaseModel):
|
||||
threshold: int = Field(
|
||||
default=25,
|
||||
title="Motion detection threshold (1-255).",
|
||||
ge=1,
|
||||
le=255,
|
||||
)
|
||||
improve_contrast: bool = Field(default=False, title="Improve Contrast")
|
||||
contour_area: Optional[int] = Field(default=30, title="Contour Area")
|
||||
delta_alpha: float = Field(default=0.2, title="Delta Alpha")
|
||||
frame_alpha: float = Field(default=0.2, title="Frame Alpha")
|
||||
frame_height: Optional[int] = Field(default=50, title="Frame Height")
|
||||
mask: Union[str, List[str]] = Field(
|
||||
default="", title="Coordinates polygon for the motion mask."
|
||||
)
|
||||
mqtt_off_delay: int = Field(
|
||||
default=30,
|
||||
title="Delay for updating MQTT with no motion detected.",
|
||||
)
|
||||
|
||||
|
||||
class RuntimeMotionConfig(MotionConfig):
|
||||
raw_mask: Union[str, List[str]] = ""
|
||||
mask: np.ndarray = None
|
||||
|
||||
def __init__(self, **config):
|
||||
frame_shape = config.get("frame_shape", (1, 1))
|
||||
|
||||
mask = config.get("mask", "")
|
||||
config["raw_mask"] = mask
|
||||
|
||||
if mask:
|
||||
config["mask"] = create_mask(frame_shape, mask)
|
||||
else:
|
||||
empty_mask = np.zeros(frame_shape, np.uint8)
|
||||
empty_mask[:] = 255
|
||||
config["mask"] = empty_mask
|
||||
|
||||
super().__init__(**config)
|
||||
|
||||
def dict(self, **kwargs):
|
||||
ret = super().dict(**kwargs)
|
||||
if "mask" in ret:
|
||||
ret["mask"] = ret["raw_mask"]
|
||||
ret.pop("raw_mask")
|
||||
return ret
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
extra = Extra.ignore
|
||||
|
||||
|
||||
class StationaryMaxFramesConfig(FrigateBaseModel):
|
||||
default: Optional[int] = Field(title="Default max frames.", ge=1)
|
||||
objects: Dict[str, int] = Field(
|
||||
default_factory=dict, title="Object specific max frames."
|
||||
)
|
||||
|
||||
|
||||
class StationaryConfig(FrigateBaseModel):
|
||||
interval: Optional[int] = Field(
|
||||
default=0,
|
||||
title="Frame interval for checking stationary objects.",
|
||||
ge=0,
|
||||
)
|
||||
threshold: Optional[int] = Field(
|
||||
title="Number of frames without a position change for an object to be considered stationary",
|
||||
ge=1,
|
||||
)
|
||||
max_frames: StationaryMaxFramesConfig = Field(
|
||||
default_factory=StationaryMaxFramesConfig,
|
||||
title="Max frames for stationary objects.",
|
||||
)
|
||||
|
||||
|
||||
class DetectConfig(FrigateBaseModel):
|
||||
height: int = Field(default=720, title="Height of the stream for the detect role.")
|
||||
width: int = Field(default=1280, title="Width of the stream for the detect role.")
|
||||
fps: int = Field(
|
||||
default=5, title="Number of frames per second to process through detection."
|
||||
)
|
||||
enabled: bool = Field(default=True, title="Detection Enabled.")
|
||||
max_disappeared: Optional[int] = Field(
|
||||
title="Maximum number of frames the object can dissapear before detection ends."
|
||||
)
|
||||
stationary: StationaryConfig = Field(
|
||||
default_factory=StationaryConfig,
|
||||
title="Stationary objects config.",
|
||||
)
|
||||
|
||||
|
||||
class FilterConfig(FrigateBaseModel):
|
||||
min_area: int = Field(
|
||||
default=0, title="Minimum area of bounding box for object to be counted."
|
||||
)
|
||||
max_area: int = Field(
|
||||
default=24000000, title="Maximum area of bounding box for object to be counted."
|
||||
)
|
||||
min_ratio: float = Field(
|
||||
default=0,
|
||||
title="Minimum ratio of bounding box's width/height for object to be counted.",
|
||||
)
|
||||
max_ratio: float = Field(
|
||||
default=24000000,
|
||||
title="Maximum ratio of bounding box's width/height for object to be counted.",
|
||||
)
|
||||
threshold: float = Field(
|
||||
default=0.7,
|
||||
title="Average detection confidence threshold for object to be counted.",
|
||||
)
|
||||
min_score: float = Field(
|
||||
default=0.5, title="Minimum detection confidence for object to be counted."
|
||||
)
|
||||
mask: Optional[Union[str, List[str]]] = Field(
|
||||
title="Detection area polygon mask for this filter configuration.",
|
||||
)
|
||||
|
||||
|
||||
class RuntimeFilterConfig(FilterConfig):
|
||||
mask: Optional[np.ndarray]
|
||||
raw_mask: Optional[Union[str, List[str]]]
|
||||
|
||||
def __init__(self, **config):
|
||||
mask = config.get("mask")
|
||||
config["raw_mask"] = mask
|
||||
|
||||
if mask is not None:
|
||||
config["mask"] = create_mask(config.get("frame_shape", (1, 1)), mask)
|
||||
|
||||
super().__init__(**config)
|
||||
|
||||
def dict(self, **kwargs):
|
||||
ret = super().dict(**kwargs)
|
||||
if "mask" in ret:
|
||||
ret["mask"] = ret["raw_mask"]
|
||||
ret.pop("raw_mask")
|
||||
return ret
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
extra = Extra.ignore
|
||||
|
||||
|
||||
# this uses the base model because the color is an extra attribute
|
||||
class ZoneConfig(BaseModel):
|
||||
filters: Dict[str, FilterConfig] = Field(
|
||||
default_factory=dict, title="Zone filters."
|
||||
)
|
||||
coordinates: Union[str, List[str]] = Field(
|
||||
title="Coordinates polygon for the defined zone."
|
||||
)
|
||||
objects: List[str] = Field(
|
||||
default_factory=list,
|
||||
title="List of objects that can trigger the zone.",
|
||||
)
|
||||
_color: Optional[Tuple[int, int, int]] = PrivateAttr()
|
||||
_contour: np.ndarray = PrivateAttr()
|
||||
|
||||
@property
|
||||
def color(self) -> Tuple[int, int, int]:
|
||||
return self._color
|
||||
|
||||
@property
|
||||
def contour(self) -> np.ndarray:
|
||||
return self._contour
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self._color = config.get("color", (0, 0, 0))
|
||||
coordinates = config["coordinates"]
|
||||
|
||||
if isinstance(coordinates, list):
|
||||
self._contour = np.array(
|
||||
[[int(p.split(",")[0]), int(p.split(",")[1])] for p in coordinates]
|
||||
)
|
||||
elif isinstance(coordinates, str):
|
||||
points = coordinates.split(",")
|
||||
self._contour = np.array(
|
||||
[[int(points[i]), int(points[i + 1])] for i in range(0, len(points), 2)]
|
||||
)
|
||||
else:
|
||||
self._contour = np.array([])
|
||||
|
||||
|
||||
class ObjectConfig(FrigateBaseModel):
|
||||
track: List[str] = Field(default=DEFAULT_TRACKED_OBJECTS, title="Objects to track.")
|
||||
filters: Optional[Dict[str, FilterConfig]] = Field(title="Object filters.")
|
||||
mask: Union[str, List[str]] = Field(default="", title="Object mask.")
|
||||
|
||||
|
||||
class BirdseyeModeEnum(str, Enum):
|
||||
objects = "objects"
|
||||
motion = "motion"
|
||||
continuous = "continuous"
|
||||
|
||||
|
||||
class BirdseyeConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="Enable birdseye view.")
|
||||
width: int = Field(default=1280, title="Birdseye width.")
|
||||
height: int = Field(default=720, title="Birdseye height.")
|
||||
quality: int = Field(
|
||||
default=8,
|
||||
title="Encoding quality.",
|
||||
ge=1,
|
||||
le=31,
|
||||
)
|
||||
mode: BirdseyeModeEnum = Field(
|
||||
default=BirdseyeModeEnum.objects, title="Tracking mode."
|
||||
)
|
||||
|
||||
|
||||
# uses BaseModel because some global attributes are not available at the camera level
|
||||
class BirdseyeCameraConfig(BaseModel):
|
||||
enabled: bool = Field(default=True, title="Enable birdseye view for camera.")
|
||||
mode: BirdseyeModeEnum = Field(
|
||||
default=BirdseyeModeEnum.objects, title="Tracking mode for camera."
|
||||
)
|
||||
|
||||
|
||||
FFMPEG_GLOBAL_ARGS_DEFAULT = ["-hide_banner", "-loglevel", "warning"]
|
||||
FFMPEG_INPUT_ARGS_DEFAULT = [
|
||||
"-avoid_negative_ts",
|
||||
"make_zero",
|
||||
"-fflags",
|
||||
"+genpts+discardcorrupt",
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
"-timeout",
|
||||
"5000000",
|
||||
"-use_wallclock_as_timestamps",
|
||||
"1",
|
||||
]
|
||||
DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-f", "rawvideo", "-pix_fmt", "yuv420p"]
|
||||
RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT = ["-c", "copy", "-f", "flv"]
|
||||
RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT = [
|
||||
"-f",
|
||||
"segment",
|
||||
"-segment_time",
|
||||
"10",
|
||||
"-segment_format",
|
||||
"mp4",
|
||||
"-reset_timestamps",
|
||||
"1",
|
||||
"-strftime",
|
||||
"1",
|
||||
"-c",
|
||||
"copy",
|
||||
"-an",
|
||||
]
|
||||
|
||||
|
||||
class FfmpegOutputArgsConfig(FrigateBaseModel):
|
||||
detect: Union[str, List[str]] = Field(
|
||||
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
||||
title="Detect role FFmpeg output arguments.",
|
||||
)
|
||||
record: Union[str, List[str]] = Field(
|
||||
default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
||||
title="Record role FFmpeg output arguments.",
|
||||
)
|
||||
rtmp: Union[str, List[str]] = Field(
|
||||
default=RTMP_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
||||
title="RTMP role FFmpeg output arguments.",
|
||||
)
|
||||
|
||||
|
||||
class FfmpegConfig(FrigateBaseModel):
|
||||
global_args: Union[str, List[str]] = Field(
|
||||
default=FFMPEG_GLOBAL_ARGS_DEFAULT, title="Global FFmpeg arguments."
|
||||
)
|
||||
hwaccel_args: Union[str, List[str]] = Field(
|
||||
default_factory=list, title="FFmpeg hardware acceleration arguments."
|
||||
)
|
||||
input_args: Union[str, List[str]] = Field(
|
||||
default=FFMPEG_INPUT_ARGS_DEFAULT, title="FFmpeg input arguments."
|
||||
)
|
||||
output_args: FfmpegOutputArgsConfig = Field(
|
||||
default_factory=FfmpegOutputArgsConfig,
|
||||
title="FFmpeg output arguments per role.",
|
||||
)
|
||||
|
||||
|
||||
class CameraRoleEnum(str, Enum):
|
||||
record = "record"
|
||||
rtmp = "rtmp"
|
||||
detect = "detect"
|
||||
|
||||
|
||||
class CameraInput(FrigateBaseModel):
|
||||
path: str = Field(title="Camera input path.")
|
||||
roles: List[CameraRoleEnum] = Field(title="Roles assigned to this input.")
|
||||
global_args: Union[str, List[str]] = Field(
|
||||
default_factory=list, title="FFmpeg global arguments."
|
||||
)
|
||||
hwaccel_args: Union[str, List[str]] = Field(
|
||||
default_factory=list, title="FFmpeg hardware acceleration arguments."
|
||||
)
|
||||
input_args: Union[str, List[str]] = Field(
|
||||
default_factory=list, title="FFmpeg input arguments."
|
||||
)
|
||||
|
||||
|
||||
class CameraFfmpegConfig(FfmpegConfig):
|
||||
inputs: List[CameraInput] = Field(title="Camera inputs.")
|
||||
|
||||
@validator("inputs")
|
||||
def validate_roles(cls, v):
|
||||
roles = [role for i in v for role in i.roles]
|
||||
roles_set = set(roles)
|
||||
|
||||
if len(roles) > len(roles_set):
|
||||
raise ValueError("Each input role may only be used once.")
|
||||
|
||||
if not "detect" in roles:
|
||||
raise ValueError("The detect role is required.")
|
||||
|
||||
return v
|
||||
|
||||
|
||||
class SnapshotsConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=False, title="Snapshots enabled.")
|
||||
clean_copy: bool = Field(
|
||||
default=True, title="Create a clean copy of the snapshot image."
|
||||
)
|
||||
timestamp: bool = Field(
|
||||
default=False, title="Add a timestamp overlay on the snapshot."
|
||||
)
|
||||
bounding_box: bool = Field(
|
||||
default=True, title="Add a bounding box overlay on the snapshot."
|
||||
)
|
||||
crop: bool = Field(default=False, title="Crop the snapshot to the detected object.")
|
||||
required_zones: List[str] = Field(
|
||||
default_factory=list,
|
||||
title="List of required zones to be entered in order to save a snapshot.",
|
||||
)
|
||||
height: Optional[int] = Field(title="Snapshot image height.")
|
||||
retain: RetainConfig = Field(
|
||||
default_factory=RetainConfig, title="Snapshot retention."
|
||||
)
|
||||
quality: int = Field(
|
||||
default=70,
|
||||
title="Quality of the encoded jpeg (0-100).",
|
||||
ge=0,
|
||||
le=100,
|
||||
)
|
||||
|
||||
|
||||
class ColorConfig(FrigateBaseModel):
|
||||
red: int = Field(default=255, ge=0, le=255, title="Red")
|
||||
green: int = Field(default=255, ge=0, le=255, title="Green")
|
||||
blue: int = Field(default=255, ge=0, le=255, title="Blue")
|
||||
|
||||
|
||||
class TimestampPositionEnum(str, Enum):
|
||||
tl = "tl"
|
||||
tr = "tr"
|
||||
bl = "bl"
|
||||
br = "br"
|
||||
|
||||
|
||||
class TimestampEffectEnum(str, Enum):
|
||||
solid = "solid"
|
||||
shadow = "shadow"
|
||||
|
||||
|
||||
class TimestampStyleConfig(FrigateBaseModel):
|
||||
position: TimestampPositionEnum = Field(
|
||||
default=TimestampPositionEnum.tl, title="Timestamp position."
|
||||
)
|
||||
format: str = Field(default=DEFAULT_TIME_FORMAT, title="Timestamp format.")
|
||||
color: ColorConfig = Field(default_factory=ColorConfig, title="Timestamp color.")
|
||||
thickness: int = Field(default=2, title="Timestamp thickness.")
|
||||
effect: Optional[TimestampEffectEnum] = Field(title="Timestamp effect.")
|
||||
|
||||
|
||||
class CameraMqttConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="Send image over MQTT.")
|
||||
timestamp: bool = Field(default=True, title="Add timestamp to MQTT image.")
|
||||
bounding_box: bool = Field(default=True, title="Add bounding box to MQTT image.")
|
||||
crop: bool = Field(default=True, title="Crop MQTT image to detected object.")
|
||||
height: int = Field(default=270, title="MQTT image height.")
|
||||
required_zones: List[str] = Field(
|
||||
default_factory=list,
|
||||
title="List of required zones to be entered in order to send the image.",
|
||||
)
|
||||
quality: int = Field(
|
||||
default=70,
|
||||
title="Quality of the encoded jpeg (0-100).",
|
||||
ge=0,
|
||||
le=100,
|
||||
)
|
||||
|
||||
|
||||
class RtmpConfig(FrigateBaseModel):
|
||||
enabled: bool = Field(default=True, title="RTMP restreaming enabled.")
|
||||
|
||||
|
||||
class CameraLiveConfig(FrigateBaseModel):
|
||||
height: int = Field(default=720, title="Live camera view height")
|
||||
quality: int = Field(default=8, ge=1, le=31, title="Live camera view quality")
|
||||
|
||||
|
||||
class CameraUiConfig(FrigateBaseModel):
|
||||
order: int = Field(default=0, title="Order of camera in UI.")
|
||||
dashboard: bool = Field(
|
||||
default=True, title="Show this camera in Frigate dashboard UI."
|
||||
)
|
||||
|
||||
|
||||
class CameraConfig(FrigateBaseModel):
|
||||
name: Optional[str] = Field(title="Camera name.", regex="^[a-zA-Z0-9_-]+$")
|
||||
ffmpeg: CameraFfmpegConfig = Field(title="FFmpeg configuration for the camera.")
|
||||
best_image_timeout: int = Field(
|
||||
default=60,
|
||||
title="How long to wait for the image with the highest confidence score.",
|
||||
)
|
||||
zones: Dict[str, ZoneConfig] = Field(
|
||||
default_factory=dict, title="Zone configuration."
|
||||
)
|
||||
record: RecordConfig = Field(
|
||||
default_factory=RecordConfig, title="Record configuration."
|
||||
)
|
||||
rtmp: RtmpConfig = Field(
|
||||
default_factory=RtmpConfig, title="RTMP restreaming configuration."
|
||||
)
|
||||
live: CameraLiveConfig = Field(
|
||||
default_factory=CameraLiveConfig, title="Live playback settings."
|
||||
)
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig, title="Snapshot configuration."
|
||||
)
|
||||
mqtt: CameraMqttConfig = Field(
|
||||
default_factory=CameraMqttConfig, title="MQTT configuration."
|
||||
)
|
||||
objects: ObjectConfig = Field(
|
||||
default_factory=ObjectConfig, title="Object configuration."
|
||||
)
|
||||
motion: Optional[MotionConfig] = Field(title="Motion detection configuration.")
|
||||
detect: DetectConfig = Field(
|
||||
default_factory=DetectConfig, title="Object detection configuration."
|
||||
)
|
||||
ui: CameraUiConfig = Field(
|
||||
default_factory=CameraUiConfig, title="Camera UI Modifications."
|
||||
)
|
||||
birdseye: BirdseyeCameraConfig = Field(
|
||||
default_factory=BirdseyeCameraConfig, title="Birdseye camera configuration."
|
||||
)
|
||||
timestamp_style: TimestampStyleConfig = Field(
|
||||
default_factory=TimestampStyleConfig, title="Timestamp style configuration."
|
||||
)
|
||||
_ffmpeg_cmds: List[Dict[str, List[str]]] = PrivateAttr()
|
||||
|
||||
def __init__(self, **config):
|
||||
# Set zone colors
|
||||
if "zones" in config:
|
||||
colors = plt.cm.get_cmap("tab10", len(config["zones"]))
|
||||
config["zones"] = {
|
||||
name: {**z, "color": tuple(round(255 * c) for c in colors(idx)[:3])}
|
||||
for idx, (name, z) in enumerate(config["zones"].items())
|
||||
}
|
||||
|
||||
# add roles to the input if there is only one
|
||||
if len(config["ffmpeg"]["inputs"]) == 1:
|
||||
config["ffmpeg"]["inputs"][0]["roles"] = ["record", "rtmp", "detect"]
|
||||
|
||||
super().__init__(**config)
|
||||
|
||||
@property
|
||||
def frame_shape(self) -> Tuple[int, int]:
|
||||
return self.detect.height, self.detect.width
|
||||
|
||||
@property
|
||||
def frame_shape_yuv(self) -> Tuple[int, int]:
|
||||
return self.detect.height * 3 // 2, self.detect.width
|
||||
|
||||
@property
|
||||
def ffmpeg_cmds(self) -> List[Dict[str, List[str]]]:
|
||||
return self._ffmpeg_cmds
|
||||
|
||||
def create_ffmpeg_cmds(self):
|
||||
if "_ffmpeg_cmds" in self:
|
||||
return
|
||||
ffmpeg_cmds = []
|
||||
for ffmpeg_input in self.ffmpeg.inputs:
|
||||
ffmpeg_cmd = self._get_ffmpeg_cmd(ffmpeg_input)
|
||||
if ffmpeg_cmd is None:
|
||||
continue
|
||||
|
||||
ffmpeg_cmds.append({"roles": ffmpeg_input.roles, "cmd": ffmpeg_cmd})
|
||||
self._ffmpeg_cmds = ffmpeg_cmds
|
||||
|
||||
def _get_ffmpeg_cmd(self, ffmpeg_input: CameraInput):
|
||||
ffmpeg_output_args = []
|
||||
if "detect" in ffmpeg_input.roles:
|
||||
detect_args = (
|
||||
self.ffmpeg.output_args.detect
|
||||
if isinstance(self.ffmpeg.output_args.detect, list)
|
||||
else self.ffmpeg.output_args.detect.split(" ")
|
||||
)
|
||||
ffmpeg_output_args = (
|
||||
[
|
||||
"-r",
|
||||
str(self.detect.fps),
|
||||
"-s",
|
||||
f"{self.detect.width}x{self.detect.height}",
|
||||
]
|
||||
+ detect_args
|
||||
+ ffmpeg_output_args
|
||||
+ ["pipe:"]
|
||||
)
|
||||
if "rtmp" in ffmpeg_input.roles and self.rtmp.enabled:
|
||||
rtmp_args = (
|
||||
self.ffmpeg.output_args.rtmp
|
||||
if isinstance(self.ffmpeg.output_args.rtmp, list)
|
||||
else self.ffmpeg.output_args.rtmp.split(" ")
|
||||
)
|
||||
ffmpeg_output_args = (
|
||||
rtmp_args + [f"rtmp://127.0.0.1/live/{self.name}"] + ffmpeg_output_args
|
||||
)
|
||||
if "record" in ffmpeg_input.roles and self.record.enabled:
|
||||
record_args = (
|
||||
self.ffmpeg.output_args.record
|
||||
if isinstance(self.ffmpeg.output_args.record, list)
|
||||
else self.ffmpeg.output_args.record.split(" ")
|
||||
)
|
||||
|
||||
ffmpeg_output_args = (
|
||||
record_args
|
||||
+ [f"{os.path.join(CACHE_DIR, self.name)}-%Y%m%d%H%M%S.mp4"]
|
||||
+ ffmpeg_output_args
|
||||
)
|
||||
|
||||
# if there arent any outputs enabled for this input
|
||||
if len(ffmpeg_output_args) == 0:
|
||||
return None
|
||||
|
||||
global_args = ffmpeg_input.global_args or self.ffmpeg.global_args
|
||||
hwaccel_args = ffmpeg_input.hwaccel_args or self.ffmpeg.hwaccel_args
|
||||
input_args = ffmpeg_input.input_args or self.ffmpeg.input_args
|
||||
|
||||
global_args = (
|
||||
global_args if isinstance(global_args, list) else global_args.split(" ")
|
||||
)
|
||||
hwaccel_args = (
|
||||
hwaccel_args if isinstance(hwaccel_args, list) else hwaccel_args.split(" ")
|
||||
)
|
||||
input_args = (
|
||||
input_args if isinstance(input_args, list) else input_args.split(" ")
|
||||
)
|
||||
|
||||
cmd = (
|
||||
["ffmpeg"]
|
||||
+ global_args
|
||||
+ hwaccel_args
|
||||
+ input_args
|
||||
+ ["-i", ffmpeg_input.path]
|
||||
+ ffmpeg_output_args
|
||||
)
|
||||
|
||||
return [part for part in cmd if part != ""]
|
||||
|
||||
|
||||
class DatabaseConfig(FrigateBaseModel):
|
||||
path: str = Field(
|
||||
default=os.path.join(BASE_DIR, "frigate.db"), title="Database path."
|
||||
)
|
||||
|
||||
|
||||
class ModelConfig(FrigateBaseModel):
|
||||
path: Optional[str] = Field(title="Custom Object detection model path.")
|
||||
labelmap_path: Optional[str] = Field(title="Label map for custom object detector.")
|
||||
width: int = Field(default=320, title="Object detection model input width.")
|
||||
height: int = Field(default=320, title="Object detection model input height.")
|
||||
labelmap: Dict[int, str] = Field(
|
||||
default_factory=dict, title="Labelmap customization."
|
||||
)
|
||||
_merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
|
||||
_colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()
|
||||
|
||||
@property
|
||||
def merged_labelmap(self) -> Dict[int, str]:
|
||||
return self._merged_labelmap
|
||||
|
||||
@property
|
||||
def colormap(self) -> Dict[int, Tuple[int, int, int]]:
|
||||
return self._colormap
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self._merged_labelmap = {
|
||||
**load_labels(config.get("labelmap_path", "/labelmap.txt")),
|
||||
**config.get("labelmap", {}),
|
||||
}
|
||||
|
||||
cmap = plt.cm.get_cmap("tab10", len(self._merged_labelmap.keys()))
|
||||
|
||||
self._colormap = {}
|
||||
for key, val in self._merged_labelmap.items():
|
||||
self._colormap[val] = tuple(int(round(255 * c)) for c in cmap(key)[:3])
|
||||
|
||||
|
||||
class LogLevelEnum(str, Enum):
|
||||
debug = "debug"
|
||||
info = "info"
|
||||
warning = "warning"
|
||||
error = "error"
|
||||
critical = "critical"
|
||||
|
||||
|
||||
class LoggerConfig(FrigateBaseModel):
|
||||
default: LogLevelEnum = Field(
|
||||
default=LogLevelEnum.info, title="Default logging level."
|
||||
)
|
||||
logs: Dict[str, LogLevelEnum] = Field(
|
||||
default_factory=dict, title="Log level for specified processes."
|
||||
)
|
||||
|
||||
|
||||
class FrigateConfig(FrigateBaseModel):
|
||||
mqtt: MqttConfig = Field(title="MQTT Configuration.")
|
||||
database: DatabaseConfig = Field(
|
||||
default_factory=DatabaseConfig, title="Database configuration."
|
||||
)
|
||||
environment_vars: Dict[str, str] = Field(
|
||||
default_factory=dict, title="Frigate environment variables."
|
||||
)
|
||||
ui: UIConfig = Field(default_factory=UIConfig, title="UI configuration.")
|
||||
model: ModelConfig = Field(
|
||||
default_factory=ModelConfig, title="Detection model configuration."
|
||||
)
|
||||
detectors: Dict[str, DetectorConfig] = Field(
|
||||
default={name: DetectorConfig(**d) for name, d in DEFAULT_DETECTORS.items()},
|
||||
title="Detector hardware configuration.",
|
||||
)
|
||||
logger: LoggerConfig = Field(
|
||||
default_factory=LoggerConfig, title="Logging configuration."
|
||||
)
|
||||
record: RecordConfig = Field(
|
||||
default_factory=RecordConfig, title="Global record configuration."
|
||||
)
|
||||
snapshots: SnapshotsConfig = Field(
|
||||
default_factory=SnapshotsConfig, title="Global snapshots configuration."
|
||||
)
|
||||
live: CameraLiveConfig = Field(
|
||||
default_factory=CameraLiveConfig, title="Global live configuration."
|
||||
)
|
||||
rtmp: RtmpConfig = Field(
|
||||
default_factory=RtmpConfig, title="Global RTMP restreaming configuration."
|
||||
)
|
||||
birdseye: BirdseyeConfig = Field(
|
||||
default_factory=BirdseyeConfig, title="Birdseye configuration."
|
||||
)
|
||||
ffmpeg: FfmpegConfig = Field(
|
||||
default_factory=FfmpegConfig, title="Global FFmpeg configuration."
|
||||
)
|
||||
objects: ObjectConfig = Field(
|
||||
default_factory=ObjectConfig, title="Global object configuration."
|
||||
)
|
||||
motion: Optional[MotionConfig] = Field(
|
||||
title="Global motion detection configuration."
|
||||
)
|
||||
detect: DetectConfig = Field(
|
||||
default_factory=DetectConfig, title="Global object tracking configuration."
|
||||
)
|
||||
cameras: Dict[str, CameraConfig] = Field(title="Camera configuration.")
|
||||
timestamp_style: TimestampStyleConfig = Field(
|
||||
default_factory=TimestampStyleConfig,
|
||||
title="Global timestamp style configuration.",
|
||||
)
|
||||
|
||||
@property
|
||||
def runtime_config(self) -> FrigateConfig:
|
||||
"""Merge camera config with globals."""
|
||||
config = self.copy(deep=True)
|
||||
|
||||
# MQTT password substitution
|
||||
if config.mqtt.password:
|
||||
config.mqtt.password = config.mqtt.password.format(**FRIGATE_ENV_VARS)
|
||||
|
||||
# Global config to propegate down to camera level
|
||||
global_config = config.dict(
|
||||
include={
|
||||
"birdseye": ...,
|
||||
"record": ...,
|
||||
"snapshots": ...,
|
||||
"live": ...,
|
||||
"rtmp": ...,
|
||||
"objects": ...,
|
||||
"motion": ...,
|
||||
"detect": ...,
|
||||
"ffmpeg": ...,
|
||||
"timestamp_style": ...,
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
|
||||
for name, camera in config.cameras.items():
|
||||
merged_config = deep_merge(camera.dict(exclude_unset=True), global_config)
|
||||
camera_config: CameraConfig = CameraConfig.parse_obj(
|
||||
{"name": name, **merged_config}
|
||||
)
|
||||
|
||||
# Default max_disappeared configuration
|
||||
max_disappeared = camera_config.detect.fps * 5
|
||||
if camera_config.detect.max_disappeared is None:
|
||||
camera_config.detect.max_disappeared = max_disappeared
|
||||
|
||||
# Default stationary_threshold configuration
|
||||
stationary_threshold = camera_config.detect.fps * 10
|
||||
if camera_config.detect.stationary.threshold is None:
|
||||
camera_config.detect.stationary.threshold = stationary_threshold
|
||||
|
||||
# FFMPEG input substitution
|
||||
for input in camera_config.ffmpeg.inputs:
|
||||
input.path = input.path.format(**FRIGATE_ENV_VARS)
|
||||
|
||||
# Add default filters
|
||||
object_keys = camera_config.objects.track
|
||||
if camera_config.objects.filters is None:
|
||||
camera_config.objects.filters = {}
|
||||
object_keys = object_keys - camera_config.objects.filters.keys()
|
||||
for key in object_keys:
|
||||
camera_config.objects.filters[key] = FilterConfig()
|
||||
|
||||
# Apply global object masks and convert masks to numpy array
|
||||
for object, filter in camera_config.objects.filters.items():
|
||||
if camera_config.objects.mask:
|
||||
filter_mask = []
|
||||
if filter.mask is not None:
|
||||
filter_mask = (
|
||||
filter.mask
|
||||
if isinstance(filter.mask, list)
|
||||
else [filter.mask]
|
||||
)
|
||||
object_mask = (
|
||||
camera_config.objects.mask
|
||||
if isinstance(camera_config.objects.mask, list)
|
||||
else [camera_config.objects.mask]
|
||||
)
|
||||
filter.mask = filter_mask + object_mask
|
||||
|
||||
# Set runtime filter to create masks
|
||||
camera_config.objects.filters[object] = RuntimeFilterConfig(
|
||||
frame_shape=camera_config.frame_shape,
|
||||
**filter.dict(exclude_unset=True),
|
||||
)
|
||||
|
||||
# Convert motion configuration
|
||||
if camera_config.motion is None:
|
||||
camera_config.motion = RuntimeMotionConfig(
|
||||
frame_shape=camera_config.frame_shape
|
||||
)
|
||||
else:
|
||||
camera_config.motion = RuntimeMotionConfig(
|
||||
frame_shape=camera_config.frame_shape,
|
||||
raw_mask=camera_config.motion.mask,
|
||||
**camera_config.motion.dict(exclude_unset=True),
|
||||
)
|
||||
|
||||
# check runtime config
|
||||
assigned_roles = list(
|
||||
set([r for i in camera_config.ffmpeg.inputs for r in i.roles])
|
||||
)
|
||||
if camera_config.record.enabled and not "record" in assigned_roles:
|
||||
raise ValueError(
|
||||
f"Camera {name} has record enabled, but record is not assigned to an input."
|
||||
)
|
||||
|
||||
if camera_config.rtmp.enabled and not "rtmp" in assigned_roles:
|
||||
raise ValueError(
|
||||
f"Camera {name} has rtmp enabled, but rtmp is not assigned to an input."
|
||||
)
|
||||
|
||||
# backwards compatibility for retain_days
|
||||
if not camera_config.record.retain_days is None:
|
||||
logger.warning(
|
||||
"The 'retain_days' config option has been DEPRECATED and will be removed in a future version. Please use the 'days' setting under 'retain'"
|
||||
)
|
||||
if camera_config.record.retain.days == 0:
|
||||
camera_config.record.retain.days = camera_config.record.retain_days
|
||||
|
||||
# warning if the higher level record mode is potentially more restrictive than the events
|
||||
rank_map = {
|
||||
RetainModeEnum.all: 0,
|
||||
RetainModeEnum.motion: 1,
|
||||
RetainModeEnum.active_objects: 2,
|
||||
}
|
||||
if (
|
||||
camera_config.record.retain.days != 0
|
||||
and rank_map[camera_config.record.retain.mode]
|
||||
> rank_map[camera_config.record.events.retain.mode]
|
||||
):
|
||||
logger.warning(
|
||||
f"{name}: Recording retention is configured for {camera_config.record.retain.mode} and event retention is configured for {camera_config.record.events.retain.mode}. The more restrictive retention policy will be applied."
|
||||
)
|
||||
# generage the ffmpeg commands
|
||||
camera_config.create_ffmpeg_cmds()
|
||||
config.cameras[name] = camera_config
|
||||
|
||||
return config
|
||||
|
||||
@validator("cameras")
|
||||
def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
|
||||
zones = [zone for camera in v.values() for zone in camera.zones.keys()]
|
||||
for zone in zones:
|
||||
if zone in v.keys():
|
||||
raise ValueError("Zones cannot share names with cameras")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
def parse_file(cls, config_file):
|
||||
with open(config_file) as f:
|
||||
raw_config = f.read()
|
||||
|
||||
if config_file.endswith(YAML_EXT):
|
||||
config = yaml.safe_load(raw_config)
|
||||
elif config_file.endswith(".json"):
|
||||
config = json.loads(raw_config)
|
||||
|
||||
return cls.parse_obj(config)
|
||||
7
frigate/const.py
Normal file
@@ -0,0 +1,7 @@
|
||||
BASE_DIR = "/media/frigate"
|
||||
CLIPS_DIR = f"{BASE_DIR}/clips"
|
||||
RECORD_DIR = f"{BASE_DIR}/recordings"
|
||||
CACHE_DIR = "/tmp/cache"
|
||||
YAML_EXT = (".yaml", ".yml")
|
||||
PLUS_ENV_VAR = "PLUS_API_KEY"
|
||||
PLUS_API_HOST = "https://api.frigate.video"
|
||||
263
frigate/edgetpu.py
Normal file
@@ -0,0 +1,263 @@
|
||||
import datetime
|
||||
import logging
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import queue
|
||||
import signal
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import numpy as np
|
||||
import tflite_runtime.interpreter as tflite
|
||||
from setproctitle import setproctitle
|
||||
from tflite_runtime.interpreter import load_delegate
|
||||
|
||||
from frigate.util import EventsPerSecond, SharedMemoryFrameManager, listen, load_labels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ObjectDetector(ABC):
|
||||
@abstractmethod
|
||||
def detect(self, tensor_input, threshold=0.4):
|
||||
pass
|
||||
|
||||
|
||||
class LocalObjectDetector(ObjectDetector):
|
||||
def __init__(self, tf_device=None, model_path=None, num_threads=3, labels=None):
|
||||
self.fps = EventsPerSecond()
|
||||
if labels is None:
|
||||
self.labels = {}
|
||||
else:
|
||||
self.labels = load_labels(labels)
|
||||
|
||||
device_config = {"device": "usb"}
|
||||
if not tf_device is None:
|
||||
device_config = {"device": tf_device}
|
||||
|
||||
edge_tpu_delegate = None
|
||||
|
||||
if tf_device != "cpu":
|
||||
try:
|
||||
logger.info(f"Attempting to load TPU as {device_config['device']}")
|
||||
edge_tpu_delegate = load_delegate("libedgetpu.so.1.0", device_config)
|
||||
logger.info("TPU found")
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path=model_path or "/edgetpu_model.tflite",
|
||||
experimental_delegates=[edge_tpu_delegate],
|
||||
)
|
||||
except ValueError:
|
||||
logger.error(
|
||||
"No EdgeTPU was detected. If you do not have a Coral device yet, you must configure CPU detectors."
|
||||
)
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
"CPU detectors are not recommended and should only be used for testing or for trial purposes."
|
||||
)
|
||||
self.interpreter = tflite.Interpreter(
|
||||
model_path=model_path or "/cpu_model.tflite", num_threads=num_threads
|
||||
)
|
||||
|
||||
self.interpreter.allocate_tensors()
|
||||
|
||||
self.tensor_input_details = self.interpreter.get_input_details()
|
||||
self.tensor_output_details = self.interpreter.get_output_details()
|
||||
|
||||
def detect(self, tensor_input, threshold=0.4):
|
||||
detections = []
|
||||
|
||||
raw_detections = self.detect_raw(tensor_input)
|
||||
|
||||
for d in raw_detections:
|
||||
if d[1] < threshold:
|
||||
break
|
||||
detections.append(
|
||||
(self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5]))
|
||||
)
|
||||
self.fps.update()
|
||||
return detections
|
||||
|
||||
def detect_raw(self, tensor_input):
|
||||
self.interpreter.set_tensor(self.tensor_input_details[0]["index"], tensor_input)
|
||||
self.interpreter.invoke()
|
||||
|
||||
boxes = self.interpreter.tensor(self.tensor_output_details[0]["index"])()[0]
|
||||
class_ids = self.interpreter.tensor(self.tensor_output_details[1]["index"])()[0]
|
||||
scores = self.interpreter.tensor(self.tensor_output_details[2]["index"])()[0]
|
||||
count = int(
|
||||
self.interpreter.tensor(self.tensor_output_details[3]["index"])()[0]
|
||||
)
|
||||
|
||||
detections = np.zeros((20, 6), np.float32)
|
||||
|
||||
for i in range(count):
|
||||
if scores[i] < 0.4 or i == 20:
|
||||
break
|
||||
detections[i] = [
|
||||
class_ids[i],
|
||||
float(scores[i]),
|
||||
boxes[i][0],
|
||||
boxes[i][1],
|
||||
boxes[i][2],
|
||||
boxes[i][3],
|
||||
]
|
||||
|
||||
return detections
|
||||
|
||||
|
||||
def run_detector(
|
||||
name: str,
|
||||
detection_queue: mp.Queue,
|
||||
out_events: dict[str, mp.Event],
|
||||
avg_speed,
|
||||
start,
|
||||
model_path,
|
||||
model_shape,
|
||||
tf_device,
|
||||
num_threads,
|
||||
):
|
||||
threading.current_thread().name = f"detector:{name}"
|
||||
logger = logging.getLogger(f"detector.{name}")
|
||||
logger.info(f"Starting detection process: {os.getpid()}")
|
||||
setproctitle(f"frigate.detector.{name}")
|
||||
listen()
|
||||
|
||||
stop_event = mp.Event()
|
||||
|
||||
def receiveSignal(signalNumber, frame):
|
||||
stop_event.set()
|
||||
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
signal.signal(signal.SIGINT, receiveSignal)
|
||||
|
||||
frame_manager = SharedMemoryFrameManager()
|
||||
object_detector = LocalObjectDetector(
|
||||
tf_device=tf_device, model_path=model_path, num_threads=num_threads
|
||||
)
|
||||
|
||||
outputs = {}
|
||||
for name in out_events.keys():
|
||||
out_shm = mp.shared_memory.SharedMemory(name=f"out-{name}", create=False)
|
||||
out_np = np.ndarray((20, 6), dtype=np.float32, buffer=out_shm.buf)
|
||||
outputs[name] = {"shm": out_shm, "np": out_np}
|
||||
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
connection_id = detection_queue.get(timeout=5)
|
||||
except queue.Empty:
|
||||
continue
|
||||
input_frame = frame_manager.get(
|
||||
connection_id, (1, model_shape[0], model_shape[1], 3)
|
||||
)
|
||||
|
||||
if input_frame is None:
|
||||
continue
|
||||
|
||||
# detect and send the output
|
||||
start.value = datetime.datetime.now().timestamp()
|
||||
detections = object_detector.detect_raw(input_frame)
|
||||
duration = datetime.datetime.now().timestamp() - start.value
|
||||
outputs[connection_id]["np"][:] = detections[:]
|
||||
out_events[connection_id].set()
|
||||
start.value = 0.0
|
||||
|
||||
avg_speed.value = (avg_speed.value * 9 + duration) / 10
|
||||
|
||||
|
||||
class EdgeTPUProcess:
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
detection_queue,
|
||||
out_events,
|
||||
model_path,
|
||||
model_shape,
|
||||
tf_device=None,
|
||||
num_threads=3,
|
||||
):
|
||||
self.name = name
|
||||
self.out_events = out_events
|
||||
self.detection_queue = detection_queue
|
||||
self.avg_inference_speed = mp.Value("d", 0.01)
|
||||
self.detection_start = mp.Value("d", 0.0)
|
||||
self.detect_process = None
|
||||
self.model_path = model_path
|
||||
self.model_shape = model_shape
|
||||
self.tf_device = tf_device
|
||||
self.num_threads = num_threads
|
||||
self.start_or_restart()
|
||||
|
||||
def stop(self):
|
||||
self.detect_process.terminate()
|
||||
logging.info("Waiting for detection process to exit gracefully...")
|
||||
self.detect_process.join(timeout=30)
|
||||
if self.detect_process.exitcode is None:
|
||||
logging.info("Detection process didnt exit. Force killing...")
|
||||
self.detect_process.kill()
|
||||
self.detect_process.join()
|
||||
|
||||
def start_or_restart(self):
|
||||
self.detection_start.value = 0.0
|
||||
if (not self.detect_process is None) and self.detect_process.is_alive():
|
||||
self.stop()
|
||||
self.detect_process = mp.Process(
|
||||
target=run_detector,
|
||||
name=f"detector:{self.name}",
|
||||
args=(
|
||||
self.name,
|
||||
self.detection_queue,
|
||||
self.out_events,
|
||||
self.avg_inference_speed,
|
||||
self.detection_start,
|
||||
self.model_path,
|
||||
self.model_shape,
|
||||
self.tf_device,
|
||||
self.num_threads,
|
||||
),
|
||||
)
|
||||
self.detect_process.daemon = True
|
||||
self.detect_process.start()
|
||||
|
||||
|
||||
class RemoteObjectDetector:
|
||||
def __init__(self, name, labels, detection_queue, event, model_shape):
|
||||
self.labels = labels
|
||||
self.name = name
|
||||
self.fps = EventsPerSecond()
|
||||
self.detection_queue = detection_queue
|
||||
self.event = event
|
||||
self.shm = mp.shared_memory.SharedMemory(name=self.name, create=False)
|
||||
self.np_shm = np.ndarray(
|
||||
(1, model_shape[0], model_shape[1], 3), dtype=np.uint8, buffer=self.shm.buf
|
||||
)
|
||||
self.out_shm = mp.shared_memory.SharedMemory(
|
||||
name=f"out-{self.name}", create=False
|
||||
)
|
||||
self.out_np_shm = np.ndarray((20, 6), dtype=np.float32, buffer=self.out_shm.buf)
|
||||
|
||||
def detect(self, tensor_input, threshold=0.4):
|
||||
detections = []
|
||||
|
||||
# copy input to shared memory
|
||||
self.np_shm[:] = tensor_input[:]
|
||||
self.event.clear()
|
||||
self.detection_queue.put(self.name)
|
||||
result = self.event.wait(timeout=10.0)
|
||||
|
||||
# if it timed out
|
||||
if result is None:
|
||||
return detections
|
||||
|
||||
for d in self.out_np_shm:
|
||||
if d[1] < threshold:
|
||||
break
|
||||
detections.append(
|
||||
(self.labels[int(d[0])], float(d[1]), (d[2], d[3], d[4], d[5]))
|
||||
)
|
||||
self.fps.update()
|
||||
return detections
|
||||
|
||||
def cleanup(self):
|
||||
self.shm.unlink()
|
||||
self.out_shm.unlink()
|
||||
303
frigate/events.py
Normal file
@@ -0,0 +1,303 @@
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from peewee import fn
|
||||
|
||||
from frigate.config import EventsConfig, FrigateConfig, RecordConfig
|
||||
from frigate.const import CLIPS_DIR
|
||||
from frigate.models import Event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def should_insert_db(prev_event, current_event):
|
||||
"""If current event has new clip or snapshot."""
|
||||
return (not prev_event["has_clip"] and not prev_event["has_snapshot"]) and (
|
||||
current_event["has_clip"] or current_event["has_snapshot"]
|
||||
)
|
||||
|
||||
|
||||
def should_update_db(prev_event, current_event):
|
||||
"""If current_event has updated fields and (clip or snapshot)."""
|
||||
return (current_event["has_clip"] or current_event["has_snapshot"]) and (
|
||||
prev_event["top_score"] != current_event["top_score"]
|
||||
or prev_event["entered_zones"] != current_event["entered_zones"]
|
||||
or prev_event["thumbnail"] != current_event["thumbnail"]
|
||||
or prev_event["has_clip"] != current_event["has_clip"]
|
||||
or prev_event["has_snapshot"] != current_event["has_snapshot"]
|
||||
)
|
||||
|
||||
|
||||
class EventProcessor(threading.Thread):
|
||||
def __init__(
|
||||
self, config, camera_processes, event_queue, event_processed_queue, stop_event
|
||||
):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "event_processor"
|
||||
self.config = config
|
||||
self.camera_processes = camera_processes
|
||||
self.cached_clips = {}
|
||||
self.event_queue = event_queue
|
||||
self.event_processed_queue = event_processed_queue
|
||||
self.events_in_process = {}
|
||||
self.stop_event = stop_event
|
||||
|
||||
def run(self):
|
||||
# set an end_time on events without an end_time on startup
|
||||
Event.update(end_time=Event.start_time + 30).where(
|
||||
Event.end_time == None
|
||||
).execute()
|
||||
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
event_type, camera, event_data = self.event_queue.get(timeout=10)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
logger.debug(f"Event received: {event_type} {camera} {event_data['id']}")
|
||||
|
||||
event_config: EventsConfig = self.config.cameras[camera].record.events
|
||||
|
||||
if event_type == "start":
|
||||
self.events_in_process[event_data["id"]] = event_data
|
||||
|
||||
elif event_type == "update" and should_insert_db(
|
||||
self.events_in_process[event_data["id"]], event_data
|
||||
):
|
||||
self.events_in_process[event_data["id"]] = event_data
|
||||
# TODO: this will generate a lot of db activity possibly
|
||||
Event.insert(
|
||||
id=event_data["id"],
|
||||
label=event_data["label"],
|
||||
camera=camera,
|
||||
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||
end_time=None,
|
||||
top_score=event_data["top_score"],
|
||||
false_positive=event_data["false_positive"],
|
||||
zones=list(event_data["entered_zones"]),
|
||||
thumbnail=event_data["thumbnail"],
|
||||
region=event_data["region"],
|
||||
box=event_data["box"],
|
||||
area=event_data["area"],
|
||||
has_clip=event_data["has_clip"],
|
||||
has_snapshot=event_data["has_snapshot"],
|
||||
).execute()
|
||||
|
||||
elif event_type == "update" and should_update_db(
|
||||
self.events_in_process[event_data["id"]], event_data
|
||||
):
|
||||
self.events_in_process[event_data["id"]] = event_data
|
||||
# TODO: this will generate a lot of db activity possibly
|
||||
Event.update(
|
||||
label=event_data["label"],
|
||||
camera=camera,
|
||||
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||
end_time=None,
|
||||
top_score=event_data["top_score"],
|
||||
false_positive=event_data["false_positive"],
|
||||
zones=list(event_data["entered_zones"]),
|
||||
thumbnail=event_data["thumbnail"],
|
||||
region=event_data["region"],
|
||||
box=event_data["box"],
|
||||
area=event_data["area"],
|
||||
ratio=event_data["ratio"],
|
||||
has_clip=event_data["has_clip"],
|
||||
has_snapshot=event_data["has_snapshot"],
|
||||
).where(Event.id == event_data["id"]).execute()
|
||||
|
||||
elif event_type == "end":
|
||||
if event_data["has_clip"] or event_data["has_snapshot"]:
|
||||
# Full update for valid end of event
|
||||
Event.update(
|
||||
label=event_data["label"],
|
||||
camera=camera,
|
||||
start_time=event_data["start_time"] - event_config.pre_capture,
|
||||
end_time=event_data["end_time"] + event_config.post_capture,
|
||||
top_score=event_data["top_score"],
|
||||
false_positive=event_data["false_positive"],
|
||||
zones=list(event_data["entered_zones"]),
|
||||
thumbnail=event_data["thumbnail"],
|
||||
region=event_data["region"],
|
||||
box=event_data["box"],
|
||||
area=event_data["area"],
|
||||
ratio=event_data["ratio"],
|
||||
has_clip=event_data["has_clip"],
|
||||
has_snapshot=event_data["has_snapshot"],
|
||||
).where(Event.id == event_data["id"]).execute()
|
||||
else:
|
||||
# Event ended after clip & snapshot disabled,
|
||||
# only end time should be updated.
|
||||
Event.update(
|
||||
end_time=event_data["end_time"] + event_config.post_capture
|
||||
).where(Event.id == event_data["id"]).execute()
|
||||
|
||||
del self.events_in_process[event_data["id"]]
|
||||
self.event_processed_queue.put((event_data["id"], camera))
|
||||
|
||||
# set an end_time on events without an end_time before exiting
|
||||
Event.update(end_time=datetime.datetime.now().timestamp()).where(
|
||||
Event.end_time == None
|
||||
).execute()
|
||||
logger.info(f"Exiting event processor...")
|
||||
|
||||
|
||||
class EventCleanup(threading.Thread):
|
||||
def __init__(self, config: FrigateConfig, stop_event):
|
||||
threading.Thread.__init__(self)
|
||||
self.name = "event_cleanup"
|
||||
self.config = config
|
||||
self.stop_event = stop_event
|
||||
self.camera_keys = list(self.config.cameras.keys())
|
||||
|
||||
def expire(self, media_type):
|
||||
## Expire events from unlisted cameras based on the global config
|
||||
if media_type == "clips":
|
||||
retain_config = self.config.record.events.retain
|
||||
file_extension = "mp4"
|
||||
update_params = {"has_clip": False}
|
||||
else:
|
||||
retain_config = self.config.snapshots.retain
|
||||
file_extension = "jpg"
|
||||
update_params = {"has_snapshot": False}
|
||||
|
||||
distinct_labels = (
|
||||
Event.select(Event.label)
|
||||
.where(Event.camera.not_in(self.camera_keys))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = Event.select().where(
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the media from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
if file_extension == "jpg":
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
|
||||
# update the clips attribute for the db entry
|
||||
update_query = Event.update(update_params).where(
|
||||
Event.camera.not_in(self.camera_keys),
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
update_query.execute()
|
||||
|
||||
## Expire events from cameras based on the camera config
|
||||
for name, camera in self.config.cameras.items():
|
||||
if media_type == "clips":
|
||||
retain_config = camera.record.events.retain
|
||||
else:
|
||||
retain_config = camera.snapshots.retain
|
||||
# get distinct objects in database for this camera
|
||||
distinct_labels = (
|
||||
Event.select(Event.label).where(Event.camera == name).distinct()
|
||||
)
|
||||
|
||||
# loop over object types in db
|
||||
for l in distinct_labels:
|
||||
# get expiration time for this label
|
||||
expire_days = retain_config.objects.get(l.label, retain_config.default)
|
||||
expire_after = (
|
||||
datetime.datetime.now() - datetime.timedelta(days=expire_days)
|
||||
).timestamp()
|
||||
# grab all events after specific time
|
||||
expired_events = Event.select().where(
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
# delete the grabbed clips from disk
|
||||
for event in expired_events:
|
||||
media_name = f"{event.camera}-{event.id}"
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}.{file_extension}"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
if file_extension == "jpg":
|
||||
media_path = Path(
|
||||
f"{os.path.join(CLIPS_DIR, media_name)}-clean.png"
|
||||
)
|
||||
media_path.unlink(missing_ok=True)
|
||||
# update the clips attribute for the db entry
|
||||
update_query = Event.update(update_params).where(
|
||||
Event.camera == name,
|
||||
Event.start_time < expire_after,
|
||||
Event.label == l.label,
|
||||
Event.retain_indefinitely == False,
|
||||
)
|
||||
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}"
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.jpg")
|
||||
media_path.unlink(missing_ok=True)
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
|
||||
media_path.unlink(missing_ok=True)
|
||||
media_path = Path(f"{os.path.join(CLIPS_DIR, media_name)}.mp4")
|
||||
media_path.unlink(missing_ok=True)
|
||||
|
||||
(
|
||||
Event.delete()
|
||||
.where(Event.id << [event.id for event in duplicate_events])
|
||||
.execute()
|
||||
)
|
||||
|
||||
def run(self):
|
||||
# only expire events every 5 minutes
|
||||
while not self.stop_event.wait(300):
|
||||
self.expire("clips")
|
||||
self.expire("snapshots")
|
||||
self.purge_duplicates()
|
||||
|
||||
# drop events from db where has_clip and has_snapshot are false
|
||||
delete_query = Event.delete().where(
|
||||
Event.has_clip == False, Event.has_snapshot == False
|
||||
)
|
||||
delete_query.execute()
|
||||
|
||||
logger.info(f"Exiting event cleanup...")
|
||||
943
frigate/http.py
Normal file
@@ -0,0 +1,943 @@
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import subprocess as sp
|
||||
import time
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
|
||||
import numpy as np
|
||||
from flask import (
|
||||
Blueprint,
|
||||
Flask,
|
||||
Response,
|
||||
current_app,
|
||||
jsonify,
|
||||
make_response,
|
||||
request,
|
||||
)
|
||||
|
||||
from peewee import SqliteDatabase, operator, fn, DoesNotExist
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from frigate.const import CLIPS_DIR, PLUS_ENV_VAR
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.stats import stats_snapshot
|
||||
from frigate.version import VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint("frigate", __name__)
|
||||
|
||||
|
||||
def create_app(
|
||||
frigate_config,
|
||||
database: SqliteDatabase,
|
||||
stats_tracking,
|
||||
detected_frames_processor,
|
||||
plus_api,
|
||||
):
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.before_request
|
||||
def _db_connect():
|
||||
if database.is_closed():
|
||||
database.connect()
|
||||
|
||||
@app.teardown_request
|
||||
def _db_close(exc):
|
||||
if not database.is_closed():
|
||||
database.close()
|
||||
|
||||
app.frigate_config = frigate_config
|
||||
app.stats_tracking = stats_tracking
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
app.plus_api = plus_api
|
||||
|
||||
app.register_blueprint(bp)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
def is_healthy():
|
||||
return "Frigate is running. Alive and healthy!"
|
||||
|
||||
|
||||
@bp.route("/events/summary")
|
||||
def events_summary():
|
||||
has_clip = request.args.get("has_clip", type=int)
|
||||
has_snapshot = request.args.get("has_snapshot", type=int)
|
||||
|
||||
clauses = []
|
||||
|
||||
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 len(clauses) == 0:
|
||||
clauses.append((True))
|
||||
|
||||
groups = (
|
||||
Event.select(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime(
|
||||
"%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||
).alias("day"),
|
||||
Event.zones,
|
||||
fn.COUNT(Event.id).alias("count"),
|
||||
)
|
||||
.where(reduce(operator.and_, clauses))
|
||||
.group_by(
|
||||
Event.camera,
|
||||
Event.label,
|
||||
fn.strftime(
|
||||
"%Y-%m-%d", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||
),
|
||||
Event.zones,
|
||||
)
|
||||
)
|
||||
|
||||
return jsonify([e for e in groups.dicts()])
|
||||
|
||||
|
||||
@bp.route("/events/<id>", methods=("GET",))
|
||||
def event(id):
|
||||
try:
|
||||
return model_to_dict(Event.get(Event.id == id))
|
||||
except DoesNotExist:
|
||||
return "Event not found", 404
|
||||
|
||||
|
||||
@bp.route("/events/<id>/retain", methods=("POST",))
|
||||
def set_retain(id):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
)
|
||||
|
||||
event.retain_indefinitely = True
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " retained"}), 200
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/plus", methods=("POST",))
|
||||
def send_to_plus(id):
|
||||
if not current_app.plus_api.is_active():
|
||||
message = "PLUS_API_KEY environment variable is not set"
|
||||
logger.error(message)
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": message,
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
message = f"Event {id} not found"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 404)
|
||||
|
||||
if event.plus_id:
|
||||
message = "Already submitted to plus"
|
||||
logger.error(message)
|
||||
return make_response(jsonify({"success": False, "message": message}), 400)
|
||||
|
||||
# load clean.png
|
||||
try:
|
||||
filename = f"{event.camera}-{event.id}-clean.png"
|
||||
image = cv2.imread(os.path.join(CLIPS_DIR, filename))
|
||||
except Exception:
|
||||
logger.error(f"Unable to load clean png for event: {event.id}")
|
||||
return make_response(
|
||||
jsonify(
|
||||
{"success": False, "message": "Unable to load clean png for event"}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
try:
|
||||
plus_id = current_app.plus_api.upload_image(image, event.camera)
|
||||
except Exception as ex:
|
||||
logger.exception(ex)
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": str(ex)}),
|
||||
400,
|
||||
)
|
||||
|
||||
# store image id in the database
|
||||
event.plus_id = plus_id
|
||||
event.save()
|
||||
|
||||
return make_response(jsonify({"success": True, "plus_id": plus_id}), 200)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/retain", methods=("DELETE",))
|
||||
def delete_retain(id):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
)
|
||||
|
||||
event.retain_indefinitely = False
|
||||
event.save()
|
||||
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " un-retained"}), 200
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/sub_label", methods=("POST",))
|
||||
def set_sub_label(id):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
)
|
||||
|
||||
if request.json:
|
||||
new_sub_label = request.json.get("subLabel")
|
||||
else:
|
||||
new_sub_label = None
|
||||
|
||||
if new_sub_label and len(new_sub_label) > 20:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": new_sub_label
|
||||
+ " exceeds the 20 character limit for sub_label",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
event.sub_label = new_sub_label
|
||||
event.save()
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Event " + id + " sub label set to " + new_sub_label,
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/sub_labels")
|
||||
def get_sub_labels():
|
||||
try:
|
||||
events = Event.select(Event.sub_label).distinct()
|
||||
except Exception as e:
|
||||
return jsonify(
|
||||
{"success": False, "message": f"Failed to get sub_labels: {e}"}, "404"
|
||||
)
|
||||
|
||||
sub_labels = [e.sub_label for e in events]
|
||||
|
||||
if None in sub_labels:
|
||||
sub_labels.remove(None)
|
||||
|
||||
return jsonify(sub_labels)
|
||||
|
||||
|
||||
@bp.route("/events/<id>", methods=("DELETE",))
|
||||
def delete_event(id):
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
)
|
||||
|
||||
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)
|
||||
media = Path(f"{os.path.join(CLIPS_DIR, media_name)}-clean.png")
|
||||
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_instance()
|
||||
return make_response(
|
||||
jsonify({"success": True, "message": "Event " + id + " deleted"}), 200
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/thumbnail.jpg")
|
||||
def event_thumbnail(id, max_cache_age=2592000):
|
||||
format = request.args.get("format", "ios")
|
||||
thumbnail_bytes = None
|
||||
event_complete = False
|
||||
try:
|
||||
event = Event.get(Event.id == id)
|
||||
if not event.end_time is None:
|
||||
event_complete = True
|
||||
thumbnail_bytes = base64.b64decode(event.thumbnail)
|
||||
except DoesNotExist:
|
||||
# see if the object is currently being tracked
|
||||
try:
|
||||
camera_states = current_app.detected_frames_processor.camera_states.values()
|
||||
for camera_state in camera_states:
|
||||
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_thumbnail()
|
||||
except:
|
||||
return "Event not found", 404
|
||||
|
||||
if thumbnail_bytes is None:
|
||||
return "Event not found", 404
|
||||
|
||||
# android notifications prefer a 2:1 ratio
|
||||
if format == "android":
|
||||
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, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
|
||||
thumbnail_bytes = jpg.tobytes()
|
||||
|
||||
response = make_response(thumbnail_bytes)
|
||||
response.headers["Content-Type"] = "image/jpeg"
|
||||
if event_complete:
|
||||
response.headers["Cache-Control"] = f"private, max-age={max_cache_age}"
|
||||
else:
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/<camera_name>/<label>/best.jpg")
|
||||
@bp.route("/<camera_name>/<label>/thumbnail.jpg")
|
||||
def label_thumbnail(camera_name, label):
|
||||
if label == "any":
|
||||
event_query = (
|
||||
Event.select()
|
||||
.where(Event.camera == camera_name)
|
||||
.where(Event.has_snapshot == True)
|
||||
.order_by(Event.start_time.desc())
|
||||
)
|
||||
else:
|
||||
event_query = (
|
||||
Event.select()
|
||||
.where(Event.camera == camera_name)
|
||||
.where(Event.label == label)
|
||||
.where(Event.has_snapshot == True)
|
||||
.order_by(Event.start_time.desc())
|
||||
)
|
||||
|
||||
try:
|
||||
event = event_query.get()
|
||||
|
||||
return event_thumbnail(event.id, 60)
|
||||
except DoesNotExist:
|
||||
frame = np.zeros((175, 175, 3), np.uint8)
|
||||
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
|
||||
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers["Content-Type"] = "image/jpeg"
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/events/<id>/snapshot.jpg")
|
||||
def event_snapshot(id):
|
||||
download = request.args.get("download", type=bool)
|
||||
event_complete = False
|
||||
jpg_bytes = None
|
||||
try:
|
||||
event = Event.get(Event.id == id, Event.end_time != None)
|
||||
event_complete = True
|
||||
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:
|
||||
camera_states = current_app.detected_frames_processor.camera_states.values()
|
||||
for camera_state in camera_states:
|
||||
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),
|
||||
quality=request.args.get("quality", default=70, type=int),
|
||||
)
|
||||
except:
|
||||
return "Event not found", 404
|
||||
except:
|
||||
return "Event not found", 404
|
||||
|
||||
if jpg_bytes is None:
|
||||
return "Event not found", 404
|
||||
|
||||
response = make_response(jpg_bytes)
|
||||
response.headers["Content-Type"] = "image/jpeg"
|
||||
if event_complete:
|
||||
response.headers["Cache-Control"] = "private, max-age=31536000"
|
||||
else:
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
if download:
|
||||
response.headers[
|
||||
"Content-Disposition"
|
||||
] = f"attachment; filename=snapshot-{id}.jpg"
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/<camera_name>/<label>/snapshot.jpg")
|
||||
def label_snapshot(camera_name, label):
|
||||
if label == "any":
|
||||
event_query = (
|
||||
Event.select()
|
||||
.where(Event.camera == camera_name)
|
||||
.where(Event.has_snapshot == True)
|
||||
.order_by(Event.start_time.desc())
|
||||
)
|
||||
else:
|
||||
event_query = (
|
||||
Event.select()
|
||||
.where(Event.camera == camera_name)
|
||||
.where(Event.label == label)
|
||||
.where(Event.has_snapshot == True)
|
||||
.order_by(Event.start_time.desc())
|
||||
)
|
||||
|
||||
try:
|
||||
event = event_query.get()
|
||||
return event_snapshot(event.id)
|
||||
except DoesNotExist:
|
||||
frame = np.zeros((720, 1280, 3), np.uint8)
|
||||
ret, jpg = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70])
|
||||
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers["Content-Type"] = "image/jpeg"
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/events/<id>/clip.mp4")
|
||||
def event_clip(id):
|
||||
download = request.args.get("download", type=bool)
|
||||
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return "Event not found.", 404
|
||||
|
||||
if not event.has_clip:
|
||||
return "Clip not available", 404
|
||||
|
||||
file_name = f"{event.camera}-{id}.mp4"
|
||||
clip_path = os.path.join(CLIPS_DIR, file_name)
|
||||
|
||||
if not os.path.isfile(clip_path):
|
||||
end_ts = (
|
||||
datetime.now().timestamp() if event.end_time is None else event.end_time
|
||||
)
|
||||
return recording_clip(event.camera, event.start_time, end_ts)
|
||||
|
||||
response = make_response()
|
||||
response.headers["Content-Description"] = "File Transfer"
|
||||
response.headers["Cache-Control"] = "no-cache"
|
||||
response.headers["Content-Type"] = "video/mp4"
|
||||
if download:
|
||||
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
|
||||
response.headers["Content-Length"] = os.path.getsize(clip_path)
|
||||
response.headers[
|
||||
"X-Accel-Redirect"
|
||||
] = f"/clips/{file_name}" # nginx: http://wiki.nginx.org/NginxXSendfile
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/events")
|
||||
def events():
|
||||
limit = request.args.get("limit", 100)
|
||||
camera = request.args.get("camera", "all")
|
||||
label = request.args.get("label", "all")
|
||||
sub_label = request.args.get("sub_label", "all")
|
||||
zone = request.args.get("zone", "all")
|
||||
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 = []
|
||||
|
||||
selected_columns = [
|
||||
Event.id,
|
||||
Event.camera,
|
||||
Event.label,
|
||||
Event.zones,
|
||||
Event.start_time,
|
||||
Event.end_time,
|
||||
Event.has_clip,
|
||||
Event.has_snapshot,
|
||||
Event.plus_id,
|
||||
Event.retain_indefinitely,
|
||||
Event.sub_label,
|
||||
Event.top_score,
|
||||
]
|
||||
|
||||
if camera != "all":
|
||||
clauses.append((Event.camera == camera))
|
||||
|
||||
if label != "all":
|
||||
clauses.append((Event.label == label))
|
||||
|
||||
if sub_label != "all":
|
||||
clauses.append((Event.sub_label == sub_label))
|
||||
|
||||
if zone != "all":
|
||||
clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
|
||||
|
||||
if after:
|
||||
clauses.append((Event.start_time > after))
|
||||
|
||||
if before:
|
||||
clauses.append((Event.start_time < before))
|
||||
|
||||
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)
|
||||
else:
|
||||
selected_columns.append(Event.thumbnail)
|
||||
|
||||
if len(clauses) == 0:
|
||||
clauses.append((True))
|
||||
|
||||
events = (
|
||||
Event.select(*selected_columns)
|
||||
.where(reduce(operator.and_, clauses))
|
||||
.order_by(Event.start_time.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
return jsonify([model_to_dict(e, exclude=excluded_fields) for e in events])
|
||||
|
||||
|
||||
@bp.route("/config")
|
||||
def config():
|
||||
config = current_app.frigate_config.dict()
|
||||
|
||||
# add in the ffmpeg_cmds
|
||||
for camera_name, camera in current_app.frigate_config.cameras.items():
|
||||
camera_dict = config["cameras"][camera_name]
|
||||
camera_dict["ffmpeg_cmds"] = copy.deepcopy(camera.ffmpeg_cmds)
|
||||
for cmd in camera_dict["ffmpeg_cmds"]:
|
||||
cmd["cmd"] = " ".join(cmd["cmd"])
|
||||
|
||||
config["plus"] = {"enabled": PLUS_ENV_VAR in os.environ}
|
||||
|
||||
return jsonify(config)
|
||||
|
||||
|
||||
@bp.route("/config/schema")
|
||||
def config_schema():
|
||||
return current_app.response_class(
|
||||
current_app.frigate_config.schema_json(), mimetype="application/json"
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/version")
|
||||
def version():
|
||||
return VERSION
|
||||
|
||||
|
||||
@bp.route("/stats")
|
||||
def stats():
|
||||
stats = stats_snapshot(current_app.stats_tracking)
|
||||
return jsonify(stats)
|
||||
|
||||
|
||||
@bp.route("/<camera_name>")
|
||||
def mjpeg_feed(camera_name):
|
||||
fps = int(request.args.get("fps", "3"))
|
||||
height = int(request.args.get("h", "360"))
|
||||
draw_options = {
|
||||
"bounding_boxes": request.args.get("bbox", type=int),
|
||||
"timestamp": request.args.get("timestamp", type=int),
|
||||
"zones": request.args.get("zones", type=int),
|
||||
"mask": request.args.get("mask", type=int),
|
||||
"motion_boxes": request.args.get("motion", type=int),
|
||||
"regions": request.args.get("regions", type=int),
|
||||
}
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
# return a multipart response
|
||||
return Response(
|
||||
imagestream(
|
||||
current_app.detected_frames_processor,
|
||||
camera_name,
|
||||
fps,
|
||||
height,
|
||||
draw_options,
|
||||
),
|
||||
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||
)
|
||||
else:
|
||||
return "Camera named {} not found".format(camera_name), 404
|
||||
|
||||
|
||||
@bp.route("/<camera_name>/latest.jpg")
|
||||
def latest_frame(camera_name):
|
||||
draw_options = {
|
||||
"bounding_boxes": request.args.get("bbox", type=int),
|
||||
"timestamp": request.args.get("timestamp", type=int),
|
||||
"zones": request.args.get("zones", type=int),
|
||||
"mask": request.args.get("mask", type=int),
|
||||
"motion_boxes": request.args.get("motion", type=int),
|
||||
"regions": request.args.get("regions", type=int),
|
||||
}
|
||||
resize_quality = request.args.get("quality", default=70, type=int)
|
||||
|
||||
if camera_name in current_app.frigate_config.cameras:
|
||||
frame = current_app.detected_frames_processor.get_current_frame(
|
||||
camera_name, draw_options
|
||||
)
|
||||
if frame is None:
|
||||
frame = np.zeros((720, 1280, 3), np.uint8)
|
||||
|
||||
height = int(request.args.get("h", str(frame.shape[0])))
|
||||
width = int(height * frame.shape[1] / frame.shape[0])
|
||||
|
||||
frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)
|
||||
|
||||
ret, jpg = cv2.imencode(
|
||||
".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
|
||||
)
|
||||
response = make_response(jpg.tobytes())
|
||||
response.headers["Content-Type"] = "image/jpeg"
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
else:
|
||||
return "Camera named {} not found".format(camera_name), 404
|
||||
|
||||
|
||||
# return hourly summary for recordings of camera
|
||||
@bp.route("/<camera_name>/recordings/summary")
|
||||
def recordings_summary(camera_name):
|
||||
recording_groups = (
|
||||
Recordings.select(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H",
|
||||
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||
).alias("hour"),
|
||||
fn.SUM(Recordings.duration).alias("duration"),
|
||||
fn.SUM(Recordings.motion).alias("motion"),
|
||||
fn.SUM(Recordings.objects).alias("objects"),
|
||||
)
|
||||
.where(Recordings.camera == camera_name)
|
||||
.group_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H",
|
||||
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d H",
|
||||
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||
).desc()
|
||||
)
|
||||
)
|
||||
|
||||
event_groups = (
|
||||
Event.select(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||
).alias("hour"),
|
||||
fn.COUNT(Event.id).alias("count"),
|
||||
)
|
||||
.where(Event.camera == camera_name, Event.has_clip)
|
||||
.group_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||
),
|
||||
)
|
||||
.objects()
|
||||
)
|
||||
|
||||
event_map = {g.hour: g.count for g in event_groups}
|
||||
|
||||
days = {}
|
||||
|
||||
for recording_group in recording_groups.objects():
|
||||
parts = recording_group.hour.split()
|
||||
hour = parts[1]
|
||||
day = parts[0]
|
||||
events_count = event_map.get(recording_group.hour, 0)
|
||||
hour_data = {
|
||||
"hour": hour,
|
||||
"events": events_count,
|
||||
"motion": recording_group.motion,
|
||||
"objects": recording_group.objects,
|
||||
"duration": round(recording_group.duration),
|
||||
}
|
||||
if day not in days:
|
||||
days[day] = {"events": events_count, "hours": [hour_data], "day": day}
|
||||
else:
|
||||
days[day]["events"] += events_count
|
||||
days[day]["hours"].append(hour_data)
|
||||
|
||||
return jsonify(list(days.values()))
|
||||
|
||||
|
||||
# return hour of recordings data for camera
|
||||
@bp.route("/<camera_name>/recordings")
|
||||
def recordings(camera_name):
|
||||
after = request.args.get(
|
||||
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
||||
)
|
||||
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||
|
||||
recordings = (
|
||||
Recordings.select(
|
||||
Recordings.id,
|
||||
Recordings.start_time,
|
||||
Recordings.end_time,
|
||||
Recordings.motion,
|
||||
Recordings.objects,
|
||||
)
|
||||
.where(
|
||||
Recordings.camera == camera_name,
|
||||
Recordings.end_time >= after,
|
||||
Recordings.start_time <= before,
|
||||
)
|
||||
.order_by(Recordings.start_time)
|
||||
)
|
||||
|
||||
return jsonify([e for e in recordings.dicts()])
|
||||
|
||||
|
||||
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
|
||||
@bp.route("/<camera>/start/<float:start_ts>/end/<float:end_ts>/clip.mp4")
|
||||
def recording_clip(camera, start_ts, end_ts):
|
||||
download = request.args.get("download", type=bool)
|
||||
|
||||
recordings = (
|
||||
Recordings.select()
|
||||
.where(
|
||||
(Recordings.start_time.between(start_ts, end_ts))
|
||||
| (Recordings.end_time.between(start_ts, end_ts))
|
||||
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
|
||||
)
|
||||
.where(Recordings.camera == camera)
|
||||
.order_by(Recordings.start_time.asc())
|
||||
)
|
||||
|
||||
playlist_lines = []
|
||||
clip: Recordings
|
||||
for clip in recordings:
|
||||
playlist_lines.append(f"file '{clip.path}'")
|
||||
# if this is the starting clip, add an inpoint
|
||||
if clip.start_time < start_ts:
|
||||
playlist_lines.append(f"inpoint {int(start_ts - clip.start_time)}")
|
||||
# if this is the ending clip, add an outpoint
|
||||
if clip.end_time > end_ts:
|
||||
playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}")
|
||||
|
||||
file_name = f"clip_{camera}_{start_ts}-{end_ts}.mp4"
|
||||
path = f"/tmp/cache/{file_name}"
|
||||
|
||||
ffmpeg_cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-protocol_whitelist",
|
||||
"pipe,file",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
"/dev/stdin",
|
||||
"-c",
|
||||
"copy",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
path,
|
||||
]
|
||||
|
||||
p = sp.run(
|
||||
ffmpeg_cmd,
|
||||
input="\n".join(playlist_lines),
|
||||
encoding="ascii",
|
||||
capture_output=True,
|
||||
)
|
||||
if p.returncode != 0:
|
||||
logger.error(p.stderr)
|
||||
return f"Could not create clip from recordings for {camera}.", 500
|
||||
|
||||
response = make_response()
|
||||
response.headers["Content-Description"] = "File Transfer"
|
||||
response.headers["Cache-Control"] = "no-cache"
|
||||
response.headers["Content-Type"] = "video/mp4"
|
||||
if download:
|
||||
response.headers["Content-Disposition"] = "attachment; filename=%s" % file_name
|
||||
response.headers["Content-Length"] = os.path.getsize(path)
|
||||
response.headers[
|
||||
"X-Accel-Redirect"
|
||||
] = f"/cache/{file_name}" # nginx: http://wiki.nginx.org/NginxXSendfile
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@bp.route("/vod/<camera>/start/<int:start_ts>/end/<int:end_ts>")
|
||||
@bp.route("/vod/<camera>/start/<float:start_ts>/end/<float:end_ts>")
|
||||
def vod_ts(camera, start_ts, end_ts):
|
||||
recordings = (
|
||||
Recordings.select()
|
||||
.where(
|
||||
Recordings.start_time.between(start_ts, end_ts)
|
||||
| Recordings.end_time.between(start_ts, end_ts)
|
||||
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
|
||||
)
|
||||
.where(Recordings.camera == camera)
|
||||
.order_by(Recordings.start_time.asc())
|
||||
)
|
||||
|
||||
clips = []
|
||||
durations = []
|
||||
|
||||
recording: Recordings
|
||||
for recording in recordings:
|
||||
clip = {"type": "source", "path": recording.path}
|
||||
duration = int(recording.duration * 1000)
|
||||
# Determine if offset is needed for first clip
|
||||
if recording.start_time < start_ts:
|
||||
offset = int((start_ts - recording.start_time) * 1000)
|
||||
clip["clipFrom"] = offset
|
||||
duration -= offset
|
||||
# Determine if we need to end the last clip early
|
||||
if recording.end_time > end_ts:
|
||||
duration -= int((recording.end_time - end_ts) * 1000)
|
||||
|
||||
if duration > 0:
|
||||
clips.append(clip)
|
||||
durations.append(duration)
|
||||
else:
|
||||
logger.warning(f"Recording clip is missing or empty: {recording.path}")
|
||||
|
||||
if not clips:
|
||||
logger.error("No recordings found for the requested time range")
|
||||
return "No recordings found.", 404
|
||||
|
||||
hour_ago = datetime.now() - timedelta(hours=1)
|
||||
return jsonify(
|
||||
{
|
||||
"cache": hour_ago.timestamp() > start_ts,
|
||||
"discontinuity": False,
|
||||
"durations": durations,
|
||||
"sequences": [{"clips": clips}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/vod/<year_month>/<day>/<hour>/<camera>")
|
||||
def vod_hour(year_month, day, hour, camera):
|
||||
start_date = datetime.strptime(f"{year_month}-{day} {hour}", "%Y-%m-%d %H")
|
||||
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||
start_ts = start_date.timestamp()
|
||||
end_ts = end_date.timestamp()
|
||||
|
||||
return vod_ts(camera, start_ts, end_ts)
|
||||
|
||||
|
||||
@bp.route("/vod/event/<id>")
|
||||
def vod_event(id):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
logger.error(f"Event not found: {id}")
|
||||
return "Event not found.", 404
|
||||
|
||||
if not event.has_clip:
|
||||
logger.error(f"Event does not have recordings: {id}")
|
||||
return "Recordings not available", 404
|
||||
|
||||
clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{id}.mp4")
|
||||
|
||||
if not os.path.isfile(clip_path):
|
||||
end_ts = (
|
||||
datetime.now().timestamp() if event.end_time is None else event.end_time
|
||||
)
|
||||
vod_response = vod_ts(event.camera, event.start_time, end_ts)
|
||||
# If the recordings are not found, set has_clip to false
|
||||
if (
|
||||
type(vod_response) == tuple
|
||||
and len(vod_response) == 2
|
||||
and vod_response[1] == 404
|
||||
):
|
||||
Event.update(has_clip=False).where(Event.id == id).execute()
|
||||
return vod_response
|
||||
|
||||
duration = int((event.end_time - event.start_time) * 1000)
|
||||
return jsonify(
|
||||
{
|
||||
"cache": True,
|
||||
"discontinuity": False,
|
||||
"durations": [duration],
|
||||
"sequences": [{"clips": [{"type": "source", "path": clip_path}]}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def imagestream(detected_frames_processor, camera_name, fps, height, draw_options):
|
||||
while True:
|
||||
# max out at specified FPS
|
||||
time.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)
|
||||
|
||||
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, [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"
|
||||
)
|
||||
75
frigate/log.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# adapted from https://medium.com/@jonathonbao/python3-logging-with-multiprocessing-f51f460b8778
|
||||
import logging
|
||||
import threading
|
||||
import os
|
||||
import signal
|
||||
import queue
|
||||
from multiprocessing.queues import Queue
|
||||
from logging import handlers
|
||||
from setproctitle import setproctitle
|
||||
from typing import Deque
|
||||
from collections import deque
|
||||
|
||||
|
||||
def listener_configurer() -> None:
|
||||
root = logging.getLogger()
|
||||
console_handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
"[%(asctime)s] %(name)-30s %(levelname)-8s: %(message)s", "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
root.addHandler(console_handler)
|
||||
root.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def root_configurer(queue: Queue) -> None:
|
||||
h = handlers.QueueHandler(queue)
|
||||
root = logging.getLogger()
|
||||
root.addHandler(h)
|
||||
root.setLevel(logging.INFO)
|
||||
|
||||
|
||||
def log_process(log_queue: Queue) -> None:
|
||||
threading.current_thread().name = f"logger"
|
||||
setproctitle("frigate.logger")
|
||||
listener_configurer()
|
||||
while True:
|
||||
try:
|
||||
record = log_queue.get(timeout=5)
|
||||
except (queue.Empty, KeyboardInterrupt):
|
||||
continue
|
||||
logger = logging.getLogger(record.name)
|
||||
logger.handle(record)
|
||||
|
||||
|
||||
# based on https://codereview.stackexchange.com/a/17959
|
||||
class LogPipe(threading.Thread):
|
||||
def __init__(self, log_name: str):
|
||||
"""Setup the object with a logger and start the thread"""
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = False
|
||||
self.logger = logging.getLogger(log_name)
|
||||
self.level = logging.ERROR
|
||||
self.deque: Deque[str] = deque(maxlen=100)
|
||||
self.fdRead, self.fdWrite = os.pipe()
|
||||
self.pipeReader = os.fdopen(self.fdRead)
|
||||
self.start()
|
||||
|
||||
def fileno(self) -> int:
|
||||
"""Return the write file descriptor of the pipe"""
|
||||
return self.fdWrite
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the thread, logging everything."""
|
||||
for line in iter(self.pipeReader.readline, ""):
|
||||
self.deque.append(line.strip("\n"))
|
||||
|
||||
self.pipeReader.close()
|
||||
|
||||
def dump(self) -> None:
|
||||
while len(self.deque) > 0:
|
||||
self.logger.log(self.level, self.deque.popleft())
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the write end of the pipe."""
|
||||
os.close(self.fdWrite)
|
||||
43
frigate/models.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from numpy import unique
|
||||
from peewee import (
|
||||
Model,
|
||||
CharField,
|
||||
DateTimeField,
|
||||
FloatField,
|
||||
BooleanField,
|
||||
TextField,
|
||||
IntegerField,
|
||||
)
|
||||
from playhouse.sqlite_ext import JSONField
|
||||
|
||||
|
||||
class Event(Model): # type: ignore[misc]
|
||||
id = CharField(null=False, primary_key=True, max_length=30)
|
||||
label = CharField(index=True, max_length=20)
|
||||
sub_label = CharField(max_length=20, null=True)
|
||||
camera = CharField(index=True, max_length=20)
|
||||
start_time = DateTimeField()
|
||||
end_time = DateTimeField()
|
||||
top_score = FloatField()
|
||||
false_positive = BooleanField()
|
||||
zones = JSONField()
|
||||
thumbnail = TextField()
|
||||
has_clip = BooleanField(default=True)
|
||||
has_snapshot = BooleanField(default=True)
|
||||
region = JSONField()
|
||||
box = JSONField()
|
||||
area = IntegerField()
|
||||
retain_indefinitely = BooleanField(default=False)
|
||||
ratio = FloatField(default=1.0)
|
||||
plus_id = CharField(max_length=30)
|
||||
|
||||
|
||||
class Recordings(Model): # type: ignore[misc]
|
||||
id = CharField(null=False, primary_key=True, max_length=30)
|
||||
camera = CharField(index=True, max_length=20)
|
||||
path = CharField(unique=True)
|
||||
start_time = DateTimeField()
|
||||
end_time = DateTimeField()
|
||||
duration = FloatField()
|
||||
motion = IntegerField(null=True)
|
||||
objects = IntegerField(null=True)
|
||||
159
frigate/motion.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import cv2
|
||||
import imutils
|
||||
import numpy as np
|
||||
from frigate.config import MotionConfig
|
||||
|
||||
|
||||
class MotionDetector:
|
||||
def __init__(
|
||||
self,
|
||||
frame_shape,
|
||||
config: MotionConfig,
|
||||
improve_contrast_enabled,
|
||||
motion_threshold,
|
||||
motion_contour_area,
|
||||
):
|
||||
self.config = config
|
||||
self.frame_shape = frame_shape
|
||||
self.resize_factor = frame_shape[0] / config.frame_height
|
||||
self.motion_frame_size = (
|
||||
config.frame_height,
|
||||
config.frame_height * frame_shape[1] // frame_shape[0],
|
||||
)
|
||||
self.avg_frame = np.zeros(self.motion_frame_size, np.float)
|
||||
self.avg_delta = np.zeros(self.motion_frame_size, np.float)
|
||||
self.motion_frame_count = 0
|
||||
self.frame_counter = 0
|
||||
resized_mask = cv2.resize(
|
||||
config.mask,
|
||||
dsize=(self.motion_frame_size[1], self.motion_frame_size[0]),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
self.mask = np.where(resized_mask == [0])
|
||||
self.save_images = False
|
||||
self.improve_contrast = improve_contrast_enabled
|
||||
self.threshold = motion_threshold
|
||||
self.contour_area = motion_contour_area
|
||||
|
||||
def detect(self, frame):
|
||||
motion_boxes = []
|
||||
|
||||
gray = frame[0 : self.frame_shape[0], 0 : self.frame_shape[1]]
|
||||
|
||||
# resize frame
|
||||
resized_frame = cv2.resize(
|
||||
gray,
|
||||
dsize=(self.motion_frame_size[1], self.motion_frame_size[0]),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
# Improve contrast
|
||||
if self.improve_contrast.value:
|
||||
minval = np.percentile(resized_frame, 4)
|
||||
maxval = np.percentile(resized_frame, 96)
|
||||
# don't adjust if the image is a single color
|
||||
if minval < maxval:
|
||||
resized_frame = np.clip(resized_frame, minval, maxval)
|
||||
resized_frame = (
|
||||
((resized_frame - minval) / (maxval - minval)) * 255
|
||||
).astype(np.uint8)
|
||||
|
||||
# mask frame
|
||||
resized_frame[self.mask] = [255]
|
||||
|
||||
# it takes ~30 frames to establish a baseline
|
||||
# dont bother looking for motion
|
||||
if self.frame_counter < 30:
|
||||
self.frame_counter += 1
|
||||
else:
|
||||
if self.save_images:
|
||||
self.frame_counter += 1
|
||||
# compare to average
|
||||
frameDelta = cv2.absdiff(resized_frame, cv2.convertScaleAbs(self.avg_frame))
|
||||
|
||||
# compute the average delta over the past few frames
|
||||
# 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
|
||||
cv2.accumulateWeighted(frameDelta, self.avg_delta, self.config.delta_alpha)
|
||||
|
||||
# compute the threshold image for the current frame
|
||||
current_thresh = cv2.threshold(
|
||||
frameDelta, self.threshold.value, 255, cv2.THRESH_BINARY
|
||||
)[1]
|
||||
|
||||
# black out everything in the avg_delta where there isnt motion in the current frame
|
||||
avg_delta_image = cv2.convertScaleAbs(self.avg_delta)
|
||||
avg_delta_image = cv2.bitwise_and(avg_delta_image, current_thresh)
|
||||
|
||||
# then look for deltas above the threshold, but only in areas where there is a delta
|
||||
# in the current frame. this prevents deltas from previous frames from being included
|
||||
thresh = cv2.threshold(
|
||||
avg_delta_image, self.threshold.value, 255, cv2.THRESH_BINARY
|
||||
)[1]
|
||||
|
||||
# dilate the thresholded image to fill in holes, then find contours
|
||||
# on thresholded image
|
||||
thresh_dilated = cv2.dilate(thresh, None, iterations=2)
|
||||
cnts = cv2.findContours(
|
||||
thresh_dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
||||
)
|
||||
cnts = imutils.grab_contours(cnts)
|
||||
|
||||
# loop over the contours
|
||||
for c in cnts:
|
||||
# if the contour is big enough, count it as motion
|
||||
contour_area = cv2.contourArea(c)
|
||||
if contour_area > self.contour_area.value:
|
||||
x, y, w, h = cv2.boundingRect(c)
|
||||
motion_boxes.append(
|
||||
(
|
||||
int(x * self.resize_factor),
|
||||
int(y * self.resize_factor),
|
||||
int((x + w) * self.resize_factor),
|
||||
int((y + h) * self.resize_factor),
|
||||
)
|
||||
)
|
||||
|
||||
if self.save_images:
|
||||
thresh_dilated = cv2.cvtColor(thresh_dilated, cv2.COLOR_GRAY2BGR)
|
||||
# print("--------")
|
||||
# print(self.frame_counter)
|
||||
for c in cnts:
|
||||
contour_area = cv2.contourArea(c)
|
||||
if contour_area > self.contour_area.value:
|
||||
x, y, w, h = cv2.boundingRect(c)
|
||||
cv2.rectangle(
|
||||
thresh_dilated,
|
||||
(x, y),
|
||||
(x + w, y + h),
|
||||
(0, 0, 255),
|
||||
2,
|
||||
)
|
||||
# print("--------")
|
||||
image_row_1 = cv2.hconcat(
|
||||
[
|
||||
cv2.cvtColor(frameDelta, cv2.COLOR_GRAY2BGR),
|
||||
cv2.cvtColor(avg_delta_image, cv2.COLOR_GRAY2BGR),
|
||||
]
|
||||
)
|
||||
image_row_2 = cv2.hconcat(
|
||||
[cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR), thresh_dilated]
|
||||
)
|
||||
combined_image = cv2.vconcat([image_row_1, image_row_2])
|
||||
cv2.imwrite(f"motion/motion-{self.frame_counter}.jpg", combined_image)
|
||||
|
||||
if len(motion_boxes) > 0:
|
||||
self.motion_frame_count += 1
|
||||
if self.motion_frame_count >= 10:
|
||||
# only average in the current frame if the difference persists for a bit
|
||||
cv2.accumulateWeighted(
|
||||
resized_frame, self.avg_frame, self.config.frame_alpha
|
||||
)
|
||||
else:
|
||||
# when no motion, just keep averaging the frames together
|
||||
cv2.accumulateWeighted(
|
||||
resized_frame, self.avg_frame, self.config.frame_alpha
|
||||
)
|
||||
self.motion_frame_count = 0
|
||||
|
||||
return motion_boxes
|
||||
414
frigate/mqtt.py
@@ -1,33 +1,399 @@
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
class MqttObjectPublisher(threading.Thread):
|
||||
def __init__(self, client, topic_prefix, objects_parsed, detected_objects):
|
||||
threading.Thread.__init__(self)
|
||||
self.client = client
|
||||
import paho.mqtt.client as mqtt
|
||||
from ws4py.server.wsgirefserver import (
|
||||
WebSocketWSGIHandler,
|
||||
WebSocketWSGIRequestHandler,
|
||||
WSGIServer,
|
||||
)
|
||||
from ws4py.server.wsgiutils import WebSocketWSGIApplication
|
||||
from ws4py.websocket import WebSocket
|
||||
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.util import restart_frigate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_mqtt_client(config: FrigateConfig, camera_metrics):
|
||||
mqtt_config = config.mqtt
|
||||
|
||||
def on_recordings_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_recordings_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
record_settings = config.cameras[camera_name].record
|
||||
|
||||
if payload == "ON":
|
||||
if not record_settings.enabled:
|
||||
logger.info(f"Turning on recordings for {camera_name} via mqtt")
|
||||
record_settings.enabled = True
|
||||
elif payload == "OFF":
|
||||
if record_settings.enabled:
|
||||
logger.info(f"Turning off recordings for {camera_name} via mqtt")
|
||||
record_settings.enabled = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_snapshots_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_snapshots_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
snapshots_settings = config.cameras[camera_name].snapshots
|
||||
|
||||
if payload == "ON":
|
||||
if not snapshots_settings.enabled:
|
||||
logger.info(f"Turning on snapshots for {camera_name} via mqtt")
|
||||
snapshots_settings.enabled = True
|
||||
elif payload == "OFF":
|
||||
if snapshots_settings.enabled:
|
||||
logger.info(f"Turning off snapshots for {camera_name} via mqtt")
|
||||
snapshots_settings.enabled = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_detect_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_detect_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
detect_settings = config.cameras[camera_name].detect
|
||||
|
||||
if payload == "ON":
|
||||
if not camera_metrics[camera_name]["detection_enabled"].value:
|
||||
logger.info(f"Turning on detection for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["detection_enabled"].value = True
|
||||
detect_settings.enabled = True
|
||||
|
||||
if not camera_metrics[camera_name]["motion_enabled"].value:
|
||||
logger.info(
|
||||
f"Turning on motion for {camera_name} due to detection being enabled."
|
||||
)
|
||||
camera_metrics[camera_name]["motion_enabled"].value = True
|
||||
elif payload == "OFF":
|
||||
if camera_metrics[camera_name]["detection_enabled"].value:
|
||||
logger.info(f"Turning off detection for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["detection_enabled"].value = False
|
||||
detect_settings.enabled = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_motion_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_motion_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
if payload == "ON":
|
||||
if not camera_metrics[camera_name]["motion_enabled"].value:
|
||||
logger.info(f"Turning on motion for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["motion_enabled"].value = True
|
||||
elif payload == "OFF":
|
||||
if camera_metrics[camera_name]["detection_enabled"].value:
|
||||
logger.error(
|
||||
f"Turning off motion is not allowed when detection is enabled."
|
||||
)
|
||||
return
|
||||
|
||||
if camera_metrics[camera_name]["motion_enabled"].value:
|
||||
logger.info(f"Turning off motion for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["motion_enabled"].value = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_improve_contrast_command(client, userdata, message):
|
||||
payload = message.payload.decode()
|
||||
logger.debug(f"on_improve_contrast_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
motion_settings = config.cameras[camera_name].motion
|
||||
|
||||
if payload == "ON":
|
||||
if not camera_metrics[camera_name]["improve_contrast_enabled"].value:
|
||||
logger.info(f"Turning on improve contrast for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["improve_contrast_enabled"].value = True
|
||||
motion_settings.improve_contrast = True
|
||||
elif payload == "OFF":
|
||||
if camera_metrics[camera_name]["improve_contrast_enabled"].value:
|
||||
logger.info(f"Turning off improve contrast for {camera_name} via mqtt")
|
||||
camera_metrics[camera_name]["improve_contrast_enabled"].value = False
|
||||
motion_settings.improve_contrast = False
|
||||
else:
|
||||
logger.warning(f"Received unsupported value at {message.topic}: {payload}")
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_motion_threshold_command(client, userdata, message):
|
||||
try:
|
||||
payload = int(message.payload.decode())
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(f"on_motion_threshold_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
motion_settings = config.cameras[camera_name].motion
|
||||
|
||||
logger.info(f"Setting motion threshold for {camera_name} via mqtt: {payload}")
|
||||
camera_metrics[camera_name]["motion_threshold"].value = payload
|
||||
motion_settings.threshold = payload
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_motion_contour_area_command(client, userdata, message):
|
||||
try:
|
||||
payload = int(message.payload.decode())
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
f"Received unsupported value at {message.topic}: {message.payload.decode()}"
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug(f"on_motion_contour_area_toggle: {message.topic} {payload}")
|
||||
|
||||
camera_name = message.topic.split("/")[-3]
|
||||
|
||||
motion_settings = config.cameras[camera_name].motion
|
||||
|
||||
logger.info(
|
||||
f"Setting motion contour area for {camera_name} via mqtt: {payload}"
|
||||
)
|
||||
camera_metrics[camera_name]["motion_contour_area"].value = payload
|
||||
motion_settings.contour_area = payload
|
||||
|
||||
state_topic = f"{message.topic[:-4]}/state"
|
||||
client.publish(state_topic, payload, retain=True)
|
||||
|
||||
def on_restart_command(client, userdata, message):
|
||||
restart_frigate()
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
threading.current_thread().name = "mqtt"
|
||||
if rc != 0:
|
||||
if rc == 3:
|
||||
logger.error(
|
||||
"Unable to connect to MQTT server: MQTT Server unavailable"
|
||||
)
|
||||
elif rc == 4:
|
||||
logger.error(
|
||||
"Unable to connect to MQTT server: MQTT Bad username or password"
|
||||
)
|
||||
elif rc == 5:
|
||||
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
|
||||
else:
|
||||
logger.error(
|
||||
"Unable to connect to MQTT server: Connection refused. Error code: "
|
||||
+ str(rc)
|
||||
)
|
||||
|
||||
logger.debug("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)
|
||||
client.on_connect = on_connect
|
||||
client.will_set(
|
||||
mqtt_config.topic_prefix + "/available", payload="offline", qos=1, retain=True
|
||||
)
|
||||
|
||||
# register callbacks
|
||||
for name in config.cameras.keys():
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/recordings/set", on_recordings_command
|
||||
)
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/snapshots/set", on_snapshots_command
|
||||
)
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/detect/set", on_detect_command
|
||||
)
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion/set", on_motion_command
|
||||
)
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/set",
|
||||
on_improve_contrast_command,
|
||||
)
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/set",
|
||||
on_motion_threshold_command,
|
||||
)
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/set",
|
||||
on_motion_contour_area_command,
|
||||
)
|
||||
|
||||
client.message_callback_add(
|
||||
f"{mqtt_config.topic_prefix}/restart", on_restart_command
|
||||
)
|
||||
|
||||
if not mqtt_config.tls_ca_certs is None:
|
||||
if (
|
||||
not mqtt_config.tls_client_cert is None
|
||||
and not mqtt_config.tls_client_key is None
|
||||
):
|
||||
client.tls_set(
|
||||
mqtt_config.tls_ca_certs,
|
||||
mqtt_config.tls_client_cert,
|
||||
mqtt_config.tls_client_key,
|
||||
)
|
||||
else:
|
||||
client.tls_set(mqtt_config.tls_ca_certs)
|
||||
if not mqtt_config.tls_insecure is None:
|
||||
client.tls_insecure_set(mqtt_config.tls_insecure)
|
||||
if not mqtt_config.user is None:
|
||||
client.username_pw_set(mqtt_config.user, password=mqtt_config.password)
|
||||
try:
|
||||
client.connect(mqtt_config.host, mqtt_config.port, 60)
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to connect to MQTT server: {e}")
|
||||
raise
|
||||
|
||||
client.loop_start()
|
||||
|
||||
for name in config.cameras.keys():
|
||||
client.publish(
|
||||
f"{mqtt_config.topic_prefix}/{name}/recordings/state",
|
||||
"ON" if config.cameras[name].record.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.publish(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion/state",
|
||||
"ON",
|
||||
retain=True,
|
||||
)
|
||||
client.publish(
|
||||
f"{mqtt_config.topic_prefix}/{name}/improve_contrast/state",
|
||||
"ON" if config.cameras[name].motion.improve_contrast else "OFF",
|
||||
retain=True,
|
||||
)
|
||||
client.publish(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion_threshold/state",
|
||||
config.cameras[name].motion.threshold,
|
||||
retain=True,
|
||||
)
|
||||
client.publish(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion_contour_area/state",
|
||||
config.cameras[name].motion.contour_area,
|
||||
retain=True,
|
||||
)
|
||||
client.publish(
|
||||
f"{mqtt_config.topic_prefix}/{name}/motion",
|
||||
"OFF",
|
||||
retain=False,
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
class MqttSocketRelay:
|
||||
def __init__(self, mqtt_client, topic_prefix):
|
||||
self.mqtt_client = mqtt_client
|
||||
self.topic_prefix = topic_prefix
|
||||
self.objects_parsed = objects_parsed
|
||||
self._detected_objects = detected_objects
|
||||
|
||||
def run(self):
|
||||
last_sent_payload = ""
|
||||
while True:
|
||||
def start(self):
|
||||
class MqttWebSocket(WebSocket):
|
||||
topic_prefix = self.topic_prefix
|
||||
mqtt_client = self.mqtt_client
|
||||
|
||||
# initialize the payload
|
||||
payload = {}
|
||||
def received_message(self, message):
|
||||
try:
|
||||
json_message = json.loads(message.data.decode("utf-8"))
|
||||
json_message = {
|
||||
"topic": f"{self.topic_prefix}/{json_message['topic']}",
|
||||
"payload": json_message.get("payload"),
|
||||
"retain": json_message.get("retain", False),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("Unable to parse websocket message as valid json.")
|
||||
return
|
||||
|
||||
# wait until objects have been parsed
|
||||
with self.objects_parsed:
|
||||
self.objects_parsed.wait()
|
||||
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"],
|
||||
)
|
||||
|
||||
# add all the person scores in detected objects
|
||||
detected_objects = self._detected_objects.copy()
|
||||
person_score = sum([obj['score'] for obj in detected_objects if obj['name'] == 'person'])
|
||||
# if the person score is more than 100, set person to ON
|
||||
payload['person'] = 'ON' if int(person_score*100) > 100 else 'OFF'
|
||||
# start a websocket server on 5002
|
||||
WebSocketWSGIHandler.http_version = "1.1"
|
||||
self.websocket_server = make_server(
|
||||
"127.0.0.1",
|
||||
5002,
|
||||
server_class=WSGIServer,
|
||||
handler_class=WebSocketWSGIRequestHandler,
|
||||
app=WebSocketWSGIApplication(handler_cls=MqttWebSocket),
|
||||
)
|
||||
self.websocket_server.initialize_websockets_manager()
|
||||
self.websocket_thread = threading.Thread(
|
||||
target=self.websocket_server.serve_forever
|
||||
)
|
||||
|
||||
# send message for objects if different
|
||||
new_payload = json.dumps(payload, sort_keys=True)
|
||||
if new_payload != last_sent_payload:
|
||||
last_sent_payload = new_payload
|
||||
self.client.publish(self.topic_prefix+'/objects', new_payload, retain=False)
|
||||
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 Exception as e:
|
||||
# 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
|
||||
|
||||
self.websocket_server.manager.broadcast(ws_message)
|
||||
|
||||
self.mqtt_client.message_callback_add(f"{self.topic_prefix}/#", send)
|
||||
|
||||
self.websocket_thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.websocket_server.manager.close_all()
|
||||
self.websocket_server.manager.stop()
|
||||
self.websocket_server.manager.join()
|
||||
self.websocket_server.shutdown()
|
||||
self.websocket_thread.join()
|
||||
|
||||