mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 14:13:28 +00:00
Compare commits
636 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7fb9e13c | ||
|
|
a526816b07 | ||
|
|
563e16b08f | ||
|
|
fd56990d64 | ||
|
|
d7aaf57e8f | ||
|
|
a2d50d54d5 | ||
|
|
1f86745252 | ||
|
|
1f4343ba2e | ||
|
|
3ad311898f | ||
|
|
799e5b53c7 | ||
|
|
833e5b34ab | ||
|
|
c99ac28e89 | ||
|
|
841475cb97 | ||
|
|
4b03a3a458 | ||
|
|
d686dd815c | ||
|
|
e0386a8922 | ||
|
|
9ef3478c88 | ||
|
|
690d160f33 | ||
|
|
59ff987bca | ||
|
|
1669f17c96 | ||
|
|
b0bfd4e05e | ||
|
|
7152671913 | ||
|
|
537c178699 | ||
|
|
77ecee110b | ||
|
|
29b163a7d8 | ||
|
|
5d74e80e90 | ||
|
|
764b6441d5 | ||
|
|
e2c43cb4ff | ||
|
|
7b6d094e8c | ||
|
|
3dfb2db02a | ||
|
|
e5a549db6a | ||
|
|
d500c815fe | ||
|
|
5f71c59b5a | ||
|
|
27407942a5 | ||
|
|
11b6963744 | ||
|
|
b9ee8866f0 | ||
|
|
bc80d31eaa | ||
|
|
327688232c | ||
|
|
2883a4ce46 | ||
|
|
1ad2fb915d | ||
|
|
fb701a32b7 | ||
|
|
7a8c661bb3 | ||
|
|
54d72fb371 | ||
|
|
e48812cec7 | ||
|
|
6c2db072c4 | ||
|
|
4bf2c0b614 | ||
|
|
a93cdb0ae4 | ||
|
|
ff85b7abc6 | ||
|
|
46dfb8d98e | ||
|
|
5240200f0f | ||
|
|
3bcb94fc6b | ||
|
|
a596bc712c | ||
|
|
f6d2dc456e | ||
|
|
441cce239e | ||
|
|
3016df32d1 | ||
|
|
5bd8ed0b1a | ||
|
|
79286a5138 | ||
|
|
8874e01072 | ||
|
|
0223a9f0f6 | ||
|
|
890c2667d0 | ||
|
|
ca14764e17 | ||
|
|
1030d7d03c | ||
|
|
2d40320868 | ||
|
|
3e32c3d019 | ||
|
|
1f9fa3966f | ||
|
|
c2d86237d6 | ||
|
|
5cfcfafc00 | ||
|
|
35b4028a47 | ||
|
|
bf6038a5d3 | ||
|
|
e1b2216543 | ||
|
|
89c1682421 | ||
|
|
5a4c527c59 | ||
|
|
9d9c10aa1e | ||
|
|
ccf20a5fca | ||
|
|
692e7964a7 | ||
|
|
57e38072b1 | ||
|
|
4e8e862482 | ||
|
|
eddcef8e54 | ||
|
|
09edc6d75e | ||
|
|
72c7c43d79 | ||
|
|
805f471ff9 | ||
|
|
6f797d53ec | ||
|
|
4903a0efcd | ||
|
|
36e3fcf429 | ||
|
|
78a126fe0a | ||
|
|
5029baf2d4 | ||
|
|
769bc014a8 | ||
|
|
096700486a | ||
|
|
b3a7d6be9c | ||
|
|
05751bce44 | ||
|
|
dced62a527 | ||
|
|
359f1cfc2f | ||
|
|
d4cae8abbb | ||
|
|
0e6b3346ed | ||
|
|
2409cc457c | ||
|
|
0b794aa381 | ||
|
|
98017a5aa6 | ||
|
|
f2e7cc4017 | ||
|
|
d7201a16a7 | ||
|
|
99d1dc7282 | ||
|
|
18ae09e41c | ||
|
|
2ebe774e59 | ||
|
|
b887b8a47c | ||
|
|
8e391dee2f | ||
|
|
469f693d58 | ||
|
|
1c96a7d492 | ||
|
|
3f1b45c435 | ||
|
|
4b715e55d2 | ||
|
|
75dc63acc3 | ||
|
|
6c79f42bb7 | ||
|
|
9d4f006caa | ||
|
|
05b206f897 | ||
|
|
1f22218b23 | ||
|
|
c9568df165 | ||
|
|
c98e91cd39 | ||
|
|
e3ecff04ce | ||
|
|
f9f50f34c3 | ||
|
|
cd298f7d76 | ||
|
|
c95248fce0 | ||
|
|
e50f3fa793 | ||
|
|
c74be7e90f | ||
|
|
4d288727ce | ||
|
|
1f19dc191d | ||
|
|
37d4e5be73 | ||
|
|
e64ec98211 | ||
|
|
8b6c0c4f7b | ||
|
|
3b16c68c75 | ||
|
|
67be05880c | ||
|
|
414a9403c2 | ||
|
|
053106415c | ||
|
|
f3690af92a | ||
|
|
c4cc12fdff | ||
|
|
58e8772f7c | ||
|
|
4ae9b72471 | ||
|
|
a8c64aa2d4 | ||
|
|
8ccbba485a | ||
|
|
2ec192e0fd | ||
|
|
e257953338 | ||
|
|
9e80eca8e1 | ||
|
|
172b32fd47 | ||
|
|
a6bf055b85 | ||
|
|
dab5be1103 | ||
|
|
126c489934 | ||
|
|
7f714b3d6a | ||
|
|
fde3c47d8c | ||
|
|
4b1623dfce | ||
|
|
1e62f7a418 | ||
|
|
83c9d9a4a6 | ||
|
|
b42afe0ca0 | ||
|
|
e8e5f9b33e | ||
|
|
15916d83b8 | ||
|
|
c1327974b2 | ||
|
|
33e2291912 | ||
|
|
2d2c5c436f | ||
|
|
8088ae20b1 | ||
|
|
4c658b8d99 | ||
|
|
aab78ec797 | ||
|
|
11ecff985d | ||
|
|
80a1a78a79 | ||
|
|
7875c51d62 | ||
|
|
b04aa75117 | ||
|
|
fc7d1eaf32 | ||
|
|
e5a7a55be8 | ||
|
|
fa9a2eb947 | ||
|
|
30891e0769 | ||
|
|
fb8256709a | ||
|
|
06d0a4a2f1 | ||
|
|
2fb6e0a368 | ||
|
|
c6ed0d8729 | ||
|
|
67c6f63dbe | ||
|
|
e62b4ad68b | ||
|
|
bfec5eb3f3 | ||
|
|
0f948ea672 | ||
|
|
a28a476d80 | ||
|
|
fdab50bf8e | ||
|
|
189be80a40 | ||
|
|
dae1b87825 | ||
|
|
8853ca2775 | ||
|
|
296652b550 | ||
|
|
fb2646a69f | ||
|
|
9f5787227b | ||
|
|
aedcc0709b | ||
|
|
756585ae95 | ||
|
|
4e8ee94012 | ||
|
|
5689792a77 | ||
|
|
ed40f29226 | ||
|
|
aad9a2123d | ||
|
|
602b5e4983 | ||
|
|
5f01cdc73b | ||
|
|
a1f82dd065 | ||
|
|
469305cc3b | ||
|
|
5ed4082918 | ||
|
|
1bfdacc476 | ||
|
|
2d03b55d8e | ||
|
|
a06894b165 | ||
|
|
e2c0b4d1bf | ||
|
|
501509dcd0 | ||
|
|
9cbc38173b | ||
|
|
8c7a4dc21e | ||
|
|
edfeacd075 | ||
|
|
20d1372d2a | ||
|
|
3999cb6696 | ||
|
|
167360a218 | ||
|
|
9b168bb012 | ||
|
|
31f2d33e57 | ||
|
|
513dd4867b | ||
|
|
08e723848f | ||
|
|
fb37061a04 | ||
|
|
56c6cb8947 | ||
|
|
4f38c6eea8 | ||
|
|
5eb2c586fa | ||
|
|
8fd89e75b4 | ||
|
|
10c9143333 | ||
|
|
eaeae02080 | ||
|
|
7460c714c1 | ||
|
|
d7874eb7a2 | ||
|
|
5847b585c7 | ||
|
|
901e0a2349 | ||
|
|
8c8c7934ff | ||
|
|
dd4efcd52f | ||
|
|
eab0746a0a | ||
|
|
385d331953 | ||
|
|
27a01a7df8 | ||
|
|
fc13a230d7 | ||
|
|
f7d88273e4 | ||
|
|
26124b7647 | ||
|
|
5ba30e6001 | ||
|
|
cfb78ebb7f | ||
|
|
83ad4ed7bc | ||
|
|
8612d8e1fb | ||
|
|
3aeddd0347 | ||
|
|
f41fa9055e | ||
|
|
6655cba3b6 | ||
|
|
4726630e29 | ||
|
|
4989aa621e | ||
|
|
772bfec55a | ||
|
|
cf9a0653f2 | ||
|
|
c5bbe5619e | ||
|
|
61be7fa58d | ||
|
|
8cb2e1516a | ||
|
|
1b15453997 | ||
|
|
7e65605ab8 | ||
|
|
793c583491 | ||
|
|
77d4b0a995 | ||
|
|
79eda5d356 | ||
|
|
86f3318133 | ||
|
|
1cf0327d2e | ||
|
|
3244956b91 | ||
|
|
6d5fedc931 | ||
|
|
7eca7f69c0 | ||
|
|
7dec399ed7 | ||
|
|
9edc63bd90 | ||
|
|
99d2f43699 | ||
|
|
ba1ecd54c5 | ||
|
|
e49f26b410 | ||
|
|
d7a417c984 | ||
|
|
1a7e0370c9 | ||
|
|
2fe4191f12 | ||
|
|
b2b5cde303 | ||
|
|
33b77b64de | ||
|
|
a41d4de97a | ||
|
|
cf367fa481 | ||
|
|
6f483f829b | ||
|
|
be69c25076 | ||
|
|
96d292d39f | ||
|
|
933c731fe6 | ||
|
|
aa2c1c65f9 | ||
|
|
5228dbff62 | ||
|
|
d3593b9e40 | ||
|
|
2ef482c47f | ||
|
|
52692c0912 | ||
|
|
98c901486a | ||
|
|
476bd3b427 | ||
|
|
f71826f6a1 | ||
|
|
ed72643d3e | ||
|
|
96219456f3 | ||
|
|
0e797c6ac6 | ||
|
|
672f01fd3f | ||
|
|
327acaec76 | ||
|
|
aac10c4f16 | ||
|
|
da4ba776f7 | ||
|
|
6cd0af492b | ||
|
|
6a2474d11e | ||
|
|
e8a5d5cfd3 | ||
|
|
f07604de4c | ||
|
|
ed35811296 | ||
|
|
ae2228f2e4 | ||
|
|
c92c8f2b52 | ||
|
|
478f1f4ad7 | ||
|
|
06c8b397f0 | ||
|
|
f8bcf196d3 | ||
|
|
d1b57ed3ad | ||
|
|
190914efd1 | ||
|
|
e26e53899e | ||
|
|
43c69914a4 | ||
|
|
73f859b1f6 | ||
|
|
a362b7d6d9 | ||
|
|
efa8515aa0 | ||
|
|
7987a78239 | ||
|
|
21752a3e7e | ||
|
|
b4d8f99cd5 | ||
|
|
6cf8f6db32 | ||
|
|
feb3b8f601 | ||
|
|
9e0e9bc22a | ||
|
|
9e495c74d9 | ||
|
|
a9baeafe71 | ||
|
|
ee68fcd7d2 | ||
|
|
af6e18dc1a | ||
|
|
ddb8c7cf58 | ||
|
|
2be3c7f3df | ||
|
|
274f449e2f | ||
|
|
1109333e0f | ||
|
|
0349977a4d | ||
|
|
48548aafd5 | ||
|
|
ab70cce1b5 | ||
|
|
83fe0c2b7a | ||
|
|
77676a27c2 | ||
|
|
015dfab7a6 | ||
|
|
7f0f0cb6bd | ||
|
|
e49e13a167 | ||
|
|
9fd353236b | ||
|
|
e006d599d7 | ||
|
|
71cbe83a2a | ||
|
|
1438af8aea | ||
|
|
2237eb3221 | ||
|
|
7b56e86383 | ||
|
|
3653fb83d3 | ||
|
|
cd766a603e | ||
|
|
3648492299 | ||
|
|
88e8530677 | ||
|
|
325f84ca7e | ||
|
|
0a4b862fd8 | ||
|
|
e7d7fd6a00 | ||
|
|
f9dda8d1ca | ||
|
|
8b7decd077 | ||
|
|
9dd5e10eba | ||
|
|
475b833508 | ||
|
|
5f9006148a | ||
|
|
b77f1a55c1 | ||
|
|
6b9163e84e | ||
|
|
bc03bdd235 | ||
|
|
2592a7c228 | ||
|
|
0a4336879c | ||
|
|
e5cef3f217 | ||
|
|
d34396afbc | ||
|
|
2622fc9256 | ||
|
|
410b1a4813 | ||
|
|
403c742be3 | ||
|
|
50a471b78f | ||
|
|
9b7ead26e0 | ||
|
|
3127bc38cb | ||
|
|
fb8b1a893d | ||
|
|
779d8eaa42 | ||
|
|
5eab99866f | ||
|
|
e10a4f3c58 | ||
|
|
2585b1832e | ||
|
|
5e8e0d7773 | ||
|
|
7c17b478d7 | ||
|
|
9f5dd55c73 | ||
|
|
b6f400382d | ||
|
|
024b2166b8 | ||
|
|
b49771840e | ||
|
|
4001fc996f | ||
|
|
0d97010ca8 | ||
|
|
e243d99d12 | ||
|
|
86a91dfbe4 | ||
|
|
c86ae752e8 | ||
|
|
b7ca477b98 | ||
|
|
c37f8926b8 | ||
|
|
4b181a8ac9 | ||
|
|
b8439aaec3 | ||
|
|
77d0c33657 | ||
|
|
0b6d61a801 | ||
|
|
71a2d27cbd | ||
|
|
f8f79f5cc2 | ||
|
|
988f297e32 | ||
|
|
6e109d89e0 | ||
|
|
6ada4854bc | ||
|
|
bc5e89668f | ||
|
|
4c11def52b | ||
|
|
8890d307f4 | ||
|
|
9f8f562dcc | ||
|
|
2ce798c8c2 | ||
|
|
4271ef321f | ||
|
|
f976903a29 | ||
|
|
4ca63aadd5 | ||
|
|
6c932aec89 | ||
|
|
d7030c3dcf | ||
|
|
172ebf06de | ||
|
|
5f28c5a291 | ||
|
|
4c9ba5073e | ||
|
|
11d67f36be | ||
|
|
d38357ded9 | ||
|
|
f22e2ccfe7 | ||
|
|
e2b2f68477 | ||
|
|
57e87fbe8d | ||
|
|
31b05162fc | ||
|
|
c63efa0fca | ||
|
|
ce5255aa45 | ||
|
|
4692be1586 | ||
|
|
632d971dd5 | ||
|
|
2f17c85e99 | ||
|
|
9c6cdc9ac3 | ||
|
|
7007456bdd | ||
|
|
73fc738c0b | ||
|
|
abd1227fab | ||
|
|
7d2226df75 | ||
|
|
8f50415920 | ||
|
|
20ed523b30 | ||
|
|
effadb1437 | ||
|
|
07c7c91c63 | ||
|
|
878ddbdf1c | ||
|
|
d95e9c78ea | ||
|
|
49dc1d8f36 | ||
|
|
425e17a88b | ||
|
|
9bca6b0a94 | ||
|
|
3a62d9cd31 | ||
|
|
8f6bedd9d8 | ||
|
|
1c2a9d767f | ||
|
|
7ecee4298c | ||
|
|
4f1aad895f | ||
|
|
94667d2136 | ||
|
|
7d13055eae | ||
|
|
f90140dbd7 | ||
|
|
8b3a66b6ba | ||
|
|
8c03852cfb | ||
|
|
d795cd527d | ||
|
|
a24d986717 | ||
|
|
60ec304e68 | ||
|
|
6a9d498ff8 | ||
|
|
c60821043b | ||
|
|
e5a63dd992 | ||
|
|
f77ea922f2 | ||
|
|
1e8deeb638 | ||
|
|
a28ecb71e1 | ||
|
|
4067455396 | ||
|
|
9b828a6045 | ||
|
|
efce576c68 | ||
|
|
66b314f2aa | ||
|
|
d6ebc1fa85 | ||
|
|
8d756a26bd | ||
|
|
81c28b86d3 | ||
|
|
73f5e03774 | ||
|
|
cd078afcf9 | ||
|
|
6e393514cf | ||
|
|
4b62bceede | ||
|
|
fbbbdd8ab5 | ||
|
|
a0e28c0a28 | ||
|
|
ff28238422 | ||
|
|
4e9744360a | ||
|
|
7336fac8c4 | ||
|
|
6771d17829 | ||
|
|
62f1ca66f6 | ||
|
|
13cc562e68 | ||
|
|
aff1e86d6f | ||
|
|
c1f1e96109 | ||
|
|
a36b3066fe | ||
|
|
cadf10b505 | ||
|
|
ed541629b2 | ||
|
|
7d022548b9 | ||
|
|
9aa9bae3a3 | ||
|
|
7f29b05980 | ||
|
|
b89573e910 | ||
|
|
18426bcdc1 | ||
|
|
f562dd5362 | ||
|
|
1f1218a594 | ||
|
|
1aca97c2ae | ||
|
|
bd41410367 | ||
|
|
291d734a05 | ||
|
|
feec534b86 | ||
|
|
9ae7e6c0b5 | ||
|
|
a6f11d6d0c | ||
|
|
a15af8005b | ||
|
|
c13a3f252a | ||
|
|
0eaf9ef2d9 | ||
|
|
b9fc69347a | ||
|
|
f6e8a363ab | ||
|
|
a6d163ec5a | ||
|
|
2d62944ac1 | ||
|
|
b564553998 | ||
|
|
6e4fdb6e99 | ||
|
|
ca00983ecd | ||
|
|
36b8b9eeed | ||
|
|
fbd6937627 | ||
|
|
7c66826657 | ||
|
|
62c4a8b240 | ||
|
|
af860d840a | ||
|
|
42eb4fc80b | ||
|
|
5c965936e9 | ||
|
|
fe5cc59872 | ||
|
|
5d965ebfa7 | ||
|
|
b462249d93 | ||
|
|
29d8abed45 | ||
|
|
65cb13b0d1 | ||
|
|
522f8e9cba | ||
|
|
16199463ec | ||
|
|
220c010232 | ||
|
|
02238f99b2 | ||
|
|
1e53234cd6 | ||
|
|
824b7327a1 | ||
|
|
81d4a3f249 | ||
|
|
db1bd07b71 | ||
|
|
35026f6b5b | ||
|
|
9160efc2f7 | ||
|
|
6bc1e6a742 | ||
|
|
475e4a60d7 | ||
|
|
1f2edf1a12 | ||
|
|
b3db0aa78f | ||
|
|
0766d67a75 | ||
|
|
d2ac428916 | ||
|
|
945fb16bd6 | ||
|
|
711eb222ed | ||
|
|
19f8bfb74a | ||
|
|
08a8428d6e | ||
|
|
4feeeda904 | ||
|
|
753373a691 | ||
|
|
2f3529b822 | ||
|
|
2501d1460b | ||
|
|
e063637100 | ||
|
|
5ec0bf4bf3 | ||
|
|
0c05b59121 | ||
|
|
cbbfa0b525 | ||
|
|
28835b1ccc | ||
|
|
0585e7bbaf | ||
|
|
b2040ea2c8 | ||
|
|
2fd2151b4f | ||
|
|
4c7974519d | ||
|
|
d91c919558 | ||
|
|
7a297761bc | ||
|
|
c15e10e5cf | ||
|
|
3494106857 | ||
|
|
7d3dfb16f0 | ||
|
|
63fc223036 | ||
|
|
6736379858 | ||
|
|
7a811b2b22 | ||
|
|
dd5cb432c9 | ||
|
|
ab3a71ab49 | ||
|
|
b5c9382180 | ||
|
|
81682678ac | ||
|
|
dec184629e | ||
|
|
f33bb53138 | ||
|
|
2d3957e086 | ||
|
|
d16ed9e54f | ||
|
|
d7e8052498 | ||
|
|
48cd3830a5 | ||
|
|
ce138d1a17 | ||
|
|
7b4919fba9 | ||
|
|
0b3dee3a03 | ||
|
|
4cef09540b | ||
|
|
92583e568a | ||
|
|
67aaa08c31 | ||
|
|
2e9f618f6f | ||
|
|
bf4d39d6af | ||
|
|
c31e68f720 | ||
|
|
6d8b3c1ce7 | ||
|
|
106fef95b4 | ||
|
|
488d68ee1c | ||
|
|
f7e35fb1ee | ||
|
|
b1bf897bdb | ||
|
|
8eb533c220 | ||
|
|
f10cdfbced | ||
|
|
8f5e9e5a8c | ||
|
|
cc0283ef39 | ||
|
|
5c7b67c973 | ||
|
|
d1be0f1b4c | ||
|
|
55d58d1e44 | ||
|
|
d9dccf36a3 | ||
|
|
33477fdf80 | ||
|
|
e6ece3aa3e | ||
|
|
6a4126191b | ||
|
|
e9f999b911 | ||
|
|
1fef31a081 | ||
|
|
659f99c33d | ||
|
|
a9deff0046 | ||
|
|
7a56cefe2a | ||
|
|
a06c6e9568 | ||
|
|
56f127a203 | ||
|
|
2ffe67b2db | ||
|
|
44dc648398 | ||
|
|
7807cc4bc6 | ||
|
|
81fb690089 | ||
|
|
8b15617f6e | ||
|
|
fd8aa70352 | ||
|
|
be888d215d | ||
|
|
ce5f568a5d | ||
|
|
336220559f | ||
|
|
8014060a54 | ||
|
|
7f4c8997b9 | ||
|
|
9f73b92dbd | ||
|
|
381892fca6 | ||
|
|
a28df23032 | ||
|
|
dc5456d36f | ||
|
|
3a23e8ed26 | ||
|
|
e0db86cb41 | ||
|
|
37ccefebd1 | ||
|
|
0076c4827f | ||
|
|
c5c07d8169 | ||
|
|
2372acc796 | ||
|
|
6b9c3e4aa0 | ||
|
|
d5b652da8c | ||
|
|
2b9a0f082d | ||
|
|
b10b4d047e | ||
|
|
74cd23bd88 | ||
|
|
ef742bdb23 | ||
|
|
6f7fa54f24 | ||
|
|
d9a575cb5a | ||
|
|
29094afa4d | ||
|
|
62a92fe083 | ||
|
|
9b8bde556c | ||
|
|
326ef11760 | ||
|
|
92a0b4a863 | ||
|
|
9fd3641455 | ||
|
|
2918cf9ae1 | ||
|
|
6f004db859 | ||
|
|
367d741c5f | ||
|
|
8f83894e49 | ||
|
|
ea6e33d159 | ||
|
|
1b5565b5b2 | ||
|
|
19692d02c6 | ||
|
|
4179698c12 | ||
|
|
1eea3a87d0 | ||
|
|
ec89a77955 | ||
|
|
443158286e | ||
|
|
b168ca52c6 | ||
|
|
fe01d3a1ba | ||
|
|
18cad22627 | ||
|
|
c67c9a028c | ||
|
|
0cff8ad5ed | ||
|
|
0269959cf3 | ||
|
|
1b6de42eca | ||
|
|
39342d5d46 | ||
|
|
c4b5af46d0 | ||
|
|
a46235d095 | ||
|
|
848d490a66 | ||
|
|
87fbb95157 | ||
|
|
c036da9ae0 |
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Github Issues is not a Forum
|
||||
|
||||
**This issue tracker is not for hardware support or feature requests**. If you are troubleshooting adding a device for the first time, use Discord, Reddit, or Github Discussions. However, if something **was working**, and is now **no longer working**, you may create a Github issue.
|
||||
Created issues that do not meet these requirements or are improperly filled out will be immediately closed.
|
||||
|
||||
|
||||
# New Issue Instructions
|
||||
|
||||
1. Delete this section and everything above it.
|
||||
2. Fill out the sections below.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. The issue tracker is only for reporting bugs in Scrypted, for general support check Discord. Hardrware support requests or assistance requests will be immediately closed.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
15
.github/workflows/docker-common.yml
vendored
15
.github/workflows/docker-common.yml
vendored
@@ -7,9 +7,10 @@ jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: self-hosted
|
||||
# runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: ["18"]
|
||||
NODE_VERSION: ["18", "20"]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite", "thin"]
|
||||
steps:
|
||||
@@ -22,24 +23,22 @@ jobs:
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.124
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.119
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
platforms: linux/arm64
|
||||
append: |
|
||||
- endpoint: ssh://koush@192.168.2.124
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://koush@192.168.2.119
|
||||
platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -62,7 +61,7 @@ jobs:
|
||||
BASE=${{ matrix.BASE }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/armhf,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
|
||||
22
.github/workflows/docker.yml
vendored
22
.github/workflows/docker.yml
vendored
@@ -16,9 +16,17 @@ jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: self-hosted
|
||||
# runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin"]
|
||||
BASE: [
|
||||
"18-jammy-full",
|
||||
"18-jammy-lite",
|
||||
# "18-jammy-thin",
|
||||
# "20-jammy-full",
|
||||
# "20-jammy-lite",
|
||||
# "20-jammy-thin",
|
||||
]
|
||||
SUPERVISOR: ["", ".s6"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -41,24 +49,22 @@ jobs:
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.124
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.119
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
platforms: linux/arm64
|
||||
append: |
|
||||
- endpoint: ssh://koush@192.168.2.124
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://koush@192.168.2.119
|
||||
platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -81,7 +87,7 @@ jobs:
|
||||
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -3,9 +3,9 @@ name: Test
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["docker/**", ".github/workflows/test.yml"]
|
||||
paths: ["install/**", ".github/workflows/test.yml"]
|
||||
pull_request:
|
||||
paths: ["docker/**", ".github/workflows/test.yml"]
|
||||
paths: ["install/**", ".github/workflows/test.yml"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
.DS_Store
|
||||
__pycache__
|
||||
venv
|
||||
.venv
|
||||
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -33,5 +33,8 @@
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
[submodule "plugins/cloud/node-nat-upnp"]
|
||||
path = plugins/cloud/node-nat-upnp
|
||||
path = plugins/cloud/external/node-nat-upnp
|
||||
url = ../../koush/node-nat-upnp.git
|
||||
[submodule "plugins/wyze/docker-wyze-bridge"]
|
||||
path = plugins/wyze/docker-wyze-bridge
|
||||
url = ../../koush/docker-wyze-bridge.git
|
||||
|
||||
59
README.md
59
README.md
@@ -1,59 +1,20 @@
|
||||
# Scrypted
|
||||
|
||||
Scrypted is a high performance home video integration and automation platform.
|
||||
* Video load instantly, everywhere: [Demo](https://www.reddit.com/r/homebridge/comments/r34k6b/if_youre_using_homebridge_for_cameras_ditch_it/)
|
||||
* [HomeKit Secure Video Support](https://github.com/koush/scrypted/wiki/HomeKit-Secure-Video-Setup)
|
||||
* Google Home support: "Ok Google, Stream Backyard"
|
||||
* Alexa Support: Streaming to Alexa app on iOS/Android and Echo Show.
|
||||
Scrypted is a high performance home video integration platform and NVR with smart detections. [Instant, low latency, streaming](https://streamable.com/xbxn7z) to HomeKit, Google Home, and Alexa. Supports most cameras. [Learn more](https://docs.scrypted.app).
|
||||
|
||||
<img width="400" alt="Scrypted_Management_Console" src="https://user-images.githubusercontent.com/73924/185666320-ae972867-6c2c-488a-8413-fd8a215e9fee.png">
|
||||
<img src="https://github.com/koush/scrypted/assets/73924/57e1d556-cd3d-4448-81f9-a6c51b6513de">
|
||||
|
||||
# Installation
|
||||
## Installation and Documentation
|
||||
|
||||
Select the appropriate guide. After installation is finished, remember to visit [HomeKit Secure Video Setup](https://github.com/koush/scrypted/wiki/HomeKit-Secure-Video-Setup).
|
||||
Installation and camera onboarding instructions can be found in the [docs](https://docs.scrypted.app).
|
||||
|
||||
* [Raspberry Pi](https://github.com/koush/scrypted/wiki/Installation:-Raspberry-Pi)
|
||||
* Linux
|
||||
* [Docker Compose](https://github.com/koush/scrypted/wiki/Installation:-Docker-Compose-Linux) - This is the recommended method. Local installation may interfere with other server software.
|
||||
* [Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Linux) - Use Docker Compose. This is a reference documentation.
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Linux) - Use this if Docker scares you or whatever.
|
||||
* Mac
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Mac)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* Windows
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Windows)
|
||||
* [WSL2 Installation](https://github.com/koush/scrypted/wiki/Installation:-WSL2-Windows)
|
||||
* [Home Assistant OS](https://github.com/koush/scrypted/wiki/Installation:-Home-Assistant-OS)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* [ReadyNAS: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-ReadyNAS)
|
||||
* [Synology: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Synology-NAS)
|
||||
* [QNAP: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-QNAP-NAS)
|
||||
* [Unraid: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Unraid)
|
||||
|
||||
## Discord
|
||||
|
||||
Chat on Discord for support, tips, announcements, and bug reporting. There is an active and helpful community.
|
||||
|
||||
[Join Scrypted Discord](https://discord.gg/DcFzmBHYGq)
|
||||
|
||||
## Wiki
|
||||
|
||||
There are many topics covered in the [Scrypted Wiki](https://github.com/koush/scrypted/wiki) sidebar. Review them for documented support, tips, and guides before asking for assistance on GitHub or Discord.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
* Google Home
|
||||
* Apple HomeKit
|
||||
* Amazon Alexa
|
||||
|
||||
Supported accessories:
|
||||
* Camera and Core Plugins: https://github.com/koush/scrypted/tree/main/plugins
|
||||
* Community Plugins: https://github.com/orgs/scryptedapp/repositories
|
||||
## Community
|
||||
|
||||
Scrypted has active communities on [Discord](https://discord.gg/DcFzmBHYGq), [Reddit](https://reddit.com/r/scrypted), and [Github](https://github.com/koush/scrypted). Check them out if you have questions!
|
||||
|
||||
## Development
|
||||
|
||||
## Debug Scrypted Plugins in VSCode
|
||||
## Debug Scrypted Plugins in VS Code
|
||||
|
||||
```sh
|
||||
# this is an example for homekit.
|
||||
@@ -66,7 +27,7 @@ cd scrypted
|
||||
code plugins/homekit
|
||||
```
|
||||
|
||||
You can now launch (using the Start Debugging play button) the HomeKit Plugin in VSCode. Please be aware that you do *not* need to restart the Scrypted Server if you make changes to a plugin. Edit the plugin, launch, and the updated plugin will deploy on the running server.
|
||||
You can now launch (using the Start Debugging play button) the HomeKit Plugin in VS Code. Please be aware that you do *not* need to restart the Scrypted Server if you make changes to a plugin. Edit the plugin, launch, and the updated plugin will deploy on the running server.
|
||||
|
||||
If you do not want to set up VS Code, you can also run build and install the plugin directly from the command line:
|
||||
|
||||
@@ -80,7 +41,7 @@ npm run build && npm run scrypted-deploy 127.0.0.1
|
||||
Want to write your own plugin? Full documentation is available here: https://developer.scrypted.app
|
||||
|
||||
|
||||
## Debug the Scrypted Server in VSCode
|
||||
## Debug the Scrypted Server in VS Code
|
||||
|
||||
Debugging the server should not be necessary, as the server only provides the hosting and RPC mechanism for plugins. The following is for reference purpose. Most development can be done by debugging the relevant plugin.
|
||||
|
||||
@@ -94,4 +55,4 @@ cd scrypted
|
||||
code server
|
||||
```
|
||||
|
||||
You can now launch the Scrypted Server in VSCode.
|
||||
You can now launch the Scrypted Server in VS Code.
|
||||
|
||||
4
common/package-lock.json
generated
4
common/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/common",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
|
||||
210
common/src/async-queue.ts
Normal file
210
common/src/async-queue.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Deferred } from "./deferred";
|
||||
|
||||
class EndError extends Error {
|
||||
}
|
||||
|
||||
export function createAsyncQueue<T>() {
|
||||
let ended: Error | undefined;
|
||||
const endDeferred = new Deferred<void>();
|
||||
const waiting: Deferred<T>[] = [];
|
||||
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
|
||||
|
||||
const dequeue = async () => {
|
||||
if (queued.length) {
|
||||
const { item, dequeued: enqueue } = queued.shift()!;
|
||||
enqueue?.resolve();
|
||||
return item;
|
||||
}
|
||||
|
||||
if (ended)
|
||||
throw ended;
|
||||
|
||||
const deferred = new Deferred<T>();
|
||||
waiting.push(deferred);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
const take = () => {
|
||||
if (queued.length) {
|
||||
const { item, dequeued: enqueue } = queued.shift()!;
|
||||
enqueue?.resolve();
|
||||
return item;
|
||||
}
|
||||
|
||||
if (ended)
|
||||
throw ended;
|
||||
}
|
||||
|
||||
const submit = (item: T, dequeued?: Deferred<void>, signal?: AbortSignal) => {
|
||||
if (ended)
|
||||
return false;
|
||||
|
||||
if (waiting.length) {
|
||||
const deferred = waiting.shift();
|
||||
dequeued?.resolve();
|
||||
deferred.resolve(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (signal)
|
||||
dequeued ||= new Deferred();
|
||||
|
||||
const qi = {
|
||||
item,
|
||||
dequeued,
|
||||
};
|
||||
queued!.push(qi);
|
||||
|
||||
if (!signal)
|
||||
return true;
|
||||
|
||||
const h = () => {
|
||||
const index = queued.indexOf(qi);
|
||||
if (index === -1)
|
||||
return;
|
||||
queued.splice(index, 1);
|
||||
dequeued?.reject(new Error('abort'));
|
||||
};
|
||||
|
||||
dequeued.promise.catch(() => {}).finally(() => signal.removeEventListener('abort', h));
|
||||
signal.addEventListener('abort', h);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function end(e?: Error) {
|
||||
if (ended)
|
||||
return false;
|
||||
// catch to prevent unhandled rejection.
|
||||
ended = e || new EndError();
|
||||
endDeferred.resolve();
|
||||
while (waiting.length) {
|
||||
waiting.shift().reject(ended);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function queue() {
|
||||
return (async function* () {
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
const item = await dequeue();
|
||||
yield item;
|
||||
}
|
||||
catch (e) {
|
||||
// the yield above may raise an error, and the queue should be ended.
|
||||
end(e);
|
||||
if (e instanceof EndError)
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// the yield above may cause an iterator return, and the queue should be ended.
|
||||
end();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function clear(error?: Error) {
|
||||
const ret: T[] = [];
|
||||
const items = queued.splice(0, queued.length);
|
||||
for (const item of items) {
|
||||
if (error)
|
||||
item.dequeued?.reject(error)
|
||||
else
|
||||
item.dequeued?.resolve(undefined);
|
||||
ret.push(item.item);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
return {
|
||||
get ended() {
|
||||
return ended;
|
||||
},
|
||||
endPromise: endDeferred.promise,
|
||||
take,
|
||||
clear() {
|
||||
return clear();
|
||||
},
|
||||
queued,
|
||||
async pipe(callback: (i: T) => void) {
|
||||
for await (const i of queue()) {
|
||||
callback(i as any);
|
||||
}
|
||||
},
|
||||
submit(item: T, signal?: AbortSignal) {
|
||||
return submit(item, undefined, signal);
|
||||
},
|
||||
end,
|
||||
async enqueue(item: T, signal?: AbortSignal) {
|
||||
const dequeued = new Deferred<void>();
|
||||
if (!submit(item, dequeued, signal))
|
||||
return false;
|
||||
await dequeued.promise;
|
||||
return true;
|
||||
},
|
||||
dequeue,
|
||||
get queue() {
|
||||
return queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// async function testSlowEnqueue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
// asyncQueue.submit(-1);
|
||||
// asyncQueue.submit(-1);
|
||||
// asyncQueue.submit(-1);
|
||||
// asyncQueue.submit(-1);
|
||||
|
||||
// (async () => {
|
||||
// console.log('go');
|
||||
// for (let i = 0; i < 10; i++) {
|
||||
// asyncQueue.submit(i);
|
||||
// await sleep(100);
|
||||
// }
|
||||
// asyncQueue.end(new Error('fail'));
|
||||
// })();
|
||||
|
||||
|
||||
// const runQueue = async (str?: string) => {
|
||||
// for await (const n of asyncQueue.queue) {
|
||||
// console.log(str, n);
|
||||
// }
|
||||
// }
|
||||
|
||||
// runQueue('start');
|
||||
|
||||
// setTimeout(runQueue, 400);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// async function testSlowDequeue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
// const runQueue = async (str?: string) => {
|
||||
// for await (const n of asyncQueue.queue) {
|
||||
// await sleep(100);
|
||||
// }
|
||||
// }
|
||||
|
||||
// runQueue()
|
||||
// .catch(e => console.error('queue threw', e));
|
||||
|
||||
// console.log('go');
|
||||
// for (let i = 0; i < 10; i++) {
|
||||
// console.log(await asyncQueue.enqueue(i));
|
||||
// console.log(i);
|
||||
// }
|
||||
// asyncQueue.end(new Error('fail'));
|
||||
// console.log(await asyncQueue.enqueue(555));
|
||||
// }
|
||||
|
||||
// testSlowDequeue();
|
||||
@@ -3,14 +3,13 @@ import sdk from "@scrypted/sdk";
|
||||
|
||||
const { systemManager } = sdk;
|
||||
|
||||
const autoIncludeToken = 'v4';
|
||||
|
||||
export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
hasEnabledMixin: { [id: string]: string } = {};
|
||||
pluginsComponent: Promise<any>;
|
||||
unshiftMixin = false;
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
constructor(nativeId?: string, public autoIncludeToken = 'v4') {
|
||||
super(nativeId);
|
||||
|
||||
try {
|
||||
@@ -30,10 +29,12 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
this.maybeEnableMixin(eventSource);
|
||||
});
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
this.maybeEnableMixin(device);
|
||||
}
|
||||
process.nextTick(() => {
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
const device = systemManager.getDeviceById(id);
|
||||
this.maybeEnableMixin(device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async shouldEnableMixin(device: ScryptedDevice) {
|
||||
@@ -44,7 +45,7 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
if (!device || device.mixins?.includes(this.id))
|
||||
return;
|
||||
|
||||
if (this.hasEnabledMixin[device.id] === autoIncludeToken)
|
||||
if (this.hasEnabledMixin[device.id] === this.autoIncludeToken)
|
||||
return;
|
||||
|
||||
const match = await this.canMixin(device.type, device.interfaces);
|
||||
@@ -66,9 +67,9 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
|
||||
}
|
||||
|
||||
setHasEnabledMixin(id: string) {
|
||||
if (this.hasEnabledMixin[id] === autoIncludeToken)
|
||||
if (this.hasEnabledMixin[id] === this.autoIncludeToken)
|
||||
return;
|
||||
this.hasEnabledMixin[id] = autoIncludeToken;
|
||||
this.hasEnabledMixin[id] = this.autoIncludeToken;
|
||||
this.storage.setItem('hasEnabledMixin', JSON.stringify(this.hasEnabledMixin));
|
||||
}
|
||||
|
||||
|
||||
@@ -263,20 +263,23 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
const rtsp = (options.parsers as any).rtsp as ReturnType<typeof createRtspParser>;
|
||||
rtsp.sdp.then(sdp => {
|
||||
const parsed = parseSdp(sdp);
|
||||
const audio = parsed.msections.find(msection=>msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection=>msection.type === 'video');
|
||||
const audio = parsed.msections.find(msection => msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection => msection.type === 'video');
|
||||
inputVideoCodec = video?.codec;
|
||||
inputAudioCodec = audio?.codec;
|
||||
});
|
||||
|
||||
const sdp = rtsp.sdp.then(sdpString => [Buffer.from(sdpString)]);
|
||||
const sdp = new Deferred<Buffer[]>();
|
||||
rtsp.sdp.then(r => sdp.resolve([Buffer.from(r)]));
|
||||
killed.then(() => sdp.reject(new Error("ffmpeg killed before sdp could be parsed")));
|
||||
|
||||
start();
|
||||
|
||||
return {
|
||||
start() {
|
||||
deferredStart.resolve();
|
||||
},
|
||||
sdp,
|
||||
sdp: sdp.promise,
|
||||
get inputAudioCodec() {
|
||||
return inputAudioCodec;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import net from 'net';
|
||||
import { once } from 'events';
|
||||
import dgram, { SocketType } from 'dgram';
|
||||
import { once } from 'events';
|
||||
import net from 'net';
|
||||
|
||||
export async function closeQuiet(socket: dgram.Socket | net.Server) {
|
||||
if (!socket)
|
||||
@@ -37,6 +37,23 @@ export async function createBindZero(socketType?: SocketType) {
|
||||
return createBindUdp(0, socketType);
|
||||
}
|
||||
|
||||
export async function createSquentialBindZero(socketType?: SocketType) {
|
||||
let attempts = 0;
|
||||
while (true) {
|
||||
const rtpServer = await createBindZero(socketType);
|
||||
try {
|
||||
const rtcpServer = await createBindUdp(rtpServer.port + 1, socketType);
|
||||
return [rtpServer, rtcpServer];
|
||||
}
|
||||
catch (e) {
|
||||
attempts++;
|
||||
closeQuiet(rtpServer.server);
|
||||
}
|
||||
if (attempts === 10)
|
||||
throw new Error('unable to reserve sequential udp ports')
|
||||
}
|
||||
}
|
||||
|
||||
export async function reserveUdpPort() {
|
||||
const udp = await createBindZero();
|
||||
await new Promise(resolve => udp.server.close(() => resolve(undefined)));
|
||||
@@ -62,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export { listenZero, listenZeroSingleClient, ListenZeroSingleClientTimeoutError } from "@scrypted/server/src/listen-zero";
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
|
||||
@@ -78,11 +78,17 @@ export function createPromiseDebouncer<T>() {
|
||||
export function createMapPromiseDebouncer<T>() {
|
||||
const map = new Map<string, Promise<T>>();
|
||||
|
||||
return (key: any, func: () => Promise<T>): Promise<T> => {
|
||||
return (key: any, debounce: number, func: () => Promise<T>): Promise<T> => {
|
||||
const keyStr = JSON.stringify(key);
|
||||
let value = map.get(keyStr);
|
||||
if (!value) {
|
||||
value = func().finally(() => map.delete(keyStr));
|
||||
value = func().finally(() => {
|
||||
if (!debounce) {
|
||||
map.delete(keyStr);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => map.delete(keyStr), debounce);
|
||||
});
|
||||
map.set(keyStr, value);
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function read16BELengthLoop(readable: Readable, options: {
|
||||
let length: number;
|
||||
let skipCount = 0;
|
||||
let readCount = 0;
|
||||
|
||||
|
||||
const resumeRead = () => {
|
||||
readCount++;
|
||||
read();
|
||||
@@ -59,13 +59,13 @@ export async function read16BELengthLoop(readable: Readable, options: {
|
||||
|
||||
export class StreamEndError extends Error {
|
||||
constructor() {
|
||||
super()
|
||||
super('stream ended');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
throw new Error("stream ended");
|
||||
throw new StreamEndError();
|
||||
|
||||
if (!length) {
|
||||
return Buffer.alloc(0);
|
||||
@@ -109,17 +109,26 @@ export async function readLength(readable: Readable, length: number): Promise<Bu
|
||||
const CHARCODE_NEWLINE = '\n'.charCodeAt(0);
|
||||
|
||||
export async function readUntil(readable: Readable, charCode: number) {
|
||||
const data = [];
|
||||
let count = 0;
|
||||
const queued: Buffer[] = [];
|
||||
while (true) {
|
||||
const buffer = await readLength(readable, 1);
|
||||
if (!buffer)
|
||||
throw new Error("end of stream");
|
||||
if (buffer[0] === charCode)
|
||||
break;
|
||||
data[count++] = buffer[0];
|
||||
const available: Buffer = readable.read();
|
||||
if (!available) {
|
||||
await once(readable, 'readable');
|
||||
continue;
|
||||
}
|
||||
const index = available.findIndex(b => b === charCode);
|
||||
if (index === -1) {
|
||||
queued.push(available);
|
||||
continue;
|
||||
}
|
||||
|
||||
const before = available.subarray(0, index);
|
||||
queued.push(before);
|
||||
|
||||
const after = available.subarray(index + 1);
|
||||
readable.unshift(after);
|
||||
return Buffer.concat(queued).toString();
|
||||
}
|
||||
return Buffer.from(data).toString();
|
||||
}
|
||||
|
||||
export async function readLine(readable: Readable) {
|
||||
|
||||
@@ -51,14 +51,8 @@ function silence() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
private pc: RTCPeerConnection;
|
||||
pcDeferred = new Deferred<RTCPeerConnection>();
|
||||
dcDeferred = new Deferred<RTCDataChannel>();
|
||||
microphone: RTCRtpSender;
|
||||
micEnabled = false;
|
||||
onPeerConnection: (pc: RTCPeerConnection) => Promise<void>;
|
||||
options: RTCSignalingOptions = {
|
||||
function createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
userAgent: getUserAgent(),
|
||||
capabilities: {
|
||||
audio: RTCRtpReceiver.getCapabilities?.('audio') || {
|
||||
@@ -76,6 +70,26 @@ export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
height: screen.height,
|
||||
},
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
// can be called on anything with getStats, ie for receiver specific reports or connection reports.
|
||||
export async function getPacketsLost(t: { getStats(): Promise<RTCStatsReport> }) {
|
||||
const stats = await t.getStats();
|
||||
const packetsLost = ([...stats.values()] as { packetsLost: number }[]).filter(stat => 'packetsLost' in stat).map(stat => stat.packetsLost);
|
||||
const total = packetsLost.reduce((p, c) => p + c, 0);
|
||||
return total;
|
||||
}
|
||||
|
||||
export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
private pc: RTCPeerConnection;
|
||||
pcDeferred = new Deferred<RTCPeerConnection>();
|
||||
dcDeferred = new Deferred<RTCDataChannel>();
|
||||
microphone: RTCRtpSender;
|
||||
micEnabled = false;
|
||||
onPeerConnection: (pc: RTCPeerConnection) => Promise<void>;
|
||||
__proxy_props = { options: createOptions() };
|
||||
options = createOptions();
|
||||
|
||||
constructor() {
|
||||
}
|
||||
@@ -84,6 +98,10 @@ export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
async getPacketsLost() {
|
||||
return getPacketsLost(this.pc);
|
||||
}
|
||||
|
||||
async setMicrophone(enabled: boolean) {
|
||||
if (this.microphone && enabled && !this.micEnabled) {
|
||||
this.micEnabled = true;
|
||||
@@ -284,6 +302,10 @@ function createCandidateQueue(console: Console, type: string, session: RTCSignal
|
||||
}
|
||||
}
|
||||
|
||||
export async function legacyGetSignalingSessionOptions(session: RTCSignalingSession) {
|
||||
return typeof session.options === 'object' ? session.options : await session.getOptions();
|
||||
}
|
||||
|
||||
export async function connectRTCSignalingClients(
|
||||
console: Console,
|
||||
offerClient: RTCSignalingSession,
|
||||
@@ -291,8 +313,8 @@ export async function connectRTCSignalingClients(
|
||||
answerClient: RTCSignalingSession,
|
||||
answerSetup: Partial<RTCAVSignalingSetup>
|
||||
) {
|
||||
const offerOptions = await offerClient.getOptions();
|
||||
const answerOptions = await answerClient.getOptions();
|
||||
const offerOptions = await legacyGetSignalingSessionOptions(offerClient);
|
||||
const answerOptions = await legacyGetSignalingSessionOptions(answerClient);
|
||||
const disableTrickle = offerOptions?.disableTrickle || answerOptions?.disableTrickle;
|
||||
|
||||
if (offerOptions?.offer && answerOptions?.offer)
|
||||
|
||||
@@ -6,14 +6,14 @@ import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
|
||||
import net from 'net';
|
||||
import { Duplex, Readable, Writable } from 'stream';
|
||||
import tls from 'tls';
|
||||
import { URL } from 'url';
|
||||
import { Deferred } from './deferred';
|
||||
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { closeQuiet, createBindZero, createSquentialBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { timeoutPromise } from './promise-utils';
|
||||
import { readLength, readLine } from './read-stream';
|
||||
import { MSection, parseSdp } from './sdp-utils';
|
||||
import { sleep } from './sleep';
|
||||
import { StreamChunk, StreamParser, StreamParserOptions } from './stream-parser';
|
||||
import { URL } from 'url';
|
||||
|
||||
const REQUIRED_WWW_AUTHENTICATE_KEYS = ['realm', 'nonce'];
|
||||
|
||||
@@ -195,48 +195,17 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
let foundIndex: number;
|
||||
let nonVideo: {
|
||||
[codec: string]: StreamChunk,
|
||||
} = {};
|
||||
|
||||
const createSyncFrame = () => {
|
||||
const ret = streamChunks.slice(foundIndex);
|
||||
// for (const nv of Object.values(nonVideo)) {
|
||||
// ret.unshift(nv);
|
||||
// }
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
nonVideo[streamChunk.type] = streamChunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS))
|
||||
foundIndex = prebufferIndex;
|
||||
}
|
||||
|
||||
if (foundIndex !== undefined)
|
||||
return createSyncFrame();
|
||||
|
||||
nonVideo = {};
|
||||
// some streams don't contain codec info, so find an idr frame instead.
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
nonVideo[streamChunk.type] = streamChunk;
|
||||
continue;
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS) || findH264NaluType(streamChunk, H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_IDR))
|
||||
foundIndex = prebufferIndex;
|
||||
}
|
||||
|
||||
if (foundIndex !== undefined)
|
||||
return createSyncFrame();
|
||||
|
||||
// oh well!
|
||||
},
|
||||
sdp: new Promise<string>(r => resolve = r),
|
||||
@@ -964,8 +933,7 @@ export class RtspServer {
|
||||
const match = transport.match(/.*?client_port=([0-9]+)-([0-9]+)/);
|
||||
const [_, rtp, rtcp] = match;
|
||||
|
||||
const rtpServer = await createBindZero();
|
||||
const rtcpServer = await createBindUdp(rtpServer.port + 1);
|
||||
const [rtpServer, rtcpServer] = await createSquentialBindZero();
|
||||
this.client.on('close', () => closeQuiet(rtpServer.server));
|
||||
this.client.on('close', () => closeQuiet(rtcpServer.server));
|
||||
this.setupTracks[msection.control] = {
|
||||
|
||||
@@ -149,7 +149,7 @@ export function parseFmtp(msection: string[]) {
|
||||
const paramLine = fmtpLine.substring(firstSpace + 1);
|
||||
const payloadType = parseInt(fmtp.split(':')[1]);
|
||||
|
||||
if (!fmtp || !paramLine || Number.isNaN( payloadType )) {
|
||||
if (!fmtp || !paramLine || Number.isNaN(payloadType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,28 +170,47 @@ export function parseFmtp(msection: string[]) {
|
||||
}
|
||||
|
||||
export type MSection = ReturnType<typeof parseMSection>;
|
||||
export type RTPMap = ReturnType<typeof parseRtpMap>;
|
||||
|
||||
export function parseRtpMap(mlineType: string, rtpmap: string) {
|
||||
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)/);
|
||||
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)(\/([\d]+))?/);
|
||||
|
||||
rtpmap = rtpmap?.toLowerCase();
|
||||
|
||||
let codec: string;
|
||||
let ffmpegEncoder: string;
|
||||
if (rtpmap?.includes('mpeg4')) {
|
||||
codec = 'aac';
|
||||
ffmpegEncoder = 'aac';
|
||||
}
|
||||
else if (rtpmap?.includes('opus')) {
|
||||
codec = 'opus';
|
||||
ffmpegEncoder = 'libopus';
|
||||
}
|
||||
else if (rtpmap?.includes('pcma')) {
|
||||
codec = 'pcm_alaw';
|
||||
ffmpegEncoder = 'pcm_alaw';
|
||||
}
|
||||
else if (rtpmap?.includes('pcmu')) {
|
||||
codec = 'pcm_ulaw';
|
||||
codec = 'pcm_mulaw';
|
||||
ffmpegEncoder = 'pcm_mulaw';
|
||||
}
|
||||
else if (rtpmap?.includes('g726')) {
|
||||
codec = 'g726';
|
||||
// disabled since it 48000 is non compliant in ffmpeg and fails.
|
||||
// ffmpegEncoder = 'g726';
|
||||
}
|
||||
else if (rtpmap?.includes('pcm')) {
|
||||
codec = 'pcm';
|
||||
}
|
||||
else if (rtpmap?.includes('l16')) {
|
||||
codec = 'pcm_s16be';
|
||||
ffmpegEncoder = 'pcm_s16be';
|
||||
}
|
||||
else if (rtpmap?.includes('speex')) {
|
||||
codec = 'speex';
|
||||
ffmpegEncoder = 'libspeex';
|
||||
}
|
||||
else if (rtpmap?.includes('h264')) {
|
||||
codec = 'h264';
|
||||
}
|
||||
@@ -207,8 +226,10 @@ export function parseRtpMap(mlineType: string, rtpmap: string) {
|
||||
return {
|
||||
line: rtpmap,
|
||||
codec,
|
||||
ffmpegEncoder,
|
||||
rawCodec: match?.[2],
|
||||
clock: parseInt(match?.[3]),
|
||||
channels: parseInt(match?.[5]) || undefined,
|
||||
payloadType: parseInt(match?.[1]),
|
||||
}
|
||||
}
|
||||
@@ -220,9 +241,11 @@ export function parseMSection(msection: string[]) {
|
||||
const mline = parseMLine(msection[0]);
|
||||
const rawRtpmaps = msection.filter(line => line.startsWith(artpmap));
|
||||
const rtpmaps = rawRtpmaps.map(line => parseRtpMap(mline.type, line));
|
||||
const codec = parseRtpMap(mline.type, rawRtpmaps[0]).codec;
|
||||
// if no rtp map is specified, pcm_alaw is used. parsing a null rtpmap is valid.
|
||||
const rtpmap = parseRtpMap(mline.type, rawRtpmaps[0]);
|
||||
const { codec } = rtpmap;
|
||||
let direction: string;
|
||||
|
||||
|
||||
for (const checkDirection of ['sendonly', 'sendrecv', 'recvonly', 'inactive']) {
|
||||
const found = msection.find(line => line === 'a=' + checkDirection);
|
||||
if (found) {
|
||||
@@ -239,6 +262,7 @@ export function parseMSection(msection: string[]) {
|
||||
contents: msection.join('\r\n'),
|
||||
control,
|
||||
codec,
|
||||
rtpmap,
|
||||
direction,
|
||||
toSdp: () => {
|
||||
return ret.lines.join('\r\n');
|
||||
|
||||
77
common/src/zygote.ts
Normal file
77
common/src/zygote.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import sdk, { PluginFork } from '@scrypted/sdk';
|
||||
import worker_threads from 'worker_threads';
|
||||
import { createAsyncQueue } from './async-queue';
|
||||
import os from 'os';
|
||||
|
||||
export type Zygote<T> = () => PluginFork<T>;
|
||||
|
||||
export function createZygote<T>(): Zygote<T> {
|
||||
if (!worker_threads.isMainThread)
|
||||
return;
|
||||
|
||||
let zygote = sdk.fork<T>();
|
||||
function* next() {
|
||||
while (true) {
|
||||
const cur = zygote;
|
||||
zygote = sdk.fork<T>();
|
||||
yield cur;
|
||||
}
|
||||
}
|
||||
|
||||
const gen = next();
|
||||
return () => gen.next().value as PluginFork<T>;
|
||||
}
|
||||
|
||||
|
||||
export function createZygoteWorkQueue<T>(maxWorkers: number = os.cpus().length >> 1) {
|
||||
const queue = createAsyncQueue<(doWork: (fork: PluginFork<T>) => Promise<any>) => Promise<any>>();
|
||||
let forks = 0;
|
||||
|
||||
return async <R>(doWork: (fork: PluginFork<T>) => Promise<R>): Promise<R> => {
|
||||
const check = queue.take();
|
||||
if (check)
|
||||
return check(doWork);
|
||||
|
||||
if (maxWorkers && forks < maxWorkers) {
|
||||
let exited = false;
|
||||
const controller = new AbortController();
|
||||
// necessary to prevent unhandledrejection errors
|
||||
controller.signal.addEventListener('abort', () => { });
|
||||
const fork = sdk.fork<T>();
|
||||
forks++;
|
||||
fork.worker.on('exit', () => {
|
||||
forks--;
|
||||
exited = true;
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
const queueFork = () => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
// keep one alive.
|
||||
if (forks === 1)
|
||||
return;
|
||||
fork.worker.terminate();
|
||||
}, 30000);
|
||||
|
||||
queue.submit(async v2 => {
|
||||
clearTimeout(timeout);
|
||||
try {
|
||||
return await v2(fork);
|
||||
}
|
||||
finally {
|
||||
if (!exited) {
|
||||
queueFork();
|
||||
}
|
||||
}
|
||||
}, controller.signal);
|
||||
}
|
||||
|
||||
queueFork();
|
||||
}
|
||||
|
||||
const d = await queue.dequeue();
|
||||
return d(doWork);
|
||||
};
|
||||
}
|
||||
2
external/axios-digest-auth
vendored
2
external/axios-digest-auth
vendored
Submodule external/axios-digest-auth updated: d0872934e6...e1735135be
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: 81f6570f59...3797e311ed
2
external/unifi-protect
vendored
2
external/unifi-protect
vendored
Submodule external/unifi-protect updated: 1f40c63e7f...bf6fdbdc65
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 91be7cf469...a0070297a4
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-bullseye-full.s6-v0.23.0"
|
||||
version: "18-jammy-full.s6-v0.72.0"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
@@ -35,6 +35,7 @@ backup_exclude:
|
||||
map:
|
||||
- config:rw
|
||||
- media:rw
|
||||
- share:rw
|
||||
devices:
|
||||
- /dev/mem
|
||||
- /dev/dri/renderD128
|
||||
|
||||
@@ -16,6 +16,7 @@ RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
@@ -24,10 +25,14 @@ RUN apt-get update && apt-get -y install \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get install -y ca-certificates curl gnupg
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN echo "Installing python."
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
@@ -35,67 +40,33 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# plugins support fallback to pillow, but vips is faster.
|
||||
RUN apt-get -y install \
|
||||
libvips
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
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-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
RUN echo "Installing gstreamer bindings."
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
# which causes weird behavior in python which looks at the arch version
|
||||
# which still reports 64bit, even if running in 32bit docker.
|
||||
# this scenario is not supported and will be reported at runtime.
|
||||
# this bit is not necessary on amd64, but leaving it for consistency.
|
||||
RUN apt-get -y install \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# python pip
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
@@ -105,26 +76,41 @@ RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
then \
|
||||
apt-get update && apt-get install -y gpg-agent && \
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg && \
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-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 && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# 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 --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
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
|
||||
|
||||
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"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=full
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
|
||||
@@ -8,6 +8,7 @@ RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
@@ -16,7 +17,10 @@ RUN apt-get update && apt-get -y install \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get install -y ca-certificates curl gnupg
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
@@ -37,7 +41,10 @@ ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=lite
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -12,13 +12,14 @@ RUN apt-get update && apt-get -y install \
|
||||
COPY fs /
|
||||
|
||||
# s6 process supervisor
|
||||
ARG S6_OVERLAY_VERSION=3.1.1.2
|
||||
ARG S6_OVERLAY_VERSION=3.1.5.0
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
||||
ENV S6_KEEP_ENV=1
|
||||
RUN case "$(uname -m)" in \
|
||||
x86_64) S6_ARCH='x86_64';; \
|
||||
armv7l) S6_ARCH='armhf';; \
|
||||
aarch64) S6_ARCH='aarch64';; \
|
||||
ARG TARGETARCH
|
||||
RUN case "${TARGETARCH}" in \
|
||||
amd64) S6_ARCH='x86_64';; \
|
||||
arm) S6_ARCH='armhf';; \
|
||||
arm64) S6_ARCH='aarch64';; \
|
||||
*) echo "Your system architecture isn't supported."; exit 1 ;; \
|
||||
esac \
|
||||
&& cd /tmp \
|
||||
|
||||
@@ -5,18 +5,21 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -y update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install curl software-properties-common apt-utils
|
||||
apt-get -y install curl software-properties-common apt-utils ffmpeg
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && apt-get update && apt-get install -y nodejs
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
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"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=thin
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="thin"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
./template/generate-dockerfile.sh
|
||||
|
||||
docker build -t koush/scrypted-common -f Dockerfile.common . && \
|
||||
docker build -t koush/scrypted-common -f Dockerfile.full . && \
|
||||
docker build -t koush/scrypted -f Dockerfile.local .
|
||||
|
||||
@@ -3,9 +3,10 @@ version: "3.5"
|
||||
# The Scrypted docker-compose.yml file typically resides at:
|
||||
# ~/.scrypted/docker-compose.yml
|
||||
|
||||
|
||||
# Scrypted NVR Storage (Optional Network Volume: Part 1 of 3)
|
||||
# Example volumes SMB (CIFS) and NFS.
|
||||
# Uncomment only one.
|
||||
|
||||
# volumes:
|
||||
# nvr:
|
||||
# driver_opts:
|
||||
@@ -20,38 +21,38 @@ version: "3.5"
|
||||
|
||||
services:
|
||||
scrypted:
|
||||
image: koush/scrypted
|
||||
environment:
|
||||
# Scrypted NVR Storage (Part 2 of 3)
|
||||
|
||||
# Uncomment the next line to configure the NVR plugin to store recordings
|
||||
# use the /nvr directory within the container. This can also be configured
|
||||
# within the plugin manually.
|
||||
# The drive or network share will ALSO need to be configured in the volumes
|
||||
# section below.
|
||||
# - SCRYPTED_NVR_VOLUME=/nvr
|
||||
|
||||
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
|
||||
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
|
||||
# nvidia support
|
||||
|
||||
# Uncomment next line to run avahi-daemon inside the container
|
||||
# Don't use if dbus and avahi run on the host and are bind-mounted
|
||||
# (see below under "volumes")
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
# runtime: nvidia
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
devices:
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
- /dev/dri:/dev/dri
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# coral PCI devices
|
||||
# - /dev/apex_0:/dev/apex_0
|
||||
# - /dev/apex_1:/dev/apex_1
|
||||
|
||||
volumes:
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
# modify and add the additional volume for Scrypted NVR
|
||||
# the following example would mount the /mnt/sda/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/sda/video:/nvr
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# or use a network mount from one of the examples above
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
# The following example would mount the /mnt/sda/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/media/video:/nvr
|
||||
|
||||
# Or use a network mount from one of the CIFS/NFS examples at the top of this file.
|
||||
# - type: volume
|
||||
# source: nvr
|
||||
# target: /nvr
|
||||
@@ -60,8 +61,37 @@ services:
|
||||
|
||||
# uncomment the following lines to expose Avahi, an mDNS advertiser.
|
||||
# make sure Avahi is running on the host machine, otherwise this will not work.
|
||||
# not compatible with Avahi enabled via SCRYPTED_DOCKER_AVAHI=true
|
||||
# - /var/run/dbus:/var/run/dbus
|
||||
# - /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
|
||||
devices: [
|
||||
# uncomment the common systems devices to pass
|
||||
# them through to docker.
|
||||
|
||||
# all usb devices, such as coral tpu
|
||||
# "/dev/bus/usb:/dev/bus/usb",
|
||||
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
# "/dev/dri:/dev/dri",
|
||||
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
|
||||
# "/dev/ttyACM0:/dev/ttyACM0",
|
||||
|
||||
# coral PCI devices
|
||||
# "/dev/apex_0:/dev/apex_0",
|
||||
# "/dev/apex_1:/dev/apex_1",
|
||||
]
|
||||
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
image: koush/scrypted
|
||||
|
||||
# logging is noisy and will unnecessarily wear on flash storage.
|
||||
# scrypted has per device in memory logging that is preferred.
|
||||
logging:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$SCRYPTED_DOCKER_AVAHI" ]
|
||||
then
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, not starting avahi-daemon" >/dev/stderr
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
@@ -13,4 +13,4 @@ until [ -e /var/run/dbus/system_bus_socket ]; do
|
||||
sleep 1s
|
||||
done
|
||||
echo "Starting Avahi daemon..."
|
||||
exec avahi-daemon --no-chroot -f /etc/avahi/avahi-daemon.conf
|
||||
exec avahi-daemon --no-chroot -f /etc/avahi/avahi-daemon.conf
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, not starting dbus-daemon" >/dev/stderr
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Starting dbus..."
|
||||
exec dbus-daemon --system --nofork
|
||||
exec dbus-daemon --system --nofork
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, won't manage dbus nor avahi-daemon" >/dev/stderr
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -qE " ((/var)?/run/dbus|(/var)?/run/avahi-daemon(/socket)?) " /proc/mounts; then
|
||||
echo "dbus and/or avahi-daemon volumes are bind mounted, won't touch them" >/dev/stderr
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# make run folders
|
||||
mkdir -p /var/run/dbus
|
||||
mkdir -p /var/run/avahi-daemon
|
||||
@@ -22,4 +32,4 @@ if [ ! -z "$DSM_HOSTNAME" ]; then
|
||||
sed -i "s/.*host-name.*/host-name=${DSM_HOSTNAME}/" /etc/avahi/avahi-daemon.conf
|
||||
else
|
||||
sed -i "s/.*host-name.*/#host-name=/" /etc/avahi/avahi-daemon.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
16
install/docker/install-intel-graphics.sh
Normal file
16
install/docker/install-intel-graphics.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
echo "Installing Intel graphics packages."
|
||||
apt-get update && apt-get install -y gpg-agent &&
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
apt-get -y update &&
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
exit $?
|
||||
else
|
||||
echo "Intel graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -6,19 +6,6 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$SERVICE_USER" == "root" ]
|
||||
then
|
||||
echo "Scrypted SERVICE_USER root is not allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
mkdir -p $SCRYPTED_HOME
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
@@ -30,6 +17,22 @@ function readyn() {
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$SERVICE_USER" == "root" ]
|
||||
then
|
||||
readyn "Scrypted will store its files in the root user home directory. Running as a non-root user is recommended. Are you sure?"
|
||||
if [ "$yn" == "n" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
mkdir -p $SCRYPTED_HOME
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
|
||||
readyn "Install Docker?"
|
||||
|
||||
if [ "$yn" == "y" ]
|
||||
@@ -43,6 +46,10 @@ WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
|
||||
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/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 ]
|
||||
then
|
||||
sed -i 's/'#' - \/dev\/dri/- \/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME
|
||||
|
||||
@@ -4,26 +4,41 @@
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
then \
|
||||
apt-get update && apt-get install -y gpg-agent && \
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg && \
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-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 && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# 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 --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
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
|
||||
|
||||
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"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=full
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
@@ -21,10 +22,14 @@ RUN apt-get update && apt-get -y install \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get install -y ca-certificates curl gnupg
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN echo "Installing python."
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
@@ -32,67 +37,33 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# plugins support fallback to pillow, but vips is faster.
|
||||
RUN apt-get -y install \
|
||||
libvips
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
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-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
RUN echo "Installing gstreamer bindings."
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
# which causes weird behavior in python which looks at the arch version
|
||||
# which still reports 64bit, even if running in 32bit docker.
|
||||
# this scenario is not supported and will be reported at runtime.
|
||||
# this bit is not necessary on amd64, but leaving it for consistency.
|
||||
RUN apt-get -y install \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# python pip
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
|
||||
@@ -12,6 +12,26 @@ then
|
||||
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
|
||||
}
|
||||
|
||||
if [ "$SERVICE_USER" = "root" ] && [ -z "$SERVICE_USER_ROOT" ]
|
||||
then
|
||||
readyn "Scrypted will store its files in the root user home directory. Running as a non-root user is recommended. Are you sure?"
|
||||
if [ "$yn" == "n" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Stopping existing service if it is running..."
|
||||
systemctl stop scrypted.service
|
||||
|
||||
@@ -49,6 +69,14 @@ ENV() {
|
||||
}
|
||||
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.header)
|
||||
if [ -z "$SCRYPTED_INSTALL_ENVIRONMENT" ]
|
||||
then
|
||||
SCRYPTED_INSTALL_ENVIRONMENT=local
|
||||
fi
|
||||
if [ "$SCRYPTED_INSTALL_ENVIRONMENT" = "lxc" ]
|
||||
then
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.footer)
|
||||
fi
|
||||
|
||||
if [ -z "$SERVICE_USER" ]
|
||||
then
|
||||
@@ -56,12 +84,6 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$SERVICE_USER" == "root" ]
|
||||
then
|
||||
echo "Scrypted SERVICE_USER root is not allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# this is not RUN as we do not care about the result
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
echo "Setting permissions on $USER_HOME/.scrypted"
|
||||
@@ -84,6 +106,7 @@ ExecStart=/usr/bin/npx -y scrypted serve
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
Environment="NODE_OPTIONS=$NODE_OPTIONS"
|
||||
Environment="SCRYPTED_INSTALL_ENVIRONMENT=$SCRYPTED_INSTALL_ENVIRONMENT"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -45,23 +45,11 @@ RUN brew install libvips
|
||||
# dlib
|
||||
RUN brew install cmake
|
||||
|
||||
### HACK WORKAROUND
|
||||
### https://github.com/koush/scrypted/issues/544
|
||||
|
||||
brew unpin gstreamer
|
||||
brew unpin gst-plugins-base
|
||||
brew unpin gst-plugins-good
|
||||
brew unpin gst-plugins-bad
|
||||
brew unpin gst-plugins-ugly
|
||||
brew unpin gst-libav
|
||||
brew unpin gst-python
|
||||
|
||||
### END HACK WORKAROUND
|
||||
# seems to be necessary for python-codecs' pycairo dependency or something?
|
||||
RUN_IGNORE gobject-introspection libffi pkg-config
|
||||
|
||||
# gstreamer plugins
|
||||
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav
|
||||
# gst python bindings
|
||||
RUN_IGNORE brew install gst-python
|
||||
RUN_IGNORE brew install gstreamer
|
||||
|
||||
ARCH=$(arch)
|
||||
if [ "$ARCH" = "arm64" ]
|
||||
|
||||
26
install/local/install-scrypted-proxmox.sh
Normal file
26
install/local/install-scrypted-proxmox.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
cd /tmp
|
||||
curl -O -L https://github.com/koush/scrypted/releases/download/v0.72.0/scrypted.tar.zst
|
||||
pct restore 10443 scrypted.tar.zst
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
fi
|
||||
|
||||
echo "Scrypted setup is complete and the container can be started."
|
||||
8
packages/cli/.vscode/launch.json
vendored
8
packages/cli/.vscode/launch.json
vendored
@@ -19,11 +19,9 @@
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"preLaunchTask": "npm: build",
|
||||
"args": [
|
||||
"ffplay",
|
||||
"Kitchen",
|
||||
"getRecordingStream",
|
||||
"{\"startTime\":1677699495709}"
|
||||
"shell",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
@@ -35,4 +33,4 @@
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
738
packages/cli/package-lock.json
generated
738
packages/cli/package-lock.json
generated
@@ -1,37 +1,32 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.67",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.0.67",
|
||||
"version": "1.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.1.43",
|
||||
"@scrypted/types": "^0.2.66",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios": "^0.21.4",
|
||||
"engine.io-client": "^5.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"mkdirp": "^1.0.4",
|
||||
"@scrypted/client": "^1.3.2",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.8",
|
||||
"tslib": "^2.5.0"
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted": "dist/main.js"
|
||||
"scrypted": "dist/packages/cli/src/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/readline-sync": "^1.4.4",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.13",
|
||||
"@types/node": "^20.9.4",
|
||||
"@types/readline-sync": "^1.4.8",
|
||||
"@types/semver": "^7.5.6",
|
||||
"rimraf": "^5.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -46,6 +41,22 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
@@ -71,69 +82,30 @@
|
||||
"@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/client": {
|
||||
"version": "1.1.43",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.1.43.tgz",
|
||||
"integrity": "sha512-qpeGdqFga/Fx51MoF3E0iBPCjE/SDEIVdGh8Ws5dqw38bxUJD264c9NsNyCguLKyYguErKTAWnQkzqhO0bUbaA==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.3.2.tgz",
|
||||
"integrity": "sha512-PZwjfKUYIMxBYm7V2o0/vAMlQmznKLN/d4rpshb5vV086mnhh578ik3h39awkwoPyWzNGDcYeoBY0BchhwtdOQ==",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.66",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/engine.io-client": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/engine.io-parser": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
|
||||
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
"engine.io-client": "^6.5.3",
|
||||
"rimraf": "^5.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.66",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.66.tgz",
|
||||
"integrity": "sha512-AL2iD7OmpqZlQMlpZKUBHpzL7H1IHhwKOi9uhRbVwG7EIDwenTspqtziH2Hyu0+XeCLf+gN69uQB6Qlz+QPf9A=="
|
||||
"version": "0.2.99",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.99.tgz",
|
||||
"integrity": "sha512-2J1FH7tpAW5X3rgA70gJ+z0HFM90c/tBA+JXdP1vI1d/0yVmh9TSxnHoCuADN4R2NQXHmoZ6Nbds9kKAQ/25XQ=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -164,57 +136,25 @@
|
||||
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mkdirp": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
|
||||
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
|
||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
||||
"dev": true
|
||||
"version": "20.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
|
||||
"integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/readline-sync": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
|
||||
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.8.tgz",
|
||||
"integrity": "sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/glob": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
|
||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
@@ -238,12 +178,26 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
|
||||
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
|
||||
"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==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
@@ -253,11 +207,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.14.7"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
@@ -265,32 +219,29 @@
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
||||
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
|
||||
"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/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"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/create-require": {
|
||||
"version": "1.1.1",
|
||||
@@ -298,6 +249,19 @@
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -323,32 +287,34 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.2.0.tgz",
|
||||
"integrity": "sha512-BcIBXGBkT7wKecwnfrSV79G2X5lSUSgeAGgoo60plXf8UsQEvCQww/KMwXSMhVjb98fFYNq20CC5eo8IOAPqsg==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "0.1.4",
|
||||
"component-emitter": "~1.3.0",
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~4.0.1",
|
||||
"has-cors": "1.1.0",
|
||||
"parseqs": "0.0.6",
|
||||
"parseuri": "0.0.6",
|
||||
"ws": "~7.4.2",
|
||||
"xmlhttprequest-ssl": "~2.0.0",
|
||||
"yeast": "0.1.2"
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
|
||||
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "0.1.4"
|
||||
},
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
@@ -370,53 +336,71 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
"cross-spawn": "^7.0.0",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/has-cors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
|
||||
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"node_modules/glob": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
|
||||
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
|
||||
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
@@ -436,25 +420,25 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
|
||||
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -462,30 +446,35 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseqs": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
|
||||
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
|
||||
},
|
||||
"node_modules/parseuri": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
|
||||
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz",
|
||||
"integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==",
|
||||
"engines": {
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/readline-sync": {
|
||||
@@ -497,23 +486,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
|
||||
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.3.8",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
|
||||
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -524,6 +516,124 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
@@ -568,40 +678,139 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
|
||||
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
@@ -629,11 +838,6 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/yeast": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.67",
|
||||
"version": "1.3.5",
|
||||
"description": "",
|
||||
"main": "./dist/main.js",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
"scrypted": "./dist/main.js"
|
||||
"scrypted": "./dist/packages/cli/src/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
@@ -16,25 +16,20 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.1.43",
|
||||
"@scrypted/types": "^0.2.66",
|
||||
"adm-zip": "^0.5.10",
|
||||
"axios": "^0.21.4",
|
||||
"engine.io-client": "^5.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"mkdirp": "^1.0.4",
|
||||
"@scrypted/client": "^1.3.2",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.8",
|
||||
"tslib": "^2.5.0"
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/readline-sync": "^1.4.4",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.13",
|
||||
"rimraf": "^5.0.5",
|
||||
"@types/node": "^20.9.4",
|
||||
"@types/readline-sync": "^1.4.8",
|
||||
"@types/semver": "^7.5.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import readline from 'readline-sync';
|
||||
import https from 'https';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { installServe, serveMain } from './service';
|
||||
import { connectScryptedClient } from '@scrypted/client';
|
||||
import { ScryptedMimeTypes, FFmpegInput } from '@scrypted/types';
|
||||
import semver from 'semver';
|
||||
import { FFmpegInput, ScryptedMimeTypes } from '@scrypted/types';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
import path from 'path';
|
||||
import readline from 'readline-sync';
|
||||
import semver from 'semver';
|
||||
import { installServe, serveMain } from './service';
|
||||
import { connectShell } from './shell';
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
@@ -64,7 +64,9 @@ async function doLogin(host: string) {
|
||||
httpsAgent,
|
||||
}, axiosConfig));
|
||||
|
||||
mkdirp.sync(scryptedHome);
|
||||
fs.mkdirSync(scryptedHome, {
|
||||
recursive: true,
|
||||
});
|
||||
let login: LoginFile;
|
||||
try {
|
||||
login = JSON.parse(fs.readFileSync(loginPath).toString());
|
||||
@@ -172,8 +174,11 @@ async function main() {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
|
||||
}
|
||||
}
|
||||
console.log('ffplay', ...ffmpegInput.inputArguments);
|
||||
child_process.spawn('ffplay', ffmpegInput.inputArguments, {
|
||||
const args = [...ffmpegInput.inputArguments];
|
||||
if (ffmpegInput.h264FilterArguments)
|
||||
args.push(...ffmpegInput.h264FilterArguments);
|
||||
console.log('ffplay', ...args);
|
||||
child_process.spawn('ffplay', args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
sdk.disconnect();
|
||||
@@ -217,6 +222,25 @@ async function main() {
|
||||
|
||||
console.log('install successful. id:', response.data.id);
|
||||
}
|
||||
else if (process.argv[2] === 'shell') {
|
||||
console.log = () => { };
|
||||
|
||||
const host = toIpAndPort(process.argv[3] || '127.0.0.1');
|
||||
const login = await getOrDoLogin(host);
|
||||
const sdk = await connectScryptedClient({
|
||||
baseUrl: `https://${host}`,
|
||||
pluginId: '@scrypted/core',
|
||||
username: login.username,
|
||||
password: login.token,
|
||||
axiosConfig: {
|
||||
httpsAgent,
|
||||
}
|
||||
});
|
||||
|
||||
const separator = process.argv.indexOf("--");
|
||||
const cmd = separator != -1 ? process.argv.slice(separator + 1) : [];
|
||||
await connectShell(sdk, ...cmd);
|
||||
}
|
||||
else {
|
||||
console.log('usage:');
|
||||
console.log(' npx scrypted install npm-package-name [127.0.0.1[:10443]]');
|
||||
@@ -228,6 +252,7 @@ async function main() {
|
||||
console.log(' npx scrypted command name-or-id[@127.0.0.1[:10443]] method-name [...method-arguments]');
|
||||
console.log(' npx scrypted ffplay name-or-id[@127.0.0.1[:10443]] method-name [...method-arguments]');
|
||||
console.log(' npx scrypted create-cert-json /path/to/key.pem /path/to/cert.pem');
|
||||
console.log(' npx scrypted shell [127.0.0.1[:10443]] [-- cmd [...cmd-args]]');
|
||||
console.log();
|
||||
console.log('examples:');
|
||||
console.log(' npx scrypted install @scrypted/rtsp');
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
import child_process from 'child_process';
|
||||
import { once } from 'events';
|
||||
import fs from 'fs';
|
||||
import rimraf from 'rimraf';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import mkdirp from 'mkdirp';
|
||||
import semver from 'semver';
|
||||
|
||||
async function sleep(ms: number) {
|
||||
@@ -20,7 +18,12 @@ async function runCommand(command: string, ...args: string[]) {
|
||||
command += '.cmd';
|
||||
console.log('running', command, ...args);
|
||||
const cp = child_process.spawn(command, args, {
|
||||
stdio: 'inherit'
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
// https://github.com/lovell/sharp/blob/eefaa998725cf345227d94b40615e090495c6d09/lib/libvips.js#L115C19-L115C46
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS: 'true',
|
||||
},
|
||||
});
|
||||
await once(cp, 'exit');
|
||||
if (cp.exitCode)
|
||||
@@ -57,17 +60,26 @@ export function getInstallDir() {
|
||||
export function cwdInstallDir(): { volume: string, installDir: string } {
|
||||
const installDir = getInstallDir();
|
||||
const volume = path.join(installDir, 'volume');
|
||||
mkdirp.sync(volume);
|
||||
fs.mkdirSync(volume, {
|
||||
recursive: true,
|
||||
});
|
||||
process.chdir(installDir);
|
||||
return { volume, installDir };
|
||||
}
|
||||
|
||||
function rimrafSync(p: string) {
|
||||
fs.rmSync(p, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function installServe(installVersion: string, ignoreError?: boolean) {
|
||||
const { installDir } = cwdInstallDir();
|
||||
const packageLockJson = path.join(installDir, 'package-lock.json');
|
||||
// apparently corrupted or old version of package-lock.json prevents upgrades, so
|
||||
// nuke it before installing.
|
||||
rimraf.sync(packageLockJson);
|
||||
rimrafSync(packageLockJson);
|
||||
|
||||
const installJson = path.join(installDir, 'install.json');
|
||||
try {
|
||||
@@ -78,7 +90,7 @@ export async function installServe(installVersion: string, ignoreError?: boolean
|
||||
catch (e) {
|
||||
const nodeModules = path.join(installDir, 'node_modules');
|
||||
console.log('Node version mismatch, missing, or corrupt. Clearing node_modules.');
|
||||
rimraf.sync(nodeModules);
|
||||
rimrafSync(nodeModules);
|
||||
}
|
||||
fs.writeFileSync(installJson, JSON.stringify({
|
||||
version: process.version,
|
||||
@@ -112,8 +124,8 @@ export async function serveMain(installVersion?: string) {
|
||||
console.log('cwd', process.cwd());
|
||||
|
||||
while (true) {
|
||||
rimraf.sync(EXIT_FILE);
|
||||
rimraf.sync(UPDATE_FILE);
|
||||
rimrafSync(EXIT_FILE);
|
||||
rimrafSync(UPDATE_FILE);
|
||||
|
||||
await startServer(installDir);
|
||||
|
||||
|
||||
90
packages/cli/src/shell.ts
Normal file
90
packages/cli/src/shell.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { DeviceProvider, ScryptedStatic, StreamService } from "@scrypted/types";
|
||||
import { createAsyncQueue } from '../../../common/src/async-queue';
|
||||
|
||||
export async function connectShell(sdk: ScryptedStatic, ...cmd: string[]) {
|
||||
const termSvc = await sdk.systemManager.getDeviceByName<DeviceProvider>("@scrypted/core").getDevice("terminalservice");
|
||||
if (!termSvc) {
|
||||
throw Error("@scrypted/core does not provide a Terminal Service");
|
||||
}
|
||||
|
||||
const termSvcDirect = await sdk.connectRPCObject<StreamService>(termSvc);
|
||||
const dataQueue = createAsyncQueue<Buffer>();
|
||||
const ctrlQueue = createAsyncQueue<any>();
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
} else {
|
||||
process.stdin.on("end", () => {
|
||||
ctrlQueue.enqueue({ eof: true });
|
||||
dataQueue.enqueue(Buffer.alloc(0));
|
||||
});
|
||||
}
|
||||
ctrlQueue.enqueue({ interactive: Boolean(process.stdin.isTTY), cmd: cmd });
|
||||
|
||||
const dim = { cols: process.stdout.columns, rows: process.stdout.rows };
|
||||
ctrlQueue.enqueue({ dim });
|
||||
|
||||
let bufferedLength = 0;
|
||||
const MAX_BUFFERED_LENGTH = 64000;
|
||||
process.stdin.on('data', async data => {
|
||||
bufferedLength += data.length;
|
||||
const promise = dataQueue.enqueue(data).then(() => bufferedLength -= data.length);
|
||||
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
|
||||
process.stdin.pause();
|
||||
await promise;
|
||||
if (bufferedLength < MAX_BUFFERED_LENGTH)
|
||||
process.stdin.resume();
|
||||
}
|
||||
});
|
||||
|
||||
async function* generator() {
|
||||
while (true) {
|
||||
const ctrlBuffers = ctrlQueue.clear();
|
||||
if (ctrlBuffers.length) {
|
||||
for (const ctrl of ctrlBuffers) {
|
||||
if (ctrl.eof) {
|
||||
// flush the buffer before sending eof
|
||||
const dataBuffers = dataQueue.clear();
|
||||
const concat = Buffer.concat(dataBuffers);
|
||||
if (concat.length) {
|
||||
yield concat;
|
||||
}
|
||||
}
|
||||
yield JSON.stringify(ctrl);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const dataBuffers = dataQueue.clear();
|
||||
if (dataBuffers.length === 0) {
|
||||
const buf = await dataQueue.dequeue();
|
||||
if (buf.length)
|
||||
yield buf;
|
||||
continue;
|
||||
}
|
||||
|
||||
const concat = Buffer.concat(dataBuffers);
|
||||
if (concat.length)
|
||||
yield concat;
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.on('resize', () => {
|
||||
const dim = { cols: process.stdout.columns, rows: process.stdout.rows };
|
||||
ctrlQueue.enqueue({ dim });
|
||||
dataQueue.enqueue(Buffer.alloc(0));
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const message of await termSvcDirect.connectStream(generator())) {
|
||||
if (!message) {
|
||||
process.exit();
|
||||
}
|
||||
process.stdout.write(new Uint8Array(Buffer.from(message)));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
27
packages/client/.vscode/launch.json
vendored
Normal file
27
packages/client/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// 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": "ts-node",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${relativeFile}"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"env": {
|
||||
"SCRYPTED_USERNAME": "koush",
|
||||
"SCRYPTED_PASSWORD": "k9copUSA",
|
||||
},
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
packages/client/examples/connectRPCObject.ts
Normal file
34
packages/client/examples/connectRPCObject.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Camera, VideoCamera, VideoFrameGenerator } from '@scrypted/types';
|
||||
import { connectScryptedClient } from '../dist/packages/client/src';
|
||||
|
||||
import https from 'https';
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
})
|
||||
|
||||
async function example() {
|
||||
const sdk = await connectScryptedClient({
|
||||
baseUrl: 'https://localhost:10443',
|
||||
pluginId: "@scrypted/core",
|
||||
username: process.env.SCRYPTED_USERNAME || 'admin',
|
||||
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
|
||||
axiosConfig: {
|
||||
httpsAgent,
|
||||
}
|
||||
});
|
||||
console.log('server version', sdk.serverVersion);
|
||||
|
||||
const office = sdk.systemManager.getDeviceByName<VideoCamera & Camera>("Office");
|
||||
const libav = sdk.systemManager.getDeviceByName<VideoFrameGenerator>("Libav");
|
||||
const mo = await office.getVideoStream();
|
||||
|
||||
const generator = await libav.generateVideoFrames(mo);
|
||||
const remote = await sdk.connectRPCObject!(generator);
|
||||
|
||||
for await (const frame of remote) {
|
||||
console.log(frame);
|
||||
}
|
||||
}
|
||||
|
||||
example();
|
||||
553
packages/client/package-lock.json
generated
553
packages/client/package-lock.json
generated
@@ -1,29 +1,54 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.54",
|
||||
"version": "1.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.54",
|
||||
"version": "1.3.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
"engine.io-client": "^6.5.3",
|
||||
"rimraf": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^20.9.4",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"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.2.91",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.91.tgz",
|
||||
"integrity": "sha512-GfWil8cl2QwlTXk506ZXDALQfuv7zN48PtPlpmBMO/IYTQFtb+RB2zr+FwC9gdvRaZgs9NCCS2Fiig1OY7uxdQ=="
|
||||
"version": "0.2.99",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.99.tgz",
|
||||
"integrity": "sha512-2J1FH7tpAW5X3rgA70gJ+z0HFM90c/tBA+JXdP1vI1d/0yVmh9TSxnHoCuADN4R2NQXHmoZ6Nbds9kKAQ/25XQ=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -31,19 +56,44 @@
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/@types/ip": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
|
||||
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.3.tgz",
|
||||
"integrity": "sha512-64waoJgkXFTYnCYDUWgSATJ/dXEBanVkaP5d4Sbk7P6U7cTTMhxVyROTckc6JKdwCrgnAjZMn0k3177aQxtDEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
|
||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
||||
"dev": true
|
||||
"version": "20.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
|
||||
"integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.25.0",
|
||||
@@ -59,18 +109,41 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
"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/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
@@ -88,22 +161,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
|
||||
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
@@ -127,53 +210,100 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
"cross-spawn": "^7.0.0",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"node_modules/glob": {
|
||||
"version": "10.3.10",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
|
||||
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"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"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"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==",
|
||||
"engines": {
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"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==",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -181,53 +311,280 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"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==",
|
||||
"dependencies": {
|
||||
"glob": "^10.3.7"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
|
||||
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
"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==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.54",
|
||||
"version": "1.3.2",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -12,14 +12,14 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^20.9.4",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
"engine.io-client": "^6.5.3",
|
||||
"rimraf": "^5.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios';
|
||||
import * as eio from 'engine.io-client';
|
||||
import { SocketOptions } from 'engine.io-client';
|
||||
import { Deferred } from "../../../common/src/deferred";
|
||||
@@ -8,13 +8,15 @@ import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceCon
|
||||
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
|
||||
import type { IOSocket } from '../../../server/src/io';
|
||||
import { MediaObject } from '../../../server/src/plugin/mediaobject';
|
||||
import type { MediaObjectRemote } from '../../../server/src/plugin/plugin-api';
|
||||
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
|
||||
import type { ClusterObject, ConnectRPCObject } from '../../../server/src/cluster/connect-rpc-object';
|
||||
import { RpcPeer } from '../../../server/src/rpc';
|
||||
import { createRpcDuplexSerializer, createRpcSerializer } from '../../../server/src/rpc-serializer';
|
||||
import packageJson from '../package.json';
|
||||
import { isIPAddress } from "./ip";
|
||||
|
||||
const sourcePeerId = RpcPeer.generateId();
|
||||
|
||||
type IOClientSocket = eio.Socket & IOSocket;
|
||||
|
||||
function once(socket: IOClientSocket, event: 'open' | 'message') {
|
||||
@@ -48,9 +50,8 @@ export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
browserSignalingSession?: BrowserSignalingSession;
|
||||
address?: string;
|
||||
connectionType: ScryptedClientConnectionType;
|
||||
authorization?: string;
|
||||
queryToken?: { [parameter: string]: string };
|
||||
rpcPeer: RpcPeer,
|
||||
rpcPeer: RpcPeer;
|
||||
loginResult: ScryptedClientLoginResult;
|
||||
}
|
||||
|
||||
export interface ScryptedConnectionOptions {
|
||||
@@ -59,6 +60,7 @@ export interface ScryptedConnectionOptions {
|
||||
webrtc?: boolean;
|
||||
baseUrl?: string;
|
||||
axiosConfig?: AxiosRequestConfig;
|
||||
previousLoginResult?: ScryptedClientLoginResult;
|
||||
}
|
||||
|
||||
export interface ScryptedLoginOptions extends ScryptedConnectionOptions {
|
||||
@@ -133,34 +135,42 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
if (response.status !== 200)
|
||||
throw new Error('status ' + response.status);
|
||||
|
||||
const addresses = response.data.addresses as string[] || [];
|
||||
// the cloud plugin will include this header.
|
||||
// should maybe move this into the cloud server itself.
|
||||
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
|
||||
const directAddress = response.headers['x-scrypted-direct-address'];
|
||||
|
||||
return {
|
||||
error: response.data.error as string,
|
||||
authorization: response.data.authorization as string,
|
||||
queryToken: response.data.queryToken as any,
|
||||
token: response.data.token as string,
|
||||
addresses,
|
||||
scryptedCloud,
|
||||
directAddress,
|
||||
addresses: response.data.addresses as string[],
|
||||
externalAddresses: response.data.externalAddresses as string[],
|
||||
// the cloud plugin will include this header.
|
||||
// should maybe move this into the cloud server itself.
|
||||
scryptedCloud: response.headers['x-scrypted-cloud'] === 'true',
|
||||
directAddress: response.headers['x-scrypted-direct-address'],
|
||||
cloudAddress: response.headers['x-scrypted-cloud-address'],
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkScryptedClientLogin(options?: ScryptedConnectionOptions) {
|
||||
let { baseUrl } = options || {};
|
||||
const url = combineBaseUrl(baseUrl, 'login');
|
||||
let url = combineBaseUrl(baseUrl, 'login');
|
||||
const headers: AxiosRequestHeaders = {};
|
||||
if (options?.previousLoginResult?.queryToken) {
|
||||
// headers.Authorization = options?.previousLoginResult?.authorization;
|
||||
// const search = new URLSearchParams(options.previousLoginResult.queryToken);
|
||||
// url += '?' + search.toString();
|
||||
const token = options?.previousLoginResult.username + ":" + options.previousLoginResult.token;
|
||||
const hash = Buffer.from(token).toString('base64');
|
||||
|
||||
headers.Authorization = `Basic ${hash}`;
|
||||
}
|
||||
const response = await axios.get(url, {
|
||||
withCredentials: true,
|
||||
headers,
|
||||
...options?.axiosConfig,
|
||||
});
|
||||
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
|
||||
const directAddress = response.headers['x-scrypted-direct-address'];
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
hostname: response.data.hostname as string,
|
||||
redirect: response.data.redirect as string,
|
||||
username: response.data.username as string,
|
||||
@@ -171,11 +181,27 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
|
||||
queryToken: response.data.queryToken as any,
|
||||
token: response.data.token as string,
|
||||
addresses: response.data.addresses as string[],
|
||||
scryptedCloud,
|
||||
directAddress,
|
||||
externalAddresses: response.data.externalAddresses as string[],
|
||||
// the cloud plugin will include this header.
|
||||
// should maybe move this into the cloud server itself.
|
||||
scryptedCloud: response.headers['x-scrypted-cloud'] === 'true',
|
||||
directAddress: response.headers['x-scrypted-direct-address'],
|
||||
cloudAddress: response.headers['x-scrypted-cloud-address'],
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScryptedClientLoginResult {
|
||||
username: string;
|
||||
token: string;
|
||||
authorization: string;
|
||||
queryToken: { [parameter: string]: string };
|
||||
localAddresses: string[];
|
||||
externalAddresses: string[];
|
||||
scryptedCloud: boolean;
|
||||
directAddress: string;
|
||||
cloudAddress: string;
|
||||
}
|
||||
|
||||
export class ScryptedClientLoginError extends Error {
|
||||
constructor(public result: Awaited<ReturnType<typeof checkScryptedClientLogin>>) {
|
||||
super(result.error);
|
||||
@@ -211,46 +237,119 @@ export async function redirectScryptedLogout(baseUrl?: string) {
|
||||
export async function connectScryptedClient(options: ScryptedClientOptions): Promise<ScryptedClientStatic> {
|
||||
const start = Date.now();
|
||||
let { baseUrl, pluginId, clientName, username, password } = options;
|
||||
|
||||
let authorization: string;
|
||||
let queryToken: any;
|
||||
|
||||
const extraHeaders: { [header: string]: string } = {};
|
||||
let localAddresses: string[];
|
||||
let externalAddresses: string[];
|
||||
let scryptedCloud: boolean;
|
||||
let directAddress: string;
|
||||
let cloudAddress: string;
|
||||
let token: string;
|
||||
|
||||
console.log('@scrypted/client', packageJson.version);
|
||||
|
||||
const extraHeaders: { [header: string]: string } = {};
|
||||
|
||||
// Chrome will complain about websites making xhr requests to self signed https sites, even
|
||||
// if the cert has been accepted. Other browsers seem fine.
|
||||
// So the default is not to connect to IP addresses on Chrome, but do so on other browsers.
|
||||
const isChrome = globalThis.navigator?.userAgent.includes('Chrome');
|
||||
const isNotChromeOrIsInstalledApp = !isChrome || isInstalledApp();
|
||||
let tryAlternateAddresses = false;
|
||||
|
||||
if (username && password) {
|
||||
const loginResult = await loginScryptedClient(options as ScryptedLoginOptions);
|
||||
if (loginResult.authorization)
|
||||
extraHeaders['Authorization'] = loginResult.authorization;
|
||||
localAddresses = loginResult.addresses;
|
||||
externalAddresses = loginResult.externalAddresses;
|
||||
scryptedCloud = loginResult.scryptedCloud;
|
||||
directAddress = loginResult.directAddress;
|
||||
cloudAddress = loginResult.cloudAddress;
|
||||
authorization = loginResult.authorization;
|
||||
queryToken = loginResult.queryToken;
|
||||
token = loginResult.token;
|
||||
console.log('login result', Date.now() - start, loginResult);
|
||||
}
|
||||
else {
|
||||
const loginCheck = await checkScryptedClientLogin({
|
||||
const urlsToCheck = new Set<string>();
|
||||
if (options?.previousLoginResult?.token) {
|
||||
for (const u of [
|
||||
...options?.previousLoginResult?.localAddresses || [],
|
||||
options?.previousLoginResult?.directAddress,
|
||||
]) {
|
||||
if (u && (isNotChromeOrIsInstalledApp || options.direct))
|
||||
urlsToCheck.add(u);
|
||||
}
|
||||
for (const u of [
|
||||
...options?.previousLoginResult?.externalAddresses || [],
|
||||
options?.previousLoginResult?.cloudAddress,
|
||||
]) {
|
||||
if (u)
|
||||
urlsToCheck.add(u);
|
||||
}
|
||||
}
|
||||
|
||||
// the alternate urls must have a valid response.
|
||||
const loginCheckPromises = [...urlsToCheck].map(async baseUrl => {
|
||||
const loginCheck = await checkScryptedClientLogin({
|
||||
baseUrl,
|
||||
previousLoginResult: options?.previousLoginResult,
|
||||
});
|
||||
|
||||
if (loginCheck.error || loginCheck.redirect)
|
||||
throw new Error('login error');
|
||||
|
||||
if (!loginCheck.authorization || !loginCheck.username || !loginCheck.queryToken) {
|
||||
console.error(loginCheck);
|
||||
throw new Error('malformed login result');
|
||||
}
|
||||
|
||||
return loginCheck;
|
||||
});
|
||||
|
||||
const baseUrlCheck = checkScryptedClientLogin({
|
||||
baseUrl,
|
||||
});
|
||||
loginCheckPromises.push(baseUrlCheck);
|
||||
|
||||
let loginCheck: Awaited<ReturnType<typeof checkScryptedClientLogin>>;
|
||||
try {
|
||||
loginCheck = await Promise.any(loginCheckPromises);
|
||||
tryAlternateAddresses ||= loginCheck.baseUrl !== baseUrl;
|
||||
}
|
||||
catch (e) {
|
||||
loginCheck = await baseUrlCheck;
|
||||
}
|
||||
|
||||
if (tryAlternateAddresses)
|
||||
console.log('Found direct login. Allowing alternate addresses.')
|
||||
|
||||
if (loginCheck.error || loginCheck.redirect)
|
||||
throw new ScryptedClientLoginError(loginCheck);
|
||||
localAddresses = loginCheck.addresses;
|
||||
externalAddresses = loginCheck.externalAddresses;
|
||||
scryptedCloud = loginCheck.scryptedCloud;
|
||||
directAddress = loginCheck.directAddress;
|
||||
cloudAddress = loginCheck.cloudAddress;
|
||||
username = loginCheck.username;
|
||||
authorization = loginCheck.authorization;
|
||||
queryToken = loginCheck.queryToken;
|
||||
token = loginCheck.token;
|
||||
console.log('login checked', Date.now() - start, loginCheck);
|
||||
}
|
||||
|
||||
let socket: IOClientSocket;
|
||||
const eioPath = `endpoint/${pluginId}/engine.io/api`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
// https://github.com/socketio/engine.io/issues/690
|
||||
const cacehBust = Math.random().toString(36).substring(3, 10);
|
||||
const eioOptions: Partial<SocketOptions> = {
|
||||
path: eioEndpoint,
|
||||
query: {
|
||||
cacehBust,
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
@@ -263,25 +362,45 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
// watch for this flush.
|
||||
const flush = new Deferred<void>();
|
||||
|
||||
// Chrome will complain about websites making xhr requests to self signed https sites, even
|
||||
// if the cert has been accepted. Other browsers seem fine.
|
||||
// So the default is not to connect to IP addresses on Chrome, but do so on other browsers.
|
||||
const isChrome = globalThis.navigator?.userAgent.includes('Chrome');
|
||||
const isNotChromeOrIsInstalledApp = !isChrome || isInstalledApp();
|
||||
|
||||
const addresses: string[] = [];
|
||||
const localAddressDefault = isNotChromeOrIsInstalledApp;
|
||||
if (((scryptedCloud && options.local === undefined && localAddressDefault) || options.local) && localAddresses) {
|
||||
|
||||
tryAlternateAddresses ||= scryptedCloud;
|
||||
|
||||
if (((tryAlternateAddresses && options.local === undefined && localAddressDefault) || options.local) && localAddresses) {
|
||||
addresses.push(...localAddresses);
|
||||
}
|
||||
|
||||
const directAddressDefault = directAddress && (isNotChromeOrIsInstalledApp || !isIPAddress(directAddress));
|
||||
if (((scryptedCloud && options.direct === undefined && directAddressDefault) || options.direct) && directAddress) {
|
||||
if (((tryAlternateAddresses && options.direct === undefined && directAddressDefault) || options.direct) && directAddress) {
|
||||
addresses.push(directAddress);
|
||||
}
|
||||
|
||||
if ((tryAlternateAddresses && options.direct === undefined) || options.direct) {
|
||||
if (cloudAddress)
|
||||
addresses.push(cloudAddress);
|
||||
for (const externalAddress of externalAddresses || []) {
|
||||
addresses.push(externalAddress);
|
||||
}
|
||||
}
|
||||
|
||||
const tryAddresses = !!addresses.length;
|
||||
const tryWebrtc = !!globalThis.RTCPeerConnection && (scryptedCloud && options.webrtc === undefined) || options.webrtc;
|
||||
const webrtcLastFailedKey = 'webrtcLastFailed';
|
||||
const canUseWebrtc = !!globalThis.RTCPeerConnection;
|
||||
let tryWebrtc = canUseWebrtc && options.webrtc;
|
||||
// try webrtc by default on scrypted cloud.
|
||||
// but webrtc takes a while to fail, so backoff if it fails to prevent continual slow starts.
|
||||
if (scryptedCloud && canUseWebrtc && globalThis.localStorage && options.webrtc === undefined) {
|
||||
tryWebrtc = true;
|
||||
const webrtcLastFailed = parseFloat(localStorage.getItem(webrtcLastFailedKey));
|
||||
// if webrtc has failed in the past day, dont attempt to use it.
|
||||
const now = Date.now();
|
||||
if (webrtcLastFailed < now && webrtcLastFailed > now - 1 * 24 * 60 * 60 * 1000) {
|
||||
tryWebrtc = false;
|
||||
console.warn('WebRTC API connection recently failed. Skipping.')
|
||||
}
|
||||
}
|
||||
|
||||
console.log({
|
||||
tryLocalAddressess: tryAddresses,
|
||||
tryWebrtc,
|
||||
@@ -309,7 +428,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
// It is probably better to simply prompt and redirect to the LAN address
|
||||
// if it is reacahble.
|
||||
|
||||
for (const address of addresses) {
|
||||
for (const address of new Set(addresses)) {
|
||||
console.log('trying', address);
|
||||
const check = new eio.Socket(address, localEioOptions);
|
||||
sockets.push(check);
|
||||
@@ -328,6 +447,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
console.log('trying webrtc');
|
||||
const webrtcEioOptions: Partial<SocketOptions> = {
|
||||
path: '/endpoint/@scrypted/webrtc/engine.io/',
|
||||
query: {
|
||||
cacehBust,
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
@@ -458,7 +580,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
const p2pPromises = [...promises];
|
||||
|
||||
promises.push((async () => {
|
||||
const waitDuration = tryWebrtc ? 3000 : (tryAddresses ? 1000 : 0);
|
||||
const waitDuration = tryWebrtc ? 10000 : (tryAddresses ? 1000 : 0);
|
||||
console.log('waiting', waitDuration);
|
||||
if (waitDuration) {
|
||||
// give the peer to peers a second, but then try connecting directly.
|
||||
@@ -478,6 +600,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
await once(check, 'open');
|
||||
return {
|
||||
ready: check,
|
||||
address: explicitBaseUrl,
|
||||
connectionType: 'http',
|
||||
};
|
||||
})());
|
||||
@@ -485,6 +608,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
const any = Promise.any(promises);
|
||||
let { ready, connectionType, address, rpcPeer } = await any;
|
||||
|
||||
if (tryWebrtc && connectionType !== 'webrtc')
|
||||
localStorage.setItem(webrtcLastFailedKey, Date.now().toString());
|
||||
|
||||
console.log('connected', connectionType, address)
|
||||
|
||||
socket = ready;
|
||||
@@ -584,6 +710,105 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
.map(id => systemManager.getDeviceById(id))
|
||||
.find(device => device.pluginId === '@scrypted/core' && device.nativeId === `user:${username}`);
|
||||
|
||||
const clusterPeers = new Map<number, Promise<RpcPeer>>();
|
||||
const ensureClusterPeer = (clusterObject: ClusterObject) => {
|
||||
let clusterPeerPromise = clusterPeers.get(clusterObject.port);
|
||||
if (!clusterPeerPromise) {
|
||||
clusterPeerPromise = (async () => {
|
||||
const eioPath = 'engine.io/connectRPCObject';
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
const clusterPeerOptions = {
|
||||
path: eioEndpoint,
|
||||
query: {
|
||||
cacehBust,
|
||||
clusterObject: JSON.stringify(clusterObject),
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
transports: options?.transports,
|
||||
};
|
||||
|
||||
const clusterPeerSocket = new eio.Socket(explicitBaseUrl, clusterPeerOptions);
|
||||
let peerReady = false;
|
||||
clusterPeerSocket.on('close', () => {
|
||||
clusterPeers.delete(clusterObject.port);
|
||||
if (!peerReady) {
|
||||
throw new Error("peer disconnected before setup completed");
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await once(clusterPeerSocket, 'open');
|
||||
|
||||
const serializer = createRpcDuplexSerializer({
|
||||
write: data => clusterPeerSocket.send(data),
|
||||
});
|
||||
clusterPeerSocket.on('message', data => serializer.onData(Buffer.from(data)));
|
||||
|
||||
const clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => {
|
||||
try {
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
}
|
||||
});
|
||||
serializer.setupRpcPeer(clusterPeer);
|
||||
clusterPeer.tags.localPort = sourcePeerId;
|
||||
peerReady = true;
|
||||
return clusterPeer;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('failure ipc connect', e);
|
||||
clusterPeerSocket.close();
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
clusterPeers.set(clusterObject.port, clusterPeerPromise);
|
||||
}
|
||||
return clusterPeerPromise;
|
||||
};
|
||||
|
||||
const resolveObject = async (proxyId: string, sourcePeerPort: number) => {
|
||||
const sourcePeer = await clusterPeers.get(sourcePeerPort);
|
||||
if (sourcePeer?.remoteWeakProxies) {
|
||||
return Object.values(sourcePeer.remoteWeakProxies).find(
|
||||
v => v.deref()?.__cluster?.proxyId == proxyId
|
||||
)?.deref();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectRPCObject = async (value: any) => {
|
||||
const clusterObject: ClusterObject = value?.__cluster;
|
||||
if (!clusterObject) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const { port, proxyId } = clusterObject;
|
||||
|
||||
// check if object is already connected
|
||||
const resolved = await resolveObject(proxyId, port);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
try {
|
||||
const clusterPeerPromise = ensureClusterPeer(clusterObject);
|
||||
const clusterPeer = await clusterPeerPromise;
|
||||
const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
|
||||
const newValue = await connectRPCObject(clusterObject);
|
||||
if (!newValue)
|
||||
throw new Error('ipc object not found?');
|
||||
return newValue;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('failure ipc', e);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const ret: ScryptedClientStatic = {
|
||||
userId: userDevice?.id,
|
||||
serverVersion,
|
||||
@@ -602,9 +827,19 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
pluginHostAPI: undefined,
|
||||
rtcConnectionManagement,
|
||||
browserSignalingSession,
|
||||
authorization,
|
||||
queryToken,
|
||||
rpcPeer,
|
||||
loginResult: {
|
||||
username,
|
||||
token,
|
||||
directAddress,
|
||||
localAddresses,
|
||||
externalAddresses,
|
||||
scryptedCloud,
|
||||
queryToken,
|
||||
authorization,
|
||||
cloudAddress,
|
||||
},
|
||||
connectRPCObject,
|
||||
}
|
||||
|
||||
socket.on('close', () => {
|
||||
|
||||
4
packages/deferred/package-lock.json
generated
4
packages/deferred/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
1
packages/deferred/src/async-queue.ts
Symbolic link
1
packages/deferred/src/async-queue.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../common/src/async-queue.ts
|
||||
1
packages/deferred/src/deferred.ts
Symbolic link
1
packages/deferred/src/deferred.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../common/src/deferred.ts
|
||||
@@ -1 +0,0 @@
|
||||
../../../common/src/deferred.ts
|
||||
2
packages/deferred/src/index.ts
Normal file
2
packages/deferred/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './deferred';
|
||||
export * from './async-queue';
|
||||
23
packages/h264-repacketizer/.vscode/launch.json
vendored
Normal file
23
packages/h264-repacketizer/.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": "ts-node",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${workspaceFolder}/test/test.ts"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
||||
296
packages/h264-repacketizer/package-lock.json
generated
296
packages/h264-repacketizer/package-lock.json
generated
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.7",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
},
|
||||
@@ -43,12 +44,121 @@
|
||||
"../sdk/types": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
@@ -64,6 +174,49 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
@@ -76,26 +229,165 @@
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
|
||||
"dev": true
|
||||
},
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
|
||||
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
93
packages/h264-repacketizer/test/test.ts
Normal file
93
packages/h264-repacketizer/test/test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { H264Repacketizer, depacketizeStapA } from '../src/index';
|
||||
import { H264_NAL_TYPE_IDR, H264_NAL_TYPE_PPS, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_A, RtspServer, getNaluTypesInNalu } from '../../../common/src/rtsp-server';
|
||||
import fs from 'fs';
|
||||
|
||||
import { getNvrSessionStream } from '../../../../nvr/nvr-plugin/src/session-stream';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
|
||||
|
||||
function parse(parameters: string) {
|
||||
const spspps = parameters.split(',');
|
||||
// empty sprop-parameter-sets is apparently a thing:
|
||||
// a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=
|
||||
if (spspps?.length !== 2) {
|
||||
return {
|
||||
sps: undefined,
|
||||
pps: undefined,
|
||||
};
|
||||
}
|
||||
const [sps, pps] = spspps;
|
||||
|
||||
return {
|
||||
sps: Buffer.from(sps, 'base64'),
|
||||
pps: Buffer.from(pps, 'base64'),
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const spspps = parse('Z2QAM6wVFKAoALWQ,aO48sA==');
|
||||
// Z2QAM6wVFKAoALWQ
|
||||
// Z00AMpY1QEABg03BQEFQAAADABAAAAMDKEA=
|
||||
|
||||
|
||||
const repacketizer = new H264Repacketizer(console, 1300, undefined);
|
||||
|
||||
const stream = fs.createReadStream('/Users/koush/Downloads/rtsp/1692537093973.rtsp', {
|
||||
start: 0,
|
||||
highWaterMark: 800000,
|
||||
});
|
||||
|
||||
let rtspParser = new RtspServer(stream as any, '');
|
||||
rtspParser.setupTracks = {
|
||||
'0': {
|
||||
codec: '0',
|
||||
protocol: 'tcp',
|
||||
control: '',
|
||||
destination: 0,
|
||||
},
|
||||
'2': {
|
||||
codec: '2',
|
||||
protocol: 'tcp',
|
||||
control: '',
|
||||
destination: 2,
|
||||
},
|
||||
}
|
||||
for await (const rtspSample of rtspParser.handleRecord()) {
|
||||
if (rtspSample.type !== '0')
|
||||
continue;
|
||||
const rtp = RtpPacket.deSerialize(rtspSample.packet);
|
||||
const nalus = getNaluTypesInNalu(rtp.payload);
|
||||
if (nalus.has(H264_NAL_TYPE_SEI)) {
|
||||
console.warn('SEI', rtp.payload)
|
||||
}
|
||||
if (nalus.has(H264_NAL_TYPE_SPS)) {
|
||||
console.warn('SPS', rtp.payload, spspps.sps)
|
||||
}
|
||||
if (nalus.has(H264_NAL_TYPE_PPS)) {
|
||||
console.warn('PPS', rtp.payload, spspps.sps)
|
||||
}
|
||||
if (nalus.has(H264_NAL_TYPE_STAP_A)) {
|
||||
const parts = depacketizeStapA(rtp.payload);
|
||||
console.log('stapa', parts);
|
||||
for (const part of parts) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (nalus.has(H264_NAL_TYPE_IDR)) {
|
||||
const h264Packetizer = new H264Repacketizer(console, 65535, spspps as any);
|
||||
// offset the stapa packet by -1 so the sequence numbers can be reused.
|
||||
h264Packetizer.extraPackets = -1;
|
||||
const stapas: RtpPacket[] = [];
|
||||
const idr = RtpPacket.deSerialize(rtspSample.packet);
|
||||
h264Packetizer.maybeSendStapACodecInfo(idr, stapas);
|
||||
if (stapas.length === 1) {
|
||||
const stapa = stapas[0].serialize();
|
||||
// console.log(stapa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
main();
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2019",
|
||||
"target": "ES2020",
|
||||
"noImplicitAny": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
@@ -10,6 +10,7 @@
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"exclude": ["**/node_modules"],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
|
||||
1
packages/python-client/.gitignore
vendored
Normal file
1
packages/python-client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.venv
|
||||
16
packages/python-client/.vscode/launch.json
vendored
Normal file
16
packages/python-client/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// 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": "Python: Current File",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/test.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
1
packages/python-client/plugin_remote.py
Symbolic link
1
packages/python-client/plugin_remote.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/plugin_remote.py
|
||||
3
packages/python-client/requirements.txt
Normal file
3
packages/python-client/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
python-engineio[asyncio_client]
|
||||
aiohttp
|
||||
aiodns
|
||||
1
packages/python-client/rpc.py
Symbolic link
1
packages/python-client/rpc.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/rpc.py
|
||||
1
packages/python-client/rpc_reader.py
Symbolic link
1
packages/python-client/rpc_reader.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/rpc_reader.py
|
||||
1
packages/python-client/scrypted_python
Symbolic link
1
packages/python-client/scrypted_python
Symbolic link
@@ -0,0 +1 @@
|
||||
../../sdk/types/scrypted_python
|
||||
151
packages/python-client/test.py
Normal file
151
packages/python-client/test.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import nullcontext
|
||||
|
||||
import aiohttp
|
||||
import engineio
|
||||
|
||||
import plugin_remote
|
||||
import rpc_reader
|
||||
from plugin_remote import DeviceManager, MediaManager, SystemManager
|
||||
from scrypted_python.scrypted_sdk import ScryptedInterface, ScryptedStatic
|
||||
|
||||
|
||||
class EioRpcTransport(rpc_reader.RpcTransport):
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||
super().__init__()
|
||||
self.eio = engineio.AsyncClient(ssl_verify=False)
|
||||
self.loop = loop
|
||||
self.write_error: Exception = None
|
||||
self.read_queue = asyncio.Queue()
|
||||
self.write_queue = asyncio.Queue()
|
||||
|
||||
@self.eio.on("message")
|
||||
def on_message(data):
|
||||
self.read_queue.put_nowait(data)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(self.send_loop(), self.loop)
|
||||
|
||||
async def read(self):
|
||||
return await self.read_queue.get()
|
||||
|
||||
async def send_loop(self):
|
||||
while True:
|
||||
data = await self.write_queue.get()
|
||||
try:
|
||||
await self.eio.send(data)
|
||||
except Exception as e:
|
||||
self.write_error = e
|
||||
self.write_queue = None
|
||||
break
|
||||
|
||||
def writeBuffer(self, buffer, reject):
|
||||
async def send():
|
||||
try:
|
||||
if self.write_error:
|
||||
raise self.write_error
|
||||
self.write_queue.put_nowait(buffer)
|
||||
except Exception as e:
|
||||
reject(e)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(send(), self.loop)
|
||||
|
||||
def writeJSON(self, json, reject):
|
||||
return self.writeBuffer(json, reject)
|
||||
|
||||
|
||||
async def connect_scrypted_client(
|
||||
transport: EioRpcTransport,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
plugin_id: str = "@scrypted/core",
|
||||
session: aiohttp.ClientSession | None = None,
|
||||
) -> ScryptedStatic:
|
||||
login_url = f"{base_url}/login"
|
||||
login_body = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
if session:
|
||||
cm = nullcontext(session)
|
||||
else:
|
||||
cm = aiohttp.ClientSession()
|
||||
|
||||
async with cm as _session:
|
||||
async with _session.post(
|
||||
login_url, verify_ssl=False, json=login_body
|
||||
) as response:
|
||||
login_response = await response.json()
|
||||
|
||||
headers = {"Authorization": login_response["authorization"]}
|
||||
|
||||
await transport.eio.connect(
|
||||
base_url,
|
||||
headers=headers,
|
||||
engineio_path=f"/endpoint/{plugin_id}/engine.io/api/",
|
||||
)
|
||||
|
||||
ret = asyncio.Future[ScryptedStatic](loop=transport.loop)
|
||||
peer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
|
||||
transport.loop, transport
|
||||
)
|
||||
peer.params["print"] = print
|
||||
|
||||
def callback(api, pluginId, hostInfo):
|
||||
remote = plugin_remote.PluginRemote(
|
||||
peer, api, pluginId, hostInfo, transport.loop
|
||||
)
|
||||
wrapped = remote.setSystemState
|
||||
|
||||
async def remoteSetSystemState(systemState):
|
||||
await wrapped(systemState)
|
||||
|
||||
async def resolve():
|
||||
sdk = ScryptedStatic()
|
||||
sdk.api = api
|
||||
sdk.remote = remote
|
||||
sdk.systemManager = SystemManager(api, remote.systemState)
|
||||
sdk.deviceManager = DeviceManager(
|
||||
remote.nativeIds, sdk.systemManager
|
||||
)
|
||||
sdk.mediaManager = MediaManager(await api.getMediaManager())
|
||||
ret.set_result(sdk)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(resolve(), transport.loop)
|
||||
|
||||
remote.setSystemState = remoteSetSystemState
|
||||
return remote
|
||||
|
||||
peer.params["getRemote"] = callback
|
||||
asyncio.run_coroutine_threadsafe(peerReadLoop(), transport.loop)
|
||||
|
||||
sdk = await ret
|
||||
return sdk
|
||||
|
||||
|
||||
async def main():
|
||||
transport = EioRpcTransport(asyncio.get_event_loop())
|
||||
sdk = await connect_scrypted_client(
|
||||
transport,
|
||||
"https://localhost:10443",
|
||||
os.environ["SCRYPTED_USERNAME"],
|
||||
os.environ["SCRYPTED_PASSWORD"],
|
||||
)
|
||||
|
||||
for id in sdk.systemManager.getSystemState():
|
||||
device = sdk.systemManager.getDeviceById(id)
|
||||
print(device.name)
|
||||
if ScryptedInterface.OnOff.value in device.interfaces:
|
||||
print(f"OnOff: device is {device.on}")
|
||||
|
||||
await transport.eio.disconnect()
|
||||
os._exit(0)
|
||||
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.run_coroutine_threadsafe(main(), loop)
|
||||
loop.run_forever()
|
||||
35
plugins/alexa/package-lock.json
generated
35
plugins/alexa/package-lock.json
generated
@@ -1,24 +1,40 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.10",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "../../common",
|
||||
"@scrypted/sdk": "../../sdk",
|
||||
"@types/node": "^18.4.2"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.101",
|
||||
"version": "0.2.108",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -54,6 +70,13 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../common": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
@@ -70,9 +93,9 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.10",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.4.2",
|
||||
"@scrypted/sdk": "../../sdk"
|
||||
"@scrypted/sdk": "../../sdk",
|
||||
"@scrypted/common": "../../common"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (status === DeviceMixinStatus.Setup)
|
||||
await this.syncEndpoints();
|
||||
|
||||
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
|
||||
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
|
||||
|
||||
if (!this.devices.has(eventSource.id)) {
|
||||
this.devices.set(eventSource.id, eventSource);
|
||||
@@ -142,7 +142,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
await this.syncEndpoints();
|
||||
}
|
||||
|
||||
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
|
||||
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any): Promise<void> {
|
||||
if (!eventSource)
|
||||
return;
|
||||
|
||||
@@ -194,14 +194,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
|
||||
// nothing to report
|
||||
if (data.context === undefined && data.event.payload === undefined)
|
||||
return;
|
||||
|
||||
return;
|
||||
|
||||
data = await this.addAccessToken(data);
|
||||
|
||||
await this.postEvent(data);
|
||||
}
|
||||
|
||||
private async addAccessToken(data: any) : Promise<any> {
|
||||
private async addAccessToken(data: any): Promise<any> {
|
||||
const accessToken = await this.getAccessToken();
|
||||
|
||||
if (data.event === undefined)
|
||||
@@ -232,7 +232,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
'api.fe.amazonalexa.com'
|
||||
];
|
||||
|
||||
async getAlexaEndpoint() : Promise<string> {
|
||||
async getAlexaEndpoint(): Promise<string> {
|
||||
if (this.storageSettings.values.apiEndpoint)
|
||||
return this.storageSettings.values.apiEndpoint;
|
||||
|
||||
@@ -276,7 +276,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
});
|
||||
}
|
||||
|
||||
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
|
||||
async getEndpoints(): Promise<DiscoveryEndpoint[]> {
|
||||
const endpoints: DiscoveryEndpoint[] = [];
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
@@ -284,7 +284,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
|
||||
if (!device.mixins?.includes(this.id))
|
||||
continue;
|
||||
|
||||
|
||||
const endpoint = await this.getEndpointForDevice(device);
|
||||
if (endpoint)
|
||||
endpoints.push(endpoint);
|
||||
@@ -319,7 +319,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const endpoints = await this.getEndpoints();
|
||||
|
||||
if (!endpoints.length)
|
||||
return [];
|
||||
return [];
|
||||
|
||||
const accessToken = await this.getAccessToken();
|
||||
const data = {
|
||||
@@ -392,14 +392,21 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
})
|
||||
}
|
||||
|
||||
private setReauthenticateAlert() {
|
||||
const msg: string = "Please reauthenticate by following the directions below.";
|
||||
this.log.a(msg);
|
||||
}
|
||||
|
||||
getAccessToken(): Promise<string> {
|
||||
if (this.accessToken)
|
||||
return this.accessToken;
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
const { tokenInfo } = this.storageSettings.values;
|
||||
|
||||
if (tokenInfo === undefined) {
|
||||
this.log.e("Please reauthenticate by following the directions below.");
|
||||
this.setReauthenticateAlert();
|
||||
throw new Error("'tokenInfo' is undefined");
|
||||
}
|
||||
|
||||
@@ -432,19 +439,19 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
case 'invalid_grant':
|
||||
case 'unauthorized_client':
|
||||
self.console.error(error?.response?.data);
|
||||
self.log.e(error?.response?.data?.error_description);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
self.storageSettings.values.tokenInfo = undefined;
|
||||
self.accessToken = undefined;
|
||||
break;
|
||||
|
||||
case 'authorization_pending':
|
||||
self.console.warn(error?.response?.data);
|
||||
self.log.w(error?.response?.data?.error_description);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
break;
|
||||
|
||||
|
||||
case 'expired_token':
|
||||
self.console.warn(error?.response?.data);
|
||||
self.log.w(error?.response?.data?.error_description);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
self.accessToken = undefined;
|
||||
break;
|
||||
|
||||
@@ -473,9 +480,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
this.storageSettings.values.tokenInfo = grant;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
|
||||
|
||||
const self = this;
|
||||
let accessToken = await this.getAccessToken().catch(reason => {
|
||||
let accessToken: any;
|
||||
|
||||
try {
|
||||
accessToken = await this.getAccessToken();
|
||||
}
|
||||
catch (reason) {
|
||||
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
@@ -484,34 +496,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
|
||||
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (accessToken !== undefined) {
|
||||
try {
|
||||
response.send({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Authorization",
|
||||
"name": "AcceptGrant.Response",
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.console.error(`AcceptGrant.Response failed because ${error}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
throw error;
|
||||
return;
|
||||
};
|
||||
this.log.clearAlerts();
|
||||
response.send({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Authorization",
|
||||
"name": "AcceptGrant.Response",
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
|
||||
async getEndpointForDevice(device: ScryptedDevice): Promise<DiscoveryEndpoint> {
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
@@ -536,7 +537,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
};
|
||||
|
||||
let supportedEndpointHealths: any[] = [];
|
||||
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.Online)) {
|
||||
supportedEndpointHealths.push({
|
||||
"name": "connectivity"
|
||||
@@ -623,8 +624,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
debug("received directive from alexa", mapName, body);
|
||||
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive]);
|
||||
if (handler) {
|
||||
await handler.apply(this, [request, response, directive]);
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceHandler = alexaDeviceHandlers.get(mapName);
|
||||
|
||||
@@ -635,7 +638,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
return;
|
||||
}
|
||||
|
||||
return deviceHandler.apply(this, [request, response, directive, device]);
|
||||
await deviceHandler.apply(this, [request, response, directive, device]);
|
||||
return;
|
||||
} else {
|
||||
this.console.error(`no handler for: ${mapName}`);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export async function getCameraCapabilities(device: ScryptedDevice): Promise<Dis
|
||||
"interface": "Alexa.RTCSessionController",
|
||||
"version": "3",
|
||||
"configuration": {
|
||||
isFullDuplexAudioSupported: true,
|
||||
"isFullDuplexAudioSupported": true,
|
||||
}
|
||||
} as DiscoveryCapability
|
||||
];
|
||||
|
||||
@@ -4,13 +4,24 @@ import { v4 as createMessageId } from 'uuid';
|
||||
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
|
||||
import { alexaDeviceHandlers } from "../../handlers";
|
||||
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
|
||||
export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
constructor(public response: AlexaHttpResponse, public directive: any) {
|
||||
this.options = this.createOptions();
|
||||
this.__proxy_props = { options: this.createOptions() };
|
||||
}
|
||||
|
||||
__proxy_props: { options: RTCSignalingOptions; };
|
||||
options: RTCSignalingOptions;
|
||||
remoteDescription = new Deferred<void>();
|
||||
|
||||
async getOptions(): Promise<RTCSignalingOptions> {
|
||||
return {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
private createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
proxy: true,
|
||||
offer: {
|
||||
type: 'offer',
|
||||
@@ -24,15 +35,23 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
|
||||
if (type !== 'offer')
|
||||
throw new Error('Alexa only supports RTC offer');
|
||||
if (type !== 'offer') {
|
||||
const e = new Error('Alexa only supports RTC offer');
|
||||
this.remoteDescription.reject(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (sendIceCandidate)
|
||||
throw new Error("Alexa does not support trickle ICE");
|
||||
if (sendIceCandidate) {
|
||||
const e = new Error("Alexa does not support trickle ICE");
|
||||
this.remoteDescription.reject(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return {
|
||||
type: type,
|
||||
@@ -56,15 +75,16 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
|
||||
data.event.header.name = "AnswerGeneratedForSession";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
|
||||
data.event.payload.answer = {
|
||||
format: 'SDP',
|
||||
value: description.sdp,
|
||||
};
|
||||
|
||||
this.remoteDescription.resolve();
|
||||
this.response.send(data);
|
||||
}
|
||||
}
|
||||
@@ -74,13 +94,14 @@ const sessionCache = new Map<string, RTCSessionControl>();
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/InitiateSessionWithOffer', async (request, response, directive: any, device: ScryptedDevice & RTCSignalingChannel) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const { sessionId } = payload;
|
||||
|
||||
|
||||
const session = new AlexaSignalingSession(response, directive);
|
||||
const control = await device.startRTCSignalingSession(session);
|
||||
control.setPlayback({
|
||||
audio: true,
|
||||
video: false,
|
||||
})
|
||||
});
|
||||
await session.remoteDescription.promise;
|
||||
|
||||
sessionCache.set(sessionId, control);
|
||||
});
|
||||
@@ -104,13 +125,13 @@ alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (re
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const { sessionId } = payload;
|
||||
|
||||
|
||||
const session = sessionCache.get(sessionId);
|
||||
if (session) {
|
||||
sessionCache.delete(sessionId);
|
||||
await session.endSession();
|
||||
}
|
||||
|
||||
|
||||
const data: WebRTCSessionDisconnectedEvent = {
|
||||
"event": {
|
||||
header,
|
||||
@@ -119,9 +140,9 @@ alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
|
||||
response.send(data);
|
||||
});
|
||||
|
||||
@@ -141,14 +162,14 @@ alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetect
|
||||
},
|
||||
"context": {
|
||||
"properties": [{
|
||||
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"name": "objectDetectionClasses",
|
||||
"value": detectionTypes.classes.map(type => ({
|
||||
"imageNetClass": type
|
||||
})),
|
||||
timeOfSample: new Date().toISOString(),
|
||||
uncertaintyInMilliseconds: 0
|
||||
}]
|
||||
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"name": "objectDetectionClasses",
|
||||
"value": detectionTypes.classes.map(type => ({
|
||||
"imageNetClass": type
|
||||
})),
|
||||
timeOfSample: new Date().toISOString(),
|
||||
uncertaintyInMilliseconds: 0
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import { supportedTypes } from ".";
|
||||
supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
let capabilities: any[] = [];
|
||||
let category: DisplayCategory = 'DOORBELL';
|
||||
const displayCategories: DisplayCategory[] = [];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
|
||||
capabilities = await getCameraCapabilities(device);
|
||||
category = 'CAMERA';
|
||||
displayCategories.push('CAMERA');
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
|
||||
@@ -24,8 +24,11 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
);
|
||||
}
|
||||
|
||||
// Important: If your device is a video doorbell, make sure that you list CAMERA before DOORBELL in the displayCategories list.
|
||||
displayCategories.push('DOORBELL');
|
||||
|
||||
return {
|
||||
displayCategories: [category],
|
||||
displayCategories,
|
||||
capabilities
|
||||
};
|
||||
},
|
||||
@@ -38,6 +41,9 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
if (response)
|
||||
return response;
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === false)
|
||||
return {};
|
||||
|
||||
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
|
||||
return {
|
||||
event: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
203
plugins/amcrest/package-lock.json
generated
203
plugins/amcrest/package-lock.json
generated
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.122",
|
||||
"lockfileVersion": 2,
|
||||
"version": "0.0.130",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.122",
|
||||
"version": "0.0.130",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
"multiparty": "^4.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.11"
|
||||
"@types/node": "^18.16.18"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.87",
|
||||
"version": "0.3.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -71,13 +71,10 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.6.tgz",
|
||||
"integrity": "sha512-e/XKs7/BYpPQkces0Cm4dUmhT9hR0rjvnNZAVRyRnNWdQ8cyCMFWS9HIrMWOdzAocKDNBXi1vKjJ8CywrW5xgQ==",
|
||||
"dependencies": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
@@ -100,9 +97,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
"version": "18.16.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
|
||||
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw=="
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
@@ -120,15 +117,15 @@
|
||||
"node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -145,15 +142,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
|
||||
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -165,11 +162,11 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/multiparty": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
|
||||
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz",
|
||||
"integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==",
|
||||
"dependencies": {
|
||||
"http-errors": "~1.8.0",
|
||||
"http-errors": "~1.8.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "2.1.5"
|
||||
},
|
||||
@@ -180,7 +177,7 @@
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -212,15 +209,15 @@
|
||||
"node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
@@ -236,147 +233,5 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"requires": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@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": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"@types/multiparty": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz",
|
||||
"integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
|
||||
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"multiparty": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
|
||||
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
|
||||
"requires": {
|
||||
"http-errors": "~1.8.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "2.1.5"
|
||||
}
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"requires": {
|
||||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.122",
|
||||
"version": "0.0.130",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -39,9 +39,9 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
"multiparty": "^4.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.11"
|
||||
"@types/node": "^18.16.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export class AmcrestCameraClient {
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
|
||||
@@ -95,6 +95,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
for (const element of deviceParameters) {
|
||||
try {
|
||||
const response = await this.getClient().digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`
|
||||
});
|
||||
|
||||
@@ -147,6 +148,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
return;
|
||||
|
||||
const response = await this.getClient().digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`
|
||||
});
|
||||
this.console.log('reconfigure result', response.data);
|
||||
@@ -190,14 +192,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|| event === AmcrestEvent.PhoneCallDetectStart
|
||||
|| event === AmcrestEvent.AlarmIPCStart
|
||||
|| event === AmcrestEvent.DahuaTalkInvite) {
|
||||
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds)
|
||||
{
|
||||
if (payload.includes(callerId))
|
||||
{
|
||||
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds) {
|
||||
if (payload.includes(callerId)) {
|
||||
this.binaryState = true;
|
||||
}
|
||||
} else
|
||||
{
|
||||
} else {
|
||||
this.binaryState = true;
|
||||
}
|
||||
}
|
||||
@@ -259,25 +258,23 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
if (!twoWayAudio)
|
||||
twoWayAudio = isDoorbell ? 'Amcrest' : 'None';
|
||||
|
||||
|
||||
if (doorbellType == DAHUA_DOORBELL_TYPE)
|
||||
{
|
||||
|
||||
|
||||
if (doorbellType == DAHUA_DOORBELL_TYPE) {
|
||||
ret.push(
|
||||
{
|
||||
title: 'Multiple Call Buttons',
|
||||
key: 'multipleCallIds',
|
||||
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
|
||||
type: 'boolean',
|
||||
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
|
||||
}
|
||||
{
|
||||
title: 'Multiple Call Buttons',
|
||||
key: 'multipleCallIds',
|
||||
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
|
||||
type: 'boolean',
|
||||
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const multipleCallIds = this.storage.getItem('multipleCallIds');
|
||||
|
||||
if (multipleCallIds)
|
||||
{
|
||||
if (multipleCallIds) {
|
||||
ret.push(
|
||||
{
|
||||
title: 'Caller ID',
|
||||
@@ -288,7 +285,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
ret.push(
|
||||
{
|
||||
@@ -309,11 +306,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
);
|
||||
|
||||
return ret;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
|
||||
@@ -401,11 +398,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
if (audioCodec?.includes('aac'))
|
||||
audioCodec = 'aac';
|
||||
else if (audioCodec.includes('g711a'))
|
||||
else if (audioCodec?.includes('g711a'))
|
||||
audioCodec = 'pcm_alaw';
|
||||
else if (audioCodec.includes('g711u'))
|
||||
audioCodec = 'pcm_ulaw';
|
||||
else if (audioCodec.includes('g711'))
|
||||
else if (audioCodec?.includes('g711u'))
|
||||
audioCodec = 'pcm_mulaw';
|
||||
else if (audioCodec?.includes('g711'))
|
||||
audioCodec = 'pcm';
|
||||
|
||||
if (vso.audio)
|
||||
@@ -490,7 +487,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.videoStreamOptions = undefined;
|
||||
|
||||
super.putSetting(key, value);
|
||||
|
||||
|
||||
this.updateDevice();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
@@ -639,6 +636,7 @@ class AmcrestProvider extends RtspProvider {
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
device.updateDeviceInfo();
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
|
||||
27
plugins/arlo/.vscode/settings.json
vendored
27
plugins/arlo/.vscode/settings.json
vendored
@@ -1,27 +0,0 @@
|
||||
|
||||
{
|
||||
// specify the following paths on the target scrypted server:
|
||||
// 1) where @scrypted/server node module resides: this may either be a checkout or a install.
|
||||
// 2) where the scrypted "volume" data is located on the server. ie, the docker volume.
|
||||
// the following default examples are provided for local and docker installations,
|
||||
// only modifying the debugHost should be necessary:
|
||||
|
||||
// local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted/node_modules/@scrypted/server",
|
||||
// "scrypted.volumeRoot": "/home/pi/.scrypted/volume",
|
||||
|
||||
// docker installation
|
||||
// "scrypted.debugHost": "192.168.2.109",
|
||||
"scrypted.serverRoot": "/server/node_modules/@scrypted/server",
|
||||
"scrypted.volumeRoot": "/server/volume",
|
||||
|
||||
// local checkout
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
//"scrypted.serverRoot": "/Volumes/Dev/scrypted/server",
|
||||
//"scrypted.volumeRoot": "${config:scrypted.serverRoot}/volume",
|
||||
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
# Arlo Plugin for Scrypted
|
||||
|
||||
The Arlo Plugin connects Scrypted to Arlo Cloud, allowing you to access all of your Arlo cameras in Scrypted.
|
||||
|
||||
It is highly recommended to create a dedicated Arlo account for use with this plugin and share your cameras from your main account, as Arlo only permits one active login to their servers per account. Using a separate account allows you to use the Arlo app or website simultaneously with this plugin, otherwise logging in from one place will log you out from all other devices.
|
||||
|
||||
The account you use for this plugin must have either SMS or email set as the default 2FA option. Once you enter your username and password on the plugin settings page, you should receive a 2FA code through your default 2FA option. Enter that code into the provided box, and your cameras will appear in Scrypted. Or, see below for configuring IMAP to auto-login with 2FA.
|
||||
|
||||
If you experience any trouble logging in, clear the username and password boxes, reload the plugin, and try again.
|
||||
|
||||
## General Setup Notes
|
||||
|
||||
* Ensure that your Arlo account's default 2FA option is set to either SMS or email.
|
||||
* Motion events notifications should be turned on in the Arlo app. If you are receiving motion push notifications, Scrypted will also receive motion events.
|
||||
* Disable smart detection and any cloud/local recording in the Arlo app. Arlo Cloud only permits one active stream per camera, so any smart detection or recording features may prevent downstream plugins (e.g. Homekit) from successfully pulling the video feed after a motion event.
|
||||
* It is highly recommended to enable the Rebroadcast plugin to allow multiple downstream plugins to pull the video feed within Scrypted.
|
||||
* If there is no audio on your camera, switch to the `FFmpeg (TCP)` parser under the `Cloud RTSP` settings.
|
||||
* Prebuffering should only be enabled if the camera is wired to a persistent power source, such as a wall outlet. Prebuffering will only work if your camera does not have a battery or `Plugged In to External Power` is selected.
|
||||
* The plugin supports pulling RTSP or DASH streams from Arlo Cloud. It is recommended to use RTSP for the lowest latency streams. DASH is inconsistent in reliability, and may return finicky codecs that require additional FFmpeg output arguments, e.g. `-vcodec h264`.
|
||||
|
||||
Note that streaming cameras uses extra Internet bandwidth, since video and audio packets will need to travel from the camera through your network, out to Arlo Cloud, and then back to your network and into Scrypted.
|
||||
|
||||
## IMAP 2FA
|
||||
|
||||
The Arlo Plugin supports using the IMAP protocol to check an email mailbox for Arlo 2FA codes. This requires you to specify an email 2FA option as the default in your Arlo account settings.
|
||||
|
||||
The plugin should work with any mailbox that supports IMAP, but so far has been tested with Gmail. To configure a Gmail mailbox, see [here](https://support.google.com/mail/answer/7126229?hl=en) to see the Gmail IMAP settings, and [here](https://support.google.com/accounts/answer/185833?hl=en) to create an App Password. Enter the App Password in place of your normal Gmail password.
|
||||
@@ -1 +0,0 @@
|
||||
from .provider import ArloProvider
|
||||
@@ -1 +0,0 @@
|
||||
from .arlo_async import Arlo
|
||||
@@ -1,976 +0,0 @@
|
||||
# This file has been modified to support async semantics and better
|
||||
# integration with scrypted.
|
||||
# Original: https://github.com/jeffreydwalter/arlo
|
||||
|
||||
"""
|
||||
Copyright 2016 Jeffrey D. Walter
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS ISBASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
# 14 Sep 2016, Len Shustek: Added Logout()
|
||||
# 17 Jul 2017, Andreas Jakl: Port to Python 3 (https://www.andreasjakl.com/using-netgear-arlo-security-cameras-for-periodic-recording/)
|
||||
|
||||
# Import helper classes that are part of this library.
|
||||
|
||||
from .request import Request
|
||||
from .host_picker import pick_host
|
||||
from .mqtt_stream_async import MQTTStream
|
||||
from .sse_stream_async import EventStream
|
||||
from .logging import logger
|
||||
|
||||
# Import all of the other stuff.
|
||||
from datetime import datetime, timedelta
|
||||
from cachetools import cached, TTLCache
|
||||
import scrypted_arlo_go
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import base64
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
stream_class = MQTTStream
|
||||
|
||||
def change_stream_class(s_class):
|
||||
global stream_class
|
||||
if s_class == "MQTT":
|
||||
stream_class = MQTTStream
|
||||
elif s_class == "SSE":
|
||||
stream_class = EventStream
|
||||
else:
|
||||
raise NotImplementedError(s_class)
|
||||
|
||||
|
||||
# https://github.com/twrecked/pyaarlo/blob/03c99b40b67529d81c0ba399fe91a3e6d1a35a80/pyaarlo/constant.py#L265-L285
|
||||
USER_AGENTS = {
|
||||
"arlo":
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_1_2 like Mac OS X) "
|
||||
"AppleWebKit/604.3.5 (KHTML, like Gecko) Mobile/15B202 NETGEAR/v1 "
|
||||
"(iOS Vuezone)",
|
||||
"iphone":
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) "
|
||||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1",
|
||||
"ipad":
|
||||
"Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) "
|
||||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1",
|
||||
"mac":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) "
|
||||
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15",
|
||||
"firefox":
|
||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:85.0) "
|
||||
"Gecko/20100101 Firefox/85.0",
|
||||
"linux":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
|
||||
}
|
||||
|
||||
|
||||
class Arlo(object):
|
||||
BASE_URL = 'my.arlo.com'
|
||||
AUTH_URL = 'ocapi-app.arlo.com'
|
||||
BACKUP_AUTH_HOSTS = list(scrypted_arlo_go.BACKUP_AUTH_HOSTS())
|
||||
TRANSID_PREFIX = 'web'
|
||||
|
||||
random.shuffle(BACKUP_AUTH_HOSTS)
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.event_stream = None
|
||||
self.request = Request()
|
||||
|
||||
def to_timestamp(self, dt):
|
||||
if sys.version[0] == '2':
|
||||
epoch = datetime.utcfromtimestamp(0)
|
||||
return int((dt - epoch).total_seconds() * 1e3)
|
||||
else:
|
||||
return int(dt.timestamp() * 1e3)
|
||||
|
||||
def genTransId(self, trans_type=TRANSID_PREFIX):
|
||||
def float2hex(f):
|
||||
MAXHEXADECIMALS = 15
|
||||
w = f // 1
|
||||
d = f % 1
|
||||
|
||||
# Do the whole:
|
||||
if w == 0: result = '0'
|
||||
else: result = ''
|
||||
while w:
|
||||
w, r = divmod(w, 16)
|
||||
r = int(r)
|
||||
if r > 9: r = chr(r+55)
|
||||
else: r = str(r)
|
||||
result = r + result
|
||||
|
||||
# And now the part:
|
||||
if d == 0: return result
|
||||
|
||||
result += '.'
|
||||
count = 0
|
||||
while d:
|
||||
d = d * 16
|
||||
w, d = divmod(d, 1)
|
||||
w = int(w)
|
||||
if w > 9: w = chr(w+55)
|
||||
else: w = str(w)
|
||||
result += w
|
||||
count += 1
|
||||
if count > MAXHEXADECIMALS: break
|
||||
|
||||
return result
|
||||
|
||||
now = datetime.today()
|
||||
return trans_type+"!" + float2hex(random.random() * math.pow(2, 32)).lower() + "!" + str(int((time.mktime(now.timetuple())*1e3 + now.microsecond/1e3)))
|
||||
|
||||
def UseExistingAuth(self, user_id, headers):
|
||||
self.user_id = user_id
|
||||
headers['Content-Type'] = 'application/json; charset=UTF-8'
|
||||
headers['User-Agent'] = USER_AGENTS['arlo']
|
||||
self.request.session.headers.update(headers)
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
|
||||
def LoginMFA(self):
|
||||
device_id = str(uuid.uuid4())
|
||||
headers = {
|
||||
'DNT': '1',
|
||||
'schemaVersion': '1',
|
||||
'Auth-Version': '2',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': USER_AGENTS['arlo'],
|
||||
'Origin': f'https://{self.BASE_URL}',
|
||||
'Referer': f'https://{self.BASE_URL}/',
|
||||
'Source': 'arloCamWeb',
|
||||
'TE': 'Trailers',
|
||||
'x-user-device-id': device_id,
|
||||
'x-user-device-automation-name': 'QlJPV1NFUg==',
|
||||
'x-user-device-type': 'BROWSER',
|
||||
'Host': self.AUTH_URL,
|
||||
}
|
||||
|
||||
self.request = Request()
|
||||
try:
|
||||
auth_host = self.AUTH_URL
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
logger.info("Using primary authentication host")
|
||||
except Exception as e:
|
||||
# in case cloudflare rejects our auth request...
|
||||
logger.warning(f"Using fallback authentication host due to: {e}")
|
||||
|
||||
auth_host = pick_host([
|
||||
base64.b64decode(h.encode("utf-8")).decode("utf-8")
|
||||
for h in self.BACKUP_AUTH_HOSTS
|
||||
], self.AUTH_URL, "/api/auth")
|
||||
logger.debug(f"Selected backup authentication host {auth_host}")
|
||||
|
||||
self.request = Request(mode="ip")
|
||||
|
||||
# Authenticate
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
auth_body = self.request.post(
|
||||
f'https://{auth_host}/api/auth',
|
||||
params={
|
||||
'email': self.username,
|
||||
'password': str(base64.b64encode(self.password.encode('utf-8')), 'utf-8'),
|
||||
'language': 'en',
|
||||
'EnvSource': 'prod'
|
||||
},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
self.user_id = auth_body['data']['userId']
|
||||
self.request.session.headers.update({'Authorization': base64.b64encode(auth_body['data']['token'].encode('utf-8')).decode()})
|
||||
|
||||
# Retrieve MFA factor id
|
||||
factors_body = self.request.get(
|
||||
f'https://{auth_host}/api/getFactors',
|
||||
params={'data': auth_body['data']['issued']},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
factor_id = next(
|
||||
iter([
|
||||
i for i in factors_body['data']['items']
|
||||
if (i['factorType'] == 'EMAIL' or i['factorType'] == 'SMS')
|
||||
and i['factorRole'] == "PRIMARY"
|
||||
]),
|
||||
{}
|
||||
).get('factorId')
|
||||
if not factor_id:
|
||||
raise Exception("Could not find valid 2FA method - is the primary 2FA set to either Email or SMS?")
|
||||
|
||||
# Start factor auth
|
||||
start_auth_body = self.request.post(
|
||||
f'https://{auth_host}/api/startAuth',
|
||||
params={'factorId': factor_id},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
factor_auth_code = start_auth_body['data']['factorAuthCode']
|
||||
|
||||
def complete_auth(code):
|
||||
nonlocal self, factor_auth_code, headers
|
||||
|
||||
finish_auth_body = self.request.post(
|
||||
f'https://{auth_host}/api/finishAuth',
|
||||
params={
|
||||
'factorAuthCode': factor_auth_code,
|
||||
'otp': code
|
||||
},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
|
||||
if finish_auth_body.get('data', {}).get('token') is None:
|
||||
raise Exception("Could not complete 2FA, maybe invalid token? If the error persists, please try reloading the plugin and logging in again.")
|
||||
|
||||
self.request = Request()
|
||||
|
||||
# Update Authorization code with new code
|
||||
headers = {
|
||||
'Auth-Version': '2',
|
||||
'Authorization': finish_auth_body['data']['token'],
|
||||
'User-Agent': USER_AGENTS['arlo'],
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
}
|
||||
self.request.session.headers.update(headers)
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
|
||||
return complete_auth
|
||||
|
||||
def Logout(self):
|
||||
self.Unsubscribe()
|
||||
return self.request.put(f'https://{self.BASE_URL}/hmsweb/logout')
|
||||
|
||||
async def Subscribe(self, basestation_camera_tuples=[]):
|
||||
"""
|
||||
Arlo uses the EventStream interface in the browser to do pub/sub style messaging.
|
||||
Unfortunately, this appears to be the only way Arlo communicates these messages.
|
||||
|
||||
This function makes the initial GET request to /subscribe, which returns the EventStream socket.
|
||||
Once we have that socket, the API requires a POST request to /notify with the subscriptions resource.
|
||||
This call registers the device (which should be the basestation) so that events will be sent to the EventStream
|
||||
when subsequent calls to /notify are made.
|
||||
"""
|
||||
async def heartbeat(self, basestations, interval=30):
|
||||
while self.event_stream and self.event_stream.active:
|
||||
for basestation in basestations:
|
||||
try:
|
||||
self.Ping(basestation)
|
||||
except:
|
||||
pass
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
if not self.event_stream or (not self.event_stream.initializing and not self.event_stream.connected):
|
||||
self.event_stream = stream_class(self)
|
||||
await self.event_stream.start()
|
||||
|
||||
while not self.event_stream.connected:
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# if tuples are provided, then this is the Subscribe initiated
|
||||
# by the top level plugin, and we should add mqtt subscriptions
|
||||
# and register basestation heartbeats
|
||||
if len(basestation_camera_tuples) > 0:
|
||||
# find unique basestations and cameras
|
||||
basestations, cameras = {}, {}
|
||||
for basestation, camera in basestation_camera_tuples:
|
||||
basestations[basestation['deviceId']] = basestation
|
||||
cameras[camera['deviceId']] = camera
|
||||
|
||||
# filter out cameras without basestation, where they are their own basestations
|
||||
# this is so battery-powered devices do not drain due to pings
|
||||
# for wired devices, keep doorbells, sirens, and arloq in the list so they get pings
|
||||
proper_basestations = {}
|
||||
for basestation in basestations.values():
|
||||
if basestation['deviceId'] == basestation.get('parentId') and \
|
||||
basestation['deviceType'] not in ['doorbell', 'siren', 'arloq', 'arloqs']:
|
||||
continue
|
||||
proper_basestations[basestation['deviceId']] = basestation
|
||||
|
||||
logger.info(f"Will send heartbeat to the following devices: {list(proper_basestations.keys())}")
|
||||
|
||||
# start heartbeat loop with only basestations
|
||||
asyncio.get_event_loop().create_task(heartbeat(self, list(proper_basestations.values())))
|
||||
|
||||
# subscribe to all camera topics
|
||||
topics = [
|
||||
f"d/{basestation['xCloudId']}/out/cameras/{camera['deviceId']}/#"
|
||||
for basestation, camera in basestation_camera_tuples
|
||||
]
|
||||
|
||||
# subscribe to basestation topics
|
||||
for basestation in basestations.values():
|
||||
x_cloud_id = basestation['xCloudId']
|
||||
topics += [
|
||||
f"d/{x_cloud_id}/out/wifi/#",
|
||||
f"d/{x_cloud_id}/out/subscriptions/#",
|
||||
f"d/{x_cloud_id}/out/audioPlayback/#",
|
||||
f"d/{x_cloud_id}/out/modes/#",
|
||||
f"d/{x_cloud_id}/out/basestation/#",
|
||||
f"d/{x_cloud_id}/out/doorbells/#",
|
||||
f"d/{x_cloud_id}/out/siren/#",
|
||||
f"d/{x_cloud_id}/out/devices/#",
|
||||
f"d/{x_cloud_id}/out/storage/#",
|
||||
f"d/{x_cloud_id}/out/schedule/#",
|
||||
f"d/{x_cloud_id}/out/diagnostics/#",
|
||||
f"d/{x_cloud_id}/out/automaticRevisionUpdate/#",
|
||||
f"d/{x_cloud_id}/out/audio/#",
|
||||
f"d/{x_cloud_id}/out/activeAutomations/#",
|
||||
f"d/{x_cloud_id}/out/lte/#",
|
||||
]
|
||||
|
||||
self.event_stream.subscribe(topics)
|
||||
|
||||
def Unsubscribe(self):
|
||||
""" This method stops the EventStream subscription and removes it from the event_stream collection. """
|
||||
if self.event_stream and self.event_stream.connected:
|
||||
self.event_stream.disconnect()
|
||||
self.request.get(f'https://{self.BASE_URL}/hmsweb/client/unsubscribe')
|
||||
|
||||
self.event_stream = None
|
||||
|
||||
def Notify(self, basestation, body):
|
||||
"""
|
||||
The following are examples of the json you would need to pass in the body of the Notify() call to interact with Arlo:
|
||||
|
||||
##############################################################################################################################
|
||||
##############################################################################################################################
|
||||
NOTE: While you can call Notify() directly, responses from these notify calls are sent to the EventStream (see Subscribe()),
|
||||
and so it's better to use the Get/Set methods that are implemented using the NotifyAndGetResponse() method.
|
||||
##############################################################################################################################
|
||||
##############################################################################################################################
|
||||
|
||||
Set System Mode (Armed, Disarmed) - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"set","resource":"modes","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"active":"mode0"}}
|
||||
Set System Mode (Calendar) - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"set","resource":"schedule","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"active":true}}
|
||||
Configure The Schedule (Calendar) - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"set","resource":"schedule","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"schedule":[{"modeId":"mode0","startTime":0},{"modeId":"mode2","startTime":28800000},{"modeId":"mode0","startTime":64800000},{"modeId":"mode0","startTime":86400000},{"modeId":"mode2","startTime":115200000},{"modeId":"mode0","startTime":151200000},{"modeId":"mode0","startTime":172800000},{"modeId":"mode2","startTime":201600000},{"modeId":"mode0","startTime":237600000},{"modeId":"mode0","startTime":259200000},{"modeId":"mode2","startTime":288000000},{"modeId":"mode0","startTime":324000000},{"modeId":"mode0","startTime":345600000},{"modeId":"mode2","startTime":374400000},{"modeId":"mode0","startTime":410400000},{"modeId":"mode0","startTime":432000000},{"modeId":"mode0","startTime":518400000}]}
|
||||
Create Mode -
|
||||
{"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"add","resource":"rules","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"name":"Record video on Camera 1 if Camera 1 detects motion","id":"ruleNew","triggers":[{"type":"pirMotionActive","deviceId":"XXXXXXXXXXXXX","sensitivity":80}],"actions":[{"deviceId":"XXXXXXXXXXXXX","type":"recordVideo","stopCondition":{"type":"timeout","timeout":15}},{"type":"sendEmailAlert","recipients":["__OWNER_EMAIL__"]},{"type":"pushNotification"}]}}
|
||||
{"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"add","resource":"modes","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"name":"Test","rules":["rule3"]}}
|
||||
Delete Mode - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"delete","resource":"modes/mode3","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true}
|
||||
Camera Off - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"set","resource":"cameras/XXXXXXXXXXXXX","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"privacyActive":false}}
|
||||
Night Vision On - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"set","resource":"cameras/XXXXXXXXXXXXX","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"zoom":{"topleftx":0,"toplefty":0,"bottomrightx":1280,"bottomrighty":720},"mirror":true,"flip":true,"nightVisionMode":1,"powerSaveMode":2}}
|
||||
Motion Detection Test - {"from":"XXX-XXXXXXX_web","to":"XXXXXXXXXXXXX","action":"set","resource":"cameras/XXXXXXXXXXXXX","transId":"web!XXXXXXXX.XXXXXXXXXXXXXXXXXXXX","publishResponse":true,"properties":{"motionSetupModeEnabled":true,"motionSetupModeSensitivity":80}}
|
||||
|
||||
device_id = locations.data.uniqueIds
|
||||
|
||||
System Properties: ("resource":"modes")
|
||||
active (string) - Mode Selection (mode2 = All Motion On, mode1 = Armed, mode0 = Disarmed, etc.)
|
||||
|
||||
System Properties: ("resource":"schedule")
|
||||
active (bool) - Mode Selection (true = Calendar)
|
||||
|
||||
Camera Properties: ("resource":"cameras/{id}")
|
||||
privacyActive (bool) - Camera On/Off
|
||||
zoom (topleftx (int), toplefty (int), bottomrightx (int), bottomrighty (int)) - Camera Zoom Level
|
||||
mirror (bool) - Mirror Image (left-to-right or right-to-left)
|
||||
flip (bool) - Flip Image Vertically
|
||||
nightVisionMode (int) - Night Mode Enabled/Disabled (1, 0)
|
||||
powerSaveMode (int) - PowerSaver Mode (3 = Best Video, 2 = Optimized, 1 = Best Battery Life)
|
||||
motionSetupModeEnabled (bool) - Motion Detection Setup Enabled/Disabled
|
||||
motionSetupModeSensitivity (int 0-100) - Motion Detection Sensitivity
|
||||
"""
|
||||
basestation_id = basestation.get('deviceId')
|
||||
|
||||
body['transId'] = self.genTransId()
|
||||
body['from'] = self.user_id+'_web'
|
||||
body['to'] = basestation_id
|
||||
|
||||
self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/notify/'+body['to'], params=body, headers={"xcloudId":basestation.get('xCloudId')})
|
||||
return body.get('transId')
|
||||
|
||||
def Ping(self, basestation):
|
||||
basestation_id = basestation.get('deviceId')
|
||||
return self.Notify(basestation, {"action":"set","resource":"subscriptions/"+self.user_id+"_web","publishResponse":False,"properties":{"devices":[basestation_id]}})
|
||||
|
||||
def SubscribeToMotionEvents(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to motion events. You must provide a callback function which will get called once per motion event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get('properties', {})
|
||||
stop = None
|
||||
if 'motionDetected' in properties:
|
||||
stop = callback(properties['motionDetected'])
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToAudioEvents(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to audio events. You must provide a callback function which will get called once per audio event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get('properties', {})
|
||||
stop = None
|
||||
if 'audioDetected' in properties:
|
||||
stop = callback(properties['audioDetected'])
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'audioDetected')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToBatteryEvents(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to battery events. You must provide a callback function which will get called once per battery event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get('properties', {})
|
||||
stop = None
|
||||
if 'batteryLevel' in properties:
|
||||
stop = callback(properties['batteryLevel'])
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'batteryLevel')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToDoorbellEvents(self, basestation, doorbell, callback):
|
||||
"""
|
||||
Use this method to subscribe to doorbell events. You must provide a callback function which will get called once per doorbell event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
|
||||
resource = f"doorbells/{doorbell.get('deviceId')}"
|
||||
|
||||
async def unpress_doorbell(callback):
|
||||
# It's unclear what events correspond to arlo doorbell presses
|
||||
# and which ones are unpresses, so we sleep and unset after
|
||||
# a period of time
|
||||
await asyncio.sleep(1)
|
||||
callback(False)
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get('properties', {})
|
||||
stop = None
|
||||
if 'buttonPressed' in properties:
|
||||
stop = callback(properties.get('buttonPressed'))
|
||||
asyncio.get_event_loop().create_task(unpress_doorbell(callback))
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'buttonPressed')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToSDPAnswers(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to pushToTalk SDP answer events. You must provide a callback function which will get called once per SDP event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get("properties", {})
|
||||
stop = None
|
||||
if properties.get("type") == "answerSdp":
|
||||
stop = callback(properties.get("data"))
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToCandidateAnswers(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to pushToTalk ICE candidate answer events. You must provide a callback function which will get called once per candidate event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get("properties", {})
|
||||
stop = None
|
||||
if properties.get("type") == "answerCandidate":
|
||||
stop = callback(properties.get("data"))
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
|
||||
)
|
||||
|
||||
async def HandleEvents(self, basestation, resource, actions, callback):
|
||||
"""
|
||||
Use this method to subscribe to the event stream and provide a callback that will be called for event event received.
|
||||
This function will allow you to potentially write a callback that can handle all of the events received from the event stream.
|
||||
"""
|
||||
if not callable(callback):
|
||||
raise Exception('The callback(self, event) should be a callable function.')
|
||||
|
||||
await self.Subscribe()
|
||||
|
||||
async def loop_action_listener(action):
|
||||
# in this function, action can either be a tuple or a string
|
||||
# if it is a tuple, we expect there to be a property key in the tuple
|
||||
property = None
|
||||
if isinstance(action, tuple):
|
||||
action, property = action
|
||||
if not isinstance(action, str):
|
||||
raise Exception('Actions must be either a tuple or a str')
|
||||
|
||||
seen_events = {}
|
||||
while self.event_stream.active:
|
||||
event, _ = await self.event_stream.get(resource, action, property, seen_events)
|
||||
|
||||
if event is None or self.event_stream is None \
|
||||
or self.event_stream.event_stream_stop_event.is_set():
|
||||
return None
|
||||
|
||||
seen_events[event.uuid] = event
|
||||
response = callback(self, event.item)
|
||||
|
||||
# always requeue so other listeners can see the event too
|
||||
self.event_stream.requeue(event, resource, action, property)
|
||||
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
# remove events that have expired
|
||||
for uuid in list(seen_events):
|
||||
if seen_events[uuid].expired:
|
||||
del seen_events[uuid]
|
||||
|
||||
if self.event_stream and self.event_stream.active:
|
||||
listeners = [loop_action_listener(action) for action in actions]
|
||||
done, pending = await asyncio.wait(listeners, return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
return done.pop().result()
|
||||
|
||||
async def TriggerAndHandleEvent(self, basestation, resource, actions, trigger, callback):
|
||||
"""
|
||||
Use this method to subscribe to the event stream and provide a callback that will be called for event event received.
|
||||
This function will allow you to potentially write a callback that can handle all of the events received from the event stream.
|
||||
NOTE: Use this function if you need to run some code after subscribing to the eventstream, but before your callback to handle the events runs.
|
||||
"""
|
||||
if trigger is not None and not callable(trigger):
|
||||
raise Exception('The trigger(self, camera) should be a callable function.')
|
||||
if not callable(callback):
|
||||
raise Exception('The callback(self, event) should be a callable function.')
|
||||
|
||||
await self.Subscribe()
|
||||
if trigger:
|
||||
trigger(self)
|
||||
|
||||
# NOTE: Calling HandleEvents() calls Subscribe() again, which basically turns into a no-op. Hackie I know, but it cleans up the code a bit.
|
||||
return await self.HandleEvents(basestation, resource, actions, callback)
|
||||
|
||||
def GetDevices(self, device_type=None, filter_provisioned=None):
|
||||
"""
|
||||
This method returns an array that contains the basestation, cameras, etc. and their metadata.
|
||||
If you pass in a valid device type, as a string or a list, this method will return an array of just those devices that match that type. An example would be ['basestation', 'camera']
|
||||
To filter provisioned or unprovisioned devices pass in a True/False value for filter_provisioned. By default both types are returned.
|
||||
"""
|
||||
devices = self._getDevicesImpl()
|
||||
if device_type:
|
||||
devices = [ device for device in devices if device.get('deviceType') in device_type]
|
||||
|
||||
if filter_provisioned is not None:
|
||||
if filter_provisioned:
|
||||
devices = [ device for device in devices if device.get("state") == 'provisioned']
|
||||
else:
|
||||
devices = [ device for device in devices if device.get("state") != 'provisioned']
|
||||
|
||||
return devices
|
||||
|
||||
@cached(cache=TTLCache(maxsize=1, ttl=60))
|
||||
def _getDevicesImpl(self):
|
||||
devices = self.request.get(f'https://{self.BASE_URL}/hmsweb/v2/users/devices')
|
||||
return devices
|
||||
|
||||
async def StartStream(self, basestation, camera, mode="rtsp"):
|
||||
"""
|
||||
This function returns the url of the rtsp video stream.
|
||||
This stream needs to be called within 30 seconds or else it becomes invalid.
|
||||
It can be streamed with: ffmpeg -re -i 'rtsps://<url>' -acodec copy -vcodec copy test.mp4
|
||||
The request to /users/devices/startStream returns: { url:rtsp://<url>:443/vzmodulelive?egressToken=b<xx>&userAgent=iOS&cameraId=<camid>}
|
||||
|
||||
If mode is set to "dash", returns the url to the mpd file for DASH streaming.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
if mode not in ["rtsp", "dash"]:
|
||||
raise ValueError("mode must be 'rtsp' or 'dash'")
|
||||
|
||||
# nonlocal variable hack for Python 2.x.
|
||||
class nl:
|
||||
stream_url_dict = None
|
||||
|
||||
def trigger(self):
|
||||
ua = USER_AGENTS['arlo'] if mode == "rtsp" else USER_AGENTS["firefox"]
|
||||
nl.stream_url_dict = self.request.post(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/devices/startStream',
|
||||
params={
|
||||
"to": camera.get('parentId'),
|
||||
"from": self.user_id + "_web",
|
||||
"resource": "cameras/" + camera.get('deviceId'),
|
||||
"action": "set",
|
||||
"responseUrl": "",
|
||||
"publishResponse": True,
|
||||
"transId": self.genTransId(),
|
||||
"properties": {
|
||||
"activityState": "startUserStream",
|
||||
"cameraId": camera.get('deviceId')
|
||||
}
|
||||
},
|
||||
headers={"xcloudId":camera.get('xCloudId'), 'User-Agent': ua}
|
||||
)
|
||||
|
||||
def callback(self, event):
|
||||
#return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
properties = event.get("properties", {})
|
||||
if properties.get("activityState") == "userStreamActive":
|
||||
if mode == "rtsp":
|
||||
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
else:
|
||||
return nl.stream_url_dict['url'].replace(":80", "")
|
||||
return None
|
||||
|
||||
return await self.TriggerAndHandleEvent(
|
||||
basestation,
|
||||
resource,
|
||||
[("is", "activityState")],
|
||||
trigger,
|
||||
callback,
|
||||
)
|
||||
|
||||
def GetMPDHeaders(self, url: str) -> dict:
|
||||
parsed = urlparse(url)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
headers = {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Connection": "keep-alive",
|
||||
"DNT": "1",
|
||||
"Egress-Token": query['egressToken'][0],
|
||||
"Origin": "https://my.arlo.com",
|
||||
"Referer": "https://my.arlo.com/",
|
||||
"User-Agent": USER_AGENTS["firefox"],
|
||||
}
|
||||
return headers
|
||||
|
||||
def GetSIPInfo(self):
|
||||
resp = self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/sipInfo')
|
||||
return resp
|
||||
|
||||
def StartPushToTalk(self, basestation, camera):
|
||||
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
|
||||
resp = self.request.get(url)
|
||||
return resp.get("uSessionId"), resp.get("data")
|
||||
|
||||
def NotifyPushToTalkSDP(self, basestation, camera, uSessionId, localSdp):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
self.Notify(basestation, {
|
||||
"action": "pushToTalk",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"data": localSdp,
|
||||
"type": "offerSdp",
|
||||
"uSessionId": uSessionId
|
||||
}
|
||||
})
|
||||
|
||||
def NotifyPushToTalkCandidate(self, basestation, camera, uSessionId, localCandidate):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
self.Notify(basestation, {
|
||||
"action": "pushToTalk",
|
||||
"resource": resource,
|
||||
"publishResponse": False,
|
||||
"properties": {
|
||||
"data": localCandidate,
|
||||
"type": "offerCandidate",
|
||||
"uSessionId": uSessionId
|
||||
}
|
||||
})
|
||||
|
||||
async def TriggerFullFrameSnapshot(self, basestation, camera):
|
||||
"""
|
||||
This function causes the camera to record a fullframe snapshot.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
def trigger(self):
|
||||
self.request.post(
|
||||
f"https://{self.BASE_URL}/hmsweb/users/devices/fullFrameSnapshot",
|
||||
params={
|
||||
"to": camera.get("parentId"),
|
||||
"from": self.user_id + "_web",
|
||||
"resource": "cameras/" + camera.get("deviceId"),
|
||||
"action": "set",
|
||||
"publishResponse": True,
|
||||
"transId": self.genTransId(),
|
||||
"properties": {
|
||||
"activityState": "fullFrameSnapshot"
|
||||
}
|
||||
},
|
||||
headers={"xcloudId":camera.get("xCloudId")}
|
||||
)
|
||||
|
||||
def callback(self, event):
|
||||
properties = event.get("properties", {})
|
||||
url = properties.get("presignedFullFrameSnapshotUrl")
|
||||
if url:
|
||||
return url
|
||||
url = properties.get("presignedLastImageUrl")
|
||||
if url:
|
||||
return url
|
||||
return None
|
||||
|
||||
return await self.TriggerAndHandleEvent(
|
||||
basestation,
|
||||
resource,
|
||||
[
|
||||
(action, property)
|
||||
for action in ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"]
|
||||
for property in ["presignedFullFrameSnapshotUrl", "presignedLastImageUrl"]
|
||||
],
|
||||
trigger,
|
||||
callback,
|
||||
)
|
||||
|
||||
def SirenOn(self, basestation, camera=None):
|
||||
if camera is not None:
|
||||
resource = f"siren/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "on",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": "siren",
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "on",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
|
||||
def SirenOff(self, basestation, camera=None):
|
||||
if camera is not None:
|
||||
resource = f"siren/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "off",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": "siren",
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "off",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
|
||||
def SpotlightOn(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"spotlight": {
|
||||
"enabled": True,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def SpotlightOff(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"spotlight": {
|
||||
"enabled": False,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def FloodlightOn(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"floodlight": {
|
||||
"on": True,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def FloodlightOff(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"floodlight": {
|
||||
"on": False,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def GetLibrary(self, device, from_date: datetime, to_date: datetime):
|
||||
"""
|
||||
This call returns the following:
|
||||
presignedContentUrl is a link to the actual video in Amazon AWS.
|
||||
presignedThumbnailUrl is a link to the thumbnail .jpg of the actual video in Amazon AWS.
|
||||
[
|
||||
{
|
||||
"mediaDurationSecond": 30,
|
||||
"contentType": "video/mp4",
|
||||
"name": "XXXXXXXXXXXXX",
|
||||
"presignedContentUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX.mp4?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"lastModified": 1472881430181,
|
||||
"localCreatedDate": XXXXXXXXXXXXX,
|
||||
"presignedThumbnailUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX_thumb.jpg?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"reason": "motionRecord",
|
||||
"deviceId": "XXXXXXXXXXXXX",
|
||||
"createdBy": "XXXXXXXXXXXXX",
|
||||
"createdDate": "20160903",
|
||||
"timeZone": "America/Chicago",
|
||||
"ownerId": "XXX-XXXXXXX",
|
||||
"utcCreatedDate": XXXXXXXXXXXXX,
|
||||
"currentState": "new",
|
||||
"mediaDuration": "00:00:30"
|
||||
}
|
||||
]
|
||||
"""
|
||||
# give the query range a bit of buffer
|
||||
from_date_internal = from_date - timedelta(days=1)
|
||||
to_date_internal = to_date + timedelta(days=1)
|
||||
|
||||
return [
|
||||
result for result in
|
||||
self._getLibraryCached(from_date_internal.strftime("%Y%m%d"), to_date_internal.strftime("%Y%m%d"))
|
||||
if result["deviceId"] == device["deviceId"]
|
||||
and datetime.fromtimestamp(int(result["name"]) / 1000.0) <= to_date
|
||||
and datetime.fromtimestamp(int(result["name"]) / 1000.0) >= from_date
|
||||
]
|
||||
|
||||
@cached(cache=TTLCache(maxsize=512, ttl=60))
|
||||
def _getLibraryCached(self, from_date: str, to_date: str):
|
||||
logger.debug(f"Library cache miss for {from_date}, {to_date}")
|
||||
return self.request.post(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/library',
|
||||
params={
|
||||
'dateFrom': from_date,
|
||||
'dateTo': to_date
|
||||
}
|
||||
)
|
||||
|
||||
def GetSmartFeatures(self, device) -> dict:
|
||||
smart_features = self._getSmartFeaturesCached()
|
||||
key = f"{device['owner']['ownerId']}_{device['deviceId']}"
|
||||
return smart_features["features"].get(key, {})
|
||||
|
||||
@cached(cache=TTLCache(maxsize=1, ttl=60))
|
||||
def _getSmartFeaturesCached(self) -> dict:
|
||||
return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/subscription/smart/features')
|
||||
@@ -1,26 +0,0 @@
|
||||
import ssl
|
||||
from socket import setdefaulttimeout
|
||||
import requests
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
import scrypted_arlo_go
|
||||
|
||||
from .logging import logger
|
||||
|
||||
|
||||
setdefaulttimeout(5)
|
||||
|
||||
|
||||
def pick_host(hosts, hostname_to_match, endpoint_to_test):
|
||||
session = requests.Session()
|
||||
session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
c = ssl.get_server_certificate((host, 443))
|
||||
scrypted_arlo_go.VerifyCertHostname(c, hostname_to_match)
|
||||
r = session.post(f"https://{host}{endpoint_to_test}", headers={"Host": hostname_to_match})
|
||||
r.raise_for_status()
|
||||
return host
|
||||
except Exception as e:
|
||||
logger.warning(f"{host} is invalid: {e}")
|
||||
raise Exception("no valid hosts found!")
|
||||
@@ -1,16 +0,0 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# construct logger instance to be used by package arlo
|
||||
logger = logging.getLogger("lib")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# output logger to stdout
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
# log formatting
|
||||
fmt = logging.Formatter("[Arlo] %(message)s")
|
||||
ch.setFormatter(fmt)
|
||||
|
||||
# configure handler to logger
|
||||
logger.addHandler(ch)
|
||||
@@ -1,85 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from .stream_async import Stream
|
||||
from .logging import logger
|
||||
|
||||
class MQTTStream(Stream):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.cached_topics = []
|
||||
|
||||
def _gen_client_number(self):
|
||||
return random.randint(1000000000, 9999999999)
|
||||
|
||||
async def start(self):
|
||||
if self.event_stream is not None:
|
||||
return
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
self.connected = True
|
||||
self.initializing = False
|
||||
|
||||
logger.info(f"MQTT {id(client)} connected")
|
||||
|
||||
client.subscribe([
|
||||
(f"u/{self.arlo.user_id}/in/userSession/connect", 0),
|
||||
(f"u/{self.arlo.user_id}/in/userSession/disconnect", 0),
|
||||
])
|
||||
|
||||
def on_disconnect(client, *args, **kwargs):
|
||||
logger.info(f"MQTT {id(client)} disconnected")
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
payload = msg.payload.decode()
|
||||
logger.debug(f"Received event: {payload}")
|
||||
|
||||
try:
|
||||
response = json.loads(payload.strip())
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
|
||||
if response.get('resource') is not None:
|
||||
self.event_loop.call_soon_threadsafe(self._queue_response, response)
|
||||
|
||||
self.event_stream = mqtt.Client(client_id=f"user_{self.arlo.user_id}_{self._gen_client_number()}", transport="websockets", clean_session=False)
|
||||
self.event_stream.username_pw_set(self.arlo.user_id, password=self.arlo.request.session.headers.get('Authorization'))
|
||||
self.event_stream.ws_set_options(path="/mqtt", headers={"Origin": "https://my.arlo.com"})
|
||||
self.event_stream.on_connect = on_connect
|
||||
self.event_stream.on_disconnect = on_disconnect
|
||||
self.event_stream.on_message = on_message
|
||||
self.event_stream.tls_set()
|
||||
self.event_stream.connect_async("mqtt-cluster.arloxcld.com", port=443)
|
||||
self.event_stream.loop_start()
|
||||
|
||||
while not self.connected and not self.event_stream_stop_event.is_set():
|
||||
await asyncio.sleep(0.5)
|
||||
if not self.event_stream_stop_event.is_set():
|
||||
self.resubscribe()
|
||||
|
||||
async def restart(self):
|
||||
self.reconnecting = True
|
||||
self.connected = False
|
||||
self.event_stream.disconnect()
|
||||
self.event_stream = None
|
||||
await self.start()
|
||||
# give it an extra sleep to ensure any previous connections have disconnected properly
|
||||
# this is so we can mark reconnecting to False properly
|
||||
await asyncio.sleep(1)
|
||||
self.reconnecting = False
|
||||
|
||||
def subscribe(self, topics):
|
||||
if topics:
|
||||
new_subscriptions = [(topic, 0) for topic in topics]
|
||||
self.event_stream.subscribe(new_subscriptions)
|
||||
self.cached_topics.extend(new_subscriptions)
|
||||
|
||||
def resubscribe(self):
|
||||
if self.cached_topics:
|
||||
self.event_stream.subscribe(self.cached_topics)
|
||||
|
||||
def disconnect(self):
|
||||
super().disconnect()
|
||||
self.event_stream.disconnect()
|
||||
@@ -1,100 +0,0 @@
|
||||
##
|
||||
# Copyright 2016 Jeffrey D. Walter
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
##
|
||||
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
import cloudscraper
|
||||
import time
|
||||
import uuid
|
||||
|
||||
#from requests_toolbelt.utils import dump
|
||||
#def print_raw_http(response):
|
||||
# data = dump.dump_all(response, request_prefix=b'', response_prefix=b'')
|
||||
# print('\n' * 2 + data.decode('utf-8'))
|
||||
|
||||
class Request(object):
|
||||
"""HTTP helper class"""
|
||||
|
||||
def __init__(self, timeout=5, mode="cloudscraper"):
|
||||
if mode == "cloudscraper":
|
||||
from .arlo_async import USER_AGENTS
|
||||
self.session = cloudscraper.CloudScraper(browser={"custom": USER_AGENTS["arlo"]})
|
||||
elif mode == "ip":
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
self.timeout = timeout
|
||||
|
||||
def gen_event_id(self):
|
||||
return f'FE!{str(uuid.uuid4())}'
|
||||
|
||||
def get_time(self):
|
||||
return int(time.time_ns() / 1_000_000)
|
||||
|
||||
def _request(self, url, method='GET', params={}, headers={}, raw=False, skip_event_id=False):
|
||||
|
||||
## uncomment for debug logging
|
||||
"""
|
||||
import logging
|
||||
import http.client
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
#logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
req_log = logging.getLogger('requests.packages.urllib3')
|
||||
req_log.setLevel(logging.DEBUG)
|
||||
req_log.propagate = True
|
||||
#"""
|
||||
|
||||
if not skip_event_id:
|
||||
url = f'{url}?eventId={self.gen_event_id()}&time={self.get_time()}'
|
||||
|
||||
if method == 'GET':
|
||||
#print('COOKIES: ', self.session.cookies.get_dict())
|
||||
r = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
elif method == 'PUT':
|
||||
r = self.session.put(url, json=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
elif method == 'POST':
|
||||
r = self.session.post(url, json=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
elif method == 'OPTIONS':
|
||||
r = self.session.options(url, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
return
|
||||
|
||||
body = r.json()
|
||||
|
||||
if raw:
|
||||
return body
|
||||
else:
|
||||
if ('success' in body and body['success'] == True) or ('meta' in body and body['meta']['code'] == 200):
|
||||
if 'data' in body:
|
||||
return body['data']
|
||||
else:
|
||||
raise HTTPError('Request ({0} {1}) failed: {2}'.format(method, url, r.json()), response=r)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._request(url, 'GET', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._request(url, 'PUT', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._request(url, 'POST', **kwargs)
|
||||
|
||||
def options(self, url, **kwargs):
|
||||
return self._request(url, 'OPTIONS', **kwargs)
|
||||
@@ -1,69 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import sseclient
|
||||
import threading
|
||||
|
||||
from .stream_async import Stream
|
||||
from .logging import logger
|
||||
|
||||
|
||||
class EventStream(Stream):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.shutting_down_stream = None # record the eventstream that is currently shutting down
|
||||
|
||||
async def start(self):
|
||||
if self.event_stream is not None:
|
||||
return
|
||||
|
||||
def thread_main(self):
|
||||
event_stream = self.event_stream
|
||||
for event in event_stream:
|
||||
logger.debug(f"Received event: {event}")
|
||||
if event is None:
|
||||
logger.info(f"SSE {id(event_stream)} appears to be broken")
|
||||
return None
|
||||
|
||||
if event.data.strip() == "":
|
||||
continue
|
||||
|
||||
try:
|
||||
response = json.loads(event.data)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
if response.get('action') == 'logout':
|
||||
if self.event_stream_stop_event.is_set() or \
|
||||
self.shutting_down_stream is event_stream:
|
||||
logger.info(f"SSE {id(event_stream)} disconnected")
|
||||
return None
|
||||
elif response.get('status') == 'connected':
|
||||
if not self.connected:
|
||||
logger.info(f"SSE {id(event_stream)} connected")
|
||||
self.initializing = False
|
||||
self.connected = True
|
||||
else:
|
||||
self.event_loop.call_soon_threadsafe(self._queue_response, response)
|
||||
|
||||
self.event_stream = sseclient.SSEClient('https://myapi.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization'), session=self.arlo.request.session)
|
||||
self.event_stream_thread = threading.Thread(name="EventStream", target=thread_main, args=(self, ))
|
||||
self.event_stream_thread.setDaemon(True)
|
||||
self.event_stream_thread.start()
|
||||
|
||||
while not self.connected and not self.event_stream_stop_event.is_set():
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def restart(self):
|
||||
self.reconnecting = True
|
||||
self.connected = False
|
||||
self.shutting_down_stream = self.event_stream
|
||||
self.event_stream = None
|
||||
await self.start()
|
||||
# give it an extra sleep to ensure any previous connections have disconnected properly
|
||||
# this is so we can mark reconnecting to False properly
|
||||
await asyncio.sleep(1)
|
||||
self.shutting_down_stream = None
|
||||
self.reconnecting = False
|
||||
|
||||
def subscribe(self, topics):
|
||||
pass
|
||||
@@ -1,235 +0,0 @@
|
||||
# This file has been modified to support async semantics and better
|
||||
# integration with scrypted.
|
||||
# Original: https://github.com/jeffreydwalter/arlo
|
||||
|
||||
##
|
||||
# Copyright 2016 Jeffrey D. Walter
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
##
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .logging import logger
|
||||
|
||||
class Stream:
|
||||
"""This class provides a queue-based EventStream object."""
|
||||
def __init__(self, arlo, expire=5):
|
||||
self.event_stream = None
|
||||
self.initializing = True
|
||||
self.connected = False
|
||||
self.reconnecting = False
|
||||
self.queues = {}
|
||||
self.expire = expire
|
||||
self.refresh = 0
|
||||
self.refresh_loop_signal = asyncio.Queue()
|
||||
self.event_stream_stop_event = threading.Event()
|
||||
self.event_stream_thread = None
|
||||
self.arlo = arlo
|
||||
self.event_loop = asyncio.get_event_loop()
|
||||
self.event_loop.create_task(self._clean_queues())
|
||||
self.event_loop.create_task(self._refresh_interval())
|
||||
|
||||
def __del__(self):
|
||||
self.disconnect()
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
"""Represents if this stream is connected or in the process of reconnecting."""
|
||||
return self.connected or self.reconnecting
|
||||
|
||||
async def _refresh_interval(self):
|
||||
while not self.event_stream_stop_event.is_set():
|
||||
if self.refresh == 0:
|
||||
# to avoid spinning, wait until an interval is set
|
||||
signal = await self.refresh_loop_signal.get()
|
||||
if signal is None:
|
||||
# exit signal received from disconnect()
|
||||
return
|
||||
continue
|
||||
|
||||
interval = self.refresh * 60 # interval in seconds from refresh in minutes
|
||||
signal_task = asyncio.create_task(self.refresh_loop_signal.get())
|
||||
|
||||
# wait until either we receive a signal or the refresh interval expires
|
||||
done, pending = await asyncio.wait([signal_task, asyncio.sleep(interval)], return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
done_task = done.pop()
|
||||
if done_task is signal_task and done_task.result() is None:
|
||||
# exit signal received from disconnect()
|
||||
return
|
||||
|
||||
logger.info("Refreshing event stream")
|
||||
await self.restart()
|
||||
|
||||
def set_refresh_interval(self, interval):
|
||||
self.refresh = interval
|
||||
self.refresh_loop_signal.put_nowait(object())
|
||||
|
||||
async def _clean_queues(self):
|
||||
interval = self.expire * 4
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
while not self.event_stream_stop_event.is_set():
|
||||
# since we interrupt the cleanup loop after every queue, there's
|
||||
# a chance the self.queues dict is modified during iteration.
|
||||
# so, we first make a copy of all the items of the dict and any
|
||||
# new queues will be processed on the next loop through
|
||||
queue_items = [i for i in self.queues.items()]
|
||||
for key, q in queue_items:
|
||||
if q.empty():
|
||||
continue
|
||||
|
||||
items = []
|
||||
num_dropped = 0
|
||||
|
||||
while not q.empty():
|
||||
item = q.get_nowait()
|
||||
q.task_done()
|
||||
|
||||
if not item:
|
||||
# exit signal received
|
||||
return
|
||||
|
||||
if item.expired:
|
||||
num_dropped += 1
|
||||
continue
|
||||
|
||||
items.append(item)
|
||||
|
||||
for item in items:
|
||||
q.put_nowait(item)
|
||||
|
||||
if num_dropped > 0:
|
||||
logger.debug(f"Cleaned {num_dropped} events from queue {key}")
|
||||
|
||||
# cleanup is not urgent, so give other tasks a chance
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async def get(self, resource, action, property=None, skip_uuids={}):
|
||||
if not property:
|
||||
key = f"{resource}/{action}"
|
||||
else:
|
||||
key = f"{resource}/{action}/{property}"
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
|
||||
first_requeued = None
|
||||
while True:
|
||||
event = await q.get()
|
||||
q.task_done()
|
||||
|
||||
if not event:
|
||||
# exit signal received
|
||||
return None, action
|
||||
|
||||
if first_requeued is not None and first_requeued is event:
|
||||
# if we reach here, we've cycled through the whole queue
|
||||
# and found nothing for us, so sleep and give the next
|
||||
# subscriber a chance
|
||||
q.put_nowait(event)
|
||||
await asyncio.sleep(random.uniform(0, 0.01))
|
||||
continue
|
||||
|
||||
if event.expired:
|
||||
continue
|
||||
elif event.uuid in skip_uuids:
|
||||
q.put_nowait(event)
|
||||
if first_requeued is None:
|
||||
first_requeued = event
|
||||
else:
|
||||
return event, action
|
||||
|
||||
async def start(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def restart(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def subscribe(self, topics):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _queue_response(self, response):
|
||||
resource = response.get('resource')
|
||||
action = response.get('action')
|
||||
key = f"{resource}/{action}"
|
||||
|
||||
now = time.time()
|
||||
event = StreamEvent(response, now, now + self.expire)
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
|
||||
# for optimized lookups, notify listeners of individual properties
|
||||
properties = response.get('properties', {})
|
||||
for property in properties.keys():
|
||||
key = f"{resource}/{action}/{property}"
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
|
||||
def requeue(self, event, resource, action, property=None):
|
||||
if not property:
|
||||
key = f"{resource}/{action}"
|
||||
else:
|
||||
key = f"{resource}/{action}/{property}"
|
||||
self.queues[key].put_nowait(event)
|
||||
|
||||
def disconnect(self):
|
||||
if self.reconnecting:
|
||||
# disconnect may be called when an old stream is being refreshed/restarted,
|
||||
# so don't completely shut down if we are reconnecting
|
||||
return
|
||||
|
||||
self.connected = False
|
||||
|
||||
def exit_queues(self):
|
||||
for q in self.queues.values():
|
||||
q.put_nowait(None)
|
||||
self.refresh_loop_signal.put_nowait(None)
|
||||
self.event_loop.call_soon_threadsafe(exit_queues, self)
|
||||
|
||||
self.event_stream_stop_event.set()
|
||||
|
||||
|
||||
class StreamEvent:
|
||||
item = None
|
||||
timestamp = None
|
||||
expiration = None
|
||||
uuid = None
|
||||
|
||||
def __init__(self, item, timestamp, expiration):
|
||||
self.item = item
|
||||
self.timestamp = timestamp
|
||||
self.expiration = expiration
|
||||
self.uuid = str(uuid.uuid4())
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
return time.time() > self.expiration
|
||||
@@ -1,69 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from scrypted_sdk import ScryptedDeviceBase
|
||||
from scrypted_sdk.types import Device
|
||||
|
||||
from .logging import ScryptedDeviceLoggerMixin
|
||||
from .util import BackgroundTaskMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
|
||||
nativeId: str = None
|
||||
arlo_device: dict = None
|
||||
arlo_basestation: dict = None
|
||||
provider: ArloProvider = None
|
||||
stop_subscriptions: bool = False
|
||||
|
||||
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
self.logger_name = nativeId
|
||||
|
||||
self.nativeId = nativeId
|
||||
self.arlo_device = arlo_device
|
||||
self.arlo_basestation = arlo_basestation
|
||||
self.provider = provider
|
||||
self.logger.setLevel(self.provider.get_current_log_level())
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.stop_subscriptions = True
|
||||
self.cancel_pending_tasks()
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
"""Returns the list of Scrypted interfaces that applies to this device."""
|
||||
return []
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
"""Returns the Scrypted device type that applies to this device."""
|
||||
return ""
|
||||
|
||||
def get_device_manifest(self) -> Device:
|
||||
"""Returns the Scrypted device manifest representing this device."""
|
||||
parent = None
|
||||
if self.arlo_device.get("parentId") and self.arlo_device["parentId"] != self.arlo_device["deviceId"]:
|
||||
parent = self.arlo_device["parentId"]
|
||||
|
||||
return {
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
"manufacturer": "Arlo",
|
||||
"firmware": self.arlo_device.get("firmwareVersion"),
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": self.arlo_device["deviceId"],
|
||||
"name": self.arlo_device["deviceName"],
|
||||
"interfaces": self.get_applicable_interfaces(),
|
||||
"type": self.get_device_type(),
|
||||
"providerNativeId": parent,
|
||||
}
|
||||
|
||||
def get_builtin_child_device_manifests(self) -> List[Device]:
|
||||
"""Returns the list of child device manifests representing hardware features built into this device."""
|
||||
return []
|
||||
@@ -1,71 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from scrypted_sdk import ScryptedDeviceBase
|
||||
from scrypted_sdk.types import Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .base import ArloDeviceBase
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloBasestation(ArloDeviceBase, DeviceProvider):
|
||||
MODELS_WITH_SIRENS = [
|
||||
"vmb4000",
|
||||
"vmb4500"
|
||||
]
|
||||
|
||||
vss: ArloSirenVirtualSecuritySystem = None
|
||||
|
||||
def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_basestation, arlo_basestation=arlo_basestation, provider=provider)
|
||||
|
||||
@property
|
||||
def has_siren(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS])
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
return [ScryptedInterface.DeviceProvider.value]
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
return ScryptedDeviceType.DeviceProvider.value
|
||||
|
||||
def get_builtin_child_device_manifests(self) -> List[Device]:
|
||||
if not self.has_siren:
|
||||
# this basestation has no builtin siren, so no manifests to return
|
||||
return []
|
||||
|
||||
vss = self.get_or_create_vss()
|
||||
return [
|
||||
{
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
"manufacturer": "Arlo",
|
||||
"firmware": self.arlo_device.get("firmwareVersion"),
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": vss.nativeId,
|
||||
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
|
||||
"interfaces": vss.get_applicable_interfaces(),
|
||||
"type": vss.get_device_type(),
|
||||
"providerNativeId": self.nativeId,
|
||||
},
|
||||
] + vss.get_builtin_child_device_manifests()
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ScryptedDeviceBase:
|
||||
if not nativeId.startswith(self.nativeId):
|
||||
# must be a camera, so get it from the provider
|
||||
return await self.provider.getDevice(nativeId)
|
||||
if not nativeId.endswith("vss"):
|
||||
return None
|
||||
return self.get_or_create_vss()
|
||||
|
||||
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
|
||||
vss_id = f'{self.arlo_device["deviceId"]}.vss'
|
||||
if not self.vss:
|
||||
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
return self.vss
|
||||
@@ -1,735 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from async_timeout import timeout as async_timeout
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import time
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
import scrypted_arlo_go
|
||||
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk.types import Setting, Settings, SettingValue, Device, Camera, VideoCamera, RequestMediaStreamOptions, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, Charger, ChargeState, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .arlo.arlo_async import USER_AGENTS
|
||||
from .experimental import EXPERIMENTAL
|
||||
from .base import ArloDeviceBase
|
||||
from .spotlight import ArloSpotlight, ArloFloodlight
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
from .child_process import HeartbeatChildProcess
|
||||
from .util import BackgroundTaskMixin, async_print_exception_guard
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, AudioSensor, Battery, Charger):
|
||||
MODELS_WITH_SPOTLIGHTS = [
|
||||
"vmc4040p",
|
||||
"vmc2030",
|
||||
"vmc2032",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc5040",
|
||||
"vml2030",
|
||||
"vml4030",
|
||||
]
|
||||
|
||||
MODELS_WITH_FLOODLIGHTS = ["fb1001"]
|
||||
|
||||
MODELS_WITH_SIRENS = [
|
||||
"vmc4040p",
|
||||
"fb1001",
|
||||
"vmc2030",
|
||||
"vmc2020",
|
||||
"vmc2032",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc5040",
|
||||
"vml2030",
|
||||
"vmc4030",
|
||||
"vml4030",
|
||||
"vmc4030p",
|
||||
]
|
||||
|
||||
MODELS_WITH_AUDIO_SENSORS = [
|
||||
"vmc4040p",
|
||||
"fb1001",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc5040",
|
||||
"vmc3040",
|
||||
"vmc3040s",
|
||||
"vmc4030",
|
||||
"vml4030",
|
||||
"vmc4030p",
|
||||
]
|
||||
|
||||
MODELS_WITHOUT_BATTERY = [
|
||||
"avd1001",
|
||||
"vmc3040",
|
||||
"vmc3040s",
|
||||
]
|
||||
|
||||
timeout: int = 30
|
||||
intercom_session = None
|
||||
goSM = None
|
||||
light: ArloSpotlight = None
|
||||
vss: ArloSirenVirtualSecuritySystem = None
|
||||
picture_lock: asyncio.Lock = None
|
||||
|
||||
# eco mode bookkeeping
|
||||
last_picture: bytes = None
|
||||
last_picture_time: datetime = datetime(1970, 1, 1)
|
||||
|
||||
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
|
||||
self.picture_lock = asyncio.Lock()
|
||||
|
||||
self.start_motion_subscription()
|
||||
self.start_audio_subscription()
|
||||
self.start_battery_subscription()
|
||||
self.create_task(self.delayed_init())
|
||||
|
||||
async def delayed_init(self) -> None:
|
||||
if not self.has_battery:
|
||||
return
|
||||
|
||||
iterations = 1
|
||||
while not self.stop_subscriptions:
|
||||
if iterations > 100:
|
||||
self.logger.error("Delayed init exceeded iteration limit, giving up")
|
||||
return
|
||||
|
||||
try:
|
||||
self.chargeState = ChargeState.Charging.value if self.wired_to_power else ChargeState.NotCharging.value
|
||||
return
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Delayed init failed, will try again: {e}")
|
||||
await asyncio.sleep(0.1)
|
||||
iterations += 1
|
||||
|
||||
def start_motion_subscription(self) -> None:
|
||||
def callback(motionDetected):
|
||||
self.motionDetected = motionDetected
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToMotionEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def start_audio_subscription(self) -> None:
|
||||
if not self.has_audio_sensor:
|
||||
return
|
||||
|
||||
def callback(audioDetected):
|
||||
self.audioDetected = audioDetected
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToAudioEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def start_battery_subscription(self) -> None:
|
||||
if not self.has_battery:
|
||||
return
|
||||
|
||||
def callback(batteryLevel):
|
||||
self.batteryLevel = batteryLevel
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToBatteryEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
results = set([
|
||||
ScryptedInterface.VideoCamera.value,
|
||||
ScryptedInterface.Camera.value,
|
||||
ScryptedInterface.MotionSensor.value,
|
||||
ScryptedInterface.Settings.value,
|
||||
])
|
||||
|
||||
if EXPERIMENTAL or not self.uses_sip_push_to_talk:
|
||||
results.add(ScryptedInterface.Intercom.value)
|
||||
|
||||
if self.has_battery:
|
||||
results.add(ScryptedInterface.Battery.value)
|
||||
results.add(ScryptedInterface.Charger.value)
|
||||
|
||||
if self.has_siren or self.has_spotlight or self.has_floodlight:
|
||||
results.add(ScryptedInterface.DeviceProvider.value)
|
||||
|
||||
if self.has_audio_sensor:
|
||||
results.add(ScryptedInterface.AudioSensor.value)
|
||||
|
||||
if self.has_cloud_recording:
|
||||
results.add(ScryptedInterface.VideoClips.value)
|
||||
|
||||
return list(results)
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
return ScryptedDeviceType.Camera.value
|
||||
|
||||
def get_builtin_child_device_manifests(self) -> List[Device]:
|
||||
results = []
|
||||
if self.has_spotlight or self.has_floodlight:
|
||||
light = self.get_or_create_spotlight_or_floodlight()
|
||||
results.append({
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
"manufacturer": "Arlo",
|
||||
"firmware": self.arlo_device.get("firmwareVersion"),
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": light.nativeId,
|
||||
"name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}',
|
||||
"interfaces": light.get_applicable_interfaces(),
|
||||
"type": light.get_device_type(),
|
||||
"providerNativeId": self.nativeId,
|
||||
})
|
||||
if self.has_siren:
|
||||
vss = self.get_or_create_vss()
|
||||
results.extend([
|
||||
{
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
"manufacturer": "Arlo",
|
||||
"firmware": self.arlo_device.get("firmwareVersion"),
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": vss.nativeId,
|
||||
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
|
||||
"interfaces": vss.get_applicable_interfaces(),
|
||||
"type": vss.get_device_type(),
|
||||
"providerNativeId": self.nativeId,
|
||||
},
|
||||
] + vss.get_builtin_child_device_manifests())
|
||||
return results
|
||||
|
||||
@property
|
||||
def wired_to_power(self) -> bool:
|
||||
if self.storage:
|
||||
return True if self.storage.getItem("wired_to_power") else False
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def eco_mode(self) -> bool:
|
||||
if self.storage:
|
||||
return True if self.storage.getItem("eco_mode") else False
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def snapshot_throttle_interval(self) -> bool:
|
||||
interval = self.storage.getItem("snapshot_throttle_interval")
|
||||
if interval is None:
|
||||
interval = 60
|
||||
self.storage.setItem("snapshot_throttle_interval", interval)
|
||||
return int(interval)
|
||||
|
||||
@property
|
||||
def has_cloud_recording(self) -> bool:
|
||||
return self.provider.arlo.GetSmartFeatures(self.arlo_device).get("planFeatures", {}).get("eventRecording", False)
|
||||
|
||||
@property
|
||||
def has_spotlight(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SPOTLIGHTS])
|
||||
|
||||
@property
|
||||
def has_floodlight(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_FLOODLIGHTS])
|
||||
|
||||
@property
|
||||
def has_siren(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS])
|
||||
|
||||
@property
|
||||
def has_audio_sensor(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_AUDIO_SENSORS])
|
||||
|
||||
@property
|
||||
def has_battery(self) -> bool:
|
||||
return not any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITHOUT_BATTERY])
|
||||
|
||||
@property
|
||||
def uses_sip_push_to_talk(self) -> bool:
|
||||
return self.arlo_device["deviceId"] == self.arlo_device["parentId"]
|
||||
|
||||
async def getSettings(self) -> List[Setting]:
|
||||
result = []
|
||||
if self.has_battery:
|
||||
result.append(
|
||||
{
|
||||
"group": "General",
|
||||
"key": "wired_to_power",
|
||||
"title": "Plugged In to External Power",
|
||||
"value": self.wired_to_power,
|
||||
"description": "Informs Scrypted that this device is plugged in to an external power source. " + \
|
||||
"Will allow features like persistent prebuffer to work. " + \
|
||||
"Note that a persistent prebuffer may cause excess battery drain if the external power is not able to charge faster than the battery consumption rate.",
|
||||
"type": "boolean",
|
||||
},
|
||||
)
|
||||
result.append(
|
||||
{
|
||||
"group": "General",
|
||||
"key": "eco_mode",
|
||||
"title": "Eco Mode",
|
||||
"value": self.eco_mode,
|
||||
"description": "Configures Scrypted to limit the number of requests made to this camera. " + \
|
||||
"Additional eco mode settings will appear when this is turned on.",
|
||||
"type": "boolean",
|
||||
}
|
||||
)
|
||||
if self.eco_mode:
|
||||
result.append(
|
||||
{
|
||||
"group": "Eco Mode",
|
||||
"key": "snapshot_throttle_interval",
|
||||
"title": "Snapshot Throttle Interval",
|
||||
"value": self.snapshot_throttle_interval,
|
||||
"description": "Time, in minutes, to throttle snapshot requests. " + \
|
||||
"When eco mode is on, snapshot requests to the camera will be throttled for the given duration. " + \
|
||||
"Cached snapshots may be returned if the time since the last snapshot has not exceeded the interval. " + \
|
||||
"A value of 0 will disable throttling even when eco mode is on.",
|
||||
"type": "number",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@async_print_exception_guard
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
if not self.validate_setting(key, value):
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
return
|
||||
|
||||
if key in ["wired_to_power"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
await self.provider.discover_devices()
|
||||
elif key in ["eco_mode"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
else:
|
||||
self.storage.setItem(key, value)
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
|
||||
def validate_setting(self, key: str, val: SettingValue) -> bool:
|
||||
if key == "snapshot_throttle_interval":
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
self.logger.error(f"Invalid snapshot throttle interval '{val}' - must be an integer")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def getPictureOptions(self) -> List[ResponsePictureOptions]:
|
||||
return []
|
||||
|
||||
@async_print_exception_guard
|
||||
async def takePicture(self, options: dict = None) -> MediaObject:
|
||||
self.logger.info("Taking picture")
|
||||
|
||||
real_device = await scrypted_sdk.systemManager.api.getDeviceById(self.getScryptedProperty("id"))
|
||||
msos = await real_device.getVideoStreamOptions()
|
||||
if any(["prebuffer" in m for m in msos]):
|
||||
self.logger.info("Getting snapshot from prebuffer")
|
||||
try:
|
||||
return await real_device.getVideoStream({"refresh": False})
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not fetch from prebuffer due to: {e}")
|
||||
self.logger.warning("Will try to fetch snapshot from Arlo cloud")
|
||||
|
||||
async with self.picture_lock:
|
||||
if self.eco_mode and self.snapshot_throttle_interval > 0:
|
||||
if datetime.now() - self.last_picture_time <= timedelta(minutes=self.snapshot_throttle_interval):
|
||||
self.logger.info("Using cached image")
|
||||
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")
|
||||
|
||||
pic_url = await asyncio.wait_for(self.provider.arlo.TriggerFullFrameSnapshot(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
|
||||
self.logger.debug(f"Got snapshot URL for at {pic_url}")
|
||||
|
||||
if pic_url is None:
|
||||
raise Exception("Error taking snapshot")
|
||||
|
||||
async with async_timeout(self.timeout):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(pic_url) as resp:
|
||||
if resp.status != 200:
|
||||
raise Exception(f"Unexpected status downloading snapshot image: {resp.status}")
|
||||
self.last_picture = await resp.read()
|
||||
self.last_picture_time = datetime.now()
|
||||
|
||||
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")
|
||||
|
||||
async def getVideoStreamOptions(self, id: str = None) -> List[ResponseMediaStreamOptions]:
|
||||
options = [
|
||||
{
|
||||
"id": 'default',
|
||||
"name": 'Cloud RTSP',
|
||||
"container": 'rtsp',
|
||||
"video": {
|
||||
"codec": 'h264',
|
||||
},
|
||||
"audio": None if self.arlo_device.get("modelId") == "VMC3030" else {
|
||||
"codec": 'aac',
|
||||
},
|
||||
"source": 'cloud',
|
||||
"tool": 'scrypted',
|
||||
"userConfigurable": False,
|
||||
},
|
||||
{
|
||||
"id": 'dash',
|
||||
"name": 'Cloud DASH',
|
||||
"container": 'dash',
|
||||
"video": {
|
||||
"codec": 'unknown',
|
||||
},
|
||||
"audio": None if self.arlo_device.get("modelId") == "VMC3030" else {
|
||||
"codec": 'unknown',
|
||||
},
|
||||
"source": 'cloud',
|
||||
"tool": 'ffmpeg',
|
||||
"userConfigurable": False,
|
||||
}
|
||||
]
|
||||
|
||||
if id is None:
|
||||
return options
|
||||
|
||||
return next(iter([o for o in options if o['id'] == id]))
|
||||
|
||||
async def _getVideoStreamURL(self, container: str) -> str:
|
||||
self.logger.info(f"Requesting {container} stream")
|
||||
url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device, mode=container), timeout=self.timeout)
|
||||
self.logger.debug(f"Got {container} stream URL at {url}")
|
||||
return url
|
||||
|
||||
@async_print_exception_guard
|
||||
async def getVideoStream(self, options: RequestMediaStreamOptions = None) -> MediaObject:
|
||||
self.logger.debug("Entered getVideoStream")
|
||||
|
||||
mso = await self.getVideoStreamOptions(id=options["id"])
|
||||
mso['refreshAt'] = round(time.time() * 1000) + 30 * 60 * 1000
|
||||
container = mso["container"]
|
||||
|
||||
url = await self._getVideoStreamURL(container)
|
||||
additional_ffmpeg_args = []
|
||||
|
||||
if container == "dash":
|
||||
headers = self.provider.arlo.GetMPDHeaders(url)
|
||||
ffmpeg_headers = '\r\n'.join([
|
||||
f'{k}: {v}'
|
||||
for k, v in headers.items()
|
||||
])
|
||||
additional_ffmpeg_args = ['-headers', ffmpeg_headers+'\r\n']
|
||||
|
||||
ffmpeg_input = {
|
||||
'url': url,
|
||||
'container': container,
|
||||
'mediaStreamOptions': mso,
|
||||
'inputArguments': [
|
||||
'-f', container,
|
||||
*additional_ffmpeg_args,
|
||||
'-i', url,
|
||||
]
|
||||
}
|
||||
return await scrypted_sdk.mediaManager.createFFmpegMediaObject(ffmpeg_input)
|
||||
|
||||
@async_print_exception_guard
|
||||
async def startIntercom(self, media) -> None:
|
||||
self.logger.info("Starting intercom")
|
||||
|
||||
if self.uses_sip_push_to_talk:
|
||||
sip_info = self.provider.arlo.GetSIPInfo()
|
||||
sip_call_info = sip_info["sipCallInfo"]
|
||||
|
||||
ice_servers = [{"url": "stun:stun.l.google.com:19302"}]
|
||||
self.logger.debug(f"Will use ice servers: {[ice['url'] for ice in ice_servers]}")
|
||||
|
||||
ice_servers = scrypted_arlo_go.Slice_webrtc_ICEServer([
|
||||
scrypted_arlo_go.NewWebRTCICEServer(
|
||||
scrypted_arlo_go.go.Slice_string([ice['url']]),
|
||||
ice.get('username', ''),
|
||||
ice.get('credential', '')
|
||||
)
|
||||
for ice in ice_servers
|
||||
])
|
||||
sip_cfg = scrypted_arlo_go.SIPInfo(
|
||||
DeviceID=self.nativeId,
|
||||
CallerURI=f"sip:{sip_call_info['id']}@{sip_call_info['domain']}:{sip_call_info['port']}",
|
||||
CalleeURI=sip_call_info['calleeUri'],
|
||||
Password=sip_call_info['password'],
|
||||
UserAgent="SIP.js/0.20.1",
|
||||
WebsocketURI="wss://livestream-z2-prod.arlo.com:7443",
|
||||
WebsocketOrigin="https://my.arlo.com",
|
||||
WebsocketHeaders=scrypted_arlo_go.HeadersMap({"User-Agent": USER_AGENTS["arlo"]}),
|
||||
)
|
||||
|
||||
self.goSM = scrypted_arlo_go.NewSIPWebRTCManager("Arlo SIP "+self.nativeId, ice_servers, sip_cfg)
|
||||
|
||||
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
|
||||
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
|
||||
audio_port = self.goSM.InitializeAudioRTPListener(scrypted_arlo_go.WebRTCMimeTypeOpus)
|
||||
|
||||
ffmpeg_path = await scrypted_sdk.mediaManager.getFFmpegPath()
|
||||
ffmpeg_args = [
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-analyzeduration", "0",
|
||||
"-fflags", "-nobuffer",
|
||||
"-probesize", "500000",
|
||||
*ffmpeg_params["inputArguments"],
|
||||
"-vn",
|
||||
"-acodec", "libopus",
|
||||
"-f", "rtp",
|
||||
"-flush_packets", "1",
|
||||
f"rtp://localhost:{audio_port}?pkt_size={scrypted_arlo_go.UDP_PACKET_SIZE()}",
|
||||
]
|
||||
self.logger.debug(f"Starting ffmpeg at {ffmpeg_path} with '{' '.join(ffmpeg_args)}'")
|
||||
|
||||
self.intercom_ffmpeg_subprocess = HeartbeatChildProcess("Arlo Subprocess "+self.logger_name, ffmpeg_path, *ffmpeg_args)
|
||||
self.intercom_ffmpeg_subprocess.start()
|
||||
|
||||
self.goSM.Start()
|
||||
else:
|
||||
# we need to do signaling through arlo cloud apis
|
||||
self.intercom_session = ArloCameraIntercomSession(self)
|
||||
await self.intercom_session.initialize_push_to_talk(media)
|
||||
|
||||
self.logger.info("Intercom ready")
|
||||
|
||||
@async_print_exception_guard
|
||||
async def stopIntercom(self) -> None:
|
||||
self.logger.info("Stopping intercom")
|
||||
if self.intercom_session is not None:
|
||||
await self.intercom_session.shutdown()
|
||||
self.intercom_session = None
|
||||
if self.goSM is not None:
|
||||
self.goSM.Close()
|
||||
self.goSM = None
|
||||
|
||||
async def getVideoClip(self, videoId: str) -> MediaObject:
|
||||
self.logger.info(f"Getting video clip {videoId}")
|
||||
|
||||
id_as_time = int(videoId) / 1000.0
|
||||
start = datetime.fromtimestamp(id_as_time) - timedelta(seconds=10)
|
||||
end = datetime.fromtimestamp(id_as_time) + timedelta(seconds=10)
|
||||
|
||||
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
|
||||
for recording in library:
|
||||
if videoId == recording["name"]:
|
||||
return await scrypted_sdk.mediaManager.createMediaObjectFromUrl(recording["presignedContentUrl"])
|
||||
self.logger.warn(f"Clip {videoId} not found")
|
||||
return None
|
||||
|
||||
async def getVideoClipThumbnail(self, thumbnailId: str) -> MediaObject:
|
||||
self.logger.info(f"Getting video clip thumbnail {thumbnailId}")
|
||||
|
||||
id_as_time = int(thumbnailId) / 1000.0
|
||||
start = datetime.fromtimestamp(id_as_time) - timedelta(seconds=10)
|
||||
end = datetime.fromtimestamp(id_as_time) + timedelta(seconds=10)
|
||||
|
||||
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
|
||||
for recording in library:
|
||||
if thumbnailId == recording["name"]:
|
||||
return await scrypted_sdk.mediaManager.createMediaObjectFromUrl(recording["presignedThumbnailUrl"])
|
||||
self.logger.warn(f"Clip thumbnail {thumbnailId} not found")
|
||||
return None
|
||||
|
||||
async def getVideoClips(self, options: VideoClipOptions = None) -> List[VideoClip]:
|
||||
self.logger.info(f"Fetching remote video clips {options}")
|
||||
|
||||
start = datetime.fromtimestamp(options["startTime"] / 1000.0)
|
||||
end = datetime.fromtimestamp(options["endTime"] / 1000.0)
|
||||
|
||||
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
|
||||
clips = []
|
||||
for recording in library:
|
||||
clip = {
|
||||
"duration": recording["mediaDurationSecond"] * 1000.0,
|
||||
"id": recording["name"],
|
||||
"thumbnailId": recording["name"],
|
||||
"videoId": recording["name"],
|
||||
"startTime": recording["utcCreatedDate"],
|
||||
"description": recording["reason"],
|
||||
"resources": {
|
||||
"thumbnail": {
|
||||
"href": recording["presignedThumbnailUrl"],
|
||||
},
|
||||
"video": {
|
||||
"href": recording["presignedContentUrl"],
|
||||
},
|
||||
},
|
||||
}
|
||||
clips.append(clip)
|
||||
|
||||
if options.get("reverseOrder"):
|
||||
clips.reverse()
|
||||
return clips
|
||||
|
||||
@async_print_exception_guard
|
||||
async def removeVideoClips(self, videoClipIds: List[str]) -> None:
|
||||
# Arlo does support deleting, but let's be safe and disable that
|
||||
raise Exception("deleting Arlo video clips is not implemented by this plugin")
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
|
||||
if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight):
|
||||
return self.get_or_create_spotlight_or_floodlight()
|
||||
if nativeId.endswith("vss") and self.has_siren:
|
||||
return self.get_or_create_vss()
|
||||
return None
|
||||
|
||||
def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight:
|
||||
if self.has_spotlight:
|
||||
light_id = f'{self.arlo_device["deviceId"]}.spotlight'
|
||||
if not self.light:
|
||||
self.light = ArloSpotlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
elif self.has_floodlight:
|
||||
light_id = f'{self.arlo_device["deviceId"]}.floodlight'
|
||||
if not self.light:
|
||||
self.light = ArloFloodlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
return self.light
|
||||
|
||||
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
|
||||
if self.has_siren:
|
||||
vss_id = f'{self.arlo_device["deviceId"]}.vss'
|
||||
if not self.vss:
|
||||
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
return self.vss
|
||||
|
||||
|
||||
class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
def __init__(self, camera):
|
||||
super().__init__()
|
||||
self.camera = camera
|
||||
self.logger = camera.logger
|
||||
self.provider = camera.provider
|
||||
self.arlo_device = camera.arlo_device
|
||||
self.arlo_basestation = camera.arlo_basestation
|
||||
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
|
||||
self.arlo_pc = None
|
||||
self.arlo_sdp_answered = False
|
||||
|
||||
self.stop_subscriptions = False
|
||||
self.start_sdp_answer_subscription()
|
||||
self.start_candidate_answer_subscription()
|
||||
|
||||
def __del__(self):
|
||||
self.stop_subscriptions = True
|
||||
self.cancel_pending_tasks()
|
||||
|
||||
def start_sdp_answer_subscription(self):
|
||||
def callback(sdp):
|
||||
if self.arlo_pc and not self.arlo_sdp_answered:
|
||||
if "a=mid:" not in sdp:
|
||||
# arlo appears to not return a mux id in the response, which
|
||||
# doesn't play nicely with our webrtc peers. let's add it
|
||||
sdp += "a=mid:0\r\n"
|
||||
self.logger.info(f"Arlo response sdp:\n{sdp}")
|
||||
|
||||
sdp = scrypted_arlo_go.WebRTCSessionDescription(scrypted_arlo_go.NewWebRTCSDPType("answer"), sdp)
|
||||
self.arlo_pc.SetRemoteDescription(sdp)
|
||||
self.arlo_sdp_answered = True
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToSDPAnswers(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def start_candidate_answer_subscription(self):
|
||||
def callback(candidate):
|
||||
if self.arlo_pc:
|
||||
prefix = "a=candidate:"
|
||||
if candidate.startswith(prefix):
|
||||
candidate = candidate[len(prefix):]
|
||||
candidate = candidate.strip()
|
||||
self.logger.info(f"Arlo response candidate: {candidate}")
|
||||
|
||||
candidate = scrypted_arlo_go.WebRTCICECandidateInit(candidate, "0", 0)
|
||||
self.arlo_pc.AddICECandidate(candidate)
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToCandidateAnswers(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
@async_print_exception_guard
|
||||
async def initialize_push_to_talk(self, media):
|
||||
self.logger.info("Initializing push to talk")
|
||||
|
||||
session_id, ice_servers = self.provider.arlo.StartPushToTalk(self.arlo_basestation, self.arlo_device)
|
||||
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")
|
||||
|
||||
ice_servers = scrypted_arlo_go.Slice_webrtc_ICEServer([
|
||||
scrypted_arlo_go.NewWebRTCICEServer(
|
||||
scrypted_arlo_go.go.Slice_string([ice['url']]),
|
||||
ice.get('username', ''),
|
||||
ice.get('credential', '')
|
||||
)
|
||||
for ice in ice_servers
|
||||
])
|
||||
self.arlo_pc = scrypted_arlo_go.NewWebRTCManager("Arlo WebRTC "+self.camera.logger_name, ice_servers)
|
||||
|
||||
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
|
||||
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
|
||||
audio_port = self.arlo_pc.InitializeAudioRTPListener(scrypted_arlo_go.WebRTCMimeTypeOpus)
|
||||
|
||||
ffmpeg_path = await scrypted_sdk.mediaManager.getFFmpegPath()
|
||||
ffmpeg_args = [
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-analyzeduration", "0",
|
||||
"-fflags", "-nobuffer",
|
||||
"-probesize", "500000",
|
||||
*ffmpeg_params["inputArguments"],
|
||||
"-vn",
|
||||
"-acodec", "libopus",
|
||||
"-f", "rtp",
|
||||
"-flush_packets", "1",
|
||||
f"rtp://localhost:{audio_port}?pkt_size={scrypted_arlo_go.UDP_PACKET_SIZE()}",
|
||||
]
|
||||
self.logger.debug(f"Starting ffmpeg at {ffmpeg_path} with '{' '.join(ffmpeg_args)}'")
|
||||
|
||||
self.intercom_ffmpeg_subprocess = HeartbeatChildProcess("Arlo Subprocess "+self.camera.logger_name, ffmpeg_path, *ffmpeg_args)
|
||||
self.intercom_ffmpeg_subprocess.start()
|
||||
|
||||
self.sdp_answered = False
|
||||
|
||||
offer = self.arlo_pc.CreateOffer()
|
||||
offer_sdp = scrypted_arlo_go.WebRTCSessionDescriptionSDP(offer)
|
||||
self.logger.info(f"Arlo offer sdp:\n{offer_sdp}")
|
||||
|
||||
self.arlo_pc.SetLocalDescription(offer)
|
||||
|
||||
self.provider.arlo.NotifyPushToTalkSDP(
|
||||
self.arlo_basestation, self.arlo_device,
|
||||
session_id, offer_sdp
|
||||
)
|
||||
|
||||
candidates = self.arlo_pc.WaitAndGetICECandidates()
|
||||
self.logger.debug(f"Gathered {len(candidates)} candidates")
|
||||
for candidate in candidates:
|
||||
candidate = scrypted_arlo_go.WebRTCICECandidateInit(
|
||||
scrypted_arlo_go.WebRTCICECandidate(handle=candidate).ToJSON()
|
||||
).Candidate
|
||||
self.logger.debug(f"Sending candidate to Arlo: {candidate}")
|
||||
self.provider.arlo.NotifyPushToTalkCandidate(
|
||||
self.arlo_basestation, self.arlo_device,
|
||||
session_id, candidate,
|
||||
)
|
||||
|
||||
async def shutdown(self):
|
||||
if self.intercom_ffmpeg_subprocess is not None:
|
||||
self.intercom_ffmpeg_subprocess.stop()
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
if self.arlo_pc is not None:
|
||||
self.arlo_pc.Close()
|
||||
self.arlo_pc = None
|
||||
@@ -1,58 +0,0 @@
|
||||
import multiprocessing
|
||||
import subprocess
|
||||
import time
|
||||
import threading
|
||||
|
||||
HEARTBEAT_INTERVAL = 5
|
||||
|
||||
|
||||
def multiprocess_main(name, child_conn, exe, args):
|
||||
print(f"[{name}] Child process starting")
|
||||
sp = subprocess.Popen([exe, *args])
|
||||
|
||||
while True:
|
||||
has_data = child_conn.poll(HEARTBEAT_INTERVAL * 3)
|
||||
if not has_data:
|
||||
break
|
||||
keep_alive = child_conn.recv()
|
||||
if not keep_alive:
|
||||
break
|
||||
|
||||
sp.terminate()
|
||||
sp.wait()
|
||||
print(f"[{name}] Child process exiting")
|
||||
|
||||
|
||||
class HeartbeatChildProcess:
|
||||
"""Class to manage running a child process that gets cleaned up if the parent exits.
|
||||
|
||||
When spawining subprocesses in Python, if the parent is forcibly killed (as is the case
|
||||
when Scrypted restarts plugins), subprocesses get orphaned. This approach uses parent-child
|
||||
heartbeats for the child to ensure that the parent process is still alive, and to cleanly
|
||||
exit the child if the parent has terminated.
|
||||
"""
|
||||
|
||||
def __init__(self, name, exe, *args):
|
||||
self.name = name
|
||||
self.exe = exe
|
||||
self.args = args
|
||||
|
||||
self.parent_conn, self.child_conn = multiprocessing.Pipe()
|
||||
self.process = multiprocessing.Process(target=multiprocess_main, args=(name, self.child_conn, exe, args))
|
||||
self.process.daemon = True
|
||||
self._stop = False
|
||||
|
||||
self.thread = threading.Thread(target=self.heartbeat)
|
||||
|
||||
def start(self):
|
||||
self.process.start()
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._stop = True
|
||||
self.parent_conn.send(False)
|
||||
|
||||
def heartbeat(self):
|
||||
while not self._stop:
|
||||
time.sleep(HEARTBEAT_INTERVAL)
|
||||
self.parent_conn.send(True)
|
||||
@@ -1,34 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from scrypted_sdk.types import BinarySensor, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .camera import ArloCamera
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloDoorbell(ArloCamera, BinarySensor):
|
||||
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
|
||||
self.start_doorbell_subscription()
|
||||
|
||||
def start_doorbell_subscription(self) -> None:
|
||||
def callback(doorbellPressed):
|
||||
self.binaryState = doorbellPressed
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToDoorbellEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
return ScryptedDeviceType.Doorbell.value
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
camera_interfaces = super().get_applicable_interfaces()
|
||||
camera_interfaces.append(ScryptedInterface.BinarySensor.value)
|
||||
return camera_interfaces
|
||||
@@ -1 +0,0 @@
|
||||
EXPERIMENTAL = False
|
||||
@@ -1,43 +0,0 @@
|
||||
import logging
|
||||
|
||||
|
||||
class ScryptedDeviceLoggingWrapper(logging.Handler):
|
||||
scrypted_device = None
|
||||
|
||||
def __init__(self, scrypted_device):
|
||||
super().__init__()
|
||||
self.scrypted_device = scrypted_device
|
||||
|
||||
def emit(self, record):
|
||||
self.scrypted_device.print(self.format(record))
|
||||
|
||||
|
||||
def createScryptedLogger(scrypted_device, name):
|
||||
logger = logging.getLogger(name)
|
||||
if logger.hasHandlers():
|
||||
return logger
|
||||
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# configure logger to output to scrypted's log stream
|
||||
sh = ScryptedDeviceLoggingWrapper(scrypted_device)
|
||||
|
||||
# log formatting
|
||||
fmt = logging.Formatter("[Arlo %(name)s] %(message)s")
|
||||
sh.setFormatter(fmt)
|
||||
|
||||
# configure handler to logger
|
||||
logger.addHandler(sh)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
class ScryptedDeviceLoggerMixin:
|
||||
_logger = None
|
||||
logger_name = None
|
||||
|
||||
@property
|
||||
def logger(self):
|
||||
if self._logger is None:
|
||||
self._logger = createScryptedLogger(self, self.logger_name)
|
||||
return self._logger
|
||||
@@ -1,703 +0,0 @@
|
||||
import asyncio
|
||||
import email
|
||||
import imaplib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
import traceback
|
||||
from typing import List
|
||||
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk import ScryptedDeviceBase
|
||||
from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, ScryptedInterface
|
||||
|
||||
from .arlo import Arlo
|
||||
from .arlo.arlo_async import change_stream_class
|
||||
from .arlo.logging import logger as arlo_lib_logger
|
||||
from .logging import ScryptedDeviceLoggerMixin
|
||||
from .util import BackgroundTaskMixin, async_print_exception_guard
|
||||
from .camera import ArloCamera
|
||||
from .doorbell import ArloDoorbell
|
||||
from .basestation import ArloBasestation
|
||||
from .base import ArloDeviceBase
|
||||
|
||||
|
||||
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
|
||||
arlo_cameras = None
|
||||
arlo_basestations = None
|
||||
_arlo_mfa_code = None
|
||||
scrypted_devices = None
|
||||
_arlo = None
|
||||
_arlo_mfa_complete_auth = None
|
||||
device_discovery_lock: asyncio.Lock = None
|
||||
|
||||
plugin_verbosity_choices = {
|
||||
"Normal": logging.INFO,
|
||||
"Verbose": logging.DEBUG
|
||||
}
|
||||
|
||||
arlo_transport_choices = ["MQTT", "SSE"]
|
||||
|
||||
mfa_strategy_choices = ["Manual", "IMAP"]
|
||||
|
||||
def __init__(self, nativeId: str = None) -> None:
|
||||
super().__init__(nativeId=nativeId)
|
||||
self.logger_name = "provider"
|
||||
|
||||
self.arlo_cameras = {}
|
||||
self.arlo_basestations = {}
|
||||
self.scrypted_devices = {}
|
||||
self.imap = None
|
||||
self.imap_signal = None
|
||||
self.imap_skip_emails = None
|
||||
self.device_discovery_lock = asyncio.Lock()
|
||||
|
||||
self.propagate_verbosity()
|
||||
self.propagate_transport()
|
||||
|
||||
def load(self):
|
||||
if self.mfa_strategy == "IMAP":
|
||||
self.initialize_imap()
|
||||
else:
|
||||
_ = self.arlo
|
||||
|
||||
asyncio.get_event_loop().call_soon(load, self)
|
||||
self.create_task(self.onDeviceEvent(ScryptedInterface.Settings.value, None))
|
||||
|
||||
def print(self, *args, **kwargs) -> None:
|
||||
"""Overrides the print() from ScryptedDeviceBase to avoid double-printing in the main plugin console."""
|
||||
print(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def arlo_username(self) -> str:
|
||||
return self.storage.getItem("arlo_username")
|
||||
|
||||
@property
|
||||
def arlo_password(self) -> str:
|
||||
return self.storage.getItem("arlo_password")
|
||||
|
||||
@property
|
||||
def arlo_auth_headers(self) -> str:
|
||||
return self.storage.getItem("arlo_auth_headers")
|
||||
|
||||
@property
|
||||
def arlo_user_id(self) -> str:
|
||||
return self.storage.getItem("arlo_user_id")
|
||||
|
||||
@property
|
||||
def arlo_transport(self) -> str:
|
||||
return "SSE"
|
||||
# This code is here for posterity, however it looks that as of 06/01/2023
|
||||
# Arlo has disabled the MQTT backend
|
||||
transport = self.storage.getItem("arlo_transport")
|
||||
if transport is None or transport not in ArloProvider.arlo_transport_choices:
|
||||
transport = "SSE"
|
||||
self.storage.setItem("arlo_transport", transport)
|
||||
return transport
|
||||
|
||||
@property
|
||||
def plugin_verbosity(self) -> str:
|
||||
verbosity = self.storage.getItem("plugin_verbosity")
|
||||
if verbosity is None or verbosity not in ArloProvider.plugin_verbosity_choices:
|
||||
verbosity = "Normal"
|
||||
self.storage.setItem("plugin_verbosity", verbosity)
|
||||
return verbosity
|
||||
|
||||
@property
|
||||
def mfa_strategy(self) -> str:
|
||||
strategy = self.storage.getItem("mfa_strategy")
|
||||
if strategy is None or strategy not in ArloProvider.mfa_strategy_choices:
|
||||
strategy = "Manual"
|
||||
self.storage.setItem("mfa_strategy", strategy)
|
||||
return strategy
|
||||
|
||||
@property
|
||||
def refresh_interval(self) -> int:
|
||||
interval = self.storage.getItem("refresh_interval")
|
||||
if interval is None:
|
||||
interval = 90
|
||||
self.storage.setItem("refresh_interval", interval)
|
||||
return int(interval)
|
||||
|
||||
@property
|
||||
def imap_mfa_host(self) -> str:
|
||||
return self.storage.getItem("imap_mfa_host")
|
||||
|
||||
@property
|
||||
def imap_mfa_port(self) -> int:
|
||||
port = self.storage.getItem("imap_mfa_port")
|
||||
if port is None:
|
||||
port = 993
|
||||
self.storage.setItem("imap_mfa_port", port)
|
||||
return int(port)
|
||||
|
||||
@property
|
||||
def imap_mfa_username(self) -> str:
|
||||
return self.storage.getItem("imap_mfa_username")
|
||||
|
||||
@property
|
||||
def imap_mfa_password(self) -> str:
|
||||
return self.storage.getItem("imap_mfa_password")
|
||||
|
||||
@property
|
||||
def imap_mfa_interval(self) -> int:
|
||||
interval = self.storage.getItem("imap_mfa_interval")
|
||||
if interval is None:
|
||||
interval = 7
|
||||
self.storage.setItem("imap_mfa_interval", interval)
|
||||
return int(interval)
|
||||
|
||||
@property
|
||||
def arlo(self) -> Arlo:
|
||||
if self._arlo is not None:
|
||||
if self._arlo_mfa_complete_auth is not None:
|
||||
if not self._arlo_mfa_code:
|
||||
return None
|
||||
|
||||
self.logger.info("Completing Arlo MFA...")
|
||||
try:
|
||||
self._arlo_mfa_complete_auth(self._arlo_mfa_code)
|
||||
finally:
|
||||
self._arlo_mfa_complete_auth = None
|
||||
self._arlo_mfa_code = None
|
||||
self.logger.info("Arlo MFA done")
|
||||
|
||||
self.storage.setItem("arlo_auth_headers", json.dumps(dict(self._arlo.request.session.headers.items())))
|
||||
self.storage.setItem("arlo_user_id", self._arlo.user_id)
|
||||
|
||||
self.create_task(self.do_arlo_setup())
|
||||
|
||||
return self._arlo
|
||||
|
||||
if not self.arlo_username or not self.arlo_password:
|
||||
return None
|
||||
|
||||
self.logger.info("Trying to initialize Arlo client...")
|
||||
try:
|
||||
self._arlo = Arlo(self.arlo_username, self.arlo_password)
|
||||
headers = self.arlo_auth_headers
|
||||
if headers:
|
||||
self._arlo.UseExistingAuth(self.arlo_user_id, json.loads(headers))
|
||||
self.logger.info(f"Initialized Arlo client, reusing stored auth headers")
|
||||
self.create_task(self.do_arlo_setup())
|
||||
return self._arlo
|
||||
else:
|
||||
self._arlo_mfa_complete_auth = self._arlo.LoginMFA()
|
||||
self.logger.info(f"Initialized Arlo client, waiting for MFA code")
|
||||
return None
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self._arlo = None
|
||||
self._arlo_mfa_complete_auth = None
|
||||
self._arlo_mfa_code = None
|
||||
return None
|
||||
|
||||
async def do_arlo_setup(self) -> None:
|
||||
try:
|
||||
await self.discover_devices()
|
||||
await self.arlo.Subscribe([
|
||||
(self.arlo_basestations[camera["parentId"]], camera) for camera in self.arlo_cameras.values()
|
||||
])
|
||||
|
||||
self.arlo.event_stream.set_refresh_interval(self.refresh_interval)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
traceback.print_exc()
|
||||
self.logger.error(f"Error logging in, will retry with fresh login")
|
||||
self._arlo = None
|
||||
self._arlo_mfa_code = None
|
||||
self.storage.setItem("arlo_auth_headers", None)
|
||||
_ = self.arlo
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
|
||||
def invalidate_arlo_client(self) -> None:
|
||||
if self._arlo is not None:
|
||||
self._arlo.Unsubscribe()
|
||||
self._arlo = None
|
||||
self._arlo_mfa_code = None
|
||||
self._arlo_mfa_complete_auth = None
|
||||
self.storage.setItem("arlo_auth_headers", "")
|
||||
self.storage.setItem("arlo_user_id", "")
|
||||
|
||||
def get_current_log_level(self) -> int:
|
||||
return ArloProvider.plugin_verbosity_choices[self.plugin_verbosity]
|
||||
|
||||
def propagate_verbosity(self) -> None:
|
||||
self.print(f"Setting plugin verbosity to {self.plugin_verbosity}")
|
||||
log_level = self.get_current_log_level()
|
||||
self.logger.setLevel(log_level)
|
||||
for _, device in self.scrypted_devices.items():
|
||||
device.logger.setLevel(log_level)
|
||||
arlo_lib_logger.setLevel(log_level)
|
||||
|
||||
def propagate_transport(self) -> None:
|
||||
self.print(f"Setting plugin transport to {self.arlo_transport}")
|
||||
change_stream_class(self.arlo_transport)
|
||||
|
||||
def initialize_imap(self) -> None:
|
||||
if not self.imap_mfa_host or not self.imap_mfa_port or \
|
||||
not self.imap_mfa_username or not self.imap_mfa_password or \
|
||||
not self.imap_mfa_interval:
|
||||
return
|
||||
|
||||
self.exit_imap()
|
||||
try:
|
||||
self.logger.info("Trying connect to IMAP")
|
||||
self.imap = imaplib.IMAP4_SSL(self.imap_mfa_host, port=self.imap_mfa_port)
|
||||
|
||||
res, _ = self.imap.login(self.imap_mfa_username, self.imap_mfa_password)
|
||||
if res.lower() != "ok":
|
||||
raise Exception(f"IMAP login failed: {res}")
|
||||
res, _ = self.imap.select(mailbox="INBOX", readonly=True)
|
||||
if res.lower() != "ok":
|
||||
raise Exception(f"IMAP failed to fetch INBOX: {res}")
|
||||
|
||||
# fetch existing arlo emails so we skip them going forward
|
||||
res, self.imap_skip_emails = self.imap.search(None, "FROM", "do_not_reply@arlo.com")
|
||||
if res.lower() != "ok":
|
||||
raise Exception(f"IMAP failed to fetch old Arlo emails: {res}")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.exit_imap()
|
||||
else:
|
||||
self.logger.info("Connected to IMAP")
|
||||
self.imap_signal = asyncio.Queue()
|
||||
self.create_task(self.imap_relogin_loop())
|
||||
|
||||
def exit_imap(self) -> None:
|
||||
if self.imap_signal:
|
||||
self.imap_signal.put_nowait(None)
|
||||
self.imap_signal = None
|
||||
self.imap_skip_emails = None
|
||||
self.imap = None
|
||||
|
||||
async def imap_relogin_loop(self) -> None:
|
||||
imap_signal = self.imap_signal
|
||||
self.logger.info(f"Starting IMAP refresh loop {id(imap_signal)}")
|
||||
while True:
|
||||
self.logger.info("Performing IMAP login flow")
|
||||
|
||||
# save old client and details in case of error
|
||||
old_arlo = self._arlo
|
||||
old_headers = self.storage.getItem("arlo_auth_headers")
|
||||
old_user_id = self.storage.getItem("arlo_user_id")
|
||||
|
||||
# clear everything
|
||||
self._arlo = None
|
||||
self._arlo_mfa_code = None
|
||||
self._arlo_mfa_complete_auth = None
|
||||
self.storage.setItem("arlo_auth_headers", "")
|
||||
self.storage.setItem("arlo_user_id", "")
|
||||
|
||||
# initialize login and prompt for MFA
|
||||
_ = self.arlo
|
||||
|
||||
# do imap lookup
|
||||
# adapted from https://github.com/twrecked/pyaarlo/blob/77c202b6f789c7104a024f855a12a3df4fc8df38/pyaarlo/tfa.py
|
||||
try:
|
||||
while True:
|
||||
self.logger.info("Checking IMAP for MFA codes")
|
||||
|
||||
self.imap.check()
|
||||
res, emails = self.imap.search(None, "FROM", "do_not_reply@arlo.com")
|
||||
if res.lower() != "ok":
|
||||
raise Exception("IMAP error: {res}")
|
||||
|
||||
if emails == self.imap_skip_emails:
|
||||
self.logger.info("No new emails found, will sleep and retry")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
skip_emails = self.imap_skip_emails[0].split()
|
||||
def search_email(msg_id):
|
||||
if msg_id in skip_emails:
|
||||
return None
|
||||
|
||||
res, msg = self.imap.fetch(msg_id, "(BODY.PEEK[])")
|
||||
if res.lower() != "ok":
|
||||
raise Exception("IMAP error: {res}")
|
||||
|
||||
if isinstance(msg[0][1], bytes):
|
||||
for part in email.message_from_bytes(msg[0][1]).walk():
|
||||
if part.get_content_type() != "text/html":
|
||||
continue
|
||||
try:
|
||||
for line in part.get_payload(decode=True).splitlines():
|
||||
code = re.match(r"^\W+(\d{6})\W*$", line.decode())
|
||||
if code is not None:
|
||||
return code.group(1)
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
for msg_id in emails[0].split():
|
||||
res = search_email(msg_id)
|
||||
if res is not None:
|
||||
self._arlo_mfa_code = res
|
||||
break
|
||||
|
||||
# update previously seen emails list
|
||||
self.imap_skip_emails = emails
|
||||
|
||||
if self._arlo_mfa_code is not None:
|
||||
self.logger.info("Found MFA code")
|
||||
break
|
||||
|
||||
self.logger.info("No MFA code found, will sleep and retry")
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.error("Will retry on next IMAP interval")
|
||||
self._arlo = old_arlo
|
||||
self.storage.setItem("arlo_auth_headers", old_headers)
|
||||
self.storage.setItem("arlo_user_id", old_user_id)
|
||||
self._arlo_mfa_code = None
|
||||
self._arlo_mfa_complete_auth = None
|
||||
else:
|
||||
# finish login
|
||||
if old_arlo:
|
||||
old_arlo.Unsubscribe()
|
||||
_ = self.arlo
|
||||
|
||||
# continue by sleeping/waiting for a signal
|
||||
interval = self.imap_mfa_interval * 24 * 60 * 60 # convert interval days to seconds
|
||||
signal_task = asyncio.create_task(imap_signal.get())
|
||||
|
||||
# wait until either we receive a signal or the refresh interval expires
|
||||
done, pending = await asyncio.wait([signal_task, asyncio.sleep(interval)], return_when=asyncio.FIRST_COMPLETED)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
done_task = done.pop()
|
||||
if done_task is signal_task and done_task.result() is None:
|
||||
# exit signal received
|
||||
self.logger.info(f"Exiting IMAP refresh loop {id(imap_signal)}")
|
||||
return
|
||||
|
||||
async def getSettings(self) -> List[Setting]:
|
||||
results = [
|
||||
{
|
||||
"group": "General",
|
||||
"key": "arlo_username",
|
||||
"title": "Arlo Username",
|
||||
"value": self.arlo_username,
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "arlo_password",
|
||||
"title": "Arlo Password",
|
||||
"type": "password",
|
||||
"value": self.arlo_password,
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "mfa_strategy",
|
||||
"title": "Two Factor Strategy",
|
||||
"description": "Mechanism to fetch the two factor code for Arlo login. Save after changing this field for more settings.",
|
||||
"value": self.mfa_strategy,
|
||||
"choices": self.mfa_strategy_choices,
|
||||
},
|
||||
]
|
||||
|
||||
if self.mfa_strategy == "Manual":
|
||||
results.extend([
|
||||
{
|
||||
"group": "General",
|
||||
"key": "arlo_mfa_code",
|
||||
"title": "Two Factor Code",
|
||||
"description": "Enter the code sent by Arlo to your email or phone number.",
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "force_reauth",
|
||||
"title": "Force Re-Authentication",
|
||||
"description": "Resets the authentication flow of the plugin. Will also re-do 2FA.",
|
||||
"value": False,
|
||||
"type": "boolean",
|
||||
},
|
||||
])
|
||||
else:
|
||||
results.extend([
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_host",
|
||||
"title": "IMAP Hostname",
|
||||
"value": self.imap_mfa_host,
|
||||
},
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_port",
|
||||
"title": "IMAP Port",
|
||||
"value": self.imap_mfa_port,
|
||||
},
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_username",
|
||||
"title": "IMAP Username",
|
||||
"value": self.imap_mfa_username,
|
||||
},
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_password",
|
||||
"title": "IMAP Password",
|
||||
"type": "password",
|
||||
"value": self.imap_mfa_password,
|
||||
},
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_interval",
|
||||
"title": "Refresh Login Interval",
|
||||
"description": "Interval, in days, to refresh the login session to Arlo Cloud. "
|
||||
"Must be a value greater than 0.",
|
||||
"type": "number",
|
||||
"value": self.imap_mfa_interval,
|
||||
}
|
||||
])
|
||||
|
||||
results.extend([
|
||||
{
|
||||
"group": "General",
|
||||
"key": "arlo_transport",
|
||||
"title": "Underlying Transport Protocol",
|
||||
"description": "Arlo Cloud currently only supports the SSE protocol.",
|
||||
"value": self.arlo_transport,
|
||||
"readonly": True,
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "refresh_interval",
|
||||
"title": "Refresh Event Stream Interval",
|
||||
"description": "Interval, in minutes, to refresh the underlying event stream connection to Arlo Cloud. "
|
||||
"A value of 0 disables this feature.",
|
||||
"type": "number",
|
||||
"value": self.refresh_interval,
|
||||
},
|
||||
{
|
||||
"group": "General",
|
||||
"key": "plugin_verbosity",
|
||||
"title": "Verbose Logging",
|
||||
"description": "Enable this option to show debug messages, including events received from connected Arlo cameras.",
|
||||
"value": self.plugin_verbosity == "Verbose",
|
||||
"type": "boolean",
|
||||
},
|
||||
])
|
||||
|
||||
return results
|
||||
|
||||
@async_print_exception_guard
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
if not self.validate_setting(key, value):
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
return
|
||||
|
||||
skip_arlo_client = False
|
||||
if key == "arlo_mfa_code":
|
||||
self._arlo_mfa_code = value
|
||||
elif key == "force_reauth":
|
||||
# force arlo client to be invalidated and reloaded
|
||||
self.invalidate_arlo_client()
|
||||
elif key == "plugin_verbosity":
|
||||
self.storage.setItem(key, "Verbose" if value == "true" or value == True else "Normal")
|
||||
self.propagate_verbosity()
|
||||
skip_arlo_client = True
|
||||
else:
|
||||
self.storage.setItem(key, value)
|
||||
|
||||
if key == "arlo_transport":
|
||||
self.propagate_transport()
|
||||
# force arlo client to be invalidated and reloaded, but
|
||||
# keep any mfa codes
|
||||
if self._arlo is not None:
|
||||
self._arlo.Unsubscribe()
|
||||
self._arlo = None
|
||||
elif key == "mfa_strategy":
|
||||
if value == "IMAP":
|
||||
self.initialize_imap()
|
||||
else:
|
||||
self.exit_imap()
|
||||
skip_arlo_client = True
|
||||
elif key == "refresh_interval":
|
||||
if self._arlo is not None and self._arlo.event_stream:
|
||||
self._arlo.event_stream.set_refresh_interval(self.refresh_interval)
|
||||
skip_arlo_client = True
|
||||
elif key.startswith("imap_mfa"):
|
||||
self.initialize_imap()
|
||||
skip_arlo_client = True
|
||||
else:
|
||||
# force arlo client to be invalidated and reloaded
|
||||
self.invalidate_arlo_client()
|
||||
|
||||
if not skip_arlo_client:
|
||||
# initialize Arlo client or continue MFA
|
||||
_ = self.arlo
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
|
||||
def validate_setting(self, key: str, val: SettingValue) -> bool:
|
||||
if key == "refresh_interval":
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
self.logger.error(f"Invalid refresh interval '{val}' - must be an integer")
|
||||
return False
|
||||
if val < 0:
|
||||
self.logger.error(f"Invalid refresh interval '{val}' - must be nonnegative")
|
||||
return False
|
||||
elif key == "imap_mfa_port":
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
self.logger.error(f"Invalid IMAP port '{val}' - must be an integer")
|
||||
return False
|
||||
if val < 0:
|
||||
self.logger.error(f"Invalid IMAP port '{val}' - must be nonnegative")
|
||||
return False
|
||||
elif key == "imap_mfa_interval":
|
||||
try:
|
||||
val = int(val)
|
||||
except ValueError:
|
||||
self.logger.error(f"Invalid IMAP interval '{val}' - must be an integer")
|
||||
return False
|
||||
if val < 1:
|
||||
self.logger.error(f"Invalid IMAP interval '{val}' - must be positive")
|
||||
return False
|
||||
return True
|
||||
|
||||
@async_print_exception_guard
|
||||
async def discover_devices(self) -> None:
|
||||
async with self.device_discovery_lock:
|
||||
return await self.discover_devices_impl()
|
||||
|
||||
async def discover_devices_impl(self) -> None:
|
||||
if not self.arlo:
|
||||
raise Exception("Arlo client not connected, cannot discover devices")
|
||||
|
||||
self.logger.info("Discovering devices...")
|
||||
self.arlo_cameras = {}
|
||||
self.arlo_basestations = {}
|
||||
self.scrypted_devices = {}
|
||||
|
||||
camera_devices = []
|
||||
provider_to_device_map = {None: []}
|
||||
|
||||
basestations = self.arlo.GetDevices(['basestation', 'siren'])
|
||||
for basestation in basestations:
|
||||
nativeId = basestation["deviceId"]
|
||||
self.logger.debug(f"Adding {nativeId}")
|
||||
|
||||
if nativeId in self.arlo_basestations:
|
||||
self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added")
|
||||
continue
|
||||
self.arlo_basestations[nativeId] = basestation
|
||||
|
||||
device = await self.getDevice_impl(nativeId)
|
||||
scrypted_interfaces = device.get_applicable_interfaces()
|
||||
manifest = device.get_device_manifest()
|
||||
self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}")
|
||||
|
||||
# for basestations, we want to add them to the top level DeviceProvider
|
||||
provider_to_device_map.setdefault(None, []).append(manifest)
|
||||
|
||||
# we want to trickle discover them so they are added without deleting all existing
|
||||
# root level devices - this is for backward compatibility
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
|
||||
|
||||
# add any builtin child devices and trickle discover them
|
||||
child_manifests = device.get_builtin_child_device_manifests()
|
||||
for child_manifest in child_manifests:
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
|
||||
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
|
||||
|
||||
self.logger.info(f"Discovered {len(basestations)} basestations")
|
||||
|
||||
cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"])
|
||||
for camera in cameras:
|
||||
nativeId = camera["deviceId"]
|
||||
self.logger.debug(f"Adding {nativeId}")
|
||||
|
||||
if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations:
|
||||
self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because its basestation was not found")
|
||||
continue
|
||||
|
||||
if nativeId in self.arlo_cameras:
|
||||
self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added")
|
||||
continue
|
||||
self.arlo_cameras[nativeId] = camera
|
||||
|
||||
if camera["deviceId"] == camera["parentId"]:
|
||||
# these are standalone cameras with no basestation, so they act as their
|
||||
# own basestation
|
||||
self.arlo_basestations[camera["deviceId"]] = camera
|
||||
|
||||
device = await self.getDevice_impl(nativeId)
|
||||
scrypted_interfaces = device.get_applicable_interfaces()
|
||||
manifest = device.get_device_manifest()
|
||||
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']} parent {camera['parentId']}): {scrypted_interfaces}")
|
||||
|
||||
if camera["deviceId"] == camera["parentId"]:
|
||||
provider_to_device_map.setdefault(None, []).append(manifest)
|
||||
else:
|
||||
provider_to_device_map.setdefault(camera["parentId"], []).append(manifest)
|
||||
|
||||
# trickle discover this camera so it exists for later steps
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
|
||||
|
||||
# add any builtin child devices and trickle discover them
|
||||
child_manifests = device.get_builtin_child_device_manifests()
|
||||
for child_manifest in child_manifests:
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
|
||||
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
|
||||
|
||||
camera_devices.append(manifest)
|
||||
|
||||
if len(cameras) != len(camera_devices):
|
||||
self.logger.info(f"Discovered {len(cameras)} cameras, but only {len(camera_devices)} are usable")
|
||||
self.logger.info(f"Are all cameras shared with admin permissions?")
|
||||
else:
|
||||
self.logger.info(f"Discovered {len(cameras)} cameras")
|
||||
|
||||
for provider_id in provider_to_device_map.keys():
|
||||
if provider_id is None:
|
||||
continue
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged({
|
||||
"devices": provider_to_device_map[provider_id],
|
||||
"providerNativeId": provider_id,
|
||||
})
|
||||
|
||||
# ensure devices at the root match all that was discovered
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged({
|
||||
"devices": provider_to_device_map[None]
|
||||
})
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
|
||||
async with self.device_discovery_lock:
|
||||
return await self.getDevice_impl(nativeId)
|
||||
|
||||
async def getDevice_impl(self, nativeId: str) -> ArloDeviceBase:
|
||||
ret = self.scrypted_devices.get(nativeId, None)
|
||||
if ret is None:
|
||||
ret = self.create_device(nativeId)
|
||||
if ret is not None:
|
||||
self.scrypted_devices[nativeId] = ret
|
||||
return ret
|
||||
|
||||
def create_device(self, nativeId: str) -> ArloDeviceBase:
|
||||
if nativeId not in self.arlo_cameras and nativeId not in self.arlo_basestations:
|
||||
self.logger.warning(f"Cannot create device for nativeId {nativeId}, maybe it hasn't been loaded yet?")
|
||||
return None
|
||||
|
||||
arlo_device = self.arlo_cameras.get(nativeId)
|
||||
if not arlo_device:
|
||||
# this is a basestation, so build the basestation object
|
||||
arlo_device = self.arlo_basestations[nativeId]
|
||||
return ArloBasestation(nativeId, arlo_device, self)
|
||||
|
||||
if arlo_device["parentId"] not in self.arlo_basestations:
|
||||
self.logger.warning(f"Cannot create camera with nativeId {nativeId} when {arlo_device['parentId']} is not a valid basestation")
|
||||
return None
|
||||
arlo_basestation = self.arlo_basestations[arlo_device["parentId"]]
|
||||
|
||||
if arlo_device["deviceType"] == "doorbell":
|
||||
return ArloDoorbell(nativeId, arlo_device, arlo_basestation, self)
|
||||
else:
|
||||
return ArloCamera(nativeId, arlo_device, arlo_basestation, self)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user