mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 14:13:28 +00:00
Compare commits
637 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e163aa8153 | ||
|
|
268225647e | ||
|
|
93f94b0b0a | ||
|
|
db73baf4c1 | ||
|
|
404cf47d2e | ||
|
|
b751f77b0b | ||
|
|
884ce3e175 | ||
|
|
0cb0071874 | ||
|
|
d9637679bf | ||
|
|
7ea849d357 | ||
|
|
e4f01f10f4 | ||
|
|
bd61e9a5dd | ||
|
|
a2f8504290 | ||
|
|
928683a429 | ||
|
|
4d6bd61650 | ||
|
|
9321a5e0dd | ||
|
|
1622a0be63 | ||
|
|
55cb62cb72 | ||
|
|
11ea37d1c4 | ||
|
|
8e1dfa8174 | ||
|
|
0cf4802385 | ||
|
|
194facb19c | ||
|
|
6438ad1e3c | ||
|
|
586f78ebc1 | ||
|
|
48c5e1a5fe | ||
|
|
a6a986a8ac | ||
|
|
0b04d92131 | ||
|
|
05e9627f4a | ||
|
|
381c6de336 | ||
|
|
4206ee4686 | ||
|
|
e33a793867 | ||
|
|
699eebaf14 | ||
|
|
45a2d5764c | ||
|
|
5f7ecc0410 | ||
|
|
92257e41c1 | ||
|
|
c5a703896c | ||
|
|
51aa79956a | ||
|
|
fc1151ce8c | ||
|
|
eaa2c37d57 | ||
|
|
162bb7bfab | ||
|
|
e467414704 | ||
|
|
8ec6a25833 | ||
|
|
56bc0d6a26 | ||
|
|
9098426c3b | ||
|
|
0d9d425ef0 | ||
|
|
4c6ca3b2a5 | ||
|
|
762e058ec5 | ||
|
|
f02509152d | ||
|
|
9d92031e4c | ||
|
|
6d0027d3e8 | ||
|
|
274e043c81 | ||
|
|
817a6f5a59 | ||
|
|
cbdf8873e0 | ||
|
|
c9c9e106db | ||
|
|
f3d7ebd2a2 | ||
|
|
0ea6b13cb9 | ||
|
|
68cbe9a4f9 | ||
|
|
c7ab9085ff | ||
|
|
45993b3cb9 | ||
|
|
82ce08ab53 | ||
|
|
262fb32085 | ||
|
|
919d2dee85 | ||
|
|
1bb7df53c7 | ||
|
|
612cf7b520 | ||
|
|
55a80f1898 | ||
|
|
44ab56a888 | ||
|
|
eaae396861 | ||
|
|
cff170a508 | ||
|
|
c811109ee9 | ||
|
|
c8e4502d11 | ||
|
|
b75c0e0ca1 | ||
|
|
f64c9226a1 | ||
|
|
95dd67cd3a | ||
|
|
3ac0ca5c7a | ||
|
|
cd68af9796 | ||
|
|
9c1be5865b | ||
|
|
675f23235b | ||
|
|
0824136458 | ||
|
|
2b1b65d723 | ||
|
|
16995ed9e8 | ||
|
|
c5fb7d20a0 | ||
|
|
8c67f1e0ff | ||
|
|
50e2ae83b4 | ||
|
|
1eb4f6fd55 | ||
|
|
9152512679 | ||
|
|
7870ed7eeb | ||
|
|
40c0dea505 | ||
|
|
3542d327ea | ||
|
|
2ef87c21b6 | ||
|
|
4585f43318 | ||
|
|
30da19510a | ||
|
|
5ea1c9467f | ||
|
|
1d6eabc9e8 | ||
|
|
9ea4b5a29b | ||
|
|
539692867b | ||
|
|
e796404995 | ||
|
|
54b21260d1 | ||
|
|
be35fb2dc2 | ||
|
|
04065a3487 | ||
|
|
ac882c723a | ||
|
|
02cde6382c | ||
|
|
f942a13e90 | ||
|
|
6299caac20 | ||
|
|
6f0501634f | ||
|
|
575e544c40 | ||
|
|
ff448e9c7f | ||
|
|
6173d67bb0 | ||
|
|
4431158bfa | ||
|
|
822054e888 | ||
|
|
d7e21d1d44 | ||
|
|
6e451a1b06 | ||
|
|
57eccd4ad7 | ||
|
|
c2d45e4357 | ||
|
|
698a4a4a4a | ||
|
|
01493e311d | ||
|
|
a6d62365dc | ||
|
|
9b504a280f | ||
|
|
5df8689236 | ||
|
|
235d408f1f | ||
|
|
9b4547be85 | ||
|
|
0b8bc0d0d1 | ||
|
|
134d4be1b7 | ||
|
|
e77487ed15 | ||
|
|
6e60fe1c09 | ||
|
|
a7424b3546 | ||
|
|
70cf3488ef | ||
|
|
d74ac6fb8e | ||
|
|
47cad5d747 | ||
|
|
4ebb7215c0 | ||
|
|
1d55830f10 | ||
|
|
0bb5c79875 | ||
|
|
e3ca09a80b | ||
|
|
ef53829ccc | ||
|
|
5f0cf6b6c2 | ||
|
|
59e09825ff | ||
|
|
b4aa20b4cd | ||
|
|
e2eba2a227 | ||
|
|
9370a163fd | ||
|
|
c65f38f251 | ||
|
|
83bde83a39 | ||
|
|
786b4b5ed9 | ||
|
|
f9c1d7704a | ||
|
|
3162d2be34 | ||
|
|
3f0a788a6a | ||
|
|
72504286ea | ||
|
|
c664cc3b4d | ||
|
|
99853906b9 | ||
|
|
ea873a527b | ||
|
|
df0b13512a | ||
|
|
1651152eec | ||
|
|
7b3ab501b2 | ||
|
|
3a77a3398d | ||
|
|
f2ece1270a | ||
|
|
8b6d8aeae6 | ||
|
|
578bba67f8 | ||
|
|
15fb7d86e2 | ||
|
|
9d23caa66d | ||
|
|
c46ed2cef5 | ||
|
|
7dcfdaa98e | ||
|
|
ee23c93132 | ||
|
|
b1b0dd8997 | ||
|
|
6cc5a0e04c | ||
|
|
a75b263141 | ||
|
|
d91ec68e6c | ||
|
|
8ccc7a6c06 | ||
|
|
a6ece48cc3 | ||
|
|
7398f280cc | ||
|
|
eb1d0f647a | ||
|
|
b5a40b27a9 | ||
|
|
84870b444c | ||
|
|
339c934dda | ||
|
|
4df0eec70a | ||
|
|
6d268ade69 | ||
|
|
6b040954a0 | ||
|
|
73d2f5b408 | ||
|
|
71bb2ec80a | ||
|
|
92b120886c | ||
|
|
9001d996e2 | ||
|
|
d060a74689 | ||
|
|
8ba4c46576 | ||
|
|
a77f82462d | ||
|
|
3a1401afbb | ||
|
|
14ae374916 | ||
|
|
52d915cc68 | ||
|
|
cb501e66c6 | ||
|
|
053b43128f | ||
|
|
702456a40d | ||
|
|
17ebbb1656 | ||
|
|
0f79cd88ce | ||
|
|
76c960100c | ||
|
|
6d56e41651 | ||
|
|
640d66474c | ||
|
|
238c82a354 | ||
|
|
25a369403c | ||
|
|
b1e1f54af5 | ||
|
|
8df38dbebe | ||
|
|
229dcd3174 | ||
|
|
c3d6dcb6a2 | ||
|
|
0c951519e2 | ||
|
|
1f406ae740 | ||
|
|
8d0de7e557 | ||
|
|
e799ada9c9 | ||
|
|
ed498ae418 | ||
|
|
5b46036b2d | ||
|
|
8e888bc6a1 | ||
|
|
c5053008b7 | ||
|
|
7bd4f4053d | ||
|
|
f83cbfa5e7 | ||
|
|
8480713ec6 | ||
|
|
dcae7ce367 | ||
|
|
101d362260 | ||
|
|
73bdca1be6 | ||
|
|
c407fa0b9f | ||
|
|
d26c595fd6 | ||
|
|
38c00f5b9b | ||
|
|
ab4738973d | ||
|
|
ea065f506c | ||
|
|
4b7b66c96b | ||
|
|
0462ad228b | ||
|
|
45ac7f2f4e | ||
|
|
6618129e1d | ||
|
|
1a72eddcc8 | ||
|
|
1c9037dc35 | ||
|
|
2c5b79291f | ||
|
|
cd0ab104ea | ||
|
|
c6f4c1a669 | ||
|
|
9f28e38716 | ||
|
|
ba8f25fde3 | ||
|
|
9f27b2f382 | ||
|
|
96fa6af0fc | ||
|
|
eca5fbecdc | ||
|
|
8e0e2854e9 | ||
|
|
1eb9d938e7 | ||
|
|
095f80e1f9 | ||
|
|
da1f6118c8 | ||
|
|
5060748e9d | ||
|
|
25df4f8376 | ||
|
|
56fdff3545 | ||
|
|
2fbfe2cb65 | ||
|
|
32ede1f7fe | ||
|
|
1a2ec8ab4e | ||
|
|
53cab91b02 | ||
|
|
02a46a9202 | ||
|
|
69f4de66e9 | ||
|
|
aed6e0c446 | ||
|
|
432c178f29 | ||
|
|
bcf698daa3 | ||
|
|
347a957cd3 | ||
|
|
459b95a0e2 | ||
|
|
1c18129449 | ||
|
|
21274df881 | ||
|
|
ca243e79bb | ||
|
|
924394d365 | ||
|
|
23167da88b | ||
|
|
c1d48e1c6b | ||
|
|
7a22e17d84 | ||
|
|
75b2ff22ce | ||
|
|
edde093140 | ||
|
|
8622934c8b | ||
|
|
153cc3ed94 | ||
|
|
2ae6113750 | ||
|
|
4ec001c2a2 | ||
|
|
794ac6c8d2 | ||
|
|
8422ffe55a | ||
|
|
285c07e33e | ||
|
|
ef04398a79 | ||
|
|
6429ea718a | ||
|
|
5f9147e720 | ||
|
|
ea4922d8e5 | ||
|
|
70293ca827 | ||
|
|
878487180e | ||
|
|
f094903ed9 | ||
|
|
5117e217cf | ||
|
|
07c3f2832a | ||
|
|
977666bc3c | ||
|
|
2ec6760308 | ||
|
|
0dbe556835 | ||
|
|
c4f76df255 | ||
|
|
0bf0ec08ab | ||
|
|
1a2216a7de | ||
|
|
d868c3b3bb | ||
|
|
882709ea51 | ||
|
|
df249c554c | ||
|
|
fcbaeb1d1d | ||
|
|
c6dda05fa4 | ||
|
|
953b7812c5 | ||
|
|
7434a5c4ba | ||
|
|
b240a17bb0 | ||
|
|
aab0507805 | ||
|
|
3e091623a8 | ||
|
|
0871898385 | ||
|
|
eea7e4be32 | ||
|
|
8167ca85bb | ||
|
|
a09291114f | ||
|
|
39efe0d994 | ||
|
|
21f56216b0 | ||
|
|
8820bac571 | ||
|
|
47a683e385 | ||
|
|
17f367a373 | ||
|
|
fad0a520ca | ||
|
|
1f0d5dc3b9 | ||
|
|
a965f9b569 | ||
|
|
eb1a388f69 | ||
|
|
b2cf5ac3c7 | ||
|
|
ce10a49f0f | ||
|
|
5e31a0db96 | ||
|
|
8f1a673db5 | ||
|
|
7405476556 | ||
|
|
7dc86f59bf | ||
|
|
2d7cef600d | ||
|
|
5de0f8937b | ||
|
|
8e8d333ea2 | ||
|
|
d66a6317de | ||
|
|
49e3fc1438 | ||
|
|
fbe3daa072 | ||
|
|
670216135b | ||
|
|
ff903fa891 | ||
|
|
ebc6ede275 | ||
|
|
4de91d0673 | ||
|
|
3da1d00f6f | ||
|
|
4ff00a7753 | ||
|
|
f245fb257d | ||
|
|
b1a21a6037 | ||
|
|
0b9f3309a2 | ||
|
|
09b9b33bac | ||
|
|
21d020919a | ||
|
|
02d090cb94 | ||
|
|
817db34357 | ||
|
|
a3eda8cfba | ||
|
|
5a62fdc06b | ||
|
|
5ff8a65c4a | ||
|
|
719dfd2f24 | ||
|
|
7d28d1d9d4 | ||
|
|
aaa924b9b4 | ||
|
|
f69b93c9fa | ||
|
|
12be06adad | ||
|
|
f6fa28b584 | ||
|
|
fc1e5210a5 | ||
|
|
7601b8f0d0 | ||
|
|
b0557704b2 | ||
|
|
572883ed98 | ||
|
|
92927c8b93 | ||
|
|
11ae57b185 | ||
|
|
9f55f0b32a | ||
|
|
ef52e0a723 | ||
|
|
3df6af1fcd | ||
|
|
a283cfb429 | ||
|
|
3ae2dd769a | ||
|
|
3b916e7e20 | ||
|
|
d93f05a228 | ||
|
|
68183775db | ||
|
|
a8db883661 | ||
|
|
4a51caa281 | ||
|
|
c3148b8ed9 | ||
|
|
bc95a15f89 | ||
|
|
8954de3c93 | ||
|
|
cbfad097db | ||
|
|
c9e83c496c | ||
|
|
442e1883c5 | ||
|
|
f819e6d29c | ||
|
|
261c07f330 | ||
|
|
2328c9dd75 | ||
|
|
15639052c3 | ||
|
|
d91c7d89b2 | ||
|
|
fbdefbe06a | ||
|
|
832ee0180c | ||
|
|
a616e95c0e | ||
|
|
7ab9208203 | ||
|
|
9db6808e85 | ||
|
|
5d48760fd8 | ||
|
|
6ce2166e0a | ||
|
|
201dc30650 | ||
|
|
84bb7865fe | ||
|
|
ab1cd379a9 | ||
|
|
9208ca9566 | ||
|
|
e62897e14c | ||
|
|
65559e6685 | ||
|
|
611b7c50bf | ||
|
|
e983526455 | ||
|
|
10c167d4a3 | ||
|
|
0c641ccf6c | ||
|
|
5899ad866a | ||
|
|
17ecb56259 | ||
|
|
ffeade08ca | ||
|
|
49a567fb51 | ||
|
|
aac104f386 | ||
|
|
b4aff117ce | ||
|
|
13d4519a35 | ||
|
|
743102c965 | ||
|
|
315e5bb6e6 | ||
|
|
6ddef853ad | ||
|
|
5848cf1e5e | ||
|
|
f00f650b4f | ||
|
|
e9d73c6faa | ||
|
|
b6d601ebc4 | ||
|
|
1b58b0dd9b | ||
|
|
1b5ef3103e | ||
|
|
78236a54b8 | ||
|
|
ec2e4d64fd | ||
|
|
44644448f5 | ||
|
|
0a86d5c4ea | ||
|
|
20282e05ea | ||
|
|
9d6b405fa9 | ||
|
|
b82ce5ff45 | ||
|
|
f461198e1e | ||
|
|
7505e6907a | ||
|
|
c1046d5706 | ||
|
|
a61c06b607 | ||
|
|
d3df5742e6 | ||
|
|
68ac42ca46 | ||
|
|
bb7c6ef8b9 | ||
|
|
446e8ed61e | ||
|
|
80372b35f2 | ||
|
|
57eff2f296 | ||
|
|
d996088041 | ||
|
|
04be70019b | ||
|
|
51732d0dcd | ||
|
|
e40bc3ddee | ||
|
|
3f4409e1c3 | ||
|
|
63b7616ab3 | ||
|
|
29059691ce | ||
|
|
531a9d28dc | ||
|
|
3314b4d9ca | ||
|
|
37df9810c8 | ||
|
|
47c1cbba3c | ||
|
|
ded7e549bb | ||
|
|
abb2b85cec | ||
|
|
7d157d2882 | ||
|
|
c6c0a225dd | ||
|
|
276fc386ec | ||
|
|
0b21afd193 | ||
|
|
1032e58e3b | ||
|
|
4987b01167 | ||
|
|
28bb8c5b3c | ||
|
|
2160170c3a | ||
|
|
c0eac9053b | ||
|
|
d57501dd42 | ||
|
|
264cb0404f | ||
|
|
dc9f4b39a8 | ||
|
|
653eeceaf2 | ||
|
|
3d8711947a | ||
|
|
38038d5f30 | ||
|
|
e21d9c3a0c | ||
|
|
7b8c014b3b | ||
|
|
55a30864fd | ||
|
|
4f419ff75c | ||
|
|
638a4f28ad | ||
|
|
8970154b8f | ||
|
|
c96debaaed | ||
|
|
fe7b479235 | ||
|
|
aa1486e641 | ||
|
|
207f00733e | ||
|
|
bc4b2c956c | ||
|
|
e15578c9ca | ||
|
|
e2c5ee2400 | ||
|
|
dec62ddbc6 | ||
|
|
e7a65c4a28 | ||
|
|
127855c57e | ||
|
|
d9f35ef461 | ||
|
|
787c452105 | ||
|
|
3556b326f5 | ||
|
|
dd42878a5f | ||
|
|
07db378763 | ||
|
|
df142635b8 | ||
|
|
24bcc32532 | ||
|
|
5856ad60dd | ||
|
|
124da3c1b7 | ||
|
|
005efbfe82 | ||
|
|
cb955f403d | ||
|
|
fa38d8c560 | ||
|
|
0e4a94cd6e | ||
|
|
a0c3721140 | ||
|
|
7efffe4c51 | ||
|
|
2ff80780f8 | ||
|
|
9a41e3bc29 | ||
|
|
23de4011fb | ||
|
|
4fba5b9484 | ||
|
|
0dcfa367a6 | ||
|
|
8e63943f53 | ||
|
|
e2d78611c5 | ||
|
|
8a94c268ac | ||
|
|
10bdef414e | ||
|
|
317ee80477 | ||
|
|
851c5caf4d | ||
|
|
03cfeffca5 | ||
|
|
196316ad97 | ||
|
|
f7ccdf0795 | ||
|
|
64d36513a0 | ||
|
|
6ae8fd6d96 | ||
|
|
7448f19b78 | ||
|
|
35f0e325c1 | ||
|
|
579e188d06 | ||
|
|
09e97a2895 | ||
|
|
700856bc2e | ||
|
|
26eb69b08a | ||
|
|
332d9716f9 | ||
|
|
511f348cbe | ||
|
|
f26b6c3bca | ||
|
|
d38abcd563 | ||
|
|
b71f6e8b7e | ||
|
|
b492e8912e | ||
|
|
e663f5d3fc | ||
|
|
21ac653c66 | ||
|
|
e6f80be974 | ||
|
|
7b41e17981 | ||
|
|
530961179d | ||
|
|
7bb1d75905 | ||
|
|
337a74a170 | ||
|
|
e21facb123 | ||
|
|
15ac061311 | ||
|
|
f66a300082 | ||
|
|
24d754b5dc | ||
|
|
0221d00a17 | ||
|
|
3c49501c2c | ||
|
|
a81e5337f0 | ||
|
|
675339f495 | ||
|
|
48c6b90e21 | ||
|
|
ccf2b8c67d | ||
|
|
cc97d7d1c1 | ||
|
|
6af2f5eecd | ||
|
|
1df4c964d7 | ||
|
|
24b5b36a44 | ||
|
|
1da7d25235 | ||
|
|
d691b21646 | ||
|
|
d691bfcef1 | ||
|
|
04d5633690 | ||
|
|
dffd20f978 | ||
|
|
5fd9579423 | ||
|
|
bd7d49833f | ||
|
|
701ca5e1e2 | ||
|
|
14cb5a2117 | ||
|
|
adf4e797c1 | ||
|
|
a946be88d3 | ||
|
|
4be844f3f7 | ||
|
|
9eff813619 | ||
|
|
e1b5eaa63f | ||
|
|
b7f1efc307 | ||
|
|
12fedc53ee | ||
|
|
1c7626c156 | ||
|
|
a578623d3f | ||
|
|
b13d32f3ed | ||
|
|
d34c3aae74 | ||
|
|
c27176f0f8 | ||
|
|
be4f4bb6d6 | ||
|
|
26db89b811 | ||
|
|
6f0738ef07 | ||
|
|
1ae1eafddc | ||
|
|
0ecacfd974 | ||
|
|
146a648f39 | ||
|
|
7015f26eee | ||
|
|
a4e484698d | ||
|
|
1d659a5fb9 | ||
|
|
80e70b6bb8 | ||
|
|
ceec1174e3 | ||
|
|
b658202d8a | ||
|
|
425ee41ab0 | ||
|
|
3f7b801ffb | ||
|
|
ccb7ae0323 | ||
|
|
252c90fb46 | ||
|
|
2beceaf869 | ||
|
|
1dbb9a0f63 | ||
|
|
85c35f5fb7 | ||
|
|
8966cd7c70 | ||
|
|
60aeae5336 | ||
|
|
eef6c3ed3f | ||
|
|
979c430303 | ||
|
|
4892b72f37 | ||
|
|
3fd81b86d7 | ||
|
|
b3a2789a1d | ||
|
|
fde5cfa51e | ||
|
|
1ada7bb3fe | ||
|
|
2445ea909d | ||
|
|
7b16e8e969 | ||
|
|
1ffe8357e3 | ||
|
|
c96b4730de | ||
|
|
d89a8c6072 | ||
|
|
8e47bbb153 | ||
|
|
b04bb414d6 | ||
|
|
bd947fb934 | ||
|
|
a643960863 | ||
|
|
83f869988d | ||
|
|
1fc0934b4e | ||
|
|
29b7e4151a | ||
|
|
ad3c4f9529 | ||
|
|
e9026372c1 | ||
|
|
9b30064896 | ||
|
|
c73ffb30c1 | ||
|
|
bae12eecae | ||
|
|
1440b3811d | ||
|
|
59a3a97577 | ||
|
|
07f02fe371 | ||
|
|
cf4f4b7c73 | ||
|
|
aab0bdfc41 | ||
|
|
438f61b729 | ||
|
|
0b3717439f | ||
|
|
a9a145bd3e | ||
|
|
2f97b39cbb | ||
|
|
8bc1fe1588 | ||
|
|
90740f3c9d | ||
|
|
f92a12b99c | ||
|
|
405453da03 | ||
|
|
d01fe4310b | ||
|
|
6bcab63e0e | ||
|
|
c2bf7a62ab | ||
|
|
bc631d6c8b | ||
|
|
66877d1efa | ||
|
|
96a4a11503 | ||
|
|
bd0393305b | ||
|
|
9033eadafb | ||
|
|
c42b8ecda2 | ||
|
|
521bb62f10 | ||
|
|
8819f0a249 | ||
|
|
341cfa1e2f | ||
|
|
96335544ad | ||
|
|
ce0adb5706 | ||
|
|
22fb257214 | ||
|
|
674034fb7e | ||
|
|
44dcfe5e12 | ||
|
|
194e8532c3 | ||
|
|
797ef79080 | ||
|
|
56dbecbf3d | ||
|
|
67acb7725f | ||
|
|
a2caad7109 | ||
|
|
599d370f7d | ||
|
|
be284a0df7 | ||
|
|
030c3432a7 | ||
|
|
7d0d26ad31 | ||
|
|
6ba8a1dea4 | ||
|
|
cafb6944f5 | ||
|
|
f800401317 | ||
|
|
d672e2271d | ||
|
|
3d558e3119 | ||
|
|
c94945c6d5 | ||
|
|
82afc6d53f | ||
|
|
16ba10da21 | ||
|
|
6f5e0700a2 | ||
|
|
11b81371b4 |
4
.github/workflows/docker-common.yml
vendored
4
.github/workflows/docker-common.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
NODE_VERSION: '20'
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["jammy"]
|
||||
BASE: ["noble"]
|
||||
FLAVOR: ["full", "lite"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["jammy"]
|
||||
BASE: ["noble"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
22
.github/workflows/docker.yml
vendored
22
.github/workflows/docker.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: [
|
||||
["jammy-nvidia", ".s6"],
|
||||
["jammy-full", ".s6"],
|
||||
["jammy-lite", ""],
|
||||
["noble-nvidia", ".s6"],
|
||||
["noble-full", ".s6"],
|
||||
["noble-lite", ""],
|
||||
]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -95,15 +95,15 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'koush/scrypted:lite' || '' }}
|
||||
${{ matrix.BASE[0] == 'noble-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-lite' && 'koush/scrypted:lite' || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ matrix.BASE[0] == 'noble-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -11,9 +11,6 @@
|
||||
[submodule "external/werift"]
|
||||
path = external/werift
|
||||
url = ../../koush/werift-webrtc
|
||||
[submodule "sdk/developer.scrypted.app"]
|
||||
path = sdk/developer.scrypted.app
|
||||
url = ../../koush/developer.scrypted.app
|
||||
[submodule "plugins/sample-cameraprovider"]
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
@@ -23,6 +20,3 @@
|
||||
[submodule "plugins/wyze/docker-wyze-bridge"]
|
||||
path = plugins/wyze/docker-wyze-bridge
|
||||
url = ../../koush/docker-wyze-bridge.git
|
||||
[submodule "plugins/onvif/onvif"]
|
||||
path = plugins/onvif/onvif
|
||||
url = ../../koush/onvif.git
|
||||
|
||||
@@ -40,7 +40,7 @@ export function createAsyncQueue<T>() {
|
||||
return false;
|
||||
|
||||
if (waiting.length) {
|
||||
const deferred = waiting.shift();
|
||||
const deferred = waiting.shift()!;
|
||||
dequeued?.resolve();
|
||||
deferred.resolve(item);
|
||||
return true;
|
||||
@@ -66,7 +66,7 @@ export function createAsyncQueue<T>() {
|
||||
dequeued?.reject(new Error('abort'));
|
||||
};
|
||||
|
||||
dequeued.promise.catch(() => {}).finally(() => signal.removeEventListener('abort', h));
|
||||
dequeued?.promise.catch(() => {}).finally(() => signal.removeEventListener('abort', h));
|
||||
signal.addEventListener('abort', h);
|
||||
|
||||
return true;
|
||||
@@ -79,7 +79,7 @@ export function createAsyncQueue<T>() {
|
||||
ended = e || new EndError();
|
||||
endDeferred.resolve();
|
||||
while (waiting.length) {
|
||||
waiting.shift().reject(ended);
|
||||
waiting.shift()!.reject(ended);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
(async() => {
|
||||
try {
|
||||
for await (const i of generator) {
|
||||
q.submit(i);
|
||||
await q.enqueue(i);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
@@ -41,11 +41,15 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
checkHasEnabledMixin(device: ScryptedDevice) {
|
||||
return this.hasEnabledMixin[device.id] === this.autoIncludeToken;
|
||||
}
|
||||
|
||||
async maybeEnableMixin(device: ScryptedDevice) {
|
||||
if (!device || device.mixins?.includes(this.id))
|
||||
return;
|
||||
|
||||
if (this.hasEnabledMixin[device.id] === this.autoIncludeToken)
|
||||
if (this.checkHasEnabledMixin(device))
|
||||
return;
|
||||
|
||||
const match = await this.canMixin(device.type, device.interfaces);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import sdk, { LockState, MixinDeviceBase, PanTiltZoomMovement, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase } from "@scrypted/sdk/settings-mixin";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import fs from 'fs';
|
||||
@@ -63,7 +63,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
|
||||
const allParams = Object.assign({}, params, {
|
||||
sdk,
|
||||
fs: require('realfs'),
|
||||
ScryptedDeviceBase,
|
||||
MixinDeviceBase,
|
||||
StorageSettings,
|
||||
@@ -76,6 +75,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
localStorage: device.storage,
|
||||
device,
|
||||
exports: {} as any,
|
||||
PanTiltZoomMovement,
|
||||
SettingsMixinDeviceBase,
|
||||
ScryptedMimeTypes,
|
||||
ScryptedInterface,
|
||||
|
||||
@@ -19,7 +19,7 @@ function isPi(model: string) {
|
||||
export function isRaspberryPi() {
|
||||
let cpuInfo: string;
|
||||
try {
|
||||
cpuInfo = require('realfs').readFileSync('/proc/cpuinfo', { encoding: 'utf8' });
|
||||
cpuInfo = require('fs').readFileSync('/proc/cpuinfo', { encoding: 'utf8' });
|
||||
}
|
||||
catch (e) {
|
||||
// if this fails, this is probably not a pi
|
||||
|
||||
@@ -4,6 +4,41 @@ import os from 'os';
|
||||
|
||||
export type Zygote<T> = () => PluginFork<T>;
|
||||
|
||||
export function createService<T, V>(options: ForkOptions, create: (t: Promise<T>) => Promise<V>): {
|
||||
getResult: () => Promise<V>,
|
||||
terminate: () => void,
|
||||
} {
|
||||
let killed = false;
|
||||
let currentResult: Promise<V>;
|
||||
let currentFork: ReturnType<typeof sdk.fork<T>>;
|
||||
|
||||
return {
|
||||
getResult() {
|
||||
if (killed)
|
||||
throw new Error('service terminated');
|
||||
|
||||
if (currentResult)
|
||||
return currentResult;
|
||||
|
||||
currentFork = sdk.fork<T>(options);
|
||||
currentFork.worker.on('exit', () => currentResult = undefined);
|
||||
currentResult = create(currentFork.result);
|
||||
currentResult.catch(() => currentResult = undefined);
|
||||
return currentResult;
|
||||
},
|
||||
|
||||
terminate() {
|
||||
if (killed)
|
||||
return;
|
||||
|
||||
killed = true;
|
||||
currentFork.worker.terminate();
|
||||
currentFork = undefined;
|
||||
currentResult = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
|
||||
let zygote = sdk.fork<T>(options);
|
||||
function* next() {
|
||||
|
||||
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: 3797e311ed...d9f51b8b6d
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.116.0-jammy-full"
|
||||
version: "v0.120.0-jammy-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -18,4 +18,4 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
|
||||
CMD npm --prefix /server exec scrypted-serve
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -14,12 +14,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
@@ -40,16 +35,12 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN echo "Installing pillow-simd dependencies."
|
||||
RUN apt-get -y install \
|
||||
libjpeg-dev zlib1g-dev
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
# python-codecs pygobject dependencies
|
||||
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
@@ -60,8 +51,9 @@ RUN apt-get -y install \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
# ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
|
||||
# RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
@@ -71,10 +63,18 @@ RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu and npu for openvino
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
|
||||
# intel opencl for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
# NPU driver will SIGILL on openvino prior to 2024.5.0
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# amd opencl
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
@@ -86,8 +86,8 @@ RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
# RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
@@ -95,16 +95,20 @@ RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" |
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# set default shell to bash
|
||||
RUN chsh -s /bin/bash
|
||||
ENV SHELL="/bin/bash"
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.12"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
|
||||
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.10"
|
||||
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.12"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES=all
|
||||
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||
|
||||
# nvidia cudnn/libcublas etc.
|
||||
# for some reason this is not provided by the nvidia container toolkit
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-graphics.sh | bash
|
||||
|
||||
@@ -48,4 +48,4 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
|
||||
CMD npm --prefix /server exec scrypted-serve
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
services:
|
||||
scrypted:
|
||||
# LXC usage only
|
||||
# lxc privileged: true
|
||||
|
||||
environment:
|
||||
# Scrypted NVR Storage (Part 2 of 3)
|
||||
|
||||
@@ -29,30 +32,26 @@ services:
|
||||
# section below.
|
||||
# - SCRYPTED_NVR_VOLUME=/nvr
|
||||
|
||||
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
|
||||
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer ${WATCHTOWER_HTTP_API_TOKEN:-env_missing_fallback}
|
||||
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
|
||||
|
||||
# LXC usage only
|
||||
# lxc - SCRYPTED_INSTALL_ENVIRONMENT=lxc-docker
|
||||
|
||||
# Avahi can be used for network discovery by passing in the host daemon
|
||||
# or running the daemon inside the container. Choose one or the other.
|
||||
# Uncomment next line to run avahi-daemon inside the container.
|
||||
# See volumes and security_opt section below to use the host daemon.
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# NVIDIA (Part 1 of 4)
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
# NVIDIA (Part 2 of 4)
|
||||
# NVIDIA (Part 1 of 2)
|
||||
# runtime: nvidia
|
||||
|
||||
# NVIDIA (Part 3 of 4) - Use NVIDIA image, and remove subsequent default image.
|
||||
# NVIDIA (Part 2 of 2) - Use NVIDIA image, and remove subsequent default image.
|
||||
# image: ghcr.io/koush/scrypted:nvidia
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
volumes:
|
||||
# NVIDIA (Part 4 of 4)
|
||||
# - /etc/OpenCL/vendors/nvidia.icd:/etc/OpenCL/vendors/nvidia.icd
|
||||
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
@@ -76,7 +75,16 @@ services:
|
||||
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
|
||||
|
||||
# Default volume for the Scrypted database. Typically should not be changed.
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
# The volume will be placed relative to this docker-compose.yml.
|
||||
- ./volume:/server/volume
|
||||
|
||||
# LXC usage only
|
||||
# lxc - /var/run/docker.sock:/var/run/docker.sock
|
||||
# lxc - /root/.scrypted/docker-compose.yml:/root/.scrypted/docker-compose.yml
|
||||
# lxc - /root/.scrypted/docker-compose.sh:/root/.scrypted/docker-compose.sh
|
||||
# lxc - /root/.scrypted/.env:/root/.scrypted/.env
|
||||
# lxc - /mnt:/mnt
|
||||
|
||||
# Uncomment the following lines to use Avahi daemon from the host
|
||||
# Without this, AppArmor will block the container's attempt to talk to Avahi via dbus
|
||||
# security_opt:
|
||||
@@ -91,6 +99,9 @@ services:
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
# "/dev/dri:/dev/dri",
|
||||
|
||||
# AMD GPU
|
||||
# "/dev/kfd:/dev/kfd",
|
||||
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
|
||||
@@ -120,12 +131,10 @@ services:
|
||||
# watchtower manages updates for Scrypted.
|
||||
watchtower:
|
||||
environment:
|
||||
- WATCHTOWER_HTTP_API_TOKEN=SET_THIS_TO_SOME_RANDOM_TEXT
|
||||
- WATCHTOWER_HTTP_API_TOKEN=${WATCHTOWER_HTTP_API_TOKEN:-env_missing_fallback}
|
||||
- WATCHTOWER_HTTP_API_UPDATE=true
|
||||
- WATCHTOWER_SCOPE=scrypted
|
||||
# remove the following line to never allow docker to auto update.
|
||||
# this is not recommended.
|
||||
- WATCHTOWER_HTTP_API_PERIODIC_POLLS=true
|
||||
- WATCHTOWER_HTTP_API_PERIODIC_POLLS=${WATCHTOWER_HTTP_API_PERIODIC_POLLS:-true}
|
||||
image: containrrr/watchtower
|
||||
container_name: scrypted-watchtower
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# disable core dumps.
|
||||
# this doesn't disable core dumps on the scrypted service itself, only stuff run by init.
|
||||
ulimit -c 0
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, won't manage dbus nor avahi-daemon" >/dev/stderr
|
||||
exit 0
|
||||
|
||||
42
install/docker/install-amd-graphics.sh
Normal file
42
install/docker/install-amd-graphics.sh
Normal file
@@ -0,0 +1,42 @@
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
then
|
||||
echo "AMD graphics will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "AMD graphics package can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="jammy"
|
||||
else
|
||||
distro="noble"
|
||||
fi
|
||||
|
||||
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
|
||||
|
||||
FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
|
||||
if [ -z "$FILENAME" ]
|
||||
then
|
||||
echo "AMD graphics package can not be installed. Could not find the package name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
mkdir -p /tmp/amd
|
||||
cd /tmp/amd
|
||||
curl -O -L http://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
|
||||
apt -y install rsync
|
||||
dpkg -i $FILENAME
|
||||
amdgpu-install --usecase=opencl --no-dkms -y --accept-eula
|
||||
cd /tmp
|
||||
rm -rf /tmp/amd
|
||||
|
||||
@@ -29,23 +29,50 @@ apt-get -y update &&
|
||||
apt-get -y install intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
apt-get install -y ocl-icd-libopencl1
|
||||
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu-dbgsym_1.3.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu_1.3.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb
|
||||
# very stupid legacy + current install process conflict.
|
||||
# install 24.35.30872.22 for legacy support. Then install latest.
|
||||
# https://github.com/intel/compute-runtime/issues/770#issuecomment-2515166915
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-core_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-opencl_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/libigdgmm12_22.5.0_amd64.deb
|
||||
|
||||
dpkg -i *.deb
|
||||
rm -f *.deb
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.45.31740.9
|
||||
# note that at time of commit, IGC supports ubuntu 24.04 only possibly due to their builder being on 24.04.
|
||||
IGC_VERSION=2_2.1.12+18087_amd64
|
||||
COMPUTE_VERSION=24.45.31740.9
|
||||
ZERO_GPU_VERSION=1.6.31740.9_amd64
|
||||
LIBIGDGMM_VERSION=22.5.2_amd64
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.1.12/intel-igc-core-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.1.12/intel-igc-opencl-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu-dbgsym_$ZERO_GPU_VERSION.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu_$ZERO_GPU_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd-dbgsym_"$COMPUTE_VERSION"_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd_"$COMPUTE_VERSION"_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/libigdgmm12_$LIBIGDGMM_VERSION.deb
|
||||
|
||||
set +e
|
||||
dpkg -i *.deb
|
||||
set -e
|
||||
# the legacy + latest process says this may be necessary but it does not seem to be in a clean environment.
|
||||
apt-get install --fix-broken
|
||||
|
||||
|
||||
cd /tmp && rm -rf /tmp/gpu
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ fi
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ]
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
@@ -23,6 +23,13 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="22.04_amd64"
|
||||
else
|
||||
distro="24.04_amd64"
|
||||
fi
|
||||
|
||||
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
|
||||
|
||||
# no errors beyond this point
|
||||
@@ -30,27 +37,23 @@ set -e
|
||||
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# different npu downloads for ubuntu versions
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-driver-compiler-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
else
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
# level zero must also be installed
|
||||
LEVEL_ZERO_VERSION=1.19.2
|
||||
# https://github.com/oneapi-src/level-zero
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
|
||||
# npu driver
|
||||
# https://github.com/intel/linux-npu-driver
|
||||
NPU_VERSION=1.10.0
|
||||
NPU_VERSION_DATE=20241107-11729849322
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-fw-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-level-zero-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
|
||||
44
install/docker/install-nvidia-container-toolkit.sh
Normal file
44
install/docker/install-nvidia-container-toolkit.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
set -e
|
||||
|
||||
# Install CUDA for 22.04
|
||||
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
|
||||
# Install CUDA for 24.04
|
||||
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
|
||||
# Do not apt install nvidia-open, must use cuda-drivers.
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "NVIDIA container toolkit can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="ubuntu2204"
|
||||
else
|
||||
distro="ubuntu2404"
|
||||
fi
|
||||
|
||||
apt update -q \
|
||||
&& apt install -y wget \
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/$distro/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& dpkg -i /cuda-keyring.deb;
|
||||
|
||||
# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
|
||||
apt -y update
|
||||
apt -y install gpg
|
||||
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
|
||||
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
|
||||
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
|
||||
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
apt -y update
|
||||
# is there a way to get a versioned package automatically?
|
||||
apt -y install nvidia-utils-560
|
||||
apt -y install nvidia-container-toolkit
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
|
||||
systemctl restart docker
|
||||
@@ -1,14 +1,52 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "NVIDIA graphics package can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="ubuntu2204"
|
||||
else
|
||||
distro="ubuntu2404"
|
||||
fi
|
||||
|
||||
echo "Installing NVIDIA graphics packages."
|
||||
apt update -q \
|
||||
&& apt install -y wget \
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/$distro/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& dpkg -i /cuda-keyring.deb \
|
||||
&& apt update -q \
|
||||
&& apt install -y cuda-nvcc-11-8 libcublas-11-8 libcudnn8 cuda-libraries-11-8 \
|
||||
&& apt install -y cuda-nvcc-12-4 libcublas-12-4 libcudnn8 cuda-libraries-12-4;
|
||||
exit $?
|
||||
&& apt install -y cuda-nvcc-12-6 libcublas-12-6 libcudnn9-cuda-12 cuda-libraries-12-6;
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Error: NVIDIA graphics packages failed to install."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Update: the libnvidia-opencl.so.1 file is not present in the container image, it is
|
||||
# mounted via the nvidia container runtime. This is why the following check is commented out.
|
||||
# this file is present but for some reason the icd file is not created by nvidia runtime.
|
||||
# if [ ! -f "/usr/lib/x86_64-linux-gnu/libnvidia-opencl.so.1" ]
|
||||
# then
|
||||
# echo "Error: NVIDIA OpenCL library not found."
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# the container runtime doesn't mount this file for some reason. seems to be a bug.
|
||||
# https://github.com/NVIDIA/nvidia-container-toolkit/issues/682
|
||||
# but the contents are simply the .so file, which is a symlink the nvidia runtime
|
||||
# will mount in.
|
||||
mkdir -p /etc/OpenCL/vendors/
|
||||
echo "libnvidia-opencl.so.1" > /etc/OpenCL/vendors/nvidia.icd
|
||||
else
|
||||
echo "NVIDIA graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ "$SCRYPTED_LXC" ]
|
||||
then
|
||||
export SERVICE_USER="root"
|
||||
export SCRYPTED_NONINTERACTIVE="true"
|
||||
fi
|
||||
|
||||
if [ -z "$SERVICE_USER" ]
|
||||
then
|
||||
echo "Scrypted SERVICE_USER environment variable was not specified. Service will not be installed."
|
||||
@@ -7,6 +13,12 @@ then
|
||||
fi
|
||||
|
||||
function readyn() {
|
||||
if [ ! -z "$SCRYPTED_NONINTERACTIVE" ]
|
||||
then
|
||||
yn="y"
|
||||
return
|
||||
fi
|
||||
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
case $yn in
|
||||
@@ -33,6 +45,11 @@ systemctl disable scrypted.service 2> /dev/null
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
mkdir -p $SCRYPTED_HOME
|
||||
# remove various things from a previous local install.
|
||||
rm -rf $SCRYPTED_HOME/node_modules
|
||||
rm -rf $SCRYPTED_HOME/install.json
|
||||
rm -rf $SCRYPTED_HOME/package.json
|
||||
rm -rf $SCRYPTED_HOME/package-lock.json
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
@@ -46,13 +63,34 @@ then
|
||||
usermod -aG docker $SERVICE_USER
|
||||
fi
|
||||
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32)
|
||||
echo "WATCHTOWER_HTTP_API_TOKEN=$WATCHTOWER_HTTP_API_TOKEN" > $SCRYPTED_HOME/.env
|
||||
# remove the following line from .env to disable autoupdates.
|
||||
# this is not recommended.
|
||||
echo "WATCHTOWER_HTTP_API_PERIODIC_POLLS=true" >> $SCRYPTED_HOME/.env
|
||||
|
||||
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml > $DOCKER_COMPOSE_YML
|
||||
echo "Created $DOCKER_COMPOSE_YML"
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
|
||||
if [ -d /dev/dri ]
|
||||
|
||||
if [ -z "$SCRYPTED_LXC" ]
|
||||
then
|
||||
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
if [ -e /dev/dri ]
|
||||
then
|
||||
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
if [ -e /dev/kfd ]
|
||||
then
|
||||
sed -i 's/'#' "\/dev\/kfd/"\/dev\/kfd/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
else
|
||||
# uncomment lxc specific stuff
|
||||
sed -i 's/'#' lxc //g' $DOCKER_COMPOSE_YML
|
||||
# never restart, systemd will handle it
|
||||
sed -i 's/restart: unless-stopped/restart: no/g' $DOCKER_COMPOSE_YML
|
||||
|
||||
sudo systemctl stop apparmor || true
|
||||
sudo apt -y purge apparmor || true
|
||||
fi
|
||||
|
||||
readyn "Install avahi-daemon? This is the recommended for reliable HomeKit discovery and pairing."
|
||||
@@ -66,7 +104,7 @@ then
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME || true
|
||||
|
||||
set +e
|
||||
|
||||
@@ -79,8 +117,41 @@ set -e
|
||||
|
||||
echo "docker compose pull"
|
||||
sudo -u $SERVICE_USER docker compose pull
|
||||
echo "docker compose up -d"
|
||||
sudo -u $SERVICE_USER docker compose up -d
|
||||
|
||||
if [ -z "$SCRYPTED_LXC" ]
|
||||
then
|
||||
echo "docker compose up -d"
|
||||
sudo -u $SERVICE_USER docker compose up -d
|
||||
else
|
||||
export DOCKER_COMPOSE_SH=$SCRYPTED_HOME/docker-compose.sh
|
||||
|
||||
curl https://raw.githubusercontent.com/koush/scrypted/main/install/proxmox/docker-compose.sh > $DOCKER_COMPOSE_SH
|
||||
|
||||
chmod +x $DOCKER_COMPOSE_SH
|
||||
|
||||
cat > /etc/systemd/system/scrypted.service <<EOT
|
||||
[Unit]
|
||||
Description=Scrypted service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
Type=simple
|
||||
ExecStart=$DOCKER_COMPOSE_SH
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable scrypted.service
|
||||
systemctl restart scrypted.service
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
@@ -91,5 +162,5 @@ echo "Note that it is https and that you'll be asked to approve/ignore the websi
|
||||
echo
|
||||
echo
|
||||
echo "Optional:"
|
||||
echo "Scrypted NVR Recording storage directory can be configured with an additional script:"
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/installation.html#docker-volume"
|
||||
echo "Scrypted NVR Recording storage directory can be configured with an additional script located at:"
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#docker-volume"
|
||||
|
||||
@@ -128,7 +128,7 @@ then
|
||||
set -e
|
||||
removescryptedfstab
|
||||
mkdir -p /mnt/scrypted-nvr
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail,noatime 0 0" >> /etc/fstab
|
||||
echo "UUID=$UUID /mnt/scrypted-nvr ext4 defaults,nofail,noatime,x-systemd.automount 0 0" >> /etc/fstab
|
||||
mount -a
|
||||
systemctl daemon-reload
|
||||
set +e
|
||||
|
||||
@@ -3,10 +3,18 @@
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu and npu for openvino
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
|
||||
# intel opencl for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
# NPU driver will SIGILL on openvino prior to 2024.5.0
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# amd opencl
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
@@ -18,8 +26,8 @@ RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
# RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
@@ -27,16 +35,20 @@ RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" |
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# set default shell to bash
|
||||
RUN chsh -s /bin/bash
|
||||
ENV SHELL="/bin/bash"
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.12"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
|
||||
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
@@ -37,16 +32,12 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN echo "Installing pillow-simd dependencies."
|
||||
RUN apt-get -y install \
|
||||
libjpeg-dev zlib1g-dev
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
# python-codecs pygobject dependencies
|
||||
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
@@ -57,8 +48,9 @@ RUN apt-get -y install \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
# ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
|
||||
# RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
|
||||
@@ -40,8 +40,6 @@ echo "Installing Scrypted dependencies..."
|
||||
RUN_IGNORE xcode-select --install
|
||||
RUN brew update
|
||||
RUN_IGNORE brew install node@20
|
||||
# snapshot plugin and others
|
||||
RUN brew install libvips
|
||||
# dlib
|
||||
RUN brew install cmake
|
||||
|
||||
@@ -71,11 +69,14 @@ then
|
||||
fi
|
||||
|
||||
RUN python$PYTHON_VERSION -m pip install --upgrade pip
|
||||
# besides debugpy, none of these dependencies are needed anymore?
|
||||
# portable python includes typing and does not need typing_extensions.
|
||||
# opencv-python-headless has wheels for macos.
|
||||
if [ "$PYTHON_VERSION" != "3.10" ]
|
||||
then
|
||||
RUN python$PYTHON_VERSION -m pip install typing
|
||||
fi
|
||||
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python psutil
|
||||
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python
|
||||
|
||||
echo "Installing Scrypted Launch Agent..."
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#Requires -RunAsAdministrator
|
||||
|
||||
# Set-PSDebug -Trace 1
|
||||
|
||||
# stop existing service if any
|
||||
@@ -8,7 +10,7 @@ sc.exe stop scrypted.exe
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
|
||||
# Install node.js
|
||||
choco upgrade -y nodejs-lts --version=20.11.1
|
||||
choco upgrade -y nodejs-lts --version=20.18.0
|
||||
|
||||
# Install VC Redist, which is necessary for portable python
|
||||
choco install -y vcredist140
|
||||
@@ -22,11 +24,19 @@ $SCRYPTED_WINDOWS_PYTHON_VERSION="-3.9"
|
||||
# Refresh environment variables for py and npx to work
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
# Workaround Windows Node no longer creating %APPDATA%\npm which causes npx to fail
|
||||
# Fixed in newer versions of NPM but not the one bundled with Node 20
|
||||
# https://github.com/nodejs/node/issues/53538
|
||||
npm i -g npm
|
||||
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install --upgrade pip
|
||||
# besides debugpy, none of these dependencies are needed anymore?
|
||||
# portable python includes typing and does not need typing_extensions.
|
||||
# opencv-python-headless has wheels for windows.
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
|
||||
|
||||
$SCRYPTED_INSTALL_VERSION=[System.Environment]::GetEnvironmentVariable("SCRYPTED_INSTALL_VERSION","User")
|
||||
|
||||
if ($SCRYPTED_INSTALL_VERSION -eq $null) {
|
||||
npx -y scrypted@latest install-server
|
||||
} else {
|
||||
@@ -41,6 +51,8 @@ npm install --prefix $SCRYPTED_HOME @koush/node-windows --save
|
||||
$NPX_PATH = (Get-Command npx).Path
|
||||
# The path needs double quotes to handle spaces in the directory path
|
||||
$NPX_PATH_ESCAPED = '"' + $NPX_PATH.replace('\', '\\') + '"'
|
||||
# On newer versions of NPM, the NPX might be a .ps1 file which doesn't work with child_process.spawn, change to .cmd
|
||||
$NPX_PATH_ESCAPED = $NPX_PATH_ESCAPED.replace('.ps1', '.cmd')
|
||||
|
||||
$SERVICE_JS = @"
|
||||
const fs = require('fs');
|
||||
@@ -54,6 +66,8 @@ child_process.spawn('$NPX_PATH_ESCAPED', ['-y', 'scrypted', 'serve'], {
|
||||
stdio: 'inherit',
|
||||
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
shell: true,
|
||||
}).on('error', (err) => {
|
||||
console.error('Error spawning child process', err);
|
||||
});
|
||||
"@
|
||||
|
||||
@@ -99,6 +113,9 @@ svc.on("install", () => {
|
||||
svc.on("start", () => {
|
||||
console.log("Service started");
|
||||
});
|
||||
svc.on("error", (err) => {
|
||||
console.log("Service error", err);
|
||||
});
|
||||
svc.install();
|
||||
"@
|
||||
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
case $yn in
|
||||
[Yy]* ) break;;
|
||||
[Nn]* ) break;;
|
||||
* ) echo "Please answer yes or no. (y/n)";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.116.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=10444
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
if [ ! -f "$SCRYPTED_TAR_ZST" ]
|
||||
then
|
||||
curl -O -L https://github.com/koush/scrypted/releases/download/$SCRYPTED_VERSION/scrypted.tar.zst
|
||||
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
|
||||
fi
|
||||
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "Existing container $VMID found. Run this script with --force to overwrite the existing container."
|
||||
echo "This will wipe all existing data. Clone the existing container to retain the data, then reassign the owner of the scrypted volume after installation is complete."
|
||||
echo ""
|
||||
echo "bash $0 --force"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "The Scrypted container installation failed (pct restore error)."
|
||||
echo ""
|
||||
echo "This may be because the server's 'local' storage device is not being a valid"
|
||||
echo "location for containers."
|
||||
echo "Try running this script again with a different storage device like"
|
||||
echo "'local-lvm' or 'local-zfs'."
|
||||
echo ""
|
||||
echo "#############################################################################"
|
||||
echo "Paste the following command into this shell to install to local-lvm instead:"
|
||||
echo ""
|
||||
echo "bash $0 --storage local-lvm"
|
||||
echo "#############################################################################"
|
||||
echo ""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct set network failed"
|
||||
echo ""
|
||||
echo "Ignoring... Please verify your container's network settings."
|
||||
fi
|
||||
|
||||
CONF=/etc/pve/lxc/$VMID.conf
|
||||
if [ -f "$CONF" ]
|
||||
then
|
||||
echo "onboot: 1" >> $CONF
|
||||
else
|
||||
echo "$CONF not found? Start on boot must be enabled manually."
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
# this copies the
|
||||
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
|
||||
|
||||
rm *.tar
|
||||
vzdump 10444 --dumpdir /tmp
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Moving data volume to backup..."
|
||||
pct restore $VMID *.tar $@
|
||||
|
||||
pct destroy 10444
|
||||
fi
|
||||
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"accel0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
fi
|
||||
|
||||
echo "Scrypted setup is complete and the container resources can be started."
|
||||
echo "Scrypted NVR users should provide at least 4 cores and 16GB RAM prior to starting."
|
||||
18
install/proxmox/docker-compose.sh
Executable file
18
install/proxmox/docker-compose.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
cd /root/.scrypted
|
||||
|
||||
# always immediately upgrade everything in case there's a broken update.
|
||||
# this will also be preferable for troubleshooting via lxc reboot.
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
yes | dpkg --configure -a
|
||||
apt -y --fix-broken install && apt -y update && apt -y dist-upgrade
|
||||
|
||||
# force a pull to ensure we have the latest images.
|
||||
# not using --pull always cause that fails everything on network down
|
||||
docker compose pull
|
||||
|
||||
# do not daemonize, when it exits, systemd will restart it.
|
||||
# force a recreate as .env may have changed.
|
||||
# furthermore force recreate gets the container back into a known state
|
||||
# which is preferable in case the user has made manual changes and then restarts.
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit
|
||||
302
install/proxmox/install-scrypted-proxmox.sh
Normal file
302
install/proxmox/install-scrypted-proxmox.sh
Normal file
@@ -0,0 +1,302 @@
|
||||
PCT=$(which pct)
|
||||
if [ -z "$PCT" ]
|
||||
then
|
||||
echo "pct command not found. This script must be run on the Proxmox host, not a container."
|
||||
echo "Installation Documentation: https://docs.scrypted.app/installation.html#proxmox-ve"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
case $yn in
|
||||
[Yy]* ) break;;
|
||||
[Nn]* ) break;;
|
||||
* ) echo "Please answer yes or no. (y/n)";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.120.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
SCRYPTED_BACKUP_VMID=10445
|
||||
function prepareScryptedRestore() {
|
||||
pct config $VMID 2>&1 > /dev/null
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "VMID $VMID not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# append existing mac address.
|
||||
HWADDR=",hwaddr=$(pct config $VMID | grep -oE 'hwaddr=[A-Z0-9:]+' | cut -d '=' -f 2)"
|
||||
RESTORE_HOSTNAME=$(pct config $VMID | grep -oE 'hostname: [^[:space:]]+' | cut -d ':' -f 2- | tr -d ' ')
|
||||
|
||||
pct destroy $SCRYPTED_BACKUP_VMID 2>&1 > /dev/null
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=$SCRYPTED_BACKUP_VMID
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
}
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
prepareScryptedRestore
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
if [ ! -f "$SCRYPTED_TAR_ZST" ]
|
||||
then
|
||||
curl -O -L https://github.com/koush/scrypted/releases/download/$SCRYPTED_VERSION/scrypted.tar.zst
|
||||
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
|
||||
fi
|
||||
|
||||
if [[ "$@" =~ "--force" ]]
|
||||
then
|
||||
IGNORE_EXISTING=true
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
IGNORE_EXISTING=true
|
||||
fi
|
||||
|
||||
if [ -z "$IGNORE_EXISTING" ]
|
||||
then
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "==============================================================="
|
||||
echo "Existing container $VMID found."
|
||||
echo "==============================================================="
|
||||
echo ""
|
||||
echo "This script can be used ro reinstall Scrypted and reset the container to a factory state."
|
||||
echo "This preserves existing data. Creating a backup within Scrypted is highly recommended in case the reset fails."
|
||||
echo "THIS WILL WIPE ADDITIONAL VOLUMES SUCH AS NVR STORAGE. NVR volumes will need to be readded after the restore:"
|
||||
readyn "Reinstall Scrypted and and retain existing configuration?"
|
||||
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
echo ""
|
||||
echo "1. To reinstall and reset Scrypted, run this script with --force to overwrite the existing container."
|
||||
echo "THIS WILL WIPE THE EXISTING CONFIGURATION:"
|
||||
echo ""
|
||||
echo "VMID=$VMID bash $0 --force"
|
||||
echo ""
|
||||
echo "2. To reinstall Scrypted and and retain existing configuration, run this script with the environment variable SCRYPTED_RESTORE=true."
|
||||
echo "This preserves existing data. Creating a backup within Scrypted is highly recommended in case the reset fails."
|
||||
echo "THIS WILL WIPE ADDITIONAL VOLUMES SUCH AS NVR STORAGE. NVR volumes will need to be readded after the restore:"
|
||||
echo ""
|
||||
echo "SCRYPTED_RESTORE=true VMID=$VMID bash $0"
|
||||
echo ""
|
||||
echo "3. To install and run multiple Scrypted containers, run this script with the environment variable specifying"
|
||||
echo "the new VMID=<number>. For example, to create a new LXC with VMID 12345:"
|
||||
echo ""
|
||||
echo "VMID=12345 bash $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRYPTED_RESTORE=true
|
||||
prepareScryptedRestore
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! "$@" =~ "--storage" ]]
|
||||
then
|
||||
HAS_LOCAL_LVM=$(pvesm status | grep local-lvm | grep active)
|
||||
HAS_LOCAL_ZFS=$(pvesm status | grep local-zfs | grep active)
|
||||
if [ ! -z "$HAS_LOCAL_LVM" ]
|
||||
then
|
||||
RESTORE_STORAGE="--storage local-lvm"
|
||||
elif [ ! -z "$HAS_LOCAL_ZFS" ]
|
||||
then
|
||||
RESTORE_STORAGE="--storage local-zfs"
|
||||
else
|
||||
echo "Could not determine a valid storage device. One may need to be specified manually."
|
||||
fi
|
||||
fi
|
||||
|
||||
pct stop $VMID 2>&1 > /dev/null
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $RESTORE_STORAGE $@
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "The Scrypted container installation failed (pct restore error)."
|
||||
echo ""
|
||||
echo "This may be because the server's 'local' storage device is not being a valid"
|
||||
echo "location for containers."
|
||||
echo "Try running this script again with a different storage device like"
|
||||
echo "'local-lvm' or 'local-zfs'."
|
||||
echo ""
|
||||
echo "#############################################################################"
|
||||
echo -e "\033[32mPaste the following command into this shell to install to local-lvm instead:\033[0m"
|
||||
echo ""
|
||||
echo "bash $0 --storage local-lvm"
|
||||
echo "#############################################################################"
|
||||
echo ""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto$HWADDR
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct set network failed"
|
||||
echo ""
|
||||
echo "Ignoring... Please verify your container's network settings."
|
||||
fi
|
||||
|
||||
if [ -n "$RESTORE_HOSTNAME" ]
|
||||
then
|
||||
pct set $VMID --hostname $RESTORE_HOSTNAME
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct hostname restore failed"
|
||||
echo ""
|
||||
echo "Ignoring... Please verify your container's dns settings."
|
||||
fi
|
||||
fi
|
||||
|
||||
CONF=/etc/pve/lxc/$VMID.conf
|
||||
if [ -f "$CONF" ]
|
||||
then
|
||||
echo "onboot: 1" >> $CONF
|
||||
else
|
||||
echo "$CONF not found? Start on boot must be enabled manually."
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
echo ""
|
||||
echo ""
|
||||
echo "This script will reset the Scrypted container to a factory state while preserving existing data."
|
||||
echo "IT IS RECOMMENDED TO CREATE A BACKUP INSIDE SCRYPTED FIRST."
|
||||
readyn "Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Stopping scrypted..."
|
||||
pct stop $RESTORE_VMID 2>&1 > /dev/null
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
|
||||
# remove the empty data volume from the downloaded image.
|
||||
pct set $SCRYPTED_BACKUP_VMID --delete mp0 && pct set $SCRYPTED_BACKUP_VMID --delete unused0
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Failed to remove data volume from image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# create a backup that contains only the root disk.
|
||||
rm -f *.tar
|
||||
vzdump $SCRYPTED_BACKUP_VMID --dumpdir /tmp
|
||||
|
||||
# this moves the data volume from the current scrypted instance to the backup target to preserve it during
|
||||
# the restore.
|
||||
pct move-volume $RESTORE_VMID mp0 --target-vmid $SCRYPTED_BACKUP_VMID --target-volume mp0
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Failed to move data volume to backup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# arguments: from to mp hide-warning
|
||||
function move_volume() {
|
||||
HAS_VOLUME=$(pct config $1 | grep $3:)
|
||||
if [ -n "$HAS_VOLUME" ]
|
||||
then
|
||||
echo "Moving $3..."
|
||||
# this may error and there may be recording loss. bailing at ths point is already too late.
|
||||
pct move-volume $1 $3 --target-vmid $2 --target-volume $3
|
||||
|
||||
# volume must be inside /mnt to get into docker container
|
||||
INSIDE_MNT=$(echo $HAS_VOLUME | grep /mnt)
|
||||
if [ -z "$INSIDE_MNT" -a -z "$4" ]
|
||||
then
|
||||
echo "##################################################################"
|
||||
echo "The following mount point is not visible to the"
|
||||
echo "Scrypted docker container within the LXC:"
|
||||
echo ""
|
||||
echo "$HAS_VOLUME"
|
||||
echo ""
|
||||
echo "This recordings directory will be unavailable."
|
||||
echo "The mount point must be updated to a path within /mnt."
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#proxmox-ve-mount-point"
|
||||
echo "##################################################################"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# try moving 5 volumes, any more than that seems unlikely
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp1 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp2 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp3 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp4 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp5 hide-warning
|
||||
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Restoring with reset image..."
|
||||
pct restore --force 1 $VMID *.tar $RESTORE_STORAGE $@
|
||||
|
||||
echo "Restoring volumes..."
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp0 hide-warning
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp1
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp2
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp3
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp4
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp5
|
||||
|
||||
pct destroy $SCRYPTED_BACKUP_VMID
|
||||
fi
|
||||
|
||||
echo "Enabling startup on boot..."
|
||||
pct set $VMID -onboot 1
|
||||
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"drm\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"kfd\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"accel\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
fi
|
||||
|
||||
# check if intel
|
||||
INTEL=$(cat /proc/cpuinfo | grep GenuineIntel)
|
||||
if [ ! -z "$INTEL" ]
|
||||
then
|
||||
readyn "Install intel-microcode package? This will update your CPU and GPU firmware."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
echo "Installing intel-microcode..."
|
||||
# remove it first to allow reinsertion
|
||||
sed -i 's/main contrib non-free-firmware/main/g' /etc/apt/sources.list
|
||||
sed -i 's/main/main contrib non-free-firmware/g' /etc/apt/sources.list
|
||||
apt update
|
||||
apt install -y intel-microcode
|
||||
echo "#############################"
|
||||
echo "System Reboot is recommended."
|
||||
echo "#############################"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Scrypted setup is complete and the container resources can be started."
|
||||
echo ""
|
||||
echo "Scrypted NVR servers should run the disk setup script in the documentation to add storage prior to starting the container."
|
||||
BIN
install/proxmox/lxc.png
Normal file
BIN
install/proxmox/lxc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
74
install/proxmox/setup-scrypted-nvr-volume.sh
Normal file
74
install/proxmox/setup-scrypted-nvr-volume.sh
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
|
||||
NVR_STORAGE=$1
|
||||
NVR_STORAGE_DIRECTORY=$2
|
||||
|
||||
DISK_TYPE="large"
|
||||
if [ ! -z "$FAST_DISK" ]
|
||||
then
|
||||
DISK_TYPE="fast"
|
||||
fi
|
||||
|
||||
if [ -z "$NVR_STORAGE" ]; then
|
||||
echo ""
|
||||
echo "Error: Directory name not provided. Usage:"
|
||||
echo ""
|
||||
echo "bash $0 directory-name [/optional/path/to/storage]"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID="10443"
|
||||
fi
|
||||
FILE="/etc/pve/lxc/$VMID.conf"
|
||||
|
||||
# valdiate file exists
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "Error: $FILE not found."
|
||||
echo "If the Scrypted container id is not 10443, please set the VMID environment variable prior to running this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -z "$NVR_STORAGE_DIRECTORY" ]
|
||||
then
|
||||
if [ ! -d "$NVR_STORAGE_DIRECTORY" ]
|
||||
then
|
||||
echo ""
|
||||
echo "Error: $NVR_STORAGE_DIRECTORY directory not found."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
STORAGE="/mnt/pve/$NVR_STORAGE"
|
||||
if [ ! -d "$STORAGE" ]
|
||||
then
|
||||
echo "Error: $STORAGE not found."
|
||||
echo "The Proxmox Directory Storage must be created using the UI prior to running this script."
|
||||
exit 1
|
||||
fi
|
||||
# use subdirectory doesn't conflict with Proxmox storage of backups etc.
|
||||
NVR_STORAGE_DIRECTORY="$STORAGE/mounts/scrypted-nvr"
|
||||
fi
|
||||
|
||||
# create the hidden folder that can be used as a marker.
|
||||
mkdir -p $NVR_STORAGE_DIRECTORY/.nvr
|
||||
chmod 0777 $NVR_STORAGE_DIRECTORY
|
||||
|
||||
echo "Stopping Scrypted..."
|
||||
pct stop "$VMID"
|
||||
|
||||
echo "Modifying $FILE."
|
||||
|
||||
if [ -z "$ADD_DISK" ]
|
||||
then
|
||||
echo "Removing previous $DISK_TYPE lxc.mount.entry."
|
||||
sed -i "/mnt\/nvr\/$DISK_TYPE/d" "$FILE"
|
||||
fi
|
||||
|
||||
echo "Adding new $DISK_TYPE lxc.mount.entry."
|
||||
echo "lxc.mount.entry: $NVR_STORAGE_DIRECTORY mnt/nvr/$DISK_TYPE/$NVR_STORAGE none bind,optional,create=dir" >> "$FILE"
|
||||
|
||||
echo "Starting Scrypted..."
|
||||
pct start $VMID
|
||||
@@ -27,7 +27,7 @@ echo "external/werift > npm install"
|
||||
npm install
|
||||
popd
|
||||
|
||||
for directory in ffmpeg-camera rtsp amcrest onvif hikvision unifi-protect webrtc homekit
|
||||
for directory in rtsp amcrest onvif hikvision reolink unifi-protect webrtc homekit
|
||||
do
|
||||
echo "$directory > npm install"
|
||||
pushd plugins/$directory
|
||||
|
||||
180
packages/client/package-lock.json
generated
180
packages/client/package-lock.json
generated
@@ -1,24 +1,25 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.9",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.3"
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -74,19 +75,11 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.40",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.40.tgz",
|
||||
"integrity": "sha512-NBjNEfoLp7zL5Tf0odzf191oReDh4FEmZexDmMj1JbKDUMB9S8xJys3vbhcFadU/aUrUkyK/FSbkXv1z87bxSw=="
|
||||
"version": "0.3.92",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
|
||||
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -127,12 +120,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
||||
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
||||
"version": "22.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
@@ -157,9 +160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -220,9 +223,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -268,15 +272,15 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.0.tgz",
|
||||
"integrity": "sha512-iBtCdW5Tk3CnMAnC44VO4LwxXnl+RIq9ua1qHvxf5KSq2rzFgQFdfCSSl6Yuz2hl899SWTkfaT3c+WZQ42dJ8A==",
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
|
||||
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
@@ -288,9 +292,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -307,9 +311,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
||||
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
"integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
"signal-exit": "^4.0.1"
|
||||
@@ -322,21 +326,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
|
||||
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
|
||||
"integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^2.3.5",
|
||||
"minimatch": "^9.0.1",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
|
||||
"path-scurry": "^1.10.1"
|
||||
"jackspeak": "^4.0.1",
|
||||
"minimatch": "^10.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -356,28 +361,25 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
|
||||
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz",
|
||||
"integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
|
||||
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz",
|
||||
"integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==",
|
||||
"engines": {
|
||||
"node": "14 || >=16.14"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
@@ -387,23 +389,23 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
|
||||
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -413,6 +415,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -422,32 +429,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
|
||||
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
|
||||
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
"glob": "^11.0.0",
|
||||
"package-json-from-dist": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
@@ -615,9 +623,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
|
||||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -628,9 +636,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
@@ -758,9 +766,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
|
||||
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.9",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -13,14 +13,15 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.3"
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
|
||||
directAddress: response.headers.get('x-scrypted-direct-address'),
|
||||
cloudAddress: response.headers.get('x-scrypted-cloud-address'),
|
||||
serverId: response.headers.get('x-scrypted-server-id'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,6 +214,7 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
|
||||
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
|
||||
directAddress: response.headers.get('x-scrypted-direct-address'),
|
||||
cloudAddress: response.headers.get('x-scrypted-cloud-address'),
|
||||
serverId: response.headers.get('x-scrypted-server-id'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,6 +229,7 @@ export interface ScryptedClientLoginResult {
|
||||
directAddress: string;
|
||||
cloudAddress: string;
|
||||
hostname: string;
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export class ScryptedClientLoginError extends Error {
|
||||
@@ -274,6 +277,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
let cloudAddress: string;
|
||||
let hostname: string;
|
||||
let token: string;
|
||||
let serverId: string;
|
||||
|
||||
console.log('@scrypted/client', packageJson.version);
|
||||
|
||||
@@ -299,6 +303,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
queryToken = loginResult.queryToken;
|
||||
token = loginResult.token;
|
||||
hostname = loginResult.hostname;
|
||||
serverId = loginResult.serverId;
|
||||
console.log('login result', Date.now() - start, loginResult);
|
||||
}
|
||||
else {
|
||||
@@ -372,6 +377,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
queryToken = loginCheck.queryToken;
|
||||
token = loginCheck.token;
|
||||
hostname = loginCheck.hostname;
|
||||
serverId = loginCheck.serverId;
|
||||
console.log('login checked', Date.now() - start, loginCheck);
|
||||
}
|
||||
|
||||
@@ -694,6 +700,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager,
|
||||
clusterManager,
|
||||
} = scrypted;
|
||||
console.log('api attached', Date.now() - start);
|
||||
|
||||
@@ -853,6 +860,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
connectionType,
|
||||
admin,
|
||||
systemManager,
|
||||
clusterManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager,
|
||||
@@ -874,6 +882,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization,
|
||||
cloudAddress,
|
||||
hostname,
|
||||
serverId,
|
||||
},
|
||||
connectRPCObject,
|
||||
fork: undefined,
|
||||
|
||||
5
plugins/alexa/package-lock.json
generated
5
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
@@ -203,4 +202,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -53,6 +53,12 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
title: "Pairing Key",
|
||||
description: "The pairing key used to validate requests from Alexa. Clear this key or delete the plugin to allow pairing with a different Alexa login.",
|
||||
},
|
||||
disableAutoAdd: {
|
||||
title: "Disable auto add",
|
||||
description: "Disable automatic enablement of devices.",
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
accessToken: Promise<string>;
|
||||
@@ -116,6 +122,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (!supportedTypes.has(device.type))
|
||||
return DeviceMixinStatus.NotSupported;
|
||||
|
||||
if (this.storageSettings.values.disableAutoAdd) {
|
||||
return DeviceMixinStatus.Skip;
|
||||
}
|
||||
|
||||
mixins.push(this.id);
|
||||
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
@@ -671,7 +681,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
enum DeviceMixinStatus {
|
||||
NotSupported = 0,
|
||||
Setup = 1,
|
||||
AlreadySetup = 2
|
||||
AlreadySetup = 2,
|
||||
Skip = 3,
|
||||
}
|
||||
|
||||
class HttpResponseLoggingImpl implements AlexaHttpResponse {
|
||||
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.162",
|
||||
"version": "0.0.164",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.162",
|
||||
"version": "0.0.164",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.162",
|
||||
"version": "0.0.164",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -199,6 +199,14 @@ export class AmcrestCameraClient {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async setWatermark(cameraNumber: number, enable: boolean) {
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&VideoWidget[${cameraNumber - 1}].PictureTitle.EncodeBlend=${enable}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async listenEvents(): Promise<Destroyable> {
|
||||
const events = new EventEmitter();
|
||||
const url = `http://${this.ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]`;
|
||||
|
||||
@@ -9,12 +9,14 @@ export const amcrestAutoConfigureSettings: Setting = {
|
||||
};
|
||||
|
||||
export async function autoconfigureSettings(client: AmcrestCameraClient, cameraNumber: number) {
|
||||
await client.setWatermark(cameraNumber, false).catch(() => { });
|
||||
|
||||
const audioOptions: AudioStreamConfiguration = {
|
||||
codec: 'aac',
|
||||
sampleRate: 8000,
|
||||
};
|
||||
|
||||
await client.resetMotionDetection(cameraNumber);
|
||||
await client.resetMotionDetection(cameraNumber).catch(() => {});
|
||||
|
||||
return ac(
|
||||
() => client.getCodecs(cameraNumber),
|
||||
|
||||
@@ -56,13 +56,13 @@ Scrypted Cloud automatically creates a login free tunnel for remote access.
|
||||
|
||||
The following steps are only necessary if you want to associate the tunnel with your existing Cloudflare account to manage it remotely.
|
||||
|
||||
1. Create the Tunnel in the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com).
|
||||
2. Copy the token shown for the tunnel shown in the `install [token]` command. For example, if you see `cloudflared service install eyJhI344aA...`, then `eyJhI344aA...` is the token you need to copy.
|
||||
3. Paste the token into the Cloud Plugin Advanced Settings.
|
||||
4. Add a `Public Hostname` to the tunnel.
|
||||
* Choose a (sub)domain.
|
||||
* Service `Type` is `HTTPS` and `URL` is `localhost:port`. Replace the port with `Forward Port` from Cloud Plugin Settings.
|
||||
* Expand `Additional Application Settings` -> `TLS` menus and enable `No TLS Verify`.
|
||||
1. Navigate to the Cloud Plugin's Cloudflare Settings.
|
||||
2. Enter the Cloudflare subdomain, e.g. `scrypted.example.org`.
|
||||
3. Open the authorization link printed in the Log in a browser.
|
||||
4. Log in to Cloudflare if prompted. Then open the authorization link again.
|
||||
5. Select the domain for the specified the subdomain.
|
||||
6. Authorization should now be complete.
|
||||
|
||||
5. Reload Cloud Plugin.
|
||||
6. Verify Cloudflare successfully connected by observing the `Console` Logs.
|
||||
::: info
|
||||
Visiting the authorization link twice as directed in the above instructions may be necessary. Cloudflare will not prompt a with a list of domains unless the browser session is already logged in.
|
||||
:::
|
||||
|
||||
2289
plugins/cloud/package-lock.json
generated
2289
plugins/cloud/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^4.2.0",
|
||||
"@eneris/push-receiver": "^4.3.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"bpmux": "^8.2.1",
|
||||
@@ -48,10 +48,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/http-proxy": "^1.17.15",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/nat-upnp": "^1.1.5",
|
||||
"@types/node": "^22.5.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"version": "0.2.37"
|
||||
"version": "0.2.48"
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ const { deviceManager, endpointManager, systemManager } = sdk;
|
||||
|
||||
export const DEFAULT_SENDER_ID = '827888101440';
|
||||
const SCRYPTED_SERVER = localStorage.getItem('scrypted-server') || 'home.scrypted.app';
|
||||
const SCRYPTED_SERVER_PORT = 4001;
|
||||
|
||||
const SCRYPTED_CLOUD_MESSAGE_PATH = '/_punch/cloudmessage';
|
||||
|
||||
@@ -147,7 +148,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
type: 'boolean',
|
||||
description: 'Optional: Create a Cloudflare Tunnel to this server at a random domain name. Providing a Cloudflare token will allow usage of a custom domain name.',
|
||||
defaultValue: true,
|
||||
onPut: () => deviceManager.requestRestart(),
|
||||
},
|
||||
cloudflaredTunnelToken: {
|
||||
group: 'Cloudflare',
|
||||
@@ -183,6 +183,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.storageSettings.values.cloudflaredTunnelCredentials = undefined;
|
||||
this.doCloudflaredLogin(nv);
|
||||
},
|
||||
console: true,
|
||||
},
|
||||
cloudflaredTunnelLoginUrl: {
|
||||
group: 'Cloudflare',
|
||||
@@ -258,6 +259,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.converters = [
|
||||
[ScryptedMimeTypes.LocalUrl, ScryptedMimeTypes.Url],
|
||||
[ScryptedMimeTypes.PushEndpoint, ScryptedMimeTypes.Url],
|
||||
['*/*', ScryptedMimeTypes.ServerId],
|
||||
];
|
||||
// legacy cleanup
|
||||
this.fromMimeType = undefined;
|
||||
@@ -266,10 +268,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
devices: [],
|
||||
});
|
||||
|
||||
this.storageSettings.settings.register.onPut = async () => {
|
||||
await this.sendRegistrationId(await this.manager.registrationId);
|
||||
}
|
||||
|
||||
this.storageSettings.settings.upnpStatus.onGet = async () => {
|
||||
return {
|
||||
hide: this.storageSettings.values.forwardingMode !== 'UPNP',
|
||||
@@ -313,11 +311,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
};
|
||||
|
||||
|
||||
this.storageSettings.settings.securePort.onPut = (ov, nv) => {
|
||||
if (ov && ov !== nv)
|
||||
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
|
||||
};
|
||||
|
||||
if (!this.storageSettings.values.certificate)
|
||||
this.storageSettings.values.certificate = createSelfSignedCertificate();
|
||||
|
||||
@@ -340,6 +333,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.refreshPortForward();
|
||||
}
|
||||
|
||||
// auto login from electron
|
||||
if (!this.storageSettings.values.token_info && process.env.SCRYPTED_CLOUD_TOKEN) {
|
||||
this.storageSettings.values.token_info = process.env.SCRYPTED_CLOUD_TOKEN;
|
||||
this.manager.registrationId.then(r => {
|
||||
@@ -366,6 +360,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async getCachedRegistrationId() {
|
||||
return this.manager.currentRegistrationId || this.storageSettings.values.lastPersistedRegistrationId;
|
||||
}
|
||||
|
||||
async updatePortForward(upnpPort: number) {
|
||||
this.storageSettings.values.upnpPort = upnpPort;
|
||||
|
||||
@@ -431,8 +429,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
if (this.storageSettings.values.lastPersistedUpnpPort !== upnpPort || ip !== this.storageSettings.values.lastPersistedIp) {
|
||||
this.console.log('Registering IP and Port', ip, upnpPort);
|
||||
|
||||
const registrationId = await this.manager.registrationId;
|
||||
const data = await this.sendRegistrationId(registrationId);
|
||||
const data = await this.sendRegistrationId(await this.getCachedRegistrationId());
|
||||
if (data?.error)
|
||||
return;
|
||||
if (ip !== 'localhost' && ip !== data.ip_address && ip !== this.cloudflareTunnelHost) {
|
||||
@@ -451,11 +448,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
public: true,
|
||||
});
|
||||
const url = new URL(`https://${SCRYPTED_SERVER}/_punch/curl`);
|
||||
let { upnp_port, hostname } = this.getAuthority();
|
||||
let { port, hostname } = this.getAuthority();
|
||||
// scrypted cloud will replace localhost with requesting ip
|
||||
if (!hostname)
|
||||
hostname = 'localhost';
|
||||
url.searchParams.set('url', `https://${hostname}:${upnp_port}${pluginPath}/testPortForward`);
|
||||
url.searchParams.set('url', `https://${hostname}:${port}${pluginPath}/testPortForward`);
|
||||
const response = await httpFetch({
|
||||
url: url.toString(),
|
||||
responseType: 'json',
|
||||
@@ -621,13 +618,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
if (!hostname) {
|
||||
return {
|
||||
upnp_port,
|
||||
port: upnp_port,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
upnp_port,
|
||||
port: upnp_port,
|
||||
hostname,
|
||||
}
|
||||
@@ -638,6 +633,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
const q = qsstringify({
|
||||
...authority,
|
||||
cloudflare_hostname: this.cloudflareTunnelHost,
|
||||
registration_id,
|
||||
server_id: this.storageSettings.values.serverId,
|
||||
server_name: this.storageSettings.values.serverName,
|
||||
@@ -677,7 +673,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
this.console.log('registered', response.body);
|
||||
this.storageSettings.values.lastPersistedRegistrationId = registration_id;
|
||||
this.storageSettings.values.lastPersistedUpnpPort = authority.upnp_port;
|
||||
this.storageSettings.values.lastPersistedUpnpPort = authority.port;
|
||||
return response.body;
|
||||
}
|
||||
catch (e) {
|
||||
@@ -729,10 +725,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
|
||||
async convertMedia(data: string | Buffer | any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<MediaObject | Buffer | any> {
|
||||
if (!toMimeType.startsWith(ScryptedMimeTypes.Url))
|
||||
throw new Error('unsupported cloud url conversion');
|
||||
|
||||
if (fromMimeType.startsWith(ScryptedMimeTypes.LocalUrl)) {
|
||||
if (toMimeType.startsWith(ScryptedMimeTypes.Url) && fromMimeType.startsWith(ScryptedMimeTypes.LocalUrl)) {
|
||||
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
|
||||
// short lived urls.
|
||||
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
|
||||
@@ -746,7 +739,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
|
||||
}
|
||||
else if (fromMimeType.startsWith(ScryptedMimeTypes.PushEndpoint)) {
|
||||
else if (toMimeType.startsWith(ScryptedMimeTypes.Url) && fromMimeType.startsWith(ScryptedMimeTypes.PushEndpoint)) {
|
||||
const validDomain = this.getSSLHostname();
|
||||
if (validDomain)
|
||||
return Buffer.from(`https://${validDomain}${await this.getCloudMessagePath()}/${data}`);
|
||||
@@ -754,6 +747,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
const url = `http://127.0.0.1/push/${data}`;
|
||||
return this.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
|
||||
}
|
||||
else if (toMimeType === ScryptedMimeTypes.ServerId) {
|
||||
return this.storageSettings.values.serverId;
|
||||
}
|
||||
|
||||
throw new Error('unsupported cloud url conversion');
|
||||
}
|
||||
@@ -792,7 +788,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
const args = qsstringify({
|
||||
...authority,
|
||||
|
||||
registration_id: await this.manager.registrationId,
|
||||
registration_id: await this.getCachedRegistrationId() || 'undefined',
|
||||
cloudflare_hostname: this.cloudflareTunnelHost,
|
||||
registration_secret: this.storageSettings.values.registrationSecret,
|
||||
server_id: this.storageSettings.values.serverId,
|
||||
server_name: this.storageSettings.values.serverName,
|
||||
@@ -833,7 +830,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
const query = qsparse(url.searchParams);
|
||||
if (!query.callback_url && query.token_info && query.user_info) {
|
||||
this.storageSettings.values.token_info = query.token_info;
|
||||
this.storageSettings.values.lastPersistedRegistrationId = await this.manager.registrationId;
|
||||
this.storageSettings.values.lastPersistedRegistrationId = await this.getCachedRegistrationId();
|
||||
res.setHeader('Location', `https://${this.getHostname()}/endpoint/@scrypted/core/public/`);
|
||||
res.writeHead(302);
|
||||
res.end();
|
||||
@@ -844,6 +841,17 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (url.pathname === '/_punch/callback') {
|
||||
const query = qsparse(url.searchParams);
|
||||
if (query.registration_secret === this.storageSettings.values.registrationSecret) {
|
||||
res.writeHead(200);
|
||||
this.serverCallback(port, SCRYPTED_SERVER_PORT, SCRYPTED_SERVER);
|
||||
}
|
||||
else {
|
||||
res.writeHead(401);
|
||||
}
|
||||
res.end();
|
||||
}
|
||||
else if (url.pathname === '/web/') {
|
||||
const validDomain = this.getSSLHostname();
|
||||
if (validDomain) {
|
||||
@@ -890,6 +898,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
const port = (this.server.address() as any).port;
|
||||
this.console.log('scrypted cloud server listening on', port);
|
||||
|
||||
this.storageSettings.settings.cloudflareEnabled.onPut = () => {
|
||||
this.cloudflared?.child.kill();
|
||||
this.startCloudflared(port);
|
||||
};
|
||||
|
||||
const agent = new http.Agent({ maxSockets: Number.MAX_VALUE, keepAlive: true });
|
||||
this.proxy = HttpProxy.createProxy({
|
||||
agent,
|
||||
@@ -900,11 +913,12 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.proxy.on('proxyRes', (res, req) => {
|
||||
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
|
||||
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
|
||||
res.headers['X-Scrypted-Server-Id'] = this.storageSettings.values.serverId;
|
||||
let domain = this.cloudflareTunnel;
|
||||
if (!domain && this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|
||||
domain = `https://${this.storageSettings.values.hostname}`;
|
||||
res.headers['X-Scrypted-Cloud-Address'] = domain;
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address, X-Scrypted-Cloud-Address';
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address, X-Scrypted-Cloud-Address, X-Scrypted-Server-Id';
|
||||
});
|
||||
|
||||
let backoff = 0;
|
||||
@@ -927,42 +941,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
if (Date.now() < backoff + 5000)
|
||||
return;
|
||||
backoff = Date.now();
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
this.console.log('scrypted server requested a connection:', random);
|
||||
|
||||
const registrationId = await this.manager.registrationId;
|
||||
|
||||
const { address } = message;
|
||||
const [serverHost, serverPort] = address?.split(':') || [SCRYPTED_SERVER, 4001];
|
||||
|
||||
this.ensureReverseConnections(registrationId, serverPort, serverHost);
|
||||
|
||||
const client = tls.connect(serverPort, serverHost, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
client.on('close', () => this.console.log('scrypted server connection ended:', random));
|
||||
client.write(registrationId + '\n');
|
||||
const mux: any = new bpmux.BPMux(client as any);
|
||||
mux.on('handshake', async (socket: Duplex) => {
|
||||
this.ensureReverseConnections(registrationId, serverPort, serverHost);
|
||||
|
||||
this.console.warn('mux connection required');
|
||||
|
||||
let local: any;
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
local = net.connect({
|
||||
port,
|
||||
host: '127.0.0.1',
|
||||
});
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
socket.pipe(local).pipe(socket);
|
||||
});
|
||||
mux.on('error', () => {
|
||||
client.destroy();
|
||||
});
|
||||
const [serverHost, serverPort] = address?.split(':') || [SCRYPTED_SERVER, SCRYPTED_SERVER_PORT];
|
||||
this.serverCallback(port, Number(serverPort), serverHost);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -989,6 +970,40 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
}
|
||||
|
||||
serverCallback(port: number, serverPort: number, serverHost: string) {
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
this.console.log('scrypted server requested a connection:', random);
|
||||
|
||||
this.ensureReverseConnections(serverPort, serverHost);
|
||||
|
||||
const client = tls.connect(serverPort, serverHost, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
client.on('close', () => this.console.log('scrypted server connection ended:', random));
|
||||
client.write(this.serverIdentifier + '\n');
|
||||
const mux: any = new bpmux.BPMux(client as any);
|
||||
mux.on('handshake', async (socket: Duplex) => {
|
||||
this.ensureReverseConnections(serverPort, serverHost);
|
||||
|
||||
this.console.warn('mux connection required');
|
||||
|
||||
let local: any;
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
local = net.connect({
|
||||
port,
|
||||
host: '127.0.0.1',
|
||||
});
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
socket.pipe(local).pipe(socket);
|
||||
});
|
||||
mux.on('error', () => {
|
||||
client.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
async startCloudflared(quickTunnelPort: number) {
|
||||
while (true) {
|
||||
try {
|
||||
@@ -1004,6 +1019,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
if (this.storageSettings.values.cloudflaredTunnelCredentials && this.storageSettings.values.cloudflaredTunnelCustomDomain) {
|
||||
const tunnelUrl = `http://127.0.0.1:${quickTunnelPort}`;
|
||||
const url = this.cloudflareTunnel = `https://${this.storageSettings.values.cloudflaredTunnelCustomDomain}`;
|
||||
this.updateExternalAddresses();
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
|
||||
|
||||
const ret = await runLocallyManagedTunnel(this.storageSettings.values.cloudflaredTunnelCredentials, tunnelUrl, cloudflareD, bin);
|
||||
@@ -1038,7 +1054,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
const lines = string.split('\n');
|
||||
for (const line of lines) {
|
||||
if ((line.includes('Unregistered tunnel connection') || line.includes('Register tunnel error'))
|
||||
if ((line.includes('Unregistered tunnel connection')
|
||||
|| line.includes('Connection terminated error')
|
||||
|| line.includes('Register tunnel error')
|
||||
|| line.includes('Failed to serve tunnel')
|
||||
|| line.includes('Failed to get tunnel'))
|
||||
&& deferred.finished) {
|
||||
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');
|
||||
cloudflareTunnel.child.kill();
|
||||
@@ -1113,13 +1133,18 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
}
|
||||
|
||||
ensureReverseConnections(registrationId: string, serverPort: number, serverHost: string) {
|
||||
get serverIdentifier() {
|
||||
const serverIdentifier = `${this.storageSettings.values.registrationSecret}@${this.storageSettings.values.serverId}`;
|
||||
return serverIdentifier;
|
||||
}
|
||||
|
||||
ensureReverseConnections(serverPort: number, serverHost: string) {
|
||||
while (this.reverseConnections.size < 10) {
|
||||
this.createReverseConnection(registrationId, serverPort, serverHost);
|
||||
this.createReverseConnection(serverPort, serverHost);
|
||||
}
|
||||
}
|
||||
|
||||
async createReverseConnection(registrationId: string, serverPort: number, serverHost: string) {
|
||||
async createReverseConnection(serverPort: number, serverHost: string) {
|
||||
const client = tls.connect(serverPort, serverHost, {
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
@@ -1131,9 +1156,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.reverseConnections.delete(client);
|
||||
|
||||
if (claimed)
|
||||
this.ensureReverseConnections(registrationId, serverPort, serverHost);
|
||||
this.ensureReverseConnections(serverPort, serverHost);
|
||||
});
|
||||
client.write(`reverse:${registrationId}\n`);
|
||||
|
||||
client.write(`reverse:${this.serverIdentifier}\n`);
|
||||
|
||||
try {
|
||||
const read = await readLine(client);
|
||||
|
||||
@@ -10,6 +10,7 @@ export declare interface PushManager {
|
||||
|
||||
export class PushManager extends EventEmitter {
|
||||
registrationId: Promise<string>;
|
||||
currentRegistrationId: string;
|
||||
|
||||
constructor(public senderId: string) {
|
||||
super();
|
||||
@@ -47,11 +48,12 @@ export class PushManager extends EventEmitter {
|
||||
}
|
||||
|
||||
const stopListeningToCredentials = instance.onCredentialsChanged(({ oldCredentials, newCredentials }) => {
|
||||
this.currentRegistrationId = newCredentials.fcm.token;
|
||||
savedConfig.credentials = newCredentials;
|
||||
deferred.resolve(newCredentials.fcm.token);
|
||||
this.registrationId = Promise.resolve(newCredentials.fcm.token);
|
||||
deferred.resolve(this.currentRegistrationId);
|
||||
this.registrationId = Promise.resolve( this.currentRegistrationId);
|
||||
saveConfig();
|
||||
this.emit('registrationId', newCredentials.fcm.token);
|
||||
this.emit('registrationId', this.currentRegistrationId);
|
||||
});
|
||||
|
||||
const stopListeningToNotifications = instance.onNotification(({ message }) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export function qsstringify(dict: any) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(dict)) {
|
||||
if (v == null)
|
||||
continue;
|
||||
params.set(k, v?.toString());
|
||||
}
|
||||
|
||||
|
||||
2
plugins/core/.vscode/settings.json
vendored
2
plugins/core/.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
1
plugins/core/fs/lxc/docker-compose.sh
Symbolic link
1
plugins/core/fs/lxc/docker-compose.sh
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../install/proxmox/docker-compose.sh
|
||||
@@ -1,22 +0,0 @@
|
||||
[Unit]
|
||||
Description=Scrypted service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/npx -y scrypted serve
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
Environment="NODE_OPTIONS=--dns-result-order=ipv4first"
|
||||
Environment="SCRYPTED_PYTHON_PATH=/usr/bin/python3"
|
||||
Environment="SCRYPTED_PYTHON39_PATH=/usr/bin/python3.9"
|
||||
Environment="SCRYPTED_PYTHON310_PATH=/usr/bin/python3.10"
|
||||
Environment="SCRYPTED_FFMPEG_PATH=/usr/bin/ffmpeg"
|
||||
Environment="SCRYPTED_INSTALL_ENVIRONMENT=lxc"
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
181
plugins/core/package-lock.json
generated
181
plugins/core/package-lock.json
generated
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.68",
|
||||
"version": "0.3.102",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.68",
|
||||
"version": "0.3.102",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"mime": "^3.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"node-pty": "^1.0.0",
|
||||
"router": "^1.3.8",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^5.6.2",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node": "^20.11.26"
|
||||
"@types/node": "^22.7.4"
|
||||
}
|
||||
},
|
||||
"../../../sdk": {
|
||||
@@ -88,23 +88,29 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.45",
|
||||
"version": "0.3.100",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -116,11 +122,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -131,19 +135,13 @@
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz",
|
||||
"integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.26",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
|
||||
"integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
|
||||
"version": "22.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
@@ -160,14 +158,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz",
|
||||
"integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa"
|
||||
],
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
"mime": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
@@ -195,7 +196,7 @@
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "1.3.8",
|
||||
@@ -233,9 +234,9 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
|
||||
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -245,9 +246,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
@@ -257,6 +258,17 @@
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -274,40 +286,38 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz",
|
||||
"integrity": "sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.11.26",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
|
||||
"integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
|
||||
"version": "22.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
@@ -321,9 +331,9 @@
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
},
|
||||
"mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz",
|
||||
"integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ=="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.20.0",
|
||||
@@ -346,7 +356,7 @@
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"router": {
|
||||
"version": "1.3.8",
|
||||
@@ -383,20 +393,25 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
|
||||
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ=="
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||
},
|
||||
"yaml": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.68",
|
||||
"version": "0.3.102",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -42,13 +42,13 @@
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"mime": "^3.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"node-pty": "^1.0.0",
|
||||
"router": "^1.3.8",
|
||||
"typescript": "^5.4.2"
|
||||
"typescript": "^5.6.2",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node": "^20.11.26"
|
||||
"@types/node": "^22.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk';
|
||||
import { AggregateDevice, createAggregateDevice } from './aggregate';
|
||||
import { AggregateDevice } from './aggregate';
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
export const AggregateCoreNativeId = 'aggregatecore';
|
||||
@@ -13,24 +13,6 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
|
||||
this.systemDevice = {
|
||||
deviceCreator: 'Device Group',
|
||||
};
|
||||
|
||||
for (const nativeId of deviceManager.getNativeIds()) {
|
||||
if (nativeId?.startsWith('aggregate:')) {
|
||||
const aggregate = createAggregateDevice(nativeId);
|
||||
this.aggregate.set(nativeId, aggregate);
|
||||
this.reportAggregate(nativeId, aggregate.computeInterfaces(), aggregate.providedName);
|
||||
}
|
||||
}
|
||||
|
||||
sdk.systemManager.listen((eventSource, eventDetails, eventData) => {
|
||||
if (eventDetails.eventInterface === 'Storage') {
|
||||
const ids = [...this.aggregate.values()].map(a => a.id);
|
||||
if (ids.includes(eventSource.id)) {
|
||||
const aggregate = [...this.aggregate.values()].find(a => a.id === eventSource.id);
|
||||
this.reportAggregate(aggregate.nativeId, aggregate.computeInterfaces(), aggregate.providedName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
@@ -51,7 +33,8 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
|
||||
const { name } = settings;
|
||||
const nativeId = `aggregate:${Math.random()}`;
|
||||
await this.reportAggregate(nativeId, [], name?.toString());
|
||||
const aggregate = createAggregateDevice(nativeId);
|
||||
const aggregate = new AggregateDevice(this, nativeId);
|
||||
aggregate.computeInterfaces();
|
||||
this.aggregate.set(nativeId, aggregate);
|
||||
return nativeId;
|
||||
}
|
||||
@@ -68,9 +51,17 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
return this.aggregate.get(nativeId);
|
||||
let device = this.aggregate.get(nativeId);
|
||||
if (device)
|
||||
return device;
|
||||
device = new AggregateDevice(this, nativeId);
|
||||
device.computeInterfaces();
|
||||
this.aggregate.set(nativeId, device);
|
||||
return device;
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
const device = this.aggregate.get(nativeId);
|
||||
device?.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import sdk, { EventListener, EventListenerRegister, FFmpegInput, LockState, MediaStreamDestination, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import type { AggregateCore } from "./aggregate-core";
|
||||
const { systemManager, mediaManager, deviceManager } = sdk;
|
||||
|
||||
export interface AggregateDevice extends ScryptedDeviceBase {
|
||||
computeInterfaces(): string[];
|
||||
}
|
||||
|
||||
interface Aggregator<T> {
|
||||
(values: T[]): T;
|
||||
}
|
||||
@@ -141,143 +138,144 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
}
|
||||
}
|
||||
|
||||
export function createAggregateDevice(nativeId: string): AggregateDevice {
|
||||
class AggregateDeviceImpl extends ScryptedDeviceBase implements Settings {
|
||||
listeners: EventListenerRegister[] = [];
|
||||
storageSettings = new StorageSettings(this, {
|
||||
deviceInterfaces: {
|
||||
title: 'Selected Device Interfaces',
|
||||
description: 'The components of other devices to combine into this device group.',
|
||||
type: 'interface',
|
||||
multiple: true,
|
||||
deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`,
|
||||
export class AggregateDevice extends ScryptedDeviceBase implements Settings {
|
||||
listeners: EventListenerRegister[] = [];
|
||||
storageSettings = new StorageSettings(this, {
|
||||
deviceInterfaces: {
|
||||
title: 'Selected Device Interfaces',
|
||||
description: 'The components of other devices to combine into this device group.',
|
||||
type: 'interface',
|
||||
multiple: true,
|
||||
deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`,
|
||||
onPut: () => {
|
||||
this.core.reportAggregate(this.nativeId, this.computeInterfaces(), this.providedName);
|
||||
}
|
||||
})
|
||||
|
||||
constructor() {
|
||||
super(nativeId);
|
||||
|
||||
try {
|
||||
const data = this.storage.getItem('data');
|
||||
if (data) {
|
||||
const { deviceInterfaces } = JSON.parse(data);
|
||||
this.storageSettings.values.deviceInterfaces = deviceInterfaces;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
this.storage.removeItem('data');
|
||||
}
|
||||
})
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
constructor(public core: AggregateCore, nativeId: string) {
|
||||
super(nativeId);
|
||||
|
||||
try {
|
||||
const data = this.storage.getItem('data');
|
||||
if (data) {
|
||||
const { deviceInterfaces } = JSON.parse(data);
|
||||
this.storageSettings.values.deviceInterfaces = deviceInterfaces;
|
||||
}
|
||||
}
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
catch (e) {
|
||||
}
|
||||
this.storage.removeItem('data');
|
||||
}
|
||||
|
||||
makeListener(iface: string, devices: ScryptedDevice[]) {
|
||||
const aggregator = aggregators.get(iface);
|
||||
if (!aggregator) {
|
||||
const ds = deviceManager.getDeviceState(this.nativeId);
|
||||
// if this device can't be aggregated for whatever reason, pass property through.
|
||||
for (const device of devices) {
|
||||
const register = device.listen({
|
||||
event: iface,
|
||||
watch: true,
|
||||
}, (source, details, data) => {
|
||||
if (details.property)
|
||||
ds[details.property] = data;
|
||||
});
|
||||
this.listeners.push(register);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0];
|
||||
if (!property) {
|
||||
this.console.warn('aggregating interface with no property?', iface);
|
||||
return;
|
||||
}
|
||||
|
||||
const runAggregator = () => {
|
||||
const values = devices.map(device => device[property]);
|
||||
(this as any)[property] = aggregator(values);
|
||||
}
|
||||
|
||||
const listener: EventListener = () => runAggregator();
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
makeListener(iface: string, devices: ScryptedDevice[]) {
|
||||
const aggregator = aggregators.get(iface);
|
||||
if (!aggregator) {
|
||||
const ds = deviceManager.getDeviceState(this.nativeId);
|
||||
// if this device can't be aggregated for whatever reason, pass property through.
|
||||
for (const device of devices) {
|
||||
const register = device.listen({
|
||||
event: iface,
|
||||
watch: true,
|
||||
}, listener);
|
||||
}, (source, details, data) => {
|
||||
if (details.property)
|
||||
ds[details.property] = data;
|
||||
});
|
||||
this.listeners.push(register);
|
||||
}
|
||||
|
||||
return runAggregator;
|
||||
return;
|
||||
}
|
||||
|
||||
computeInterfaces(): string[] {
|
||||
this.listeners.forEach(listener => listener.removeListener());
|
||||
this.listeners = [];
|
||||
|
||||
try {
|
||||
const interfaces = new Map<string, string[]>();
|
||||
for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) {
|
||||
const parts = deviceInterface.split('#');
|
||||
const id = parts[0];
|
||||
const iface = parts[1];
|
||||
if (!interfaces.has(iface))
|
||||
interfaces.set(iface, []);
|
||||
interfaces.get(iface).push(id);
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const runAggregator = this.makeListener(iface, devices);
|
||||
runAggregator?.();
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const descriptor = ScryptedInterfaceDescriptors[iface];
|
||||
if (!descriptor) {
|
||||
this.console.warn(`descriptor not found for ${iface}, skipping method generation`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (iface === ScryptedInterface.VideoCamera) {
|
||||
const camera = createVideoCamera(devices as any, this.console);
|
||||
for (const method of descriptor.methods) {
|
||||
AggregateDeviceImpl.prototype[method] = (...args: any[]) => camera[method](...args);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const method of descriptor.methods) {
|
||||
AggregateDeviceImpl.prototype[method] = async function (...args: any[]) {
|
||||
const ret: Promise<any>[] = [];
|
||||
for (const device of devices) {
|
||||
ret.push(device[method](...args));
|
||||
}
|
||||
|
||||
const results = await Promise.all(ret);
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...interfaces.keys()];
|
||||
}
|
||||
catch (e) {
|
||||
// this.console.error('error loading aggregate device', e);
|
||||
return [];
|
||||
}
|
||||
const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0];
|
||||
if (!property) {
|
||||
this.console.warn('aggregating interface with no property?', iface);
|
||||
return;
|
||||
}
|
||||
|
||||
const runAggregator = () => {
|
||||
const values = devices.map(device => device[property]);
|
||||
(this as any)[property] = aggregator(values);
|
||||
}
|
||||
|
||||
const listener: EventListener = () => runAggregator();
|
||||
|
||||
for (const device of devices) {
|
||||
const register = device.listen({
|
||||
event: iface,
|
||||
watch: true,
|
||||
}, listener);
|
||||
this.listeners.push(register);
|
||||
}
|
||||
|
||||
return runAggregator;
|
||||
}
|
||||
|
||||
const ret = new AggregateDeviceImpl();
|
||||
ret.computeInterfaces();
|
||||
return new AggregateDeviceImpl();
|
||||
}
|
||||
release() {
|
||||
this.listeners.forEach(listener => listener.removeListener());
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
computeInterfaces(): string[] {
|
||||
this.release();
|
||||
|
||||
try {
|
||||
const interfaces = new Map<string, string[]>();
|
||||
for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) {
|
||||
const parts = deviceInterface.split('#');
|
||||
const id = parts[0];
|
||||
const iface = parts[1];
|
||||
if (!interfaces.has(iface))
|
||||
interfaces.set(iface, []);
|
||||
interfaces.get(iface).push(id);
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const runAggregator = this.makeListener(iface, devices);
|
||||
runAggregator?.();
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const descriptor = ScryptedInterfaceDescriptors[iface];
|
||||
if (!descriptor) {
|
||||
this.console.warn(`descriptor not found for ${iface}, skipping method generation`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (iface === ScryptedInterface.VideoCamera) {
|
||||
const camera = createVideoCamera(devices as any, this.console);
|
||||
for (const method of descriptor.methods) {
|
||||
this[method] = (...args: any[]) => camera[method](...args);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const method of descriptor.methods) {
|
||||
this[method] = async function (...args: any[]) {
|
||||
const ret: Promise<any>[] = [];
|
||||
for (const device of devices) {
|
||||
ret.push(device[method](...args));
|
||||
}
|
||||
|
||||
const results = await Promise.all(ret);
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...interfaces.keys()];
|
||||
}
|
||||
catch (e) {
|
||||
// this.console.error('error loading aggregate device', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
160
plugins/core/src/cluster.ts
Normal file
160
plugins/core/src/cluster.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { createAsyncQueue } from "@scrypted/common/src/async-queue";
|
||||
import sdk, { Readme, ScryptedDeviceBase, ScryptedInterface, ScryptedSettings, Setting, Settings } from "@scrypted/sdk";
|
||||
|
||||
export const ClusterCoreNativeId = 'clustercore';
|
||||
|
||||
export class ClusterCore extends ScryptedDeviceBase implements Settings, Readme, ScryptedSettings {
|
||||
writeQueue = createAsyncQueue<() => Promise<void>>();
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
|
||||
(async () => {
|
||||
for await (const write of this.writeQueue.queue) {
|
||||
try {
|
||||
await write();
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error writing settings', e);
|
||||
}
|
||||
finally {
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const mode = sdk.clusterManager?.getClusterMode?.();
|
||||
if (!mode)
|
||||
return [];
|
||||
|
||||
const workers = await sdk.clusterManager.getClusterWorkers();
|
||||
|
||||
const ret: Setting[] = [];
|
||||
|
||||
const clientWorkers = Object.values(workers);
|
||||
|
||||
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
|
||||
|
||||
for (const worker of clientWorkers) {
|
||||
const group = `Worker: ${worker.name}`;
|
||||
const name: Setting = {
|
||||
key: `${worker.id}:name`,
|
||||
group,
|
||||
title: 'Name',
|
||||
description: 'The friendly name of the worker.',
|
||||
value: worker.name,
|
||||
};
|
||||
ret.push(name);
|
||||
|
||||
const mode: Setting = {
|
||||
key: `${worker.id}:mode`,
|
||||
group,
|
||||
title: 'Mode',
|
||||
description: 'The mode of the worker.',
|
||||
value: worker.mode,
|
||||
readonly: true,
|
||||
};
|
||||
ret.push(mode);
|
||||
|
||||
|
||||
const envControl = await clusterFork.getEnvControl(worker.id);
|
||||
// catch in case env is coming from vscode launch.json and no .env actually exists.
|
||||
const dotEnv: string = await envControl.getDotEnv().catch(() => {});
|
||||
const dotEnvLines = dotEnv?.split('\n') || worker.labels;
|
||||
const dotEnvParsed = dotEnvLines.map(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('#')) {
|
||||
return { line };
|
||||
}
|
||||
const [key, ...value] = trimmed.split('=');
|
||||
return { key, value: value.join('='), line };
|
||||
});
|
||||
|
||||
const workerLabels = dotEnvParsed.find(line => line.key === 'SCRYPTED_CLUSTER_LABELS')?.value?.split(',') || [];
|
||||
|
||||
const labelChoices = new Set<string>([
|
||||
...workerLabels,
|
||||
'storage',
|
||||
'compute',
|
||||
'compute.preferred',
|
||||
'@scrypted/coreml',
|
||||
'@scrypted/openvino',
|
||||
'@scrypted/onnx',
|
||||
'@scrypted/tensorflow-lite',
|
||||
]);
|
||||
const labels: Setting = {
|
||||
key: `${worker.id}:labels`,
|
||||
group,
|
||||
title: 'Labels',
|
||||
description: 'The labels to apply to this worker. Modifying the labels will restart the worker. Some labels, such as the host OS and architecture, cannot be changed.',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
choices: [...labelChoices],
|
||||
value: workerLabels,
|
||||
};
|
||||
ret.push(labels);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: any) {
|
||||
await this.writeQueue.enqueue(async () => {
|
||||
const split = key.split(':');
|
||||
const [workerId, setting] = split;
|
||||
const workers = await sdk.clusterManager.getClusterWorkers();
|
||||
const worker = workers[workerId];
|
||||
if (!worker)
|
||||
return;
|
||||
|
||||
|
||||
switch (setting) {
|
||||
case 'name':
|
||||
case 'labels':
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
|
||||
const envControl = await clusterFork.getEnvControl(worker.id);
|
||||
const dotEnv: string = await envControl.getDotEnv().catch(() => {});
|
||||
|
||||
const dotEnvLines = dotEnv?.split('\n') || worker.labels;
|
||||
const dotEnvParsed = dotEnvLines.map(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('#')) {
|
||||
return { line };
|
||||
}
|
||||
const [key, ...value] = trimmed.split('=');
|
||||
return { key, value: value.join('='), line };
|
||||
});
|
||||
|
||||
const updateDotEnv = async (key: string, newValue: string) => {
|
||||
let entry = dotEnvParsed.find(line => line.key === key);
|
||||
if (!entry) {
|
||||
entry = { key, value: '', line: '' };
|
||||
dotEnvParsed.push(entry);
|
||||
}
|
||||
entry.line = `${key}=${newValue}`;
|
||||
await envControl.setDotEnv(dotEnvParsed.filter(line => line).map(line => line.line).join('\n'));
|
||||
};
|
||||
|
||||
if (setting === 'labels') {
|
||||
await updateDotEnv('SCRYPTED_CLUSTER_LABELS', value.join(','));
|
||||
} else if (setting === 'name') {
|
||||
await updateDotEnv('SCRYPTED_CLUSTER_WORKER_NAME', value);
|
||||
}
|
||||
setTimeout(async () => {
|
||||
const serviceControl = await clusterFork.getServiceControl(worker.id);
|
||||
await serviceControl.restart().catch(() => { });
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
return `Manage Scrypted's cluster mode. Run storage devices and compute services on separate servers.`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import sdk, { BufferConverter, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import crypto from 'crypto';
|
||||
import mime from "mime/lite";
|
||||
import path from 'path';
|
||||
|
||||
const { endpointManager } = sdk;
|
||||
@@ -47,18 +46,21 @@ export class BufferHost extends ScryptedDeviceBase implements HttpRequestHandler
|
||||
options.headers['Content-Disposition'] = 'attachment';
|
||||
}
|
||||
|
||||
response.send(file.data as Buffer, );
|
||||
response.send(file.data as Buffer,);
|
||||
}
|
||||
|
||||
async convert(buffer: string, fromMimeType: string, toMimeType: string): Promise<Buffer> {
|
||||
const uuid = uuidv4();
|
||||
|
||||
const { default: mime } = await import('mime');
|
||||
|
||||
const endpoint = await (this.secure ? endpointManager.getPublicLocalEndpoint(this.nativeId) : endpointManager.getInsecurePublicLocalEndpoint(this.nativeId));
|
||||
const extension = mime.getExtension(fromMimeType);
|
||||
|
||||
const filename = uuid + (extension ? `.${extension}` : '');
|
||||
|
||||
this.hosted.set(`/${filename}`, { data: buffer, fromMimeType, toMimeType });
|
||||
setTimeout(() => this.hosted.delete(`/${filename}`), 10 * 60 * 1000); // free this resource after 10 min.
|
||||
|
||||
return Buffer.from(`${endpoint}${filename}`);
|
||||
}
|
||||
@@ -131,6 +133,7 @@ export class RequestMediaObjectHost extends ScryptedDeviceBase implements HttpRe
|
||||
const endpoint = await (toMimeType === ScryptedMimeTypes.LocalUrl ? endpointManager.getPublicLocalEndpoint(this.nativeId) : endpointManager.getInsecurePublicLocalEndpoint(this.nativeId));
|
||||
const data = await request();
|
||||
fromMimeType = data.mimeType;
|
||||
const { default: mime } = await import('mime');
|
||||
const extension = mime.getExtension(fromMimeType);
|
||||
|
||||
const filename = uuid + (extension ? `.${extension}` : '');
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { readFileAsString, tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
|
||||
import sdk, { DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import Router from 'router';
|
||||
import yaml from 'yaml';
|
||||
import { getUsableNetworkAddresses } from '../../../server/src/ip';
|
||||
import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
|
||||
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
|
||||
import { LauncherMixin } from './launcher-mixin';
|
||||
import { MediaCore } from './media-core';
|
||||
import { checkLxcDependencies } from './platform/lxc';
|
||||
import { checkLegacyLxc, checkLxc } from './platform/lxc';
|
||||
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
|
||||
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
|
||||
const { deviceManager, endpointManager } = sdk;
|
||||
|
||||
@@ -24,6 +28,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
publicRouter: any = Router();
|
||||
mediaCore: MediaCore;
|
||||
scriptCore: ScriptCore;
|
||||
clusterCore: ClusterCore;
|
||||
aggregateCore: AggregateCore;
|
||||
automationCore: AutomationCore;
|
||||
users: UsersCore;
|
||||
@@ -48,7 +53,41 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
const service = await sdk.systemManager.getComponent('addresses');
|
||||
service.setLocalAddresses(this.localAddresses);
|
||||
},
|
||||
}
|
||||
},
|
||||
releaseChannel: {
|
||||
group: 'Advanced',
|
||||
title: 'Server Release Channel',
|
||||
description: 'The release channel to use for server updates. A specific version or tag can be manually entered as well. Changing this setting will update the image field in /root/.scrypted/docker-compose.yml. Invalid values may prevent the server from properly starting.',
|
||||
defaultValue: 'Default',
|
||||
choices: [
|
||||
'Default',
|
||||
'latest',
|
||||
'beta',
|
||||
`v${sdk.serverVersion}-jammy-full`,
|
||||
],
|
||||
combobox: true,
|
||||
onPut: (ov, nv) => {
|
||||
this.updateReleaseChannel(nv);
|
||||
},
|
||||
mapGet: () => {
|
||||
try {
|
||||
const dockerCompose = yaml.parseDocument(readFileAsString('/root/.scrypted/docker-compose.yml'));
|
||||
// @ts-ignore
|
||||
const image: string = dockerCompose.contents.get('services').get('scrypted').get('image');
|
||||
const label = image.split(':')[1] || undefined;
|
||||
return label || 'Default';
|
||||
}
|
||||
catch (e) {
|
||||
return 'Default';
|
||||
}
|
||||
}
|
||||
},
|
||||
pullImage: {
|
||||
hide: true,
|
||||
onPut: () => {
|
||||
this.setPullImage();
|
||||
}
|
||||
},
|
||||
});
|
||||
indexHtml: string;
|
||||
|
||||
@@ -59,10 +98,23 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
settings: "General",
|
||||
}
|
||||
|
||||
checkLxcDependencies();
|
||||
checkLegacyLxc();
|
||||
checkLxc();
|
||||
|
||||
this.storageSettings.settings.releaseChannel.hide = process.env.SCRYPTED_INSTALL_ENVIRONMENT !== 'lxc-docker';
|
||||
|
||||
this.indexHtml = readFileAsString('dist/index.html');
|
||||
|
||||
(async () => {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
name: 'Cluster',
|
||||
nativeId: ClusterCoreNativeId,
|
||||
interfaces: [ScryptedInterface.Settings, ScryptedInterface.Readme, ScryptedInterface.ScryptedSettings],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
})();
|
||||
(async () => {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
@@ -175,6 +227,8 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
if (nativeId === ClusterCoreNativeId)
|
||||
return this.clusterCore ||= new ClusterCore(ClusterCoreNativeId);
|
||||
if (nativeId === 'launcher')
|
||||
return new LauncherMixin('launcher');
|
||||
if (nativeId === 'mediacore')
|
||||
@@ -248,6 +302,27 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setPullImage() {
|
||||
writeFileSync(path.join(process.env.SCRYPTED_VOLUME, '.pull'), '');
|
||||
}
|
||||
|
||||
async updateReleaseChannel(releaseChannel: string) {
|
||||
if (!releaseChannel || releaseChannel === 'Default')
|
||||
releaseChannel = '';
|
||||
else
|
||||
releaseChannel = `:${releaseChannel}`;
|
||||
const dockerCompose = yaml.parseDocument(readFileAsString('/root/.scrypted/docker-compose.yml'));
|
||||
// @ts-ignore
|
||||
dockerCompose.contents.get('services').get('scrypted').set('image', `ghcr.io/koush/scrypted${releaseChannel}`);
|
||||
yaml.stringify(dockerCompose);
|
||||
writeFileSync('/root/.scrypted/docker-compose.yml', yaml.stringify(dockerCompose));
|
||||
this.setPullImage();
|
||||
|
||||
const serviceControl = await sdk.systemManager.getComponent("service-control");
|
||||
await serviceControl.exit().catch(() => { });
|
||||
await serviceControl.restart();
|
||||
}
|
||||
}
|
||||
|
||||
export default ScryptedCore;
|
||||
|
||||
@@ -1,102 +1,35 @@
|
||||
import fs from 'fs';
|
||||
import child_process from 'child_process';
|
||||
import { once } from 'events';
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { stdout } from 'process';
|
||||
|
||||
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC = 'lxc';
|
||||
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER = 'lxc-docker';
|
||||
|
||||
export async function checkLxcDependencies() {
|
||||
export async function checkLegacyLxc() {
|
||||
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC)
|
||||
return;
|
||||
|
||||
let needRestart = false;
|
||||
if (!process.version.startsWith('v20.')) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'apt update -y && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt install -y nodejs']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install Node.js 20.x.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
|
||||
if (!fs.existsSync('/var/run/avahi-daemon/socket')) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'apt update -y && apt install -y avahi-daemon && apt upgrade -y']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install avahi-daemon.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
|
||||
const scryptedService = fs.readFileSync('lxc/scrypted.service').toString();
|
||||
const installedScryptedService = fs.readFileSync('/etc/systemd/system/scrypted.service').toString();
|
||||
|
||||
if (installedScryptedService !== scryptedService) {
|
||||
fs.writeFileSync('/etc/systemd/system/scrypted.service', scryptedService);
|
||||
needRestart = true;
|
||||
|
||||
const cp = child_process.spawn('systemctl', ['daemon-reload']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to daemon-reload systemd.');
|
||||
}
|
||||
|
||||
try {
|
||||
// intel opencl icd is broken from their official apt repos on kernel versions 6.8, which ships with ubuntu 24.04 and proxmox 8.2.
|
||||
// the intel apt repo has not been updated yet.
|
||||
// the current workaround is to install the release manually.
|
||||
// https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
const output = await new Promise<string>((r,f)=> child_process.exec("sh -c 'apt show versions intel-opencl-icd'", (err, stdout, stderr) => {
|
||||
if (err && !stdout && !stderr)
|
||||
f(err);
|
||||
else
|
||||
r(stdout + '\n' + stderr);
|
||||
}));
|
||||
|
||||
if (
|
||||
// apt
|
||||
output.includes('Version: 23')
|
||||
// was installed via script at some point
|
||||
|| output.includes('Version: 24.13.29138.7')
|
||||
// current script version: 24.17.29377.6
|
||||
) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install intel-opencl-icd.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
sdk.log.a('Failed to verify/install intel-opencl-icd version.');
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await new Promise<string>((r,f)=> child_process.exec("sh -c 'apt show versions intel-driver-compiler-npu'", (err, stdout, stderr) => {
|
||||
if (err && !stdout && !stderr)
|
||||
f(err);
|
||||
else
|
||||
r(stdout + '\n' + stderr);
|
||||
}));
|
||||
|
||||
if (
|
||||
// apt
|
||||
output.includes('No packages found')
|
||||
) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install intel-driver-compiler-npu.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
sdk.log.a('Failed to verify/install intel-driver-compiler-npu.');
|
||||
}
|
||||
|
||||
if (needRestart)
|
||||
sdk.log.a('A system update is pending. Please restart Scrypted to apply changes.');
|
||||
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/installation.html#proxmox-ve-container-reset');
|
||||
}
|
||||
|
||||
const DOCKER_COMPOSE_SH_PATH = '/root/.scrypted/docker-compose.sh';
|
||||
const LXC_DOCKER_COMPOSE_SH_PATH = 'lxc/docker-compose.sh';
|
||||
|
||||
export async function checkLxc() {
|
||||
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER)
|
||||
return;
|
||||
|
||||
const foundDockerComposeSh = await fs.promises.readFile(DOCKER_COMPOSE_SH_PATH, 'utf8');
|
||||
const dockerComposeSh = await fs.promises.readFile(LXC_DOCKER_COMPOSE_SH_PATH, 'utf8');
|
||||
|
||||
if (foundDockerComposeSh === dockerComposeSh) {
|
||||
// check if the file is executable
|
||||
const stats = await fs.promises.stat(DOCKER_COMPOSE_SH_PATH);
|
||||
if (stats.mode & 0o111)
|
||||
return;
|
||||
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.promises.copyFile(LXC_DOCKER_COMPOSE_SH_PATH, DOCKER_COMPOSE_SH_PATH);
|
||||
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,13 @@ export class PluginSocketService extends ScryptedDeviceBase implements StreamSer
|
||||
throw new Error('must provide pluginId');
|
||||
|
||||
const plugins = await sdk.systemManager.getComponent('plugins');
|
||||
const replPort: number = await plugins.getRemoteServicePort(pluginId, this.serviceName);
|
||||
const servicePort = await plugins.getRemoteServicePort(pluginId, this.serviceName) as number | [number, string];
|
||||
const [port, host] = Array.isArray(servicePort) ? servicePort : [servicePort, undefined];
|
||||
|
||||
const socket = net.connect(replPort);
|
||||
const socket = net.connect({
|
||||
port,
|
||||
host,
|
||||
});
|
||||
await once(socket, 'connect');
|
||||
|
||||
const queue = createAsyncQueue<Buffer>();
|
||||
|
||||
@@ -206,14 +206,7 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
if (parsed.interactive) {
|
||||
let spawn: typeof ptySpawn;
|
||||
try {
|
||||
try {
|
||||
spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
|
||||
if (!spawn)
|
||||
throw new Error();
|
||||
}
|
||||
catch (e) {
|
||||
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
|
||||
}
|
||||
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
|
||||
cp = new InteractiveTerminal(cmd, extraPaths, spawn);
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
2
plugins/coreml/.vscode/settings.json
vendored
2
plugins/coreml/.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
68
plugins/coreml/package-lock.json
generated
68
plugins/coreml/package-lock.json
generated
@@ -1,36 +1,35 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.70",
|
||||
"version": "0.1.76",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.70",
|
||||
"version": "0.1.76",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.31",
|
||||
"version": "0.3.77",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -42,11 +41,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -61,25 +60,24 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,18 @@
|
||||
"interfaces": [
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"ClusterForkInterface",
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"@scrypted/coreml"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.70"
|
||||
"version": "0.1.76"
|
||||
}
|
||||
|
||||
@@ -69,9 +69,13 @@ def parse_labels(userDefined):
|
||||
return parse_label_contents(classes)
|
||||
|
||||
|
||||
class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
class CoreMLPlugin(
|
||||
PredictPlugin,
|
||||
scrypted_sdk.Settings,
|
||||
scrypted_sdk.DeviceProvider,
|
||||
):
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
@@ -139,7 +143,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
@@ -148,6 +154,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "CoreML Face Recognition",
|
||||
@@ -160,6 +167,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "CoreML Text Recognition",
|
||||
@@ -176,10 +184,10 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
|
||||
async def getDevice(self, nativeId: str) -> Any:
|
||||
if nativeId == "facerecognition":
|
||||
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(nativeId)
|
||||
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(self, nativeId)
|
||||
return self.faceDevice
|
||||
if nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or CoreMLTextRecognition(nativeId)
|
||||
self.textDevice = self.textDevice or CoreMLTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
|
||||
@@ -227,7 +235,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
return ret
|
||||
|
||||
if self.scrypted_yolo_nas:
|
||||
predictions = list(out_dict.values())
|
||||
predictions = list(out_dict.values())
|
||||
objs = yolo.parse_yolo_nas(predictions)
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
return ret
|
||||
@@ -250,13 +258,13 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
|
||||
for r in objects:
|
||||
obj = Prediction(
|
||||
r["classId"].astype(float),
|
||||
r["confidence"].astype(float),
|
||||
r["classId"],
|
||||
r["confidence"],
|
||||
Rectangle(
|
||||
r["xmin"].astype(float),
|
||||
r["ymin"].astype(float),
|
||||
r["xmax"].astype(float),
|
||||
r["ymax"].astype(float),
|
||||
r["xmin"],
|
||||
r["ymin"],
|
||||
r["xmax"],
|
||||
r["ymax"],
|
||||
),
|
||||
)
|
||||
objs.append(obj)
|
||||
@@ -275,9 +283,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
),
|
||||
)
|
||||
|
||||
coordinatesList = out_dict["coordinates"].astype(float)
|
||||
coordinatesList = out_dict["coordinates"]
|
||||
|
||||
for index, confidenceList in enumerate(out_dict["confidence"].astype(float)):
|
||||
for index, confidenceList in enumerate(out_dict["confidence"]):
|
||||
values = confidenceList
|
||||
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
|
||||
maxConfidence = confidenceList[maxConfidenceIndex]
|
||||
|
||||
@@ -29,8 +29,8 @@ def cosine_similarity(vector_a, vector_b):
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
|
||||
|
||||
class CoreMLFaceRecognition(FaceRecognizeDetection):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin, nativeId)
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-face")
|
||||
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-face")
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from predict.text_recognize import TextRecognition
|
||||
|
||||
|
||||
class CoreMLTextRecognition(TextRecognition):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin, nativeId)
|
||||
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-text")
|
||||
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-text")
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from coreml import CoreMLPlugin
|
||||
import predict
|
||||
|
||||
def create_scrypted_plugin():
|
||||
return CoreMLPlugin()
|
||||
|
||||
async def fork():
|
||||
return predict.Fork(CoreMLPlugin)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
|
||||
numpy==1.26.4
|
||||
coremltools==7.2
|
||||
coremltools==8.0
|
||||
Pillow==10.3.0
|
||||
opencv-python==4.10.0.84
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
5
plugins/diagnostics/.gitignore
vendored
Normal file
5
plugins/diagnostics/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
.venv
|
||||
9
plugins/diagnostics/.npmignore
Normal file
9
plugins/diagnostics/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
*.map
|
||||
fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
.venv
|
||||
23
plugins/diagnostics/.vscode/launch.json
vendored
Normal file
23
plugins/diagnostics/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
plugins/diagnostics/.vscode/settings.json
vendored
Normal file
4
plugins/diagnostics/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "koushik-winvm",
|
||||
}
|
||||
20
plugins/diagnostics/.vscode/tasks.json
vendored
Normal file
20
plugins/diagnostics/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
3
plugins/diagnostics/README.md
Normal file
3
plugins/diagnostics/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Scrypted Diagnostics Plugin
|
||||
|
||||
This plugin can be used to run diagnostics on the system and supported devices. The results from the diagnostics can be seen in the `Log`.
|
||||
848
plugins/diagnostics/package-lock.json
generated
Normal file
848
plugins/diagnostics/package-lock.json
generated
Normal file
@@ -0,0 +1,848 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.69",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
|
||||
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"semver": "^7.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-darwin-x64": "0.33.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
||||
"@img/sharp-linux-arm": "0.33.5",
|
||||
"@img/sharp-linux-arm64": "0.33.5",
|
||||
"@img/sharp-linux-s390x": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
||||
"@img/sharp-wasm32": "0.33.5",
|
||||
"@img/sharp-win32-ia32": "0.33.5",
|
||||
"@img/sharp-win32-x64": "0.33.5"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
|
||||
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-darwin-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
|
||||
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
|
||||
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-linux-arm": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5"
|
||||
}
|
||||
},
|
||||
"@img/sharp-linux-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-linux-s390x": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
|
||||
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-linux-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||
}
|
||||
},
|
||||
"@img/sharp-wasm32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
|
||||
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@emnapi/runtime": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@img/sharp-win32-ia32": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
|
||||
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@img/sharp-win32-x64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
|
||||
"optional": true
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "22.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="
|
||||
},
|
||||
"sharp": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
|
||||
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
|
||||
"requires": {
|
||||
"@img/sharp-darwin-arm64": "0.33.5",
|
||||
"@img/sharp-darwin-x64": "0.33.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.0.5",
|
||||
"@img/sharp-libvips-linux-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.0.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
|
||||
"@img/sharp-linux-arm": "0.33.5",
|
||||
"@img/sharp-linux-arm64": "0.33.5",
|
||||
"@img/sharp-linux-s390x": "0.33.5",
|
||||
"@img/sharp-linux-x64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.33.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.33.5",
|
||||
"@img/sharp-wasm32": "0.33.5",
|
||||
"@img/sharp-win32-ia32": "0.33.5",
|
||||
"@img/sharp-win32-x64": "0.33.5",
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.3",
|
||||
"semver": "^7.6.3"
|
||||
}
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"requires": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
|
||||
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
|
||||
"optional": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
37
plugins/diagnostics/package.json
Normal file
37
plugins/diagnostics/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
"build": "scrypted-webpack",
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"diagnostics"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Diagnostics",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"Settings"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
}
|
||||
}
|
||||
565
plugins/diagnostics/src/main.ts
Normal file
565
plugins/diagnostics/src/main.ts
Normal file
@@ -0,0 +1,565 @@
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
|
||||
import sdk, { Camera, FFmpegInput, Image, MediaObject, MediaStreamDestination, MotionSensor, Notifier, ObjectDetection, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import child_process from 'child_process';
|
||||
import { once } from 'events';
|
||||
import fs from 'fs';
|
||||
import net from 'net';
|
||||
import os from 'os';
|
||||
import sharp from 'sharp';
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
testDevice: {
|
||||
group: 'Device',
|
||||
title: 'Validation Device',
|
||||
description: "Select a device to validate.",
|
||||
type: 'device',
|
||||
deviceFilter: `type === '${ScryptedDeviceType.Camera}' || type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Notifier}'`,
|
||||
immediate: true,
|
||||
},
|
||||
validateDevice: {
|
||||
console: true,
|
||||
group: 'Device',
|
||||
title: 'Validate Device',
|
||||
description: 'Validate the device configuration.',
|
||||
type: 'button',
|
||||
onPut: async () => {
|
||||
this.validateDevice();
|
||||
},
|
||||
},
|
||||
validateSystem: {
|
||||
console: true,
|
||||
group: 'System',
|
||||
title: 'Validate System',
|
||||
description: 'Validate the system configuration.',
|
||||
type: 'button',
|
||||
onPut: () => this.validateSystem(),
|
||||
},
|
||||
});
|
||||
|
||||
loggedMotion = new Map<string, number>();
|
||||
loggedButton = new Map<string, number>();
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
this.on = this.on || false;
|
||||
|
||||
sdk.systemManager.listen((eventSource, eventDetails, eventData) => {
|
||||
if (!eventData || !eventSource?.id)
|
||||
return;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor) {
|
||||
this.loggedMotion.set(eventSource.id, Date.now());
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor) {
|
||||
this.loggedButton.set(eventSource.id, Date.now());
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: any) {
|
||||
await this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
warnStep(console: Console, result: string) {
|
||||
console.log(''.padEnd(24), `\x1b[33m${result}\x1b[0m`);
|
||||
}
|
||||
|
||||
async validate(console: Console, stepName: string, step: Promise<any> | (() => Promise<any>)) {
|
||||
try {
|
||||
if (step instanceof Function)
|
||||
step = step();
|
||||
console.log(stepName.padEnd(24), '\x1b[34mRunning\x1b[0m');
|
||||
const result = await step;
|
||||
console.log(''.padEnd(24), `\x1b[32m${result || 'OK'}\x1b[0m`);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(stepName.padEnd(24), '\x1b[31m Failed\x1b[0m'.padEnd(24), (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
async validateDevice() {
|
||||
const device = this.storageSettings.values.testDevice as ScryptedDevice & any;
|
||||
const console = sdk.deviceManager.getMixinConsole(device.id);
|
||||
|
||||
console.log(''.padEnd(44, '='));
|
||||
console.log(`Device Validation: ${device?.name}`);
|
||||
console.log(''.padEnd(44, '='));
|
||||
|
||||
await this.validate(console, 'Device Selected', async () => {
|
||||
if (!device)
|
||||
throw new Error('Select a device in the Settings UI.');
|
||||
});
|
||||
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
if (device.type === ScryptedDeviceType.Camera || device.type === ScryptedDeviceType.Doorbell) {
|
||||
await this.validateCamera(device);
|
||||
}
|
||||
else if (device.type === ScryptedDeviceType.Notifier) {
|
||||
await this.validateNotifier(device);
|
||||
}
|
||||
|
||||
console.log(''.padEnd(44, '='));
|
||||
console.log(`Device Validation Complete: ${device?.name}`);
|
||||
console.log(''.padEnd(44, '='));
|
||||
}
|
||||
|
||||
async validateNotifier(device: ScryptedDevice & Notifier) {
|
||||
const console = sdk.deviceManager.getMixinConsole(device.id);
|
||||
|
||||
await this.validate(console, 'Test Notification', async () => {
|
||||
const logo = await httpFetch({
|
||||
url: 'https://home.scrypted.app/_punch/web_hi_res_512.png',
|
||||
responseType: 'buffer',
|
||||
});
|
||||
|
||||
const mo = await sdk.mediaManager.createMediaObject(logo.body, 'image/png');
|
||||
await device.sendNotification('Scrypted Diagnostics', {
|
||||
body: 'Body',
|
||||
subtitle: 'Subtitle',
|
||||
android: {
|
||||
channel: 'diagnostics',
|
||||
}
|
||||
}, mo);
|
||||
|
||||
this.warnStep(console, 'Check the device for the notification.');
|
||||
});
|
||||
}
|
||||
|
||||
async validateCamera(device: ScryptedDevice & Camera & VideoCamera & MotionSensor) {
|
||||
const console = sdk.deviceManager.getMixinConsole(device.id);
|
||||
|
||||
await this.validate(console, 'Device Capabilities', async () => {
|
||||
if (!device.interfaces.includes(ScryptedInterface.MotionSensor))
|
||||
throw new Error('Motion Sensor not found.');
|
||||
|
||||
if (device.type === ScryptedDeviceType.Doorbell && !device.interfaces.includes(ScryptedInterface.BinarySensor))
|
||||
throw new Error('Doorbell button not found.');
|
||||
});
|
||||
|
||||
await this.validate(console, 'Motion Detection', async () => {
|
||||
if (!device.interfaces.includes(ScryptedInterface.MotionSensor))
|
||||
throw new Error('Motion Sensor not found. Enabling a software motion sensor extension is recommended.');
|
||||
|
||||
if (device.providedInterfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
if (device.interfaces.find(i => i.startsWith('ObjectDetection:true')))
|
||||
this.warnStep(console, 'Camera hardware provides motion events, but a software motion detector is enabled. Consider disabling the software motion detector.');
|
||||
}
|
||||
});
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
|
||||
await this.validate(console, 'Recent Motion', async () => {
|
||||
|
||||
const lastMotion = this.loggedMotion.get(device.id);
|
||||
if (!lastMotion)
|
||||
throw new Error('No recent motion detected. Go wave your hand in front of the camera.');
|
||||
if (Date.now() - lastMotion > 8 * 60 * 60 * 1000)
|
||||
throw new Error('Last motion was over 8 hours ago.');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (device.type === ScryptedDeviceType.Doorbell) {
|
||||
await this.validate(console, 'Recent Button Press', async () => {
|
||||
const lastButton = this.loggedButton.get(device.id);
|
||||
if (!lastButton)
|
||||
throw new Error('No recent button press detected. Go press the doorbell button.');
|
||||
if (Date.now() - lastButton > 8 * 60 * 60 * 1000)
|
||||
throw new Error('Last button press was over 8 hours ago.');
|
||||
});
|
||||
}
|
||||
|
||||
const validateMedia = async (stepName: string, mo: Promise<MediaObject>, snapshot = false, and?: () => Promise<void>) => {
|
||||
await this.validate(console, stepName, async () => {
|
||||
if (snapshot && !device.interfaces.includes(ScryptedInterface.Camera))
|
||||
throw new Error('Snapshot not supported. Enable the Snapshot extension.');
|
||||
const jpeg = await sdk.mediaManager.convertMediaObjectToBuffer(await mo, 'image/jpeg');
|
||||
const metadata = await sharp(jpeg).metadata();
|
||||
if (!metadata.width || !metadata.height || metadata.width < 100 || metadata.height < 100)
|
||||
throw new Error('Malformed image.');
|
||||
if (!and && snapshot && device.pluginId === '@scrypted/unifi-protect' && metadata.width < 1280)
|
||||
this.warnStep(console, 'Unifi Protect provides low quality snapshots. Consider using Snapshot from Prebuffer for full resolution screenshots.');
|
||||
await and?.();
|
||||
});
|
||||
};
|
||||
|
||||
await validateMedia('Snapshot', device.takePicture({
|
||||
reason: 'event',
|
||||
}), true);
|
||||
|
||||
await this.validate(console, 'Streams', async () => {
|
||||
const vsos = await device.getVideoStreamOptions();
|
||||
|
||||
if (!vsos?.length)
|
||||
throw new Error('Stream configuration invalid.');
|
||||
|
||||
if (vsos.length < 3)
|
||||
this.warnStep(console, `Camera has ${vsos.length} substream. Three streams are recommended.`);
|
||||
|
||||
const cloudStreams = vsos.filter(vso => vso.source === 'cloud');
|
||||
if (cloudStreams.length)
|
||||
this.warnStep(console, `Cloud camera. Upgrade recommended.`);
|
||||
|
||||
const usedStreams = vsos.filter(vso => vso.destinations?.length);
|
||||
if (usedStreams.length < Math.min(3, vsos.length))
|
||||
this.warnStep(console, `Unused streams detected.`);
|
||||
});
|
||||
|
||||
const getVideoStream = async (destination: MediaStreamDestination) => {
|
||||
if (!device.interfaces.includes(ScryptedInterface.VideoCamera))
|
||||
throw new Error('Streaming not supported.');
|
||||
return await device.getVideoStream({
|
||||
destination,
|
||||
prebuffer: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const validated = new Set<string | undefined>();
|
||||
const validateMediaStream = async (stepName: string, destination: MediaStreamDestination) => {
|
||||
const vsos = await device.getVideoStreamOptions();
|
||||
const streamId = vsos.find(vso => vso.destinations?.includes(destination))?.id;
|
||||
|
||||
if (validated.has(streamId)) {
|
||||
await this.validate(console, stepName, async () => "Skipped (Duplicate)");
|
||||
return;
|
||||
}
|
||||
|
||||
validated.add(streamId);
|
||||
|
||||
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await getVideoStream(destination), ScryptedMimeTypes.FFmpegInput);
|
||||
if (ffmpegInput.mediaStreamOptions?.video?.codec !== 'h264')
|
||||
this.warnStep(console, `Stream ${stepName} is using codec ${ffmpegInput.mediaStreamOptions?.video?.codec}. h264 is recommended.`);
|
||||
|
||||
await validateMedia(stepName, getVideoStream(destination));
|
||||
const start = Date.now();
|
||||
await validateMedia(stepName + ' (IDR)', getVideoStream(destination), false, async () => {
|
||||
const end = Date.now();
|
||||
if (end - start > 5000)
|
||||
throw new Error(`High IDR Interval. This may cause issues with HomeKit Secure Video. Adjust codec configuration if possible.`);
|
||||
});
|
||||
};
|
||||
|
||||
await validateMediaStream('Local', 'local');
|
||||
|
||||
await validateMediaStream('Local Recorder', 'local-recorder');
|
||||
|
||||
await validateMediaStream('Remote Recorder', 'remote-recorder');
|
||||
|
||||
await validateMediaStream('Remote', 'remote');
|
||||
|
||||
await validateMediaStream('Low Resolution', 'low-resolution');
|
||||
|
||||
await this.validate(console, 'Audio Codecs', async () => {
|
||||
const vsos = await device.getVideoStreamOptions();
|
||||
|
||||
let codec: string | undefined;
|
||||
const codecs = new Set<string>();
|
||||
for (const vso of vsos) {
|
||||
if (vso.audio?.codec) {
|
||||
codec = vso.audio.codec;
|
||||
codecs.add(vso.audio.codec);
|
||||
}
|
||||
}
|
||||
|
||||
if (codecs.size > 1) {
|
||||
this.warnStep(console, `Mismatched audio codecs detected.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!codec)
|
||||
return;
|
||||
|
||||
if (codec !== 'pcm_mulaw' && codec !== 'aac' && codec !== 'opus') {
|
||||
this.warnStep(console, `Audio codec is ${codec}. pcm_mulaw, aac, or opus is recommended.`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async validateSystem() {
|
||||
this.console.log(''.padEnd(44, '='));
|
||||
this.console.log('System Validation');
|
||||
this.console.log(''.padEnd(44, '='));
|
||||
|
||||
const nvrPlugin = sdk.systemManager.getDeviceById('@scrypted/nvr');
|
||||
const cloudPlugin = sdk.systemManager.getDeviceById('@scrypted/cloud');
|
||||
const hasCUDA = process.env.NVIDIA_VISIBLE_DEVICES && process.env.NVIDIA_DRIVER_CAPABILITIES;
|
||||
const onnxPlugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>('@scrypted/onnx');
|
||||
const openvinoPlugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>('@scrypted/openvino');
|
||||
|
||||
await this.validate(this.console, 'Scrypted Installation', async () => {
|
||||
const e = process.env.SCRYPTED_INSTALL_ENVIRONMENT;
|
||||
if (process.platform !== 'linux') {
|
||||
if (e !== 'electron')
|
||||
this.warnStep(this.console, 'Upgrading to the Scrypted Desktop application is recommened for Windows and macOS.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (e !== 'docker' && e !== 'lxc' && e !== 'ha' && e !== 'lxc-docker')
|
||||
throw new Error('Unrecognized Linux installation. Installation via Docker image or the official Proxmox LXC script (not tteck) is recommended: https://docs.scrypted.app/installation');
|
||||
});
|
||||
|
||||
await this.validate(this.console, 'IPv4 (jsonip.com)', httpFetch({
|
||||
url: 'https://jsonip.com',
|
||||
family: 4,
|
||||
responseType: 'json',
|
||||
timeout: 5000,
|
||||
}).then(r => r.body.ip));
|
||||
|
||||
await this.validate(this.console, 'IPv6 (jsonip.com)', httpFetch({
|
||||
url: 'https://jsonip.com',
|
||||
family: 6,
|
||||
responseType: 'json',
|
||||
timeout: 5000,
|
||||
}).then(r => r.body.ip));
|
||||
|
||||
await this.validate(this.console, 'IPv4 (wtfismyip.com)', httpFetch({
|
||||
url: 'https://wtfismyip.com/text',
|
||||
family: 4,
|
||||
responseType: 'text',
|
||||
timeout: 5000,
|
||||
}).then(r => r.body.trim()));
|
||||
|
||||
await this.validate(this.console, 'IPv6 (wtfismyip.com)', httpFetch({
|
||||
url: 'https://wtfismyip.com/text',
|
||||
family: 6,
|
||||
responseType: 'text',
|
||||
timeout: 5000,
|
||||
}).then(r => r.body.trim()));
|
||||
|
||||
await this.validate(this.console, 'Scrypted Server Address', async () => {
|
||||
const addresses = await sdk.endpointManager.getLocalAddresses();
|
||||
const hasIPv4 = addresses?.find(address => net.isIPv4(address));
|
||||
const hasIPv6 = addresses?.find(address => net.isIPv6(address));
|
||||
if (addresses?.length)
|
||||
this.warnStep(this.console, addresses.join(', '));
|
||||
if (!hasIPv4)
|
||||
throw new Error('Scrypted Settings IPv4 address not set.');
|
||||
if (!hasIPv6)
|
||||
throw new Error('Scrypted Settings IPv6 address not set.');
|
||||
});
|
||||
|
||||
await this.validate(this.console, 'CPU Count', async () => {
|
||||
if (os.cpus().length < 2)
|
||||
throw new Error('CPU Count is too low. 4 CPUs are recommended.');
|
||||
return os.cpus().length;
|
||||
});
|
||||
|
||||
await this.validate(this.console, 'Memory', async () => {
|
||||
if (!nvrPlugin) {
|
||||
if (os.totalmem() < 8 * 1024 * 1024 * 1024)
|
||||
throw new Error('Memory is too low. 8GB is recommended.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (os.totalmem() < 14 * 1024 * 1024 * 1024)
|
||||
throw new Error('Memory is too low. 16GB is recommended for NVR.');
|
||||
return Math.floor(os.totalmem() / 1024 / 1024 / 1024) + " GB";
|
||||
});
|
||||
|
||||
if (process.platform === 'linux' && nvrPlugin) {
|
||||
// ensure /dev/dri/renderD128 or /dev/dri/renderD129 is available
|
||||
await this.validate(this.console, 'GPU Passthrough', async () => {
|
||||
if (!fs.existsSync('/dev/dri/renderD128') && !fs.existsSync('/dev/dri/renderD129'))
|
||||
throw new Error('GPU device unvailable or not passed through to container. (/dev/dri/renderD128, /dev/dri/renderD129)');
|
||||
// also check /dev/kfd for AMD CPU
|
||||
const amdCPU = os.cpus().find(c => c.model.includes('AMD'));
|
||||
if (amdCPU && !fs.existsSync('/dev/kfd'))
|
||||
throw new Error('GPU device unvailable or not passed through to container. (/dev/kfd)');
|
||||
});
|
||||
}
|
||||
|
||||
await this.validate(this.console, 'Cloud Plugin', async () => {
|
||||
if (!cloudPlugin) {
|
||||
this.warnStep(this.console, 'Cloud plugin not installed. Consider installing for remote access.');
|
||||
return;
|
||||
}
|
||||
|
||||
const logo = await httpFetch({
|
||||
url: 'https://home.scrypted.app/_punch/web_hi_res_512.png',
|
||||
responseType: 'buffer',
|
||||
});
|
||||
|
||||
const mo = await sdk.mediaManager.createMediaObject(logo.body, 'image/png');
|
||||
const url = await sdk.mediaManager.convertMediaObjectToUrl(mo, 'image/png');
|
||||
|
||||
const logoCheck = await httpFetch({
|
||||
url,
|
||||
responseType: 'buffer',
|
||||
});
|
||||
|
||||
if (Buffer.compare(logo.body, logoCheck.body))
|
||||
throw new Error('Invalid response received.');
|
||||
|
||||
const shortUrl: any = await sdk.mediaManager.convertMediaObject(mo, ScryptedMimeTypes.Url + ";short-lived=true");
|
||||
const shortLogoCheck = await httpFetch({
|
||||
url: shortUrl.toString(),
|
||||
responseType: 'buffer',
|
||||
});
|
||||
|
||||
if (Buffer.compare(logo.body, shortLogoCheck.body))
|
||||
throw new Error('Invalid response received from short lived URL.');
|
||||
});
|
||||
|
||||
if ((hasCUDA || process.platform === 'win32') && onnxPlugin) {
|
||||
await this.validate(this.console, 'ONNX Plugin', async () => {
|
||||
const settings = await onnxPlugin.getSettings();
|
||||
const executionDevice = settings.find(s => s.key === 'execution_device');
|
||||
if (executionDevice?.value?.toString().includes('CPU'))
|
||||
this.warnStep(this.console, 'GPU device unvailable or not passed through to container.');
|
||||
|
||||
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
|
||||
const detected = await onnxPlugin.detectObjects(zidane);
|
||||
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
|
||||
if (!personFound)
|
||||
throw new Error('Person not detected in test image.');
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasCUDA && openvinoPlugin && (process.platform !== 'win32' || !onnxPlugin)) {
|
||||
await this.validate(this.console, 'OpenVINO Plugin', async () => {
|
||||
const settings = await openvinoPlugin.getSettings();
|
||||
const availbleDevices = settings.find(s => s.key === 'available_devices');
|
||||
if (!availbleDevices?.value?.toString().includes('GPU'))
|
||||
this.warnStep(this.console, 'GPU device unvailable or not passed through to container.');
|
||||
|
||||
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
|
||||
const detected = await openvinoPlugin.detectObjects(zidane);
|
||||
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
|
||||
if (!personFound)
|
||||
throw new Error('Person not detected in test image.');
|
||||
});
|
||||
}
|
||||
|
||||
if (nvrPlugin) {
|
||||
await this.validate(this.console, "GPU Decode", async () => {
|
||||
const ffmpegPath = await sdk.mediaManager.getFFmpegPath();
|
||||
let hasVaapi = false;
|
||||
{
|
||||
const args = [
|
||||
'-y',
|
||||
'-hwaccel', 'auto',
|
||||
'-i', 'https://github.com/koush/scrypted-sample-cameraprovider/raw/main/fs/dog.mp4',
|
||||
'-f', 'rawvideo',
|
||||
'pipe:3',
|
||||
];
|
||||
const cp = child_process.spawn(ffmpegPath, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
const deferred = new Deferred<void>();
|
||||
deferred.promise.catch(() => { }).finally(() => safeKillFFmpeg(cp));
|
||||
cp.stdio[3]?.on('data', () => { });
|
||||
|
||||
cp.stderr!.on('data', data => {
|
||||
const str = data.toString();
|
||||
hasVaapi ||= str.includes('Using auto hwaccel type vaapi');
|
||||
|
||||
if (str.includes('nv12'))
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
deferred.reject(new Error('GPU Decode timed out.'));
|
||||
}, 5000);
|
||||
|
||||
await deferred.promise;
|
||||
}
|
||||
|
||||
if (!hasVaapi || !openvinoPlugin)
|
||||
return;
|
||||
|
||||
{
|
||||
const args = [
|
||||
'-y',
|
||||
'-init_hw_device', 'vaapi=renderD128:/dev/dri/renderD128',
|
||||
'-hwaccel', 'vaapi',
|
||||
'-hwaccel_output_format', 'vaapi',
|
||||
'-i', 'https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg',
|
||||
'-vf', 'format=nv12,hwupload,scale_vaapi=w=320:h=-2,hwdownload,format=nv12',
|
||||
'-f', 'mjpeg',
|
||||
'-frames:v', '1',
|
||||
'pipe:3',
|
||||
];
|
||||
|
||||
const cp = child_process.spawn(ffmpegPath, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
let std = '';
|
||||
cp.stderr!.on('data', data => {
|
||||
std += data.toString();
|
||||
});
|
||||
|
||||
cp.stdout!.on('data', data => {
|
||||
std += data.toString();
|
||||
});
|
||||
|
||||
const buffers: Buffer[] = [];
|
||||
cp.stdio[3]?.on('data', buffer => {
|
||||
buffers.push(buffer);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
safeKillFFmpeg(cp)
|
||||
}, 5000);
|
||||
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode) {
|
||||
this.warnStep(this.console, std);
|
||||
throw new Error('GPU Transform failed.');
|
||||
}
|
||||
|
||||
const jpeg = Buffer.concat(buffers);
|
||||
const zidane = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
|
||||
const image = await sdk.mediaManager.convertMediaObject<Image>(zidane, ScryptedMimeTypes.Image);
|
||||
if (image.width !== 320)
|
||||
throw new Error('Unexpected image with from GPU transform.')
|
||||
const detected = await openvinoPlugin.detectObjects(zidane);
|
||||
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
|
||||
if (!personFound)
|
||||
throw new Error('Person not detected in test image.');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
await this.validate(this.console, 'Deprecated Plugins', async () => {
|
||||
const defunctPlugins = [
|
||||
'@scrypted/electron-core',
|
||||
'@scrypted/opencv',
|
||||
'@scrypted/python-codecs',
|
||||
'@scrypted/pam-diff',
|
||||
];
|
||||
let found = false;
|
||||
|
||||
for (const plugin of defunctPlugins) {
|
||||
const pluginDevice = sdk.systemManager.getDeviceById(plugin);
|
||||
if (pluginDevice) {
|
||||
this.warnStep(this.console, `Scrypted NVR users can remove: ${plugin}`);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (found)
|
||||
throw new Error('Deprecated plugins found.');
|
||||
});
|
||||
}
|
||||
|
||||
this.console.log(''.padEnd(44, '='));
|
||||
this.console.log('System Validation Complete');
|
||||
this.console.log(''.padEnd(44, '='));
|
||||
}
|
||||
}
|
||||
|
||||
export default DiagnosticsPlugin;
|
||||
14
plugins/diagnostics/tsconfig.json
Normal file
14
plugins/diagnostics/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-server",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
4
plugins/google-device-access/package-lock.json
generated
4
plugins/google-device-access/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/google-device-access",
|
||||
"version": "0.0.98",
|
||||
"version": "0.0.99",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/google-device-access",
|
||||
"version": "0.0.98",
|
||||
"version": "0.0.99",
|
||||
"dependencies": {
|
||||
"@googleapis/smartdevicemanagement": "^1.0.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -49,5 +49,5 @@
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.4.1"
|
||||
},
|
||||
"version": "0.0.98"
|
||||
"version": "0.0.99"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
@@ -111,6 +111,12 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
hide: true,
|
||||
description: 'The last home hub to request a recording. Internally used to determine if a streaming request is coming from remote wifi.',
|
||||
},
|
||||
autoAdd: {
|
||||
title: "Auto enable",
|
||||
description: "Automatically enable this mixin on new devices.",
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
mergedDevices = new Set<string>();
|
||||
|
||||
@@ -218,7 +224,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
|
||||
try {
|
||||
const mixins = (device.mixins || []).slice();
|
||||
if (!mixins.includes(this.id)) {
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
if (!mixins.includes(this.id) && autoAdd) {
|
||||
// don't sync this by default, as it's solely for automations
|
||||
if (device.type === ScryptedDeviceType.Notifier)
|
||||
continue;
|
||||
|
||||
2
plugins/mqtt/.vscode/settings.json
vendored
2
plugins/mqtt/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
4
plugins/mqtt/package-lock.json
generated
4
plugins/mqtt/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/mqtt",
|
||||
"version": "0.0.82",
|
||||
"version": "0.0.86",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/mqtt",
|
||||
"version": "0.0.82",
|
||||
"version": "0.0.86",
|
||||
"dependencies": {
|
||||
"aedes": "^0.46.1",
|
||||
"axios": "^0.23.0",
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"@types/node": "^18.4.2",
|
||||
"@types/nunjucks": "^3.2.0"
|
||||
},
|
||||
"version": "0.0.82"
|
||||
"version": "0.0.86"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,63 @@
|
||||
import type { MqttClient } from "./mqtt-client";
|
||||
import type { ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
import type { MqttClient, MqttEvent, MqttSubscriptions } from "./mqtt-client";
|
||||
|
||||
declare const device: ScryptedDeviceBase;
|
||||
export declare const mqtt: MqttClient;
|
||||
|
||||
export function createSensor(options: {
|
||||
type: string,
|
||||
topic: string,
|
||||
when: (message: MqttEvent) => boolean;
|
||||
set: (value: boolean) => void,
|
||||
delay?: number;
|
||||
}) {
|
||||
const subscriptions: MqttSubscriptions = {};
|
||||
let timeout: NodeJS.Timeout;
|
||||
subscriptions[options.topic] = message => {
|
||||
const detected = options.when(message);
|
||||
|
||||
if (!options.delay) {
|
||||
options.set(detected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!detected)
|
||||
return;
|
||||
|
||||
options.set(true);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => options.set(false), options.delay * 1000);
|
||||
};
|
||||
|
||||
mqtt.subscribe(subscriptions);
|
||||
|
||||
mqtt.handleTypes(options.type);
|
||||
}
|
||||
|
||||
export function createMotionSensor(options: {
|
||||
topic: string,
|
||||
when: (message: MqttEvent) => boolean;
|
||||
delay?: number;
|
||||
}) {
|
||||
return createSensor({
|
||||
type: "MotionSensor",
|
||||
topic: options.topic,
|
||||
set: (value: boolean) => device.motionDetected = value,
|
||||
when: options.when,
|
||||
delay: options.delay,
|
||||
})
|
||||
}
|
||||
|
||||
export function createBinarySensor(options: {
|
||||
topic: string,
|
||||
when: (message: MqttEvent) => boolean;
|
||||
delay?: number;
|
||||
}) {
|
||||
return createSensor({
|
||||
type: "BinarySensor",
|
||||
topic: options.topic,
|
||||
set: (value: boolean) => device.binaryState = value,
|
||||
when: options.when,
|
||||
delay: options.delay,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ScriptDeviceImpl, scryptedEval as scryptedEvalBase } from "@scrypted/co
|
||||
|
||||
const util = require("!!raw-loader!./api/util.ts").default;
|
||||
const libs = {
|
||||
util,
|
||||
util: util.replace('export', ''),
|
||||
};
|
||||
|
||||
export async function scryptedEval(device: ScryptedDeviceBase, script: string, params: { [name: string]: any }) {
|
||||
|
||||
2
plugins/objectdetector/.vscode/settings.json
vendored
2
plugins/objectdetector/.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
@@ -13,4 +13,8 @@ benefits to HomeKit, which does its own detection processing.
|
||||
|
||||
## Smart Motion Sensors
|
||||
|
||||
This plugin can be used to create smart motion sensors that trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This feature requires cameras with hardware or software object detection capability.
|
||||
This plugin can be used to create smart motion sensors that trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires cameras with hardware or software object detection capability.
|
||||
|
||||
## Smart Occupancy Sensors
|
||||
|
||||
This plugin can be used to create smart occupancy sensors remains triggered when a specific type of object (vehicle, person, animal, etc) is detected on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.43",
|
||||
"version": "0.1.60",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.43",
|
||||
"version": "0.1.60",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.43",
|
||||
"version": "0.1.60",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { CpuInfo, cpus } from 'os';
|
||||
|
||||
function getIdleTotal(cpu: CpuInfo) {
|
||||
const t = cpu.times;
|
||||
const total = t.user + t.nice + t.sys + t.idle + t.irq;
|
||||
const idle = t.idle;
|
||||
return {
|
||||
idle,
|
||||
total,
|
||||
}
|
||||
}
|
||||
|
||||
export class CpuTimer {
|
||||
previousSample: ReturnType<typeof cpus>;
|
||||
maxSpeed = 0;
|
||||
|
||||
sample(): number {
|
||||
const sample = cpus();
|
||||
const previousSample = this.previousSample;
|
||||
this.previousSample = sample;
|
||||
|
||||
// can cpu count change at runtime, who knows
|
||||
if (!previousSample || previousSample.length !== sample.length)
|
||||
return 0;
|
||||
|
||||
// cpu may be throttled in low power mode, so observe total speed to scale
|
||||
let totalSpeed = 0;
|
||||
|
||||
const times = sample.map((v, i) => {
|
||||
totalSpeed += v.speed;
|
||||
const c = getIdleTotal(v);
|
||||
const p = getIdleTotal(previousSample[i]);
|
||||
const total = c.total - p.total;
|
||||
const idle = c.idle - p.idle;
|
||||
return 1 - idle / total;
|
||||
});
|
||||
|
||||
this.maxSpeed = Math.max(this.maxSpeed, totalSpeed);
|
||||
|
||||
// will return a value between 0 and 1, where 1 is full cpu speed
|
||||
// the cpu usage is scaled by the clock speed
|
||||
// so if the cpu is running at 1ghz out of 3ghz, the cpu usage is scaled by 1/3
|
||||
const clockScale = totalSpeed / this.maxSpeed;
|
||||
|
||||
const total = times.reduce((p, c) => p + c, 0);
|
||||
return total / sample.length * clockScale;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, DeviceState, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator, WritableDeviceState } from '@scrypted/sdk';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator, WritableDeviceState } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { CpuTimer } from './cpu-timer';
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
|
||||
import { insidePolygon, normalizeBox, polygonOverlap } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor, createObjectDetectorStorageSetting } from './smart-motionsensor';
|
||||
import { fixLegacyClipPath, insidePolygon, normalizeBoxToClipPath, polygonOverlap } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
|
||||
|
||||
@@ -20,6 +20,17 @@ const defaultMotionDuration = 30;
|
||||
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
|
||||
const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
|
||||
|
||||
// at 5fps object detection speed, the camera is considered throttled.
|
||||
// throttling may be due to cpu, gpu, npu or whatever.
|
||||
// regardless, purging low fps object detection sessions will likely
|
||||
// restore performance.
|
||||
const fpsKillWaterMark = 5
|
||||
const fpsLowWaterMark = 7;
|
||||
// cameras may have low performance due to low framerate or intensive tasks such as
|
||||
// LPR and face recognition. if multiple cams are in low performance mode, then
|
||||
// the system may be struggling.
|
||||
const lowPerformanceMinThreshold = 2;
|
||||
|
||||
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
|
||||
|
||||
type ClipPath = Point[];
|
||||
@@ -85,6 +96,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
|
||||
];
|
||||
return {
|
||||
hide: this.model?.decoder,
|
||||
choices,
|
||||
}
|
||||
},
|
||||
@@ -103,6 +115,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
analyzeStop: number;
|
||||
detectorSignal = new Deferred<void>().resolve();
|
||||
released = false;
|
||||
sampleHistory: number[] = [];
|
||||
// settings: Setting[];
|
||||
|
||||
get detectorRunning() {
|
||||
@@ -162,7 +175,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
getCurrentSettings() {
|
||||
const settings = this.model.settings;
|
||||
if (!settings)
|
||||
return { id : this.id };
|
||||
return { id: this.id };
|
||||
|
||||
const ret: { [key: string]: any } = {};
|
||||
for (const setting of settings) {
|
||||
@@ -174,6 +187,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
else {
|
||||
value = this.storage.getItem(setting.key);
|
||||
if (setting.type === 'number')
|
||||
value = parseFloat(value);
|
||||
}
|
||||
value ||= setting.value;
|
||||
|
||||
@@ -334,13 +349,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
const stream = await this.cameraDevice.getVideoStream({
|
||||
prebuffer: this.model.prebuffer,
|
||||
destination,
|
||||
// ask rebroadcast to mute audio, not needed.
|
||||
audio: null,
|
||||
});
|
||||
|
||||
if (this.model.decoder) {
|
||||
if (!options?.suppress)
|
||||
this.console.log(this.objectDetection.name, '(with builtin decoder)');
|
||||
this.console.log(this.objectDetection.name, '(with builtin decoder)');
|
||||
return stream;
|
||||
}
|
||||
|
||||
@@ -419,7 +432,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
const frameGenerator = this.model.decoder ? undefined : this.getFrameGenerator();
|
||||
for await (const detected of
|
||||
await sdk.connectRPCObject(
|
||||
await this.objectDetection.generateObjectDetections(
|
||||
await this.objectDetection.generateObjectDetections(
|
||||
await this.createFrameGenerator(
|
||||
frameGenerator,
|
||||
options,
|
||||
@@ -436,13 +449,18 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
break;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// stop when analyze period ends.
|
||||
if (!this.hasMotionType && this.analyzeStop && Date.now() > this.analyzeStop) {
|
||||
if (!this.hasMotionType && this.analyzeStop && now > this.analyzeStop) {
|
||||
this.analyzeStop = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!longObjectDetectionWarning && !this.hasMotionType && Date.now() - start > 5 * 60 * 1000) {
|
||||
this.purgeSampleHistory(now);
|
||||
this.sampleHistory.push(now);
|
||||
|
||||
if (!longObjectDetectionWarning && !this.hasMotionType && now - start > 5 * 60 * 1000) {
|
||||
longObjectDetectionWarning = true;
|
||||
this.console.warn('Camera has been performing object detection for 5 minutes due to persistent motion. This may adversely affect system performance. Read the Optimizing System Performance guide for tips and tricks. https://github.com/koush/nvr.scrypted.app/wiki/Optimizing-System-Performance')
|
||||
}
|
||||
@@ -453,21 +471,18 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
const zonedDetections = this.applyZones(detected.detected);
|
||||
detected.detected.detections = zonedDetections;
|
||||
|
||||
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
|
||||
|
||||
if (!this.hasMotionType) {
|
||||
this.plugin.trackDetection();
|
||||
|
||||
// const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
|
||||
// const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
|
||||
// if (numZonedDetections !== numOriginalDetections)
|
||||
// this.console.log('Zone filtered detections:', numZonedDetections - numOriginalDetections);
|
||||
const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
|
||||
const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
|
||||
if (numZonedDetections !== numOriginalDetections)
|
||||
currentDetections.set('filtered', (currentDetections.get('filtered') || 0) + 1);
|
||||
|
||||
for (const d of detected.detected.detections) {
|
||||
currentDetections.set(d.className, Math.max(currentDetections.get(d.className) || 0, d.score));
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now > lastReport + 10000) {
|
||||
const found = [...currentDetections.entries()].map(([className, score]) => `${className} (${score})`);
|
||||
if (!found.length)
|
||||
@@ -480,23 +495,20 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
if (detected.detected.detectionId) {
|
||||
updatePipelineStatus('creating jpeg');
|
||||
// const start = Date.now();
|
||||
let { image } = detected.videoFrame;
|
||||
image = await sdk.connectRPCObject(image);
|
||||
const jpeg = await image.toBuffer({
|
||||
format: 'jpg',
|
||||
});
|
||||
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
|
||||
// this.console.log('retain took', Date.now() -start);
|
||||
this.setDetection(detected.detected, mo);
|
||||
// this.console.log('image saved', detected.detected.detections);
|
||||
}
|
||||
const motionFound = this.reportObjectDetections(detected.detected);
|
||||
if (this.hasMotionType) {
|
||||
// if motion is detected, stop processing and exit loop allowing it to sleep.
|
||||
if (motionFound) {
|
||||
// however, when running in analyze mode, continue to allow viewing motion boxes for test purposes.
|
||||
if (!this.analyzeStop || Date.now() > this.analyzeStop) {
|
||||
if (!this.analyzeStop || now > this.analyzeStop) {
|
||||
this.analyzeStop = undefined;
|
||||
clearInterval(interval);
|
||||
return true;
|
||||
@@ -505,10 +517,25 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
await sleep(250);
|
||||
}
|
||||
updatePipelineStatus('waiting result');
|
||||
// this.handleDetectionEvent(detected.detected);
|
||||
}
|
||||
}
|
||||
|
||||
purgeSampleHistory(now: number) {
|
||||
while (this.sampleHistory.length && now - this.sampleHistory[0] > 10000) {
|
||||
this.sampleHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
get detectionFps() {
|
||||
const now = Date.now();
|
||||
this.purgeSampleHistory(now);
|
||||
const first = this.sampleHistory[0];
|
||||
// require at least 5 seconds of samples.
|
||||
if (!first || (now - first) < 8000)
|
||||
return Infinity;
|
||||
return this.sampleHistory.length / ((now - first) / 1000);
|
||||
}
|
||||
|
||||
applyZones(detection: ObjectsDetected) {
|
||||
// determine zones of the objects, if configured.
|
||||
if (!detection.detections)
|
||||
@@ -518,7 +545,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!o.boundingBox)
|
||||
continue;
|
||||
|
||||
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
const box = normalizeBoxToClipPath(o.boundingBox, detection.inputDimensions);
|
||||
|
||||
let included: boolean;
|
||||
// need a way to explicitly include package zone.
|
||||
@@ -526,12 +553,17 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
included = true;
|
||||
else
|
||||
o.zones = [];
|
||||
for (const [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
for (let [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
zoneValue = fixLegacyClipPath(zoneValue);
|
||||
if (zoneValue.length < 3) {
|
||||
// this.console.warn(zone, 'Zone is unconfigured, skipping.');
|
||||
continue;
|
||||
}
|
||||
|
||||
// object detection may report motion, don't filter these at all.
|
||||
if (!this.hasMotionType && o.className === 'motion')
|
||||
continue;
|
||||
|
||||
const zoneInfo = this.zoneInfos[zone];
|
||||
const exclusion = zoneInfo?.filterMode ? zoneInfo.filterMode === 'exclude' : zoneInfo?.exclusion;
|
||||
// track if there are any inclusion zones
|
||||
@@ -571,7 +603,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// use a default inclusion zone that crops the top and bottom to
|
||||
// prevents errant motion from the on screen time changing every second.
|
||||
if (this.hasMotionType && included === undefined) {
|
||||
const defaultInclusionZone: ClipPath = [[0, 10], [100, 10], [100, 90], [0, 90]];
|
||||
const defaultInclusionZone: ClipPath = [[0, .1], [1, .1], [1, .9], [0, .9]];
|
||||
included = polygonOverlap(box, defaultInclusionZone);
|
||||
}
|
||||
|
||||
@@ -612,7 +644,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
this.detections.set(detectionId, detectionInput);
|
||||
setTimeout(() => {
|
||||
this.detections.delete(detectionId);
|
||||
}, 2000);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async getNativeObjectTypes(): Promise<ObjectDetectionTypes> {
|
||||
@@ -661,12 +693,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
frameGenerator = this.plugin.storageSettings.values.defaultDecoder || 'Default';
|
||||
|
||||
const pipelines = getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
|
||||
const webcodec = process.env.SCRYPTED_INSTALL_ENVIRONMENT === 'electron' ? sdk.systemManager.getDeviceById('@scrypted/electron-core', 'webcodec') : undefined;
|
||||
const webassembly = sdk.systemManager.getDeviceById('@scrypted/nvr', 'decoder') || undefined;
|
||||
const gstreamer = sdk.systemManager.getDeviceById('@scrypted/python-codecs', 'gstreamer') || undefined;
|
||||
const libav = sdk.systemManager.getDeviceById('@scrypted/python-codecs', 'libav') || undefined;
|
||||
const ffmpeg = sdk.systemManager.getDeviceById('@scrypted/objectdetector', 'ffmpeg') || undefined;
|
||||
const use = pipelines.find(p => p.name === frameGenerator) || webcodec || webassembly || gstreamer || libav || ffmpeg;
|
||||
const use = pipelines.find(p => p.name === frameGenerator) || webassembly || gstreamer || libav || ffmpeg;
|
||||
return use.id;
|
||||
}
|
||||
|
||||
@@ -820,7 +851,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (key.startsWith('zone-')) {
|
||||
const zoneName = key.substring('zone-'.length);
|
||||
if (this.zones[zoneName]) {
|
||||
this.zones[zoneName] = JSON.parse(vs);
|
||||
this.zones[zoneName] = Array.isArray(value) ? value : JSON.parse(vs);
|
||||
this.storage.setItem('zones', JSON.stringify(this.zones));
|
||||
}
|
||||
return;
|
||||
@@ -1010,14 +1041,12 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
},
|
||||
});
|
||||
devices = new Map<string, any>();
|
||||
cpuTimer = new CpuTimer();
|
||||
cpuUsage = 0;
|
||||
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
super(nativeId, 'v5');
|
||||
|
||||
this.systemDevice = {
|
||||
deviceCreator: 'Smart Motion Sensor',
|
||||
deviceCreator: 'Smart Sensor',
|
||||
};
|
||||
|
||||
process.nextTick(() => {
|
||||
@@ -1031,19 +1060,28 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
})
|
||||
});
|
||||
|
||||
// on an interval check to see if system load allows squelched detectors to start up.
|
||||
setInterval(() => {
|
||||
this.cpuUsage = this.cpuTimer.sample();
|
||||
// this.console.log('cpu usage', Math.round(this.cpuUsage * 100));
|
||||
|
||||
const runningDetections = this.runningObjectDetections;
|
||||
|
||||
// don't allow too many cams to start up at once if resuming from a low performance state.
|
||||
let allowStart = 2;
|
||||
// always allow 2 cameras to push past cpu throttling
|
||||
if (runningDetections.length > 2) {
|
||||
const cpuPerDetector = this.cpuUsage / runningDetections.length;
|
||||
allowStart = Math.ceil(1 / cpuPerDetector) - runningDetections.length;
|
||||
if (allowStart <= 0)
|
||||
|
||||
// allow minimum amount of concurrent cameras regardless of system specs
|
||||
if (runningDetections.length > lowPerformanceMinThreshold) {
|
||||
// if anything is below the kill threshold, do not start
|
||||
const killable = runningDetections.filter(o => o.detectionFps < fpsKillWaterMark && !o.analyzeStop);
|
||||
if (killable.length > lowPerformanceMinThreshold) {
|
||||
const cameraNames = runningDetections.map(o => `${o.name} ${o.detectionFps}`).join(', ');
|
||||
const first = killable[0];
|
||||
first.console.warn(`System at capacity. Ending object detection.`, cameraNames);
|
||||
first.endObjectDetection();
|
||||
return;
|
||||
}
|
||||
|
||||
const lowWatermark = runningDetections.filter(o => o.detectionFps < fpsLowWaterMark);
|
||||
if (lowWatermark.length > lowPerformanceMinThreshold)
|
||||
allowStart = 1;
|
||||
}
|
||||
|
||||
const idleDetectors = [...this.currentMixins.values()]
|
||||
@@ -1057,7 +1095,11 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, 10000)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
checkHasEnabledMixin(device: ScryptedDevice): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
pruneOldStatistics() {
|
||||
@@ -1080,26 +1122,28 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
if (runningDetections.find(o => o.id === mixin.id))
|
||||
return false;
|
||||
|
||||
// always allow 2 cameras to push past cpu throttling
|
||||
if (runningDetections.length < 2)
|
||||
// allow minimum amount of concurrent cameras regardless of system specs
|
||||
if (runningDetections.length < lowPerformanceMinThreshold)
|
||||
return true;
|
||||
|
||||
const cpuPerDetector = this.cpuUsage / runningDetections.length;
|
||||
const cpuPercent = Math.round(this.cpuUsage * 100);
|
||||
if (cpuPerDetector * (runningDetections.length + 1) > .9) {
|
||||
const [first] = runningDetections;
|
||||
// find any cameras struggling with a with low detection fps.
|
||||
const lowWatermark = runningDetections.filter(o => o.detectionFps < fpsLowWaterMark);
|
||||
if (lowWatermark.length > lowPerformanceMinThreshold) {
|
||||
const [first] = lowWatermark;
|
||||
// if cameras have been detecting enough to catch the activity, kill it for new camera.
|
||||
const cameraNames = runningDetections.map(o => `${o.name} ${o.detectionFps}`).join(', ');
|
||||
if (Date.now() - first.detectionStartTime > 30000) {
|
||||
first.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Ending object detection to process activity on ${mixin.name}.`);
|
||||
first.console.warn(`System at capacity. Ending object detection to process activity on ${mixin.name}.`, cameraNames);
|
||||
first.endObjectDetection();
|
||||
mixin.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Ending object detection on ${first.name} to process activity.`);
|
||||
mixin.console.warn(`System at capacity. Ending object detection on ${first.name} to process activity.`, cameraNames);
|
||||
return true;
|
||||
}
|
||||
|
||||
mixin.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Not starting object detection to continue processing recent activity on ${first.name}.`);
|
||||
mixin.console.warn(`System at capacity. Not starting object detection to continue processing recent activity on ${first.name}.`, cameraNames);
|
||||
return false;
|
||||
}
|
||||
|
||||
// CPU capacity is fine
|
||||
// System capacity is fine. Start the detection.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1154,6 +1198,8 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
|
||||
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartOccupancySensor(this, nativeId);
|
||||
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
@@ -1164,6 +1210,13 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX)) {
|
||||
const smart = this.devices.get(nativeId) as SmartMotionSensor;
|
||||
smart?.detectionListener?.removeListener();
|
||||
smart?.resetMotionTimeout();
|
||||
}
|
||||
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX)) {
|
||||
const smart = this.devices.get(nativeId) as SmartOccupancySensor;
|
||||
smart?.detectionListener?.removeListener();
|
||||
smart?.resetOccupiedTimeout();
|
||||
smart?.clearOccupancyInterval();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1199,32 +1252,71 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
createObjectDetectorStorageSetting(),
|
||||
{
|
||||
key: 'sensorType',
|
||||
title: 'Sensor Type',
|
||||
description: 'Select the type of sensor to create.',
|
||||
choices: [
|
||||
'Smart Motion Sensor',
|
||||
'Smart Occupancy Sensor',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'camera',
|
||||
title: 'Camera',
|
||||
description: 'Select a camera or doorbell.',
|
||||
type: 'device',
|
||||
deviceFilter: `type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}'`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
|
||||
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
|
||||
const objectDetector = sdk.systemManager.getDeviceById(settings.objectDetector as string);
|
||||
let name = objectDetector.name || 'New';
|
||||
name += ' Smart Motion Sensor'
|
||||
const sensorType = settings.sensorType;
|
||||
const camera = sdk.systemManager.getDeviceById(settings.camera as string);
|
||||
if (sensorType === 'Smart Motion Sensor') {
|
||||
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
|
||||
let name = camera.name || 'New';
|
||||
name += ' Smart Motion Sensor'
|
||||
|
||||
const id = await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
const id = await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
|
||||
const sensor = new SmartMotionSensor(this, nativeId);
|
||||
sensor.storageSettings.values.objectDetector = objectDetector?.id;
|
||||
const sensor = new SmartMotionSensor(this, nativeId);
|
||||
sensor.storageSettings.values.objectDetector = camera?.id;
|
||||
|
||||
return id;
|
||||
return id;
|
||||
}
|
||||
else if (sensorType === 'Smart Occupancy Sensor') {
|
||||
const nativeId = SMART_OCCUPANCYSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
|
||||
let name = camera.name || 'New';
|
||||
name += ' Smart Occupancy Sensor'
|
||||
|
||||
const id = await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.OccupancySensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
|
||||
const sensor = new SmartOccupancySensor(this, nativeId);
|
||||
sensor.storageSettings.values.camera = camera?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Point } from '@scrypted/sdk';
|
||||
import type { ClipPath, Point } from '@scrypted/sdk';
|
||||
import polygonClipping from 'polygon-clipping';
|
||||
|
||||
// const polygonOverlap = require('polygon-overlap');
|
||||
@@ -14,15 +14,36 @@ export function insidePolygon(point: Point, polygon: Point[]) {
|
||||
return !!intersect.length;
|
||||
}
|
||||
|
||||
export function normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
|
||||
export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
|
||||
if (!clipPath)
|
||||
return;
|
||||
|
||||
// if any value is over abs 2, then divide by 100.
|
||||
// this is a workaround for the old scrypted bug where the path was not normalized.
|
||||
// this is a temporary workaround until the path is normalized in the UI.
|
||||
let needNormalize = false;
|
||||
for (const p of clipPath) {
|
||||
for (const c of p) {
|
||||
if (Math.abs(c) >= 2)
|
||||
needNormalize = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needNormalize)
|
||||
return clipPath;
|
||||
|
||||
return clipPath.map(p => p.map(c => c / 100)) as ClipPath;
|
||||
}
|
||||
|
||||
export function normalizeBoxToClipPath(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x * 100 / inputDimensions[0];
|
||||
y = y * 100 / inputDimensions[1];
|
||||
x2 = x2 * 100 / inputDimensions[0];
|
||||
y2 = y2 * 100 / inputDimensions[1];
|
||||
x = x / inputDimensions[0];
|
||||
y = y / inputDimensions[1];
|
||||
x2 = x2 / inputDimensions[0];
|
||||
y2 = y2 / inputDimensions[1];
|
||||
return [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import sdk, { Camera, EventListenerRegister, MediaObject, MotionSensor, ObjectDetector, ObjectsDetected, Readme, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { levenshteinDistance } from "./edit-distance";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
|
||||
export const SMART_MOTIONSENSOR_PREFIX = 'smart-motionsensor-';
|
||||
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
|
||||
|
||||
export function createObjectDetectorStorageSetting(): StorageSetting {
|
||||
return {
|
||||
key: 'objectDetector',
|
||||
title: 'Object Detector',
|
||||
description: 'Select the camera or doorbell that provides smart detection event.',
|
||||
type: 'device',
|
||||
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
|
||||
};
|
||||
}
|
||||
|
||||
export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, Readme, MotionSensor, Camera {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
objectDetector: createObjectDetectorStorageSetting(),
|
||||
objectDetector: {
|
||||
title: 'Camera',
|
||||
description: 'Select a camera or doorbell that provides smart detection events.',
|
||||
type: 'device',
|
||||
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
|
||||
},
|
||||
detections: {
|
||||
title: 'Detections',
|
||||
description: 'The detections that will trigger this smart motion sensor.',
|
||||
@@ -145,13 +139,13 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
return;
|
||||
}
|
||||
|
||||
resetTrigger() {
|
||||
resetMotionTimeout() {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
}
|
||||
|
||||
trigger() {
|
||||
this.resetTrigger();
|
||||
this.resetMotionTimeout();
|
||||
this.motionDetected = true;
|
||||
const duration: number = this.storageSettings.values.detectionTimeout;
|
||||
if (!duration)
|
||||
@@ -167,7 +161,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
this.detectionListener = undefined;
|
||||
this.motionListener?.removeListener();
|
||||
this.motionListener = undefined;
|
||||
this.resetTrigger();
|
||||
this.resetMotionTimeout();
|
||||
|
||||
|
||||
const objectDetector: ObjectDetector & MotionSensor & ScryptedDevice = this.storageSettings.values.objectDetector;
|
||||
@@ -178,8 +172,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
if (!detections?.length)
|
||||
return;
|
||||
|
||||
const console = sdk.deviceManager.getMixinConsole(objectDetector.id, this.nativeId);
|
||||
|
||||
this.motionListener = objectDetector.listen({
|
||||
event: ScryptedInterface.MotionSensor,
|
||||
watch: true,
|
||||
@@ -258,7 +250,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
|
||||
if (match) {
|
||||
if (!this.motionDetected)
|
||||
console.log('Smart Motion Sensor triggered on', match);
|
||||
this.console.log('Smart Motion Sensor triggered on', match);
|
||||
if (detected.detectionId)
|
||||
this.lastPicture = objectDetector.getDetectionInput(detected.detectionId, details.eventId);
|
||||
this.trigger();
|
||||
@@ -278,6 +270,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
return `
|
||||
## Smart Motion Sensor
|
||||
|
||||
This Smart Motion Sensor can trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
|
||||
This Smart Motion Sensor can trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
|
||||
}
|
||||
}
|
||||
|
||||
316
plugins/objectdetector/src/smart-occupancy-sensor.ts
Normal file
316
plugins/objectdetector/src/smart-occupancy-sensor.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import sdk, { Camera, ClipPath, EventListenerRegister, Image, ObjectDetection, ObjectDetector, ObjectsDetected, OccupancySensor, Readme, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { levenshteinDistance } from "./edit-distance";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
import { normalizeBoxToClipPath, polygonOverlap } from "./polygon";
|
||||
|
||||
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
|
||||
|
||||
const nvrAcceleratedMotionSensorId = sdk.systemManager.getDeviceById('@scrypted/nvr', 'motion')?.id;
|
||||
|
||||
export class SmartOccupancySensor extends ScryptedDeviceBase implements Settings, Readme, OccupancySensor {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
camera: {
|
||||
title: 'Camera',
|
||||
description: 'Select the camera or doorbell image to analyze periodically.',
|
||||
type: 'device',
|
||||
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.Camera}')`,
|
||||
immediate: true,
|
||||
},
|
||||
objectDetection: {
|
||||
title: 'Object Detector',
|
||||
description: 'Select the object detection plugin to use for detecting objects.',
|
||||
type: 'device',
|
||||
deviceFilter: `interfaces.includes('ObjectDetectionPreview') && id !== '${nvrAcceleratedMotionSensorId}'`,
|
||||
immediate: true,
|
||||
},
|
||||
detections: {
|
||||
title: 'Detections',
|
||||
description: 'The detections that will trigger this occupancy sensor.',
|
||||
multiple: true,
|
||||
choices: [],
|
||||
},
|
||||
occupancyInterval: {
|
||||
title: 'Occupancy Check Interval',
|
||||
description: 'The interval in minutes that the sensor will check for occupancy.',
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
// save and restore in seconds for consistency.
|
||||
mapPut(oldValue, newValue) {
|
||||
return newValue * 60;
|
||||
},
|
||||
mapGet(value) {
|
||||
return value / 60;
|
||||
},
|
||||
},
|
||||
zone: {
|
||||
title: 'Edit Intersect Zone',
|
||||
description: 'Optional: Configure the intersect zone for the occupancy check. Objects intersecting this zone will trigger the occupancy sensor.',
|
||||
type: 'clippath',
|
||||
},
|
||||
captureZone: {
|
||||
title: 'Edit Crop Zone',
|
||||
description: 'Optional: Configure the capture zone for the occupancy check. The image will be cropped to this zone before detection. Cropping to desired location will improve detection performance.',
|
||||
type: 'clippath',
|
||||
},
|
||||
minScore: {
|
||||
title: 'Minimum Score',
|
||||
description: 'The minimum score required for a detection to trigger the occupancy sensor.',
|
||||
type: 'number',
|
||||
defaultValue: 0.4,
|
||||
},
|
||||
labels: {
|
||||
group: 'Recognition',
|
||||
title: 'Labels',
|
||||
description: 'The labels (license numbers, names) that will trigger this smart occupancy sensor.',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
choices: [],
|
||||
},
|
||||
labelDistance: {
|
||||
group: 'Recognition',
|
||||
title: 'Label Distance',
|
||||
description: 'The maximum edit distance between the detected label and the desired label. Ie, a distance of 1 will match "abcde" to "abcbe" or "abcd".',
|
||||
type: 'number',
|
||||
defaultValue: 2,
|
||||
},
|
||||
labelScore: {
|
||||
group: 'Recognition',
|
||||
title: 'Label Score',
|
||||
description: 'The minimum score required for a label to trigger the occupancy sensor.',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
}
|
||||
});
|
||||
|
||||
detectionListener: EventListenerRegister;
|
||||
occupancyTimeout: NodeJS.Timeout;
|
||||
occupancyInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(public plugin: ObjectDetectionPlugin, nativeId?: ScryptedNativeId) {
|
||||
super(nativeId);
|
||||
|
||||
this.storageSettings.settings.zone.onGet = async () => {
|
||||
return {
|
||||
deviceFilter: this.storageSettings.values.camera?.id,
|
||||
}
|
||||
};
|
||||
|
||||
this.storageSettings.settings.captureZone.onGet = async () => {
|
||||
return {
|
||||
deviceFilter: this.storageSettings.values.camera?.id,
|
||||
}
|
||||
};
|
||||
|
||||
this.storageSettings.settings.detections.onGet = async () => {
|
||||
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
|
||||
const choices = (await objectDetection?.getDetectionModel())?.classes || [];
|
||||
return {
|
||||
hide: !objectDetection,
|
||||
choices,
|
||||
};
|
||||
};
|
||||
|
||||
this.storageSettings.settings.detections.onPut = () => this.rebind();
|
||||
this.storageSettings.settings.objectDetection.onPut = () => this.rebind();
|
||||
this.storageSettings.settings.zone.onPut = () => this.rebind();
|
||||
this.storageSettings.settings.captureZone.onPut = () => this.rebind();
|
||||
|
||||
this.rebind();
|
||||
}
|
||||
|
||||
resetOccupiedTimeout() {
|
||||
clearTimeout(this.occupancyTimeout);
|
||||
this.occupancyTimeout = undefined;
|
||||
}
|
||||
|
||||
clearOccupancyInterval() {
|
||||
clearInterval(this.occupancyInterval);
|
||||
this.occupancyInterval = undefined;
|
||||
}
|
||||
|
||||
trigger() {
|
||||
this.resetOccupiedTimeout();
|
||||
this.occupied = true;
|
||||
const duration: number = this.storageSettings.values.occupancyInterval;
|
||||
if (!duration)
|
||||
return;
|
||||
this.occupancyTimeout = setTimeout(() => {
|
||||
this.occupied = false;
|
||||
}, duration * 60000 + 10000);
|
||||
}
|
||||
|
||||
checkDetection(detections: string[], labels: string[], labelDistance: number, labelScore: number, detected: ObjectsDetected) {
|
||||
const match = detected.detections?.find(d => {
|
||||
if (d.score && d.score < this.storageSettings.values.minScore)
|
||||
return false;
|
||||
if (!detections?.includes(d.className))
|
||||
return false;
|
||||
const zone: ClipPath = this.storageSettings.values.zone;
|
||||
if (zone?.length >= 3) {
|
||||
if (!d.boundingBox)
|
||||
return false;
|
||||
const detectionBoxPath = normalizeBoxToClipPath(d.boundingBox, detected.inputDimensions);
|
||||
if (!polygonOverlap(detectionBoxPath, zone))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!labels?.length)
|
||||
return true;
|
||||
|
||||
if (!d.label)
|
||||
return false;
|
||||
|
||||
for (const label of labels) {
|
||||
if (label === d.label) {
|
||||
if (!labelScore || d.labelScore >= labelScore)
|
||||
return true;
|
||||
this.console.log('Label score too low.', d.labelScore);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!labelDistance)
|
||||
continue;
|
||||
|
||||
if (levenshteinDistance(label, d.label) > labelDistance) {
|
||||
this.console.log('Label does not match.', label, d.label, d.labelScore);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!labelScore || d.labelScore >= labelScore)
|
||||
return true;
|
||||
this.console.log('Label score too low.', d.labelScore);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
if (!this.occupied)
|
||||
this.console.log('Occupancy Sensor triggered on', match);
|
||||
this.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
async runDetection() {
|
||||
try {
|
||||
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
|
||||
if (!objectDetection) {
|
||||
this.console.error('no object detection plugin selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const camera: ScryptedDevice & Camera = this.storageSettings.values.camera;
|
||||
if (!camera) {
|
||||
this.console.error('no camera selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const picture = await camera.takePicture({
|
||||
reason: 'event',
|
||||
});
|
||||
const zone: ClipPath = this.storageSettings.values.captureZone;
|
||||
let detected: ObjectsDetected;
|
||||
if (zone?.length >= 3) {
|
||||
const image = await sdk.mediaManager.convertMediaObject<Image>(picture, ScryptedMimeTypes.Image);
|
||||
let left = image.width;
|
||||
let top = image.height;
|
||||
let right = 0;
|
||||
let bottom = 0;
|
||||
for (const point of zone) {
|
||||
left = Math.min(left, point[0]);
|
||||
top = Math.min(top, point[1]);
|
||||
right = Math.max(right, point[0]);
|
||||
bottom = Math.max(bottom, point[1]);
|
||||
}
|
||||
|
||||
left = left * image.width;
|
||||
top = top * image.height;
|
||||
right = right * image.width;
|
||||
bottom = bottom * image.height;
|
||||
|
||||
let width = right - left;
|
||||
let height = bottom - top;
|
||||
// square it for standard detection
|
||||
width = height = Math.max(width, height);
|
||||
// recenter it
|
||||
left = left + (right - left - width) / 2;
|
||||
top = top + (bottom - top - height) / 2;
|
||||
// ensure bounds are within image.
|
||||
left = Math.max(0, left);
|
||||
top = Math.max(0, top);
|
||||
width = Math.min(width, image.width - left);
|
||||
height = Math.min(height, image.height - top);
|
||||
|
||||
const cropped = await image.toImage({
|
||||
crop: {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
});
|
||||
detected = await objectDetection.detectObjects(cropped);
|
||||
|
||||
// adjust the origin of the bounding boxes for the crop.
|
||||
for (const d of detected.detections) {
|
||||
d.boundingBox[0] += left;
|
||||
d.boundingBox[1] += top;
|
||||
}
|
||||
detected.inputDimensions = [image.width, image.height];
|
||||
}
|
||||
else {
|
||||
detected = await objectDetection.detectObjects(picture);
|
||||
}
|
||||
|
||||
this.checkDetection(this.storageSettings.values.detections, this.storageSettings.values.labels, this.storageSettings.values.labelDistance, this.storageSettings.values.labelScore, detected);
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('failed to take picture', e);
|
||||
}
|
||||
}
|
||||
|
||||
rebind() {
|
||||
this.occupied = false;
|
||||
this.detectionListener?.removeListener();
|
||||
this.detectionListener = undefined;
|
||||
this.resetOccupiedTimeout();
|
||||
this.clearOccupancyInterval();
|
||||
|
||||
this.runDetection();
|
||||
this.occupancyInterval = setInterval(() => {
|
||||
this.runDetection();
|
||||
}, this.storageSettings.values.occupancyInterval * 60000);
|
||||
|
||||
// camera may have an object detector that can also be observed for occupancy for free.
|
||||
const objectDetector: ObjectDetector & ScryptedDevice = this.storageSettings.values.camera;
|
||||
if (!objectDetector)
|
||||
return;
|
||||
|
||||
const detections: string[] = this.storageSettings.values.detections;
|
||||
if (!detections?.length)
|
||||
return;
|
||||
|
||||
const { labels, labelDistance, labelScore } = this.storageSettings.values;
|
||||
|
||||
this.detectionListener = objectDetector.listen(ScryptedInterface.ObjectDetector, (source, details, data) => {
|
||||
const detected: ObjectsDetected = data;
|
||||
this.checkDetection(detections, labels, labelDistance, labelScore, detected);
|
||||
});
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
return `
|
||||
## Smart Occupancy Sensor
|
||||
|
||||
This Occupancy Sensor remains triggered while specified objects (vehicle, person, animal, etc) are detected on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.`;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user