Compare commits
704 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33cacd15d | ||
|
|
e225b746c4 | ||
|
|
1e1fda6b9a | ||
|
|
e23c9c22dc | ||
|
|
28f1ce9d4e | ||
|
|
5f4e2793ff | ||
|
|
1e1755fa7e | ||
|
|
ebd56b86e4 | ||
|
|
4607bec07c | ||
|
|
a26cdfea2a | ||
|
|
6d0da449ad | ||
|
|
ad15fe3324 | ||
|
|
fbe3e83884 | ||
|
|
0d4cf34930 | ||
|
|
3a3e15cd74 | ||
|
|
8b9cfebbfa | ||
|
|
b3087058a7 | ||
|
|
4e923a78da | ||
|
|
b5593d6251 | ||
|
|
40b7b621a0 | ||
|
|
7104ad6378 | ||
|
|
51f893ef63 | ||
|
|
29c5dfd73b | ||
|
|
1615f79a0b | ||
|
|
dc5a6126b9 | ||
|
|
0fbe3e4686 | ||
|
|
c9641568f8 | ||
|
|
dceea38eb8 | ||
|
|
cd1fce71e2 | ||
|
|
3b6454f107 | ||
|
|
d238d8d4ba | ||
|
|
a24d46f3d2 | ||
|
|
df7deef4aa | ||
|
|
e94cea0236 | ||
|
|
4794a6dbf3 | ||
|
|
57439634e5 | ||
|
|
89e6e50b12 | ||
|
|
c21ef069bd | ||
|
|
5d41bb38da | ||
|
|
b289024083 | ||
|
|
56572bcec9 | ||
|
|
3e78209817 | ||
|
|
299204313f | ||
|
|
88b134f4b9 | ||
|
|
e8bd72a329 | ||
|
|
214dbc8153 | ||
|
|
3f0c706154 | ||
|
|
013131e816 | ||
|
|
0342bf91f6 | ||
|
|
6fb98c7e84 | ||
|
|
f4168ff4eb | ||
|
|
a5febd7ca0 | ||
|
|
31428b4c28 | ||
|
|
3e0dfc6bda | ||
|
|
4290eb0abb | ||
|
|
fd2d7e9485 | ||
|
|
a57cf3b1e6 | ||
|
|
fad485c0d7 | ||
|
|
9e3cf83b07 | ||
|
|
ebe0b6ea7f | ||
|
|
4ce0ecaaa2 | ||
|
|
44264fb50b | ||
|
|
ab188bfe80 | ||
|
|
83d32da7f1 | ||
|
|
e68b3f401f | ||
|
|
43a73c6d89 | ||
|
|
6c6613d841 | ||
|
|
479ef136a6 | ||
|
|
b42abf377b | ||
|
|
5713935ccc | ||
|
|
09fc609c7f | ||
|
|
e44ba222b8 | ||
|
|
e34a5a7c3d | ||
|
|
e490225c4a | ||
|
|
e769e8ea98 | ||
|
|
ec5c164552 | ||
|
|
64a213424d | ||
|
|
e81c454c1e | ||
|
|
776307bcbc | ||
|
|
95c97e3eb2 | ||
|
|
08926a35a9 | ||
|
|
c037548ffb | ||
|
|
462189efc2 | ||
|
|
a4fed9c7ad | ||
|
|
3c8f94ab2f | ||
|
|
fe3391c89c | ||
|
|
270b43bed6 | ||
|
|
c0fe12827f | ||
|
|
3bbda53107 | ||
|
|
0d7ee00485 | ||
|
|
c605c3ddb5 | ||
|
|
099ba4f081 | ||
|
|
c14487ac27 | ||
|
|
991c31dda8 | ||
|
|
3865efd1f7 | ||
|
|
9c5ce45c1e | ||
|
|
4ab7bc1298 | ||
|
|
0ebbc5ea8f | ||
|
|
86dcb66e6a | ||
|
|
11831e5d87 | ||
|
|
6b3dc8c1ae | ||
|
|
a49256f073 | ||
|
|
ea408377ec | ||
|
|
2da762dfc2 | ||
|
|
bfbc6ba6ce | ||
|
|
c2747c80dc | ||
|
|
2b58de196e | ||
|
|
731744afbc | ||
|
|
f4d88493b1 | ||
|
|
265fc4b481 | ||
|
|
40a300cff1 | ||
|
|
c410907c58 | ||
|
|
199f333fc1 | ||
|
|
e39bc8c5e6 | ||
|
|
a55099de12 | ||
|
|
17900f0589 | ||
|
|
075d8bc4ab | ||
|
|
28fce1bb8b | ||
|
|
14a2790825 | ||
|
|
c2cd491c96 | ||
|
|
6301089620 | ||
|
|
c591a87015 | ||
|
|
544700ac12 | ||
|
|
2c54dc07ae | ||
|
|
0dad6eaa76 | ||
|
|
d18b8e0694 | ||
|
|
a4130ed047 | ||
|
|
1f89dcb34c | ||
|
|
e86ed47533 | ||
|
|
5fb75e351d | ||
|
|
8ef3fe7a24 | ||
|
|
5c8f034d7c | ||
|
|
561852bc15 | ||
|
|
d14c592d55 | ||
|
|
bc439d6b7c | ||
|
|
aedb4212fe | ||
|
|
7b780a0eb9 | ||
|
|
6aaaccaece | ||
|
|
0e42b71e4b | ||
|
|
71a6f9d6a6 | ||
|
|
68c77aaca4 | ||
|
|
4b73c168b3 | ||
|
|
8f4b67dc5c | ||
|
|
8a8bee33c1 | ||
|
|
2b353cf4c8 | ||
|
|
6d9cd45936 | ||
|
|
0bc6fadb23 | ||
|
|
407f573a29 | ||
|
|
39674ef9b6 | ||
|
|
ffaed01dc3 | ||
|
|
9a144a9a05 | ||
|
|
76e63120f0 | ||
|
|
154bc6fef7 | ||
|
|
8302c564c2 | ||
|
|
affda9e94f | ||
|
|
30148f453c | ||
|
|
259313c454 | ||
|
|
4f82d49f15 | ||
|
|
a0b7fc54de | ||
|
|
5501093ff9 | ||
|
|
b2622d92f2 | ||
|
|
957bf742d9 | ||
|
|
b06a3ac55f | ||
|
|
9c195594ea | ||
|
|
d32efd6500 | ||
|
|
03f1957739 | ||
|
|
94f564a218 | ||
|
|
286aa61a18 | ||
|
|
43d30a91f9 | ||
|
|
7be81ab7e2 | ||
|
|
31c7c9d86a | ||
|
|
820c66311d | ||
|
|
84cbe28a47 | ||
|
|
cad1cdd5ce | ||
|
|
2a624a95e5 | ||
|
|
c6d8333402 | ||
|
|
96dbb1776c | ||
|
|
b43a002650 | ||
|
|
a58d66d484 | ||
|
|
bcc9be62e9 | ||
|
|
38dd9e2ee2 | ||
|
|
3033cd9626 | ||
|
|
12273b7d1e | ||
|
|
afe1421c39 | ||
|
|
a9fdb71402 | ||
|
|
9cac4cfd18 | ||
|
|
fbaf9b97aa | ||
|
|
0a55732919 | ||
|
|
ece0ecbd8f | ||
|
|
d2743465c3 | ||
|
|
e41af930c5 | ||
|
|
582b5182e6 | ||
|
|
2b85aa0f27 | ||
|
|
e74e0b7e21 | ||
|
|
70d6813938 | ||
|
|
c1d84d6e08 | ||
|
|
5c69c70013 | ||
|
|
6379aa89ef | ||
|
|
c2756a3a4a | ||
|
|
303ced735a | ||
|
|
bc70803cc0 | ||
|
|
171b04f267 | ||
|
|
1cd5c194cc | ||
|
|
c15fe4281e | ||
|
|
1bd7f37041 | ||
|
|
170bc5f6ad | ||
|
|
8daf74e6db | ||
|
|
b84adf514e | ||
|
|
947aa151a5 | ||
|
|
12c734fe1b | ||
|
|
724b9332f4 | ||
|
|
a1f90607af | ||
|
|
1a954cc232 | ||
|
|
22cb23a075 | ||
|
|
608f82cf81 | ||
|
|
2f4608e697 | ||
|
|
c08ce3115a | ||
|
|
470c28d6ef | ||
|
|
7ea1d8e85d | ||
|
|
1669438312 | ||
|
|
d4b77cac66 | ||
|
|
f1bebd0612 | ||
|
|
003e1f79f0 | ||
|
|
97702da9ef | ||
|
|
35cf9f9352 | ||
|
|
5225823e8b | ||
|
|
2569e7c823 | ||
|
|
aa5c4d5064 | ||
|
|
40ff2a8315 | ||
|
|
bf150712a0 | ||
|
|
92531ff675 | ||
|
|
6919c26311 | ||
|
|
f19c09f239 | ||
|
|
7cadb8ffac | ||
|
|
ea204b24a6 | ||
|
|
45799362ce | ||
|
|
2836e10262 | ||
|
|
a04d463e0f | ||
|
|
3ce8a57daa | ||
|
|
1647c73375 | ||
|
|
b7980b7cbf | ||
|
|
0a6114cc60 | ||
|
|
149675cfb3 | ||
|
|
b8eec159bc | ||
|
|
ddb93b28cd | ||
|
|
4ed6d1a9fd | ||
|
|
bd60e39b24 | ||
|
|
b6636b10f0 | ||
|
|
8c8beea2eb | ||
|
|
3592a98f2a | ||
|
|
56f8418d13 | ||
|
|
5bb8ea0f86 | ||
|
|
daddf10035 | ||
|
|
2ac8e1509d | ||
|
|
3cd3208183 | ||
|
|
be217021a2 | ||
|
|
a2bbb67670 | ||
|
|
465189f4b8 | ||
|
|
173f7fa4f6 | ||
|
|
d405232d81 | ||
|
|
673fd95bbd | ||
|
|
25b2a663c8 | ||
|
|
962ecf2cae | ||
|
|
4c3f1c43fa | ||
|
|
82dfa96699 | ||
|
|
83d3add41a | ||
|
|
54db7dc64e | ||
|
|
4c04e9e403 | ||
|
|
de44217f65 | ||
|
|
3ae6079615 | ||
|
|
3f3409ef1b | ||
|
|
fd90b8d5f0 | ||
|
|
782c9930da | ||
|
|
d0b46c35a9 | ||
|
|
6c7671dc21 | ||
|
|
bdc3c204d4 | ||
|
|
2013830677 | ||
|
|
95906a9ed5 | ||
|
|
6fdd4bd0f4 | ||
|
|
37cf7aada0 | ||
|
|
dfdc41626b | ||
|
|
237b3ce0d9 | ||
|
|
05745bf3c5 | ||
|
|
8a6eaa5389 | ||
|
|
7ed298139d | ||
|
|
82908b82c0 | ||
|
|
946d88236c | ||
|
|
1aa1df885d | ||
|
|
7c94ed9b50 | ||
|
|
f7dbff4753 | ||
|
|
00b5e762a7 | ||
|
|
e1440eb76f | ||
|
|
4adb8e4202 | ||
|
|
e870370d0c | ||
|
|
f944b76c1f | ||
|
|
62543bdfcf | ||
|
|
447a321eb7 | ||
|
|
d094934bd9 | ||
|
|
c4f8959072 | ||
|
|
d682bd2ebb | ||
|
|
437ab70cd9 | ||
|
|
15031cde1f | ||
|
|
e88008552c | ||
|
|
fd49deefb8 | ||
|
|
1f2973abd2 | ||
|
|
317cd7671f | ||
|
|
9556efc224 | ||
|
|
2f9db83868 | ||
|
|
c627832ebd | ||
|
|
7d2df3af42 | ||
|
|
e9288bd4a1 | ||
|
|
191620b55b | ||
|
|
90b6fc1e49 | ||
|
|
cd3c748dd0 | ||
|
|
34dbc7930e | ||
|
|
112633a776 | ||
|
|
56416109b1 | ||
|
|
a889abae98 | ||
|
|
dcb50ba3ff | ||
|
|
3c61ddb806 | ||
|
|
2a9aba1df8 | ||
|
|
450acbdcb1 | ||
|
|
80aff3199a | ||
|
|
834eff20c7 | ||
|
|
6314f5e45a | ||
|
|
5b3793e810 | ||
|
|
9a2bff48c5 | ||
|
|
aa9903b328 | ||
|
|
c1f4ae96fa | ||
|
|
d5995d93e2 | ||
|
|
f13844cf3e | ||
|
|
6d7add8272 | ||
|
|
983a683d54 | ||
|
|
f3e5cf2a8b | ||
|
|
40db551799 | ||
|
|
fdb9e03656 | ||
|
|
48976b2947 | ||
|
|
3ee022c2be | ||
|
|
ab24a61fd3 | ||
|
|
8d2237b26f | ||
|
|
8ad05bbd5b | ||
|
|
7499e79dc7 | ||
|
|
7132278204 | ||
|
|
60fa494ed0 | ||
|
|
815f204136 | ||
|
|
fe80fab811 | ||
|
|
2699ecd93d | ||
|
|
5d634f5876 | ||
|
|
9979789e08 | ||
|
|
79d2d4e366 | ||
|
|
90b4bcfec9 | ||
|
|
49df286cfa | ||
|
|
20edf8a622 | ||
|
|
bb76102171 | ||
|
|
e71f9b585c | ||
|
|
1effc45f18 | ||
|
|
2e5e5b7be0 | ||
|
|
1df5cfefd0 | ||
|
|
5d1e2663b8 | ||
|
|
59441c414b | ||
|
|
419d007445 | ||
|
|
90bca27bde | ||
|
|
0050624880 | ||
|
|
c4ea7938d1 | ||
|
|
b5d58455b6 | ||
|
|
82993df715 | ||
|
|
555a688c16 | ||
|
|
0241a5fb93 | ||
|
|
db7351e7d4 | ||
|
|
891e9792f8 | ||
|
|
97e7333415 | ||
|
|
dc4dd07ced | ||
|
|
937f615c8c | ||
|
|
7578cf092e | ||
|
|
3041207177 | ||
|
|
46d66122aa | ||
|
|
d05e3a92f3 | ||
|
|
4a4b077132 | ||
|
|
cf5e010faf | ||
|
|
46616467f4 | ||
|
|
3dcb36adf9 | ||
|
|
855940fb03 | ||
|
|
1f25e1a308 | ||
|
|
232298d7f4 | ||
|
|
fa9b4f1a1c | ||
|
|
355c2719fd | ||
|
|
dfb18ce882 | ||
|
|
07187d058b | ||
|
|
5060b5f8c7 | ||
|
|
50c628a25e | ||
|
|
7bf4609d3d | ||
|
|
548a8eb321 | ||
|
|
627f9e7a0a | ||
|
|
4faf85c988 | ||
|
|
259c6434da | ||
|
|
321d5b364f | ||
|
|
e56491ec27 | ||
|
|
8fe5d1bace | ||
|
|
4efa58ee8b | ||
|
|
8249a5efa1 | ||
|
|
08b0717407 | ||
|
|
c277833332 | ||
|
|
37d9f2870d | ||
|
|
cc71d1292b | ||
|
|
3ca6841ea2 | ||
|
|
c81cdd0df1 | ||
|
|
bd0cbe5e97 | ||
|
|
fdd4eebd96 | ||
|
|
34eeaf5cce | ||
|
|
09c38e427a | ||
|
|
fca2773282 | ||
|
|
c138cc81c0 | ||
|
|
91be95e158 | ||
|
|
e172b45047 | ||
|
|
0a6c07551f | ||
|
|
fa33f850f7 | ||
|
|
605513d165 | ||
|
|
d635ab8662 | ||
|
|
4862705dcd | ||
|
|
4d471eb285 | ||
|
|
470d315eaf | ||
|
|
4e267e3de9 | ||
|
|
bd28cd1766 | ||
|
|
f5c324bd68 | ||
|
|
b7bf995303 | ||
|
|
68516817aa | ||
|
|
9dc5f2a063 | ||
|
|
d2564efe46 | ||
|
|
696e97914d | ||
|
|
cafc5da8bf | ||
|
|
a24b6432c2 | ||
|
|
68668c1b91 | ||
|
|
460441abd2 | ||
|
|
3875afd002 | ||
|
|
f769c1fbec | ||
|
|
644df95f21 | ||
|
|
95f1e618f9 | ||
|
|
03e6cf1070 | ||
|
|
f01a207166 | ||
|
|
1795996825 | ||
|
|
375f7bcc09 | ||
|
|
76f10ced5f | ||
|
|
37c791f147 | ||
|
|
9a8034eb4c | ||
|
|
ff70ed301e | ||
|
|
3f66594821 | ||
|
|
f2cd0218fd | ||
|
|
028d601674 | ||
|
|
e06d012875 | ||
|
|
5995400414 | ||
|
|
91fbc2fdf8 | ||
|
|
6b00324c74 | ||
|
|
1369197a11 | ||
|
|
a30580f3b8 | ||
|
|
fc93a85e21 | ||
|
|
5351d869d4 | ||
|
|
a61d9af25c | ||
|
|
2111413704 | ||
|
|
a2781f9af2 | ||
|
|
09eeae3802 | ||
|
|
0408b7e23d | ||
|
|
ea606de22f | ||
|
|
9fbff43120 | ||
|
|
bc358af5fa | ||
|
|
4452568058 | ||
|
|
53c4aa7066 | ||
|
|
ce5547e4e7 | ||
|
|
95bdf5c2b5 | ||
|
|
8953a96089 | ||
|
|
0d270454ab | ||
|
|
e740a695c0 | ||
|
|
78118daa69 | ||
|
|
61a824d322 | ||
|
|
06bac3c748 | ||
|
|
16b10dc353 | ||
|
|
6892b443e0 | ||
|
|
8b303e037e | ||
|
|
76efef37ea | ||
|
|
e64a66aa66 | ||
|
|
05578d28c6 | ||
|
|
0889aea3be | ||
|
|
a081e6e3c9 | ||
|
|
5dfa0889b7 | ||
|
|
ed1d09b9be | ||
|
|
2d8a986155 | ||
|
|
1fb4cfd3b6 | ||
|
|
2d987747a2 | ||
|
|
d39e4e3ff1 | ||
|
|
012ca48f9a | ||
|
|
cca1f3e000 | ||
|
|
40a38cfd31 | ||
|
|
d2b39e8fa3 | ||
|
|
20101cda2e | ||
|
|
c90724daa6 | ||
|
|
fedb22fab2 | ||
|
|
994f1974d7 | ||
|
|
d648fe552d | ||
|
|
ccafff28cd | ||
|
|
3da49d47af | ||
|
|
e1918cfa89 | ||
|
|
7b19204d77 | ||
|
|
5dac1de87e | ||
|
|
c9a2474f17 | ||
|
|
e5d9d0d054 | ||
|
|
1272582510 | ||
|
|
51271a0e02 | ||
|
|
9b32952a22 | ||
|
|
5b92aea54b | ||
|
|
61b59f4ca0 | ||
|
|
93f8f43de2 | ||
|
|
dc88e0b07f | ||
|
|
14a9f953a9 | ||
|
|
528885d5e2 | ||
|
|
e779f37689 | ||
|
|
c6c2a8dc49 | ||
|
|
d8d2fd25cd | ||
|
|
301a5b6685 | ||
|
|
2a4bac42ed | ||
|
|
f55cadedb5 | ||
|
|
dd9ff45b21 | ||
|
|
a0aada2f03 | ||
|
|
8499843f31 | ||
|
|
672a33b93b | ||
|
|
f9a744c7dc | ||
|
|
5b124013b7 | ||
|
|
d2f1c69e98 | ||
|
|
2a2f96a771 | ||
|
|
dc9b5f447e | ||
|
|
1fb0c01e7e | ||
|
|
014d7b35ac | ||
|
|
b08267dab0 | ||
|
|
97d78516f2 | ||
|
|
360c2437c1 | ||
|
|
0b230bfc74 | ||
|
|
d25dc8d266 | ||
|
|
5f4d1e99cd | ||
|
|
ee38ef7817 | ||
|
|
80af38d3e1 | ||
|
|
2f19866f05 | ||
|
|
cf1c500e9d | ||
|
|
9a770e9dc9 | ||
|
|
6dbb8863a0 | ||
|
|
5eac8d0ab9 | ||
|
|
272bad8f29 | ||
|
|
83a3352862 | ||
|
|
4d5a693208 | ||
|
|
70e7f944c0 | ||
|
|
5a52c03a3d | ||
|
|
f9f597ef01 | ||
|
|
2e07788c0c | ||
|
|
9c0fbc1cb6 | ||
|
|
239d49899d | ||
|
|
2d3589b5a3 | ||
|
|
96ec465a38 | ||
|
|
5bb6b87c7d | ||
|
|
fcfedccaf8 | ||
|
|
98373833fd | ||
|
|
03588be125 | ||
|
|
cdd81daec5 | ||
|
|
d64f90c0c8 | ||
|
|
ec31dee36e | ||
|
|
11f2e88590 | ||
|
|
bf51ddb2d5 | ||
|
|
26000f1828 | ||
|
|
f65485af97 | ||
|
|
72c5690d05 | ||
|
|
e076d61122 | ||
|
|
7071808514 | ||
|
|
1e2fd46cd3 | ||
|
|
e3cdd4326f | ||
|
|
227f932ad8 | ||
|
|
67cec188ce | ||
|
|
1ee276185e | ||
|
|
42ed855b05 | ||
|
|
93da4eed30 | ||
|
|
a72a596578 | ||
|
|
72663dd68c | ||
|
|
108d57dbdd | ||
|
|
bc71fd8515 | ||
|
|
a51070767b | ||
|
|
269cc4dbc9 | ||
|
|
684961fa4b | ||
|
|
4f60b7e379 | ||
|
|
5d72061151 | ||
|
|
f2c940c1d3 | ||
|
|
7e817b0b30 | ||
|
|
75bb15d3b7 | ||
|
|
ba1a1eff67 | ||
|
|
5432b5b917 | ||
|
|
f677cf7393 | ||
|
|
bdf9278131 | ||
|
|
0ae93a9c3f | ||
|
|
72422cdd8b | ||
|
|
390d1b3329 | ||
|
|
024e99766a | ||
|
|
0160502da8 | ||
|
|
f0d65982de | ||
|
|
1445933bd4 | ||
|
|
508f31c254 | ||
|
|
fd1aa10a2a | ||
|
|
fceed68d75 | ||
|
|
955e780c64 | ||
|
|
452fe20e8f | ||
|
|
9083e16cdb | ||
|
|
840a278e5d | ||
|
|
6d036dbd60 | ||
|
|
d5ba6f34d6 | ||
|
|
0321846c22 | ||
|
|
714747fcee | ||
|
|
e3906da3c4 | ||
|
|
820ef70033 | ||
|
|
0c95f5c052 | ||
|
|
4cfd7c4362 | ||
|
|
1e8126dec8 | ||
|
|
d3fbc58736 | ||
|
|
46113744b3 | ||
|
|
3947624ae0 | ||
|
|
4ac5ded012 | ||
|
|
aadfacf50a | ||
|
|
bb1e0ac82b | ||
|
|
23a15a1533 | ||
|
|
01dd480c01 | ||
|
|
364cae3273 | ||
|
|
8a986ab707 | ||
|
|
ca96959de8 | ||
|
|
2f0ae9ef50 | ||
|
|
8b84bac2c2 | ||
|
|
976ed7f1a5 | ||
|
|
b4e6821da8 | ||
|
|
540b990a08 | ||
|
|
ce75b072da | ||
|
|
5bca9b7156 | ||
|
|
ae4914346b | ||
|
|
b593209558 | ||
|
|
9df399708f | ||
|
|
ea25682488 | ||
|
|
06e25e6a16 | ||
|
|
10847ef3f2 | ||
|
|
78184390ac | ||
|
|
9a0c88ac61 | ||
|
|
646dd3613c | ||
|
|
ab87abb859 | ||
|
|
5ce1a2b406 | ||
|
|
1abda3b425 | ||
|
|
c759becac6 | ||
|
|
b3a16c0000 | ||
|
|
0163a804cd | ||
|
|
ab157b16f1 | ||
|
|
905a9aec21 | ||
|
|
8e63dcdb15 | ||
|
|
05cad811e8 | ||
|
|
69a3e1138b | ||
|
|
9c9e29068b | ||
|
|
b8bb6dfa61 | ||
|
|
809956a2a4 | ||
|
|
0be72a70a5 | ||
|
|
9d03566246 | ||
|
|
7c023dbdf6 | ||
|
|
1f2187fd6a | ||
|
|
83b60b7b2b | ||
|
|
edfdd5c1a8 | ||
|
|
cdd350f52b | ||
|
|
1594364194 | ||
|
|
8dac20ed1c | ||
|
|
20beacb746 | ||
|
|
ac51fa6355 | ||
|
|
05a60831e6 | ||
|
|
dd13fee049 | ||
|
|
31fd833873 | ||
|
|
a0e5dd4c89 | ||
|
|
215daf5af7 | ||
|
|
a82972d967 | ||
|
|
6fd6c7af14 | ||
|
|
6d1cf5d3c1 | ||
|
|
0cfef48954 | ||
|
|
e9722d3875 | ||
|
|
fa8d17bec9 | ||
|
|
d69ec69038 | ||
|
|
106fc1bf58 | ||
|
|
4b055f55e1 | ||
|
|
3a70625308 | ||
|
|
7a382a8eba | ||
|
|
6d520dc4b2 | ||
|
|
40c7132ec0 | ||
|
|
4d2a038f19 | ||
|
|
a8bfdb6610 | ||
|
|
9817b0144e | ||
|
|
f662bd7de4 | ||
|
|
de52cec190 | ||
|
|
9a8e48e3c4 | ||
|
|
0560d857c1 | ||
|
|
4ee72cd074 | ||
|
|
7120ff430f | ||
|
|
167c66f8d6 | ||
|
|
4d98ccf86b | ||
|
|
ff2d1d5f97 | ||
|
|
ebe19532fc | ||
|
|
1294fc291a | ||
|
|
39c637a95f | ||
|
|
2fb6331e7b | ||
|
|
e7fd88bf2a | ||
|
|
96455dc38e | ||
|
|
4301911e86 | ||
|
|
1ddbe2fac8 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -13,11 +13,11 @@ Before opening an issue, view the device's Console logs in the Scrypted Manageme
|
||||
|
||||
**DO NOT OPEN ISSUES FOR ANY OF THE FOLLOWING:**
|
||||
|
||||
* Server setup assistance. Use Discord, Reddit, or Github Discussions.
|
||||
* Hardware setup assistance. Use Discord, Reddit, or Github Discussions.
|
||||
* Server or hardware setup assistance. Use Discord, Reddit, or Github Discussions.
|
||||
* Feature Requests. Use Discord, Reddit, or Github Discussions.
|
||||
* Packet loss in your camera logs. This is wifi/network congestion.
|
||||
* HomeKit weirdness. See HomeKit troubleshooting guide.
|
||||
* Release schedules or timelines. Releases are rolled out unevenly across the different server platforms.
|
||||
|
||||
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.
|
||||
|
||||
6
.github/workflows/build-sdk.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
paths: ["sdk/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
@@ -15,11 +15,11 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./sdk
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22.4.1
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
97
.github/workflows/docker-common.yml
vendored
@@ -7,13 +7,10 @@ jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: self-hosted
|
||||
# runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: [
|
||||
# "18",
|
||||
"20"
|
||||
]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite"]
|
||||
steps:
|
||||
@@ -23,12 +20,26 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
@@ -54,14 +65,84 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
NODE_VERSION=${{ matrix.NODE_VERSION }}
|
||||
NODE_VERSION=${{ env.NODE_VERSION }}
|
||||
BASE=${{ matrix.BASE }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-nvidia:
|
||||
name: Push NVIDIA Docker image to Docker Hub
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["jammy"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image (scrypted-common)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
BASE=ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-full
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.nvidia
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
44
.github/workflows/docker.yml
vendored
@@ -20,10 +20,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: [
|
||||
"20-jammy-full",
|
||||
"20-jammy-lite",
|
||||
["jammy-nvidia", ".s6"],
|
||||
["jammy-full", ".s6"],
|
||||
["jammy-lite", ""],
|
||||
]
|
||||
SUPERVISOR: ["", ".s6"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -42,12 +42,26 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_AMD64 }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
@@ -73,23 +87,23 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
BASE=${{ matrix.BASE }}
|
||||
BASE=${{ matrix.BASE[0] }}
|
||||
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
file: install/docker/Dockerfile${{ matrix.BASE[1] }}
|
||||
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) }}
|
||||
${{ matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
|
||||
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'koush/scrypted:lite' || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
|
||||
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
44
.github/workflows/static-sites.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["sites/static/**", ".github/workflows/static-sites.yml"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './sites/static'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
64
.github/workflows/test.yml
vendored
@@ -9,52 +9,28 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test_linux_local:
|
||||
name: Test Linux local installation
|
||||
runs-on: ubuntu-latest
|
||||
test_local:
|
||||
name: Test local installation on ${{ matrix.runner }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-latest, macos-14, macos-13, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run install script
|
||||
|
||||
- name: Parse latest server release
|
||||
id: parse_server
|
||||
shell: bash
|
||||
run: |
|
||||
cat ./install/local/install-scrypted-dependencies-linux.sh | sudo SERVICE_USER=$USER bash
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
systemctl status scrypted.service
|
||||
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
|
||||
|
||||
test_mac_local:
|
||||
name: Test Mac local installation
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
mkdir -p ~/.scrypted
|
||||
bash ./install/local/install-scrypted-dependencies-mac.sh
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
|
||||
|
||||
test_windows_local:
|
||||
name: Test Windows local installation
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run install script
|
||||
run: |
|
||||
.\install\local\install-scrypted-dependencies-win.ps1
|
||||
|
||||
- name: Test server is running
|
||||
run: |
|
||||
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
|
||||
VERSION=$(cat ./server/package-lock.json | jq -r '.version')
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Will test @scrypted/server@$VERSION"
|
||||
|
||||
- name: Install scrypted server
|
||||
uses: scryptedapp/setup-scrypted@v0.0.2
|
||||
with:
|
||||
branch: ${{ github.sha }}
|
||||
version: ${{ steps.parse_server.outputs.version }}
|
||||
6
.gitmodules
vendored
@@ -1,9 +1,6 @@
|
||||
[submodule "plugins/unifi-protect/src/unifi-protect"]
|
||||
path = external/unifi-protect
|
||||
url = ../../koush/unifi-protect.git
|
||||
[submodule "plugins/myq/src/myq"]
|
||||
path = plugins/myq/src/myq
|
||||
url = ../../koush/myq.git
|
||||
[submodule "external/ring-client-api"]
|
||||
path = external/ring-client-api
|
||||
url = ../../koush/ring
|
||||
@@ -14,9 +11,6 @@
|
||||
[submodule "external/werift"]
|
||||
path = external/werift
|
||||
url = ../../koush/werift-webrtc
|
||||
[submodule "plugins/zwave/file-stream-rotator"]
|
||||
path = plugins/zwave/file-stream-rotator
|
||||
url = ../../koush/file-stream-rotator.git
|
||||
[submodule "sdk/developer.scrypted.app"]
|
||||
path = sdk/developer.scrypted.app
|
||||
url = ../../koush/developer.scrypted.app
|
||||
|
||||
145
common/package-lock.json
generated
@@ -10,12 +10,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.45",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -111,64 +111,58 @@
|
||||
},
|
||||
"../server": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.82.0",
|
||||
"version": "0.115.0",
|
||||
"extraneous": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/types": "^0.3.4",
|
||||
"adm-zip": "^0.5.10",
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
||||
"@scrypted/node-pty": "^1.0.18",
|
||||
"@scrypted/types": "^0.3.33",
|
||||
"adm-zip": "^0.5.14",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.18.2",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"engine.io": "^6.6.0",
|
||||
"express": "^4.19.2",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"level": "^8.0.0",
|
||||
"linkfs": "^2.1.0",
|
||||
"ip": "^2.0.1",
|
||||
"level": "^8.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^4.6.0",
|
||||
"mime": "^3.0.0",
|
||||
"nan": "^2.18.0",
|
||||
"nan": "^2.20.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^10.0.1",
|
||||
"node-gyp": "^10.1.0",
|
||||
"py": "npm:@bjia56/portable-python@^0.1.54",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.1",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.33.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^6.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"tar": "^7.4.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.16.0"
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-serve": "bin/scrypted-serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.10",
|
||||
"@types/pem": "^1.14.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/tar": "^6.1.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -212,10 +206,6 @@
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/server": {
|
||||
"resolved": "../server",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -308,6 +298,12 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
|
||||
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -352,9 +348,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -449,59 +445,6 @@
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/server": {
|
||||
"version": "file:../server",
|
||||
"requires": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/types": "^0.3.4",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.10",
|
||||
"@types/pem": "^1.14.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/tar": "^6.1.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"adm-zip": "^0.5.10",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.18.2",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"level": "^8.0.0",
|
||||
"linkfs": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^4.6.0",
|
||||
"mime": "^3.0.0",
|
||||
"nan": "^2.18.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^10.0.1",
|
||||
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^6.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -579,6 +522,12 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
|
||||
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -601,9 +550,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
28
common/src/activity-timeout.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function createActivityTimeout(timeout: number, timeoutCallback: () => void) {
|
||||
let dataTimeout: NodeJS.Timeout;
|
||||
|
||||
let lastTime = Date.now();
|
||||
function resetActivityTimer() {
|
||||
lastTime = Date.now();
|
||||
}
|
||||
|
||||
function clearActivityTimer() {
|
||||
clearInterval(dataTimeout);
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
dataTimeout = setInterval(() => {
|
||||
if (Date.now() > lastTime + timeout) {
|
||||
clearInterval(dataTimeout);
|
||||
dataTimeout = undefined;
|
||||
timeoutCallback();
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
resetActivityTimer();
|
||||
return {
|
||||
resetActivityTimer,
|
||||
clearActivityTimer,
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ export function createAsyncQueue<T>() {
|
||||
}
|
||||
catch (e) {
|
||||
// the yield above may raise an error, and the queue should be ended.
|
||||
end(e);
|
||||
end(e as Error);
|
||||
if (e instanceof EndError)
|
||||
return;
|
||||
throw e;
|
||||
@@ -155,6 +155,23 @@ export function createAsyncQueue<T>() {
|
||||
}
|
||||
}
|
||||
|
||||
export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
const q = createAsyncQueue<T>();
|
||||
(async() => {
|
||||
try {
|
||||
for await (const i of generator) {
|
||||
q.submit(i);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
q.end(e as Error);
|
||||
}
|
||||
q.end();
|
||||
})();
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
// async function testSlowEnqueue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
|
||||
209
common/src/autoconfigure-codecs.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import sdk, { AudioStreamOptions, MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, ScryptedDeviceBase, Setting } from "@scrypted/sdk";
|
||||
|
||||
export const automaticallyConfigureSettings: Setting = {
|
||||
key: 'autoconfigure',
|
||||
title: 'Automatically Configure Settings',
|
||||
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
};
|
||||
|
||||
export const onvifAutoConfigureSettings: Setting = {
|
||||
key: 'onvif-autoconfigure',
|
||||
type: 'html',
|
||||
value: 'ONVIF autoconfiguration will configure the camera codecs. <b>The camera motion sensor must still be <a target="_blank" href="https://docs.scrypted.app/camera-preparation.html#motion-sensor-setup">configured manually</a>.</b>',
|
||||
};
|
||||
|
||||
const MEGABIT = 1024 * 1000;
|
||||
|
||||
function getBitrateForResolution(resolution: number) {
|
||||
if (resolution >= 3840 * 2160)
|
||||
return 8 * MEGABIT;
|
||||
if (resolution >= 2688 * 1520)
|
||||
return 3 * MEGABIT;
|
||||
if (resolution >= 1920 * 1080)
|
||||
return 2 * MEGABIT;
|
||||
if (resolution >= 1280 * 720)
|
||||
return MEGABIT;
|
||||
if (resolution >= 640 * 480)
|
||||
return MEGABIT / 2;
|
||||
return MEGABIT / 4;
|
||||
}
|
||||
|
||||
export async function checkPluginNeedsAutoConfigure(plugin: ScryptedDeviceBase, extraDevices = 0) {
|
||||
if (plugin.storage.getItem('autoconfigure') === 'true')
|
||||
return;
|
||||
|
||||
plugin.storage.setItem('autoconfigure', 'true');
|
||||
if (sdk.deviceManager.getNativeIds().length <= 1 + extraDevices)
|
||||
return;
|
||||
plugin.log.a(`${plugin.name} now has support for automatic camera configuration for optimal performance. Cameras can be autoconfigured in their respective settings.`);
|
||||
}
|
||||
|
||||
export async function autoconfigureCodecs(
|
||||
getCodecs: () => Promise<MediaStreamOptions[]>,
|
||||
configureCodecs: (options: MediaStreamOptions) => Promise<MediaStreamConfiguration>,
|
||||
audioOptions?: AudioStreamOptions,
|
||||
) {
|
||||
audioOptions ||= {
|
||||
codec: 'pcm_mulaw',
|
||||
bitrate: 64000,
|
||||
sampleRate: 8000,
|
||||
};
|
||||
|
||||
const codecs = await getCodecs();
|
||||
const configurable: MediaStreamConfiguration[] = [];
|
||||
for (const codec of codecs) {
|
||||
const config = await configureCodecs({
|
||||
id: codec.id,
|
||||
});
|
||||
configurable.push(config);
|
||||
}
|
||||
|
||||
const used: MediaStreamConfiguration[] = [];
|
||||
|
||||
for (const _ of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
|
||||
// find stream with the highest configurable resolution.
|
||||
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
|
||||
for (const codec of configurable) {
|
||||
if (used.includes(codec))
|
||||
continue;
|
||||
for (const resolution of codec.video.resolutions) {
|
||||
if (resolution[0] * resolution[1] > highest[1]) {
|
||||
highest = [codec, resolution[0] * resolution[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = highest[0];
|
||||
if (!config)
|
||||
break;
|
||||
|
||||
used.push(config);
|
||||
}
|
||||
|
||||
const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
|
||||
let diff = 999999999;
|
||||
let ret: [number, number];
|
||||
|
||||
const targetArea = width * height;
|
||||
for (const res of config.video.resolutions) {
|
||||
const actualArea = res[0] * res[1];
|
||||
const diffArea = Math.abs(targetArea - actualArea);
|
||||
if (diffArea < diff) {
|
||||
diff = diffArea;
|
||||
ret = res;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// find the highest resolution
|
||||
const l = used[0];
|
||||
const resolution = findResolutionTarget(l, 8192, 8192);
|
||||
|
||||
// get the fps of 20 or highest available
|
||||
let fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
|
||||
let errors = '';
|
||||
|
||||
const logConfigureCodecs = async (config: MediaStreamConfiguration) => {
|
||||
try {
|
||||
await configureCodecs(config);
|
||||
}
|
||||
catch (e) {
|
||||
errors += e;
|
||||
}
|
||||
}
|
||||
|
||||
await logConfigureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: resolution[0],
|
||||
height: resolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
|
||||
if (used.length === 3) {
|
||||
// find remote and low
|
||||
const r = used[1];
|
||||
const l = used[2];
|
||||
|
||||
const rResolution = findResolutionTarget(r, 1280, 720);
|
||||
const lResolution = findResolutionTarget(l, 640, 360);
|
||||
|
||||
fps = Math.min(20, Math.max(...r.video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: r.id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: 1 * MEGABIT,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
|
||||
fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: lResolution[0],
|
||||
height: lResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: MEGABIT / 2,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
}
|
||||
else if (used.length == 2) {
|
||||
let target: [number, number];
|
||||
if (resolution[0] * resolution[1] > 1920 * 1080)
|
||||
target = [1280, 720];
|
||||
else
|
||||
target = [640, 360];
|
||||
|
||||
const rResolution = findResolutionTarget(used[1], target[0], target[1]);
|
||||
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: used[1].id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
}
|
||||
else if (used.length === 1) {
|
||||
// no nop
|
||||
}
|
||||
|
||||
if (errors)
|
||||
throw new Error(errors);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export class Deferred<T> {
|
||||
finished = false;
|
||||
resolve!: (value: T|PromiseLike<T>) => this;
|
||||
reject!: (error: Error) => this;
|
||||
promise: Promise<T> = new Promise((resolve, reject) => {
|
||||
this.resolve = v => {
|
||||
this.finished = true;
|
||||
resolve(v);
|
||||
return this;
|
||||
};
|
||||
this.reject = e => {
|
||||
this.finished = true;
|
||||
reject(e);
|
||||
return this;
|
||||
};
|
||||
});
|
||||
}
|
||||
1
common/src/deferred.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/src/deferred.ts
|
||||
96
common/src/eval/monaco-libs.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type * as monacoEditor from 'monaco-editor';
|
||||
|
||||
export interface StandardLibs {
|
||||
'@types/node/globals.d.ts': string,
|
||||
'@types/node/buffer.d.ts': string,
|
||||
'@types/node/process.d.ts': string,
|
||||
'@types/node/events.d.ts': string,
|
||||
'@types/node/stream.d.ts': string,
|
||||
'@types/node/fs.d.ts': string,
|
||||
'@types/node/net.d.ts': string,
|
||||
'@types/node/child_process.d.ts': string,
|
||||
}
|
||||
|
||||
export interface ScryptedLibs {
|
||||
'@types/sdk/settings-mixin.d.ts': string,
|
||||
'@types/sdk/storage-settings.d.ts': string,
|
||||
'@types/sdk/types.d.ts': string,
|
||||
'@types/sdk/index.d.ts': string,
|
||||
}
|
||||
|
||||
export function createMonacoEvalDefaultsWithLibs(standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
|
||||
// const libs = Object.assign(scryptedLibs, extraLibs);
|
||||
|
||||
function monacoEvalDefaultsFunction(monaco: typeof monacoEditor, standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
|
||||
{
|
||||
diagnosticCodesToIgnore: [1108, 1375, 1378],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
{
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const libs: any = {
|
||||
...scryptedLibs,
|
||||
...extraLibs,
|
||||
};
|
||||
|
||||
const catLibs = Object.values(libs).join('\n');
|
||||
const catlibsNoExport = Object.keys(libs)
|
||||
.map(lib => libs[lib]).map(lib =>
|
||||
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
|
||||
.join('\n');
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
|
||||
${catLibs}
|
||||
|
||||
declare global {
|
||||
${catlibsNoExport}
|
||||
|
||||
const log: Logger;
|
||||
|
||||
const deviceManager: DeviceManager;
|
||||
const endpointManager: EndpointManager;
|
||||
const mediaManager: MediaManager;
|
||||
const systemManager: SystemManager;
|
||||
|
||||
const eventSource: ScryptedDevice;
|
||||
const eventDetails: EventDetails;
|
||||
const eventData: any;
|
||||
}
|
||||
`,
|
||||
|
||||
"node_modules/@types/scrypted__sdk/types/index.d.ts"
|
||||
);
|
||||
|
||||
for (const lib of Object.keys(standardLibs)) {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
standardLibs[lib as keyof StandardLibs],
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `(function() {
|
||||
const standardLibs = ${JSON.stringify(standardLibs)};
|
||||
const scryptedLibs = ${JSON.stringify(scryptedLibs)};
|
||||
const extraLibs = ${JSON.stringify(extraLibs)};
|
||||
|
||||
return (monaco) => {
|
||||
(${monacoEvalDefaultsFunction})(monaco, standardLibs, scryptedLibs, extraLibs);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
|
||||
export interface ScriptDevice {
|
||||
/**
|
||||
* @deprecated Use the default export to specify the device handler.
|
||||
@@ -6,3 +8,5 @@ export interface ScriptDevice {
|
||||
handle<T>(handler?: T & object): void;
|
||||
handleTypes(...interfaces: string[]): void;
|
||||
}
|
||||
|
||||
export declare const device: ScryptedDeviceBase & ScriptDevice;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase } from "@scrypted/sdk/settings-mixin";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import fs from 'fs';
|
||||
import type { TranspileOptions } from "typescript";
|
||||
import vm from "vm";
|
||||
import { createMonacoEvalDefaultsWithLibs, ScryptedLibs, StandardLibs } from "./monaco-libs";
|
||||
import { ScriptDevice } from "./monaco/script-device";
|
||||
|
||||
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
|
||||
@@ -28,22 +29,18 @@ export function readFileAsString(f: string) {
|
||||
return fs.readFileSync(f).toString();;
|
||||
}
|
||||
|
||||
function getTypeDefs() {
|
||||
const settingsMixinDefs = readFileAsString('@types/sdk/settings-mixin.d.ts');
|
||||
const storageSettingsDefs = readFileAsString('@types/sdk/storage-settings.d.ts');
|
||||
const scryptedTypesDefs = readFileAsString('@types/sdk/types.d.ts');
|
||||
const scryptedIndexDefs = readFileAsString('@types/sdk/index.d.ts');
|
||||
function getScryptedLibs(): ScryptedLibs {
|
||||
return {
|
||||
settingsMixinDefs,
|
||||
storageSettingsDefs,
|
||||
scryptedIndexDefs,
|
||||
scryptedTypesDefs,
|
||||
};
|
||||
"@types/sdk/index.d.ts": readFileAsString('@types/sdk/index.d.ts'),
|
||||
"@types/sdk/settings-mixin.d.ts": readFileAsString('@types/sdk/settings-mixin.d.ts'),
|
||||
"@types/sdk/storage-settings.d.ts": readFileAsString('@types/sdk/storage-settings.d.ts'),
|
||||
"@types/sdk/types.d.ts": readFileAsString('@types/sdk/types.d.ts'),
|
||||
}
|
||||
}
|
||||
|
||||
export async function scryptedEval(device: ScryptedDeviceBase, script: string, extraLibs: { [lib: string]: string }, params: { [name: string]: any }) {
|
||||
const libs = Object.assign({
|
||||
types: getTypeDefs().scryptedTypesDefs,
|
||||
types: getScryptedLibs()['@types/sdk/types.d.ts'],
|
||||
}, extraLibs);
|
||||
const allScripts = Object.values(libs).join('\n').toString() + script;
|
||||
let compiled: string;
|
||||
@@ -117,102 +114,18 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
}
|
||||
|
||||
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
|
||||
const safeLibs: any = {};
|
||||
const standardlibs: StandardLibs = {
|
||||
"@types/node/globals.d.ts": readFileAsString('@types/node/globals.d.ts'),
|
||||
"@types/node/buffer.d.ts": readFileAsString('@types/node/buffer.d.ts'),
|
||||
"@types/node/process.d.ts": readFileAsString('@types/node/process.d.ts'),
|
||||
"@types/node/events.d.ts": readFileAsString('@types/node/events.d.ts'),
|
||||
"@types/node/stream.d.ts": readFileAsString('@types/node/stream.d.ts'),
|
||||
"@types/node/fs.d.ts": readFileAsString('@types/node/fs.d.ts'),
|
||||
"@types/node/net.d.ts": readFileAsString('@types/node/net.d.ts'),
|
||||
"@types/node/child_process.d.ts": readFileAsString('@types/node/child_process.d.ts'),
|
||||
};
|
||||
|
||||
for (const safeLib of [
|
||||
'@types/node/globals.d.ts',
|
||||
'@types/node/buffer.d.ts',
|
||||
'@types/node/process.d.ts',
|
||||
'@types/node/events.d.ts',
|
||||
'@types/node/stream.d.ts',
|
||||
'@types/node/fs.d.ts',
|
||||
'@types/node/net.d.ts',
|
||||
'@types/node/child_process.d.ts',
|
||||
]) {
|
||||
safeLibs[`node_modules/${safeLib}`] = readFileAsString(safeLib)
|
||||
}
|
||||
|
||||
const libs = Object.assign(getTypeDefs(), extraLibs);
|
||||
|
||||
function monacoEvalDefaultsFunction(monaco: any, safeLibs: any, libs: any) {
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
|
||||
{
|
||||
diagnosticCodesToIgnore: [1108, 1375, 1378],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
{
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const catLibs = Object.values(libs).join('\n');
|
||||
const catlibsNoExport = Object.keys(libs).filter(lib => lib !== 'sdk')
|
||||
.map(lib => libs[lib]).map(lib =>
|
||||
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
|
||||
.join('\n');
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
|
||||
${catLibs}
|
||||
|
||||
declare global {
|
||||
${catlibsNoExport}
|
||||
|
||||
const log: Logger;
|
||||
|
||||
const deviceManager: DeviceManager;
|
||||
const endpointManager: EndpointManager;
|
||||
const mediaManager: MediaManager;
|
||||
const systemManager: SystemManager;
|
||||
const mqtt: MqttClient;
|
||||
const device: ScryptedDeviceBase & { pathname : string };
|
||||
}
|
||||
`,
|
||||
|
||||
"node_modules/@types/scrypted__sdk/types/index.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['settingsMixin'],
|
||||
"node_modules/@types/scrypted__sdk/settings-mixin.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['storageSettings'],
|
||||
"node_modules/@types/scrypted__sdk/storage-settings.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['sdk'],
|
||||
"node_modules/@types/scrypted__sdk/index.d.ts"
|
||||
);
|
||||
|
||||
for (const lib of Object.keys(safeLibs)) {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
safeLibs[lib],
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `(function() {
|
||||
const safeLibs = ${JSON.stringify(safeLibs)};
|
||||
const libs = ${JSON.stringify(libs)};
|
||||
|
||||
return (monaco) => {
|
||||
(${monacoEvalDefaultsFunction})(monaco, safeLibs, libs);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
return createMonacoEvalDefaultsWithLibs(standardlibs, getScryptedLibs(), extraLibs);
|
||||
}
|
||||
|
||||
export interface ScriptDeviceImpl extends ScriptDevice {
|
||||
|
||||
@@ -79,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "../../server/src/listen-zero";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from '@scrypted/server/src/media-helpers';
|
||||
export { safeKillFFmpeg, ffmpegLogInitialOutput, safePrintFFmpegArguments } from '../../server/src/media-helpers';
|
||||
|
||||
@@ -54,18 +54,18 @@ export async function read16BELengthLoop(readable: Readable, options: {
|
||||
readable.on('readable', read);
|
||||
|
||||
await once(readable, 'end');
|
||||
throw new Error('stream ended');
|
||||
throw new StreamEndError('read16BELengthLoop');
|
||||
}
|
||||
|
||||
export class StreamEndError extends Error {
|
||||
constructor() {
|
||||
super('stream ended');
|
||||
constructor(where: string) {
|
||||
super(`stream ended: ${where}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
throw new StreamEndError();
|
||||
throw new StreamEndError('readLength start');
|
||||
|
||||
if (!length) {
|
||||
return Buffer.alloc(0);
|
||||
@@ -88,12 +88,12 @@ export async function readLength(readable: Readable, length: number): Promise<Bu
|
||||
}
|
||||
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
reject(new Error("stream ended during read"));
|
||||
reject(new StreamEndError('readLength readable'));
|
||||
};
|
||||
|
||||
const e = () => {
|
||||
cleanup();
|
||||
reject(new StreamEndError())
|
||||
reject(new StreamEndError('readLength end'));
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RpcPeer } from "@scrypted/server/src/rpc";
|
||||
import { createRpcSerializer } from "@scrypted/server/src/rpc-serializer";
|
||||
import { RpcPeer } from "../../server/src/rpc";
|
||||
import { createRpcSerializer } from "../../server/src/rpc-serializer";
|
||||
import type { RTCSignalingSession } from "@scrypted/sdk";
|
||||
|
||||
export async function createBrowserSignalingSession(ws: WebSocket, localName: string, remoteName: string) {
|
||||
|
||||
@@ -41,15 +41,15 @@ export function isPeerConnectionClosed(pc: RTCPeerConnection) {
|
||||
|| pc.iceConnectionState === 'closed';
|
||||
}
|
||||
|
||||
function silence() {
|
||||
let ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||
const dest = ctx.createMediaStreamDestination();
|
||||
oscillator.connect(dest);
|
||||
oscillator.start();
|
||||
const ret = dest.stream.getAudioTracks()[0];
|
||||
ret.enabled = false;
|
||||
return ret;
|
||||
}
|
||||
// function silence() {
|
||||
// let ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||
// const dest = ctx.createMediaStreamDestination();
|
||||
// oscillator.connect(dest);
|
||||
// oscillator.start();
|
||||
// const ret = dest.stream.getAudioTracks()[0];
|
||||
// ret.enabled = false;
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
function createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
|
||||
@@ -89,27 +89,44 @@ export const H264_NAL_TYPE_FU_B = 29;
|
||||
export const H264_NAL_TYPE_MTAP16 = 26;
|
||||
export const H264_NAL_TYPE_MTAP32 = 27;
|
||||
|
||||
export const H265_NAL_TYPE_AGG = 48;
|
||||
export const H265_NAL_TYPE_VPS = 32;
|
||||
export const H265_NAL_TYPE_SPS = 33;
|
||||
export const H265_NAL_TYPE_PPS = 34;
|
||||
export const H265_NAL_TYPE_IDR_N = 19;
|
||||
export const H265_NAL_TYPE_IDR_W = 20;
|
||||
|
||||
export function findH264NaluType(streamChunk: StreamChunk, naluType: number) {
|
||||
if (streamChunk.type !== 'h264')
|
||||
return;
|
||||
return findH264NaluTypeInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12), naluType);
|
||||
}
|
||||
|
||||
export function findH265NaluType(streamChunk: StreamChunk, naluType: number) {
|
||||
if (streamChunk.type !== 'h265')
|
||||
return;
|
||||
return findH265NaluTypeInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12), naluType);
|
||||
}
|
||||
|
||||
export function parseH264NaluType(firstNaluByte: number) {
|
||||
return firstNaluByte & 0x1f;
|
||||
}
|
||||
|
||||
export function findH264NaluTypeInNalu(nalu: Buffer, naluType: number) {
|
||||
const checkNaluType = nalu[0] & 0x1f;
|
||||
const checkNaluType = parseH264NaluType(nalu[0]);
|
||||
if (checkNaluType === H264_NAL_TYPE_STAP_A) {
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = nalu[pos] & 0x1f;
|
||||
const stapaType = parseH264NaluType(nalu[pos]);
|
||||
if (stapaType === naluType)
|
||||
return nalu.subarray(pos, pos + naluLength);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else if (checkNaluType === H264_NAL_TYPE_FU_A) {
|
||||
const fuaType = nalu[1] & 0x1f;
|
||||
const fuaType = parseH264NaluType(nalu[1]);
|
||||
const isFuStart = !!(nalu[1] & 0x80);
|
||||
|
||||
if (fuaType === naluType && isFuStart)
|
||||
@@ -121,39 +138,52 @@ export function findH264NaluTypeInNalu(nalu: Buffer, naluType: number) {
|
||||
return;
|
||||
}
|
||||
|
||||
function parseH265NaluType(firstNaluByte: number) {
|
||||
return (firstNaluByte & 0b01111110) >> 1;
|
||||
}
|
||||
|
||||
export function findH265NaluTypeInNalu(nalu: Buffer, naluType: number) {
|
||||
const checkNaluType = parseH265NaluType(nalu[0]);
|
||||
if (checkNaluType === H265_NAL_TYPE_AGG) {
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = parseH265NaluType(nalu[pos]);
|
||||
if (stapaType === naluType)
|
||||
return nalu.subarray(pos, pos + naluLength);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else if (checkNaluType === naluType) {
|
||||
return nalu;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function getNaluTypes(streamChunk: StreamChunk) {
|
||||
if (streamChunk.type !== 'h264')
|
||||
return new Set<number>();
|
||||
return getNaluTypesInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12))
|
||||
}
|
||||
|
||||
export function getNaluFragmentInformation(nalu: Buffer) {
|
||||
const naluType = nalu[0] & 0x1f;
|
||||
const fua = naluType === H264_NAL_TYPE_FU_A;
|
||||
return {
|
||||
fua,
|
||||
fuaStart: fua && !!(nalu[1] & 0x80),
|
||||
fuaEnd: fua && !!(nalu[1] & 0x40),
|
||||
}
|
||||
}
|
||||
|
||||
export function getNaluTypesInNalu(nalu: Buffer, fuaRequireStart = false, fuaRequireEnd = false) {
|
||||
const ret = new Set<number>();
|
||||
const naluType = nalu[0] & 0x1f;
|
||||
const naluType = parseH264NaluType(nalu[0]);
|
||||
if (naluType === H264_NAL_TYPE_STAP_A) {
|
||||
ret.add(H264_NAL_TYPE_STAP_A);
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = nalu[pos] & 0x1f;
|
||||
const stapaType = parseH264NaluType(nalu[pos]);
|
||||
ret.add(stapaType);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else if (naluType === H264_NAL_TYPE_FU_A) {
|
||||
ret.add(H264_NAL_TYPE_FU_A);
|
||||
const fuaType = nalu[1] & 0x1f;
|
||||
const fuaType = parseH264NaluType(nalu[1]);
|
||||
if (fuaRequireStart) {
|
||||
const isFuStart = !!(nalu[1] & 0x80);
|
||||
if (isFuStart)
|
||||
@@ -175,6 +205,33 @@ export function getNaluTypesInNalu(nalu: Buffer, fuaRequireStart = false, fuaReq
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getH265NaluTypes(streamChunk: StreamChunk) {
|
||||
if (streamChunk.type !== 'h265')
|
||||
return new Set<number>();
|
||||
return getNaluTypesInH265Nalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12))
|
||||
}
|
||||
|
||||
export function getNaluTypesInH265Nalu(nalu: Buffer, fuaRequireStart = false, fuaRequireEnd = false) {
|
||||
const ret = new Set<number>();
|
||||
const naluType = parseH265NaluType(nalu[0]);
|
||||
if (naluType === H265_NAL_TYPE_AGG) {
|
||||
ret.add(H265_NAL_TYPE_AGG);
|
||||
let pos = 1;
|
||||
while (pos < nalu.length) {
|
||||
const naluLength = nalu.readUInt16BE(pos);
|
||||
pos += 2;
|
||||
const stapaType = parseH265NaluType(nalu[pos]);
|
||||
ret.add(stapaType);
|
||||
pos += naluLength;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret.add(naluType);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function createRtspParser(options?: StreamParserOptions): RtspStreamParser {
|
||||
let resolve: any;
|
||||
|
||||
@@ -195,12 +252,23 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
continue;
|
||||
if (streamChunk.type === 'h264') {
|
||||
const naluTypes = getNaluTypes(streamChunk);
|
||||
if (naluTypes.has(H264_NAL_TYPE_SPS) || naluTypes.has(H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
}
|
||||
else if (streamChunk.type === 'h265') {
|
||||
const naluTypes = getH265NaluTypes(streamChunk);
|
||||
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS) || findH264NaluType(streamChunk, H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
if (naluTypes.has(H265_NAL_TYPE_VPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_SPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_PPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_N)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_W)
|
||||
) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +506,7 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.client.destroy(e);
|
||||
this.client.destroy(e as Error);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -504,7 +572,8 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
deferred.reject(e);
|
||||
if (!deferred.finished)
|
||||
deferred.reject(e as Error);
|
||||
this.client.destroy();
|
||||
}
|
||||
};
|
||||
@@ -540,6 +609,7 @@ export class RtspClient extends RtspBase {
|
||||
throw new Error('no WWW-Authenticate found');
|
||||
|
||||
const { BASIC } = await import('http-auth-utils');
|
||||
// @ts-ignore
|
||||
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
|
||||
|
||||
if (this.wwwAuthenticate.includes('Basic')) {
|
||||
@@ -656,7 +726,10 @@ export class RtspClient extends RtspBase {
|
||||
Accept: 'application/sdp',
|
||||
});
|
||||
|
||||
this.contentBase = response.headers['content-base'] || response.headers['content-location'];;
|
||||
this.contentBase = response.headers['content-base'] || response.headers['content-location'];
|
||||
// content base may be a relative path? seems odd.
|
||||
if (this.contentBase)
|
||||
this.contentBase = new URL(this.contentBase, this.url).toString();
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1054,7 +1127,7 @@ export class RtspServer {
|
||||
}
|
||||
|
||||
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
|
||||
hostname?: string,
|
||||
hostname: string,
|
||||
pathToken?: string,
|
||||
createServer?(duplex: Duplex): T,
|
||||
}) {
|
||||
|
||||
@@ -227,6 +227,10 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
|
||||
codec = 'pcm_alaw';
|
||||
ffmpegEncoder = 'pcm_alaw';
|
||||
}
|
||||
else if (mline.payloadTypes?.includes(14)) {
|
||||
codec = 'mp3';
|
||||
ffmpegEncoder = 'mp3';
|
||||
}
|
||||
else {
|
||||
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
|
||||
// is this the default?
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "@scrypted/server/src/sleep"
|
||||
export { sleep } from "../../server/src/sleep";
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import sdk, { PluginFork } from '@scrypted/sdk';
|
||||
import worker_threads from 'worker_threads';
|
||||
import sdk, { ForkOptions, PluginFork } from '@scrypted/sdk';
|
||||
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>();
|
||||
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
|
||||
let zygote = sdk.fork<T>(options);
|
||||
function* next() {
|
||||
while (true) {
|
||||
const cur = zygote;
|
||||
zygote = sdk.fork<T>();
|
||||
zygote = sdk.fork<T>(options);
|
||||
yield cur;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node16",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||||
|
Before Width: | Height: | Size: 667 B |
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Scrypted Management Console</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "Scrypted Management Console",
|
||||
"short_name": "Scrypted",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#424242"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
2
external/unifi-protect
vendored
2
external/werift
vendored
@@ -1,13 +1,12 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "20-jammy-full.s6-v0.97.0"
|
||||
version: "v0.116.0-jammy-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
arch:
|
||||
- amd64
|
||||
- aarch64
|
||||
- armv7
|
||||
init: false
|
||||
ingress: true
|
||||
ingress_port: 11080
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="jammy"
|
||||
ARG REPO="ubuntu"
|
||||
FROM ${REPO}:${BASE} as header
|
||||
FROM ubuntu:${BASE} as header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -72,8 +71,9 @@ RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
# intel opencl gpu and npu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -17,9 +17,6 @@ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | 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
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
FROM ghcr.io/koush/scrypted:20-jammy-full.s6
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install miniconda
|
||||
ENV CONDA_DIR /opt/conda
|
||||
RUN apt update -y && apt -y install wget && wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
|
||||
/bin/bash ~/miniconda.sh -b -p /opt/conda
|
||||
# Put conda in path so we can use conda activate
|
||||
ENV PATH=$CONDA_DIR/bin:$PATH
|
||||
|
||||
RUN conda -y install -c conda-forge cudatoolkit cudnn
|
||||
ENV CONDA_PREFIX=/opt/conda
|
||||
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/
|
||||
# nvidia cudnn/libcublas etc.
|
||||
# for some reason this is not provided by the nvidia container toolkit
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-graphics.sh | bash
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
# The Scrypted docker-compose.yml file typically resides at:
|
||||
# ~/.scrypted/docker-compose.yml
|
||||
|
||||
@@ -37,17 +35,24 @@ services:
|
||||
# Avahi can be used for network discovery by passing in the host daemon
|
||||
# or running the daemon inside the container. Choose one or the other.
|
||||
# Uncomment next line to run avahi-daemon inside the container.
|
||||
# See volumes section below to use the host daemon.
|
||||
# See volumes and security_opt section below to use the host daemon.
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# NVIDIA (Part 1 of 4)
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
# NVIDIA (Part 2 of 4)
|
||||
# runtime: nvidia
|
||||
# Necessary to communicate with host dbus for avahi-daemon.
|
||||
security_opt:
|
||||
- apparmor:unconfined
|
||||
|
||||
# NVIDIA (Part 3 of 4) - Use NVIDIA image, and remove subsequent default image.
|
||||
# image: ghcr.io/koush/scrypted:nvidia
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
volumes:
|
||||
# NVIDIA (Part 4 of 4)
|
||||
# - /etc/OpenCL/vendors/nvidia.icd:/etc/OpenCL/vendors/nvidia.icd
|
||||
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
@@ -66,11 +71,16 @@ services:
|
||||
# Ensure Avahi is running on the host machine:
|
||||
# It can be installed with: sudo apt-get install avahi-daemon
|
||||
# This is not compatible with running avahi inside the container (see above).
|
||||
# Also, uncomment the lines under security_opt
|
||||
# - /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
|
||||
# Uncomment the following lines to use Avahi daemon from the host
|
||||
# Without this, AppArmor will block the container's attempt to talk to Avahi via dbus
|
||||
# security_opt:
|
||||
# - apparmor:unconfined
|
||||
devices: [
|
||||
# uncomment the common systems devices to pass
|
||||
# them through to docker.
|
||||
@@ -94,15 +104,16 @@ services:
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
# logging is noisy and will unnecessarily wear on flash storage.
|
||||
# scrypted has per device in memory logging that is preferred.
|
||||
# enable the log file if enhanced debugging is necessary.
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "10"
|
||||
driver: "none"
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "10m"
|
||||
# max-file: "10"
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=scrypted"
|
||||
|
||||
|
||||
@@ -1,16 +1,52 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
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."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
# no errors beyond this point
|
||||
set -e
|
||||
|
||||
# the intel provided script is disabled since it does not work with the 6.8 kernel in Ubuntu 24.04 or Proxmox 8.2.
|
||||
# manual installation of the Intel graphics stuff is required.
|
||||
|
||||
# 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 &&
|
||||
# apt-get -y dist-upgrade;
|
||||
|
||||
# need intel-media-va-driver-non-free, but all the other intel packages are installed from Intel github.
|
||||
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-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
apt-get install -y ocl-icd-libopencl1
|
||||
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu-dbgsym_1.3.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu_1.3.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb
|
||||
|
||||
dpkg -i *.deb
|
||||
|
||||
cd /tmp && rm -rf /tmp/gpu
|
||||
|
||||
apt-get -y dist-upgrade
|
||||
|
||||
69
install/docker/install-intel-npu.sh
Normal file
@@ -0,0 +1,69 @@
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
then
|
||||
echo "Intel NPU will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
then
|
||||
UBUNTU_22_04=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "Intel NPU will not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
|
||||
|
||||
# no errors beyond this point
|
||||
set -e
|
||||
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# different npu downloads for ubuntu versions
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-driver-compiler-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
else
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
dpkg -i *.deb
|
||||
|
||||
cd /tmp && rm -rf /tmp/npu
|
||||
|
||||
apt-get -y dist-upgrade
|
||||
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
echo
|
||||
echo "###############################################################################"
|
||||
echo "Intel NPU firmware was installed. Reboot the host to complete the installation."
|
||||
echo "###############################################################################"
|
||||
fi
|
||||
16
install/docker/install-nvidia-graphics.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
echo "Installing NVIDIA graphics packages."
|
||||
apt update -q \
|
||||
&& apt install -y wget \
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& dpkg -i /cuda-keyring.deb \
|
||||
&& apt update -q \
|
||||
&& apt install -y cuda-nvcc-11-8 libcublas-11-8 libcudnn8 cuda-libraries-11-8 \
|
||||
&& apt install -y cuda-nvcc-12-4 libcublas-12-4 libcudnn8 cuda-libraries-12-4;
|
||||
exit $?
|
||||
else
|
||||
echo "NVIDIA graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -61,6 +61,8 @@ then
|
||||
sudo apt-get -y install avahi-daemon
|
||||
sed -i 's/'#' - \/var\/run\/dbus/- \/var\/run\/dbus/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' - \/var\/run\/avahi-daemon/- \/var\/run\/avahi-daemon/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' security_opt:/security_opt:/g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/'#' - apparmor:unconfined/ - apparmor:unconfined/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
|
||||
@@ -72,6 +72,7 @@ function removescryptedfstab() {
|
||||
grep -v "scrypted-nvr" /etc/fstab > /tmp/fstab && cp /tmp/fstab /etc/fstab
|
||||
# ensure newline
|
||||
sed -i -e '$a\' /etc/fstab
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
BLOCK_DEVICE="/dev/$1"
|
||||
@@ -95,7 +96,17 @@ then
|
||||
set +e
|
||||
|
||||
sync
|
||||
mkfs -F -t ext4 "$BLOCK_DEVICE"1
|
||||
PARTITION_DEVICE="$BLOCK_DEVICE"1
|
||||
if [ ! -e "$PARTITION_DEVICE" ]
|
||||
then
|
||||
PARTITION_DEVICE="$BLOCK_DEVICE"p1
|
||||
if [ ! -e "$PARTITION_DEVICE" ]
|
||||
then
|
||||
echo "Unable to determine block device partition from block device: $BLOCK_DEVICE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mkfs -F -t ext4 "$PARTITION_DEVICE"
|
||||
sync
|
||||
|
||||
# parse/evaluate blkid line as env vars
|
||||
@@ -117,8 +128,9 @@ then
|
||||
set -e
|
||||
removescryptedfstab
|
||||
mkdir -p /mnt/scrypted-nvr
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail 0 0" >> /etc/fstab
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail,noatime 0 0" >> /etc/fstab
|
||||
mount -a
|
||||
systemctl daemon-reload
|
||||
set +e
|
||||
|
||||
DIR="/mnt/scrypted-nvr"
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
# intel opencl gpu and npu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -97,7 +97,7 @@ echo "docker compose rm -rf"
|
||||
sudo -u $SERVICE_USER docker rm -f /scrypted /scrypted-watchtower 2> /dev/null
|
||||
|
||||
echo "Installing Scrypted..."
|
||||
RUN sudo -u $SERVICE_USER npx -y scrypted@latest install-server
|
||||
RUN sudo -u $SERVICE_USER npx -y scrypted@latest install-server $SCRYPTED_INSTALL_VERSION
|
||||
|
||||
cat > /etc/systemd/system/scrypted.service <<EOT
|
||||
|
||||
@@ -110,10 +110,12 @@ User=$SERVICE_USER
|
||||
Group=$SERVICE_USER
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/npx -y scrypted serve
|
||||
Restart=on-failure
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
Environment="NODE_OPTIONS=$NODE_OPTIONS"
|
||||
Environment="SCRYPTED_INSTALL_ENVIRONMENT=$SCRYPTED_INSTALL_ENVIRONMENT"
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -121,7 +121,7 @@ then
|
||||
fi
|
||||
|
||||
echo "Installing Scrypted..."
|
||||
RUN $NPX_PATH -y scrypted@latest install-server
|
||||
RUN $NPX_PATH -y scrypted@latest install-server $SCRYPTED_INSTALL_VERSION
|
||||
|
||||
cat > ~/Library/LaunchAgents/app.scrypted.server.plist <<EOT
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
@@ -11,7 +11,7 @@ iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/in
|
||||
choco upgrade -y nodejs-lts --version=20.11.1
|
||||
|
||||
# Install VC Redist, which is necessary for portable python
|
||||
choco install vcredist140
|
||||
choco install -y vcredist140
|
||||
|
||||
# TODO: remove python install, and use portable python
|
||||
# Install Python
|
||||
@@ -26,7 +26,12 @@ $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";"
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install --upgrade pip
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
|
||||
|
||||
npx -y scrypted@latest install-server
|
||||
$SCRYPTED_INSTALL_VERSION=[System.Environment]::GetEnvironmentVariable("SCRYPTED_INSTALL_VERSION","User")
|
||||
if ($SCRYPTED_INSTALL_VERSION -eq $null) {
|
||||
npx -y scrypted@latest install-server
|
||||
} else {
|
||||
npx -y scrypted@latest install-server $SCRYPTED_INSTALL_VERSION
|
||||
}
|
||||
|
||||
$USER_HOME_ESCAPED = $env:USERPROFILE.replace('\', '\\')
|
||||
$SCRYPTED_HOME = $env:USERPROFILE + '\.scrypted'
|
||||
@@ -34,7 +39,8 @@ $SCRYPTED_HOME_ESCAPED_PATH = $SCRYPTED_HOME.replace('\', '\\')
|
||||
npm install --prefix $SCRYPTED_HOME @koush/node-windows --save
|
||||
|
||||
$NPX_PATH = (Get-Command npx).Path
|
||||
$NPX_PATH_ESCAPED = $NPX_PATH.replace('\', '\\')
|
||||
# The path needs double quotes to handle spaces in the directory path
|
||||
$NPX_PATH_ESCAPED = '"' + $NPX_PATH.replace('\', '\\') + '"'
|
||||
|
||||
$SERVICE_JS = @"
|
||||
const fs = require('fs');
|
||||
@@ -44,8 +50,10 @@ try {
|
||||
catch (e) {
|
||||
}
|
||||
const child_process = require('child_process');
|
||||
child_process.spawn('$($NPX_PATH_ESCAPED)', ['-y', 'scrypted', 'serve'], {
|
||||
child_process.spawn('$NPX_PATH_ESCAPED', ['-y', 'scrypted', 'serve'], {
|
||||
stdio: 'inherit',
|
||||
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
shell: true,
|
||||
});
|
||||
"@
|
||||
|
||||
|
||||
@@ -10,13 +10,20 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.96.0
|
||||
SCRYPTED_VERSION=v0.116.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=10444
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
if [ ! -f "$SCRYPTED_TAR_ZST" ]
|
||||
then
|
||||
@@ -41,12 +48,19 @@ pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct restore failed"
|
||||
echo "The Scrypted container installation failed (pct restore error)."
|
||||
echo ""
|
||||
echo "This may be caused by the server's 'local' storage not supporting containers."
|
||||
echo "Try running this script again with a different storage device (local-lvm, local-zfs). For example:"
|
||||
echo "This may be because the server's 'local' storage device is not being a valid"
|
||||
echo "location for containers."
|
||||
echo "Try running this script again with a different storage device like"
|
||||
echo "'local-lvm' or 'local-zfs'."
|
||||
echo ""
|
||||
echo "#############################################################################"
|
||||
echo "Paste the following command into this shell to install to local-lvm instead:"
|
||||
echo ""
|
||||
echo "bash $0 --storage local-lvm"
|
||||
echo "#############################################################################"
|
||||
echo ""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@@ -68,6 +82,27 @@ else
|
||||
echo "$CONF not found? Start on boot must be enabled manually."
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
# this copies the
|
||||
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
|
||||
|
||||
rm *.tar
|
||||
vzdump 10444 --dumpdir /tmp
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Moving data volume to backup..."
|
||||
pct restore $VMID *.tar $@
|
||||
|
||||
pct destroy 10444
|
||||
fi
|
||||
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
@@ -75,6 +110,7 @@ then
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"accel0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
#!/bin/bash
|
||||
echo 'if (!process.version.startsWith("v18")) throw new Error("Node 18 is required. Install Node Version Manager (nvm) for versioned node installations. See https://github.com/koush/scrypted/pull/498#issuecomment-1373854020")' | node
|
||||
if [ "$?" != 0 ]
|
||||
then
|
||||
exit
|
||||
fi
|
||||
|
||||
echo ######################################
|
||||
echo "Setting up popular plugins."
|
||||
echo "Additional will need npm install manually."
|
||||
@@ -15,7 +9,7 @@ cd $(dirname $0)
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
for directory in sdk common server packages/client packages/auth-fetch
|
||||
for directory in sdk server common packages/client packages/auth-fetch
|
||||
do
|
||||
echo "$directory > npm install"
|
||||
pushd $directory
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, fetcher, getFetchMethod, setDefaultHttpFetchAccept } from '../../../server/src/fetch';
|
||||
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, createHeadersArray, fetcher, getFetchMethod, hasHeader, setDefaultHttpFetchAccept, setHeader } from '../../../server/src/fetch';
|
||||
|
||||
export interface AuthFetchCredentialState {
|
||||
username: string;
|
||||
@@ -70,36 +70,54 @@ async function getAuth(options: AuthFetchOptions, url: string | URL, method: str
|
||||
|
||||
export function createAuthFetch<B, M>(
|
||||
h: fetcher<B, M>,
|
||||
parser: (body: M, responseType: HttpFetchResponseType) => Promise<any>
|
||||
parser: (body: M, responseType: HttpFetchResponseType | undefined) => Promise<any>
|
||||
) {
|
||||
const authHttpFetch = async <T extends HttpFetchOptions<B>>(options: T & AuthFetchOptions): ReturnType<typeof h<T>> => {
|
||||
const method = getFetchMethod(options);
|
||||
const headers = new Headers(options.headers);
|
||||
const headers = createHeadersArray(options.headers);
|
||||
options.headers = headers;
|
||||
setDefaultHttpFetchAccept(headers, options.responseType);
|
||||
|
||||
const initialHeader = await getAuth(options, options.url, method);
|
||||
// try to provide an authorization if a session exists, but don't override Authorization if provided already.
|
||||
// 401 will trigger a proper auth.
|
||||
if (initialHeader && !headers.has('Authorization'))
|
||||
headers.set('Authorization', initialHeader);
|
||||
if (initialHeader && !hasHeader(headers, 'Authorization'))
|
||||
setHeader(headers, 'Authorization', initialHeader);
|
||||
|
||||
|
||||
const controller = new AbortController();
|
||||
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
|
||||
|
||||
const initialResponse = await h({
|
||||
...options,
|
||||
ignoreStatusCode: true,
|
||||
signal: controller.signal,
|
||||
// need to intercept the status code to check for 401.
|
||||
// all other status codes will be handled according to the initial request options.
|
||||
checkStatusCode(statusCode) {
|
||||
// can handle a 401 if an credential is provided.
|
||||
// however, not providing a credential is also valid, and should
|
||||
// fall through to the normal response handling which may be interested
|
||||
// in the 401 response.
|
||||
if (statusCode === 401 && options.credential)
|
||||
return true;
|
||||
if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
|
||||
const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
|
||||
return checker(statusCode);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
responseType: 'readable',
|
||||
});
|
||||
|
||||
if (initialResponse.statusCode !== 401 || !options.credential) {
|
||||
if (!options?.ignoreStatusCode)
|
||||
checkStatus(initialResponse.statusCode);
|
||||
// if it's not a 401, just return the response.
|
||||
if (initialResponse.statusCode !== 401) {
|
||||
return {
|
||||
...initialResponse,
|
||||
body: await parser(initialResponse.body, options.responseType),
|
||||
};
|
||||
}
|
||||
|
||||
let authenticateHeaders: string | string[] = initialResponse.headers.get('www-authenticate');
|
||||
let authenticateHeaders: string | string[] | null = initialResponse.headers.get('www-authenticate');
|
||||
if (!authenticateHeaders)
|
||||
throw new Error('Did not find WWW-Authenticate header.');
|
||||
|
||||
@@ -126,7 +144,7 @@ export function createAuthFetch<B, M>(
|
||||
|
||||
const header = await getAuth(options, options.url, method);
|
||||
if (header)
|
||||
headers.set('Authorization', header);
|
||||
setHeader(headers, 'Authorization', header);
|
||||
|
||||
return h(options);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
||||
2
packages/cli/.vscode/launch.json
vendored
@@ -21,7 +21,7 @@
|
||||
],
|
||||
"preLaunchTask": "npm: build",
|
||||
"args": [
|
||||
"login",
|
||||
"serve",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
|
||||
18
packages/cli/package-lock.json
generated
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.14",
|
||||
"version": "1.3.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.3.14",
|
||||
"version": "1.3.20",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.3.3",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"@scrypted/types": "^0.3.30",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"semver": "^7.5.4",
|
||||
@@ -101,15 +101,11 @@
|
||||
"rimraf": "^5.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/client/node_modules/@scrypted/types": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.4.tgz",
|
||||
"integrity": "sha512-k/YMx8lIWOkePgXfKW9POr12mb+erFU2JKxO7TW92GyW8ojUWw9VOc0PK6O9bybi0vhsEnvMFkO6pO6bAonsVA=="
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.99",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.99.tgz",
|
||||
"integrity": "sha512-2J1FH7tpAW5X3rgA70gJ+z0HFM90c/tBA+JXdP1vI1d/0yVmh9TSxnHoCuADN4R2NQXHmoZ6Nbds9kKAQ/25XQ=="
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.30.tgz",
|
||||
"integrity": "sha512-1k+JVSR6WSNmE/5mLdqfrTmV3uRbvZp0OwKb8ikNi39ysBuC000tQGcEdXZqhYqRgWdhDTWtxXe9XsYoAZGKmA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.14",
|
||||
"version": "1.3.20",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.3.3",
|
||||
"@scrypted/types": "^0.2.99",
|
||||
"@scrypted/types": "^0.3.30",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"readline-sync": "^1.4.10",
|
||||
"semver": "^7.5.4",
|
||||
|
||||
@@ -10,6 +10,7 @@ import semver from 'semver';
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
import { installServe, serveMain } from './service';
|
||||
import { connectShell } from './shell';
|
||||
import { convertRtspToMp4, printRtspUsage } from './rtsp-file';
|
||||
|
||||
if (!semver.gte(process.version, '16.0.0')) {
|
||||
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
||||
@@ -160,11 +161,11 @@ async function main() {
|
||||
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
|
||||
if (ffmpegInput.url && ffmpegInput.urls?.[0]) {
|
||||
const url = new URL(ffmpegInput.url);
|
||||
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0]) {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
|
||||
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0] && ffmpegInput.inputArguments) {
|
||||
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url && ffmpegInput.urls ? ffmpegInput.urls?.[0] : i);
|
||||
}
|
||||
}
|
||||
const args = [...ffmpegInput.inputArguments];
|
||||
const args = ffmpegInput.inputArguments ? [...ffmpegInput.inputArguments] : [];
|
||||
if (ffmpegInput.h264FilterArguments)
|
||||
args.push(...ffmpegInput.h264FilterArguments);
|
||||
console.log('ffplay', ...args);
|
||||
@@ -173,6 +174,14 @@ async function main() {
|
||||
});
|
||||
sdk.disconnect();
|
||||
}
|
||||
else if (process.argv[2] === 'rtsp') {
|
||||
if (!process.argv[3]) {
|
||||
printRtspUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await convertRtspToMp4(process.argv[3], process.argv[4]);
|
||||
}
|
||||
else if (process.argv[2] === 'create-cert-json' && process.argv.length === 5) {
|
||||
const key = fs.readFileSync(process.argv[3]).toString();
|
||||
const cert = fs.readFileSync(process.argv[4]).toString();
|
||||
|
||||
72
packages/cli/src/rtsp-file.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { listenSingleRtspClient } from '../../../common/src/rtsp-server';
|
||||
import { parseSdp } from '../../../common/src/sdp-utils';
|
||||
import { once } from 'events';
|
||||
|
||||
export async function convertRtspToMp4(rtspFile: string, sessionFile?: string) {
|
||||
// rtsp file will be in roughly:
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/[hour-timestamp]/[segment-timestamp].rtsp
|
||||
|
||||
// sdp can be found in
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/session.json
|
||||
// or legacy:
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/session.sdp
|
||||
|
||||
const sessionDir = path.dirname(path.dirname(rtspFile));
|
||||
let sdp: string;
|
||||
let sessionJson = path.join(sessionDir, 'session.json');
|
||||
if (!fs.existsSync(sessionJson) && sessionFile)
|
||||
sessionJson = sessionFile.endsWith('.json') && sessionFile;
|
||||
|
||||
let sessionSdp = path.join(sessionDir, 'session.sdp');
|
||||
if (!fs.existsSync(sessionSdp) && sessionFile)
|
||||
sessionSdp = sessionFile.endsWith('.sdp') && sessionFile;
|
||||
|
||||
if (fs.existsSync(sessionJson)) {
|
||||
sdp = JSON.parse(fs.readFileSync(sessionJson).toString()).sdp;
|
||||
}
|
||||
else if (fs.existsSync(sessionSdp)) {
|
||||
sdp = fs.readFileSync(sessionSdp).toString();
|
||||
}
|
||||
else {
|
||||
console.error('Could not find session sdp. Ensure the rtsp directory structure is intact or specify the path to the session file.');
|
||||
console.error();
|
||||
printRtspUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parsedSdp = parseSdp(sdp);
|
||||
const hasAudio = parsedSdp.msections.some(msection => msection.type === 'audio');
|
||||
const rtspContents = fs.readFileSync(rtspFile);
|
||||
|
||||
const clientPromise = await listenSingleRtspClient();
|
||||
clientPromise.rtspServerPromise.then(async rtspServer => {
|
||||
rtspServer.sdp = sdp;
|
||||
await rtspServer.handlePlayback();
|
||||
console.log('playing')
|
||||
rtspServer.client.write(rtspContents);
|
||||
rtspServer.client.end();
|
||||
});
|
||||
|
||||
const mp4 = rtspFile + '.mp4';
|
||||
|
||||
const cp = child_process.spawn('ffmpeg', [
|
||||
'-y',
|
||||
'-i', clientPromise.url,
|
||||
'-vcodec', 'copy',
|
||||
...(hasAudio ? ['-acodec', 'aac'] : []),
|
||||
mp4,
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
await once(cp, 'exit');
|
||||
|
||||
console.log('mp4 written to:', mp4);
|
||||
}
|
||||
|
||||
export function printRtspUsage() {
|
||||
console.log('usage: npx rtsp /path/to/nvr/file.rtsp [/path/to/nvr/session.json | /path/to/nvr/session.sdp]');
|
||||
}
|
||||
@@ -12,10 +12,15 @@ async function sleep(ms: number) {
|
||||
|
||||
const EXIT_FILE = '.exit';
|
||||
const UPDATE_FILE = '.update';
|
||||
const VERSION_FILE = '.version';
|
||||
|
||||
async function runCommand(command: string, ...args: string[]) {
|
||||
if (os.platform() === 'win32')
|
||||
if (os.platform() === 'win32') {
|
||||
command += '.cmd';
|
||||
// wrap each argument in a quote to handle spaces in paths
|
||||
// https://github.com/nodejs/node/issues/38490#issuecomment-927330248
|
||||
args = args.map(arg => '"' + arg + '"');
|
||||
}
|
||||
console.log('running', command, ...args);
|
||||
const cp = child_process.spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
@@ -24,6 +29,8 @@ async function runCommand(command: string, ...args: string[]) {
|
||||
// https://github.com/lovell/sharp/blob/eefaa998725cf345227d94b40615e090495c6d09/lib/libvips.js#L115C19-L115C46
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS: 'true',
|
||||
},
|
||||
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
shell: os.platform() === 'win32' ? true : undefined,
|
||||
});
|
||||
await once(cp, 'exit');
|
||||
if (cp.exitCode)
|
||||
@@ -84,7 +91,13 @@ export async function installServe(installVersion: string, ignoreError?: boolean
|
||||
const installJson = path.join(installDir, 'install.json');
|
||||
try {
|
||||
const { version } = JSON.parse(fs.readFileSync(installJson).toString());
|
||||
if (semver.parse(process.version).major !== semver.parse(version).major)
|
||||
const processSemver = semver.parse(process.version);
|
||||
if (!processSemver)
|
||||
throw new Error('error parsing process version');
|
||||
const installSemver = semver.parse(version);
|
||||
if (!installSemver)
|
||||
throw new Error('error parsing install.json version');
|
||||
if (processSemver.major !== installSemver.major)
|
||||
throw new Error('mismatch');
|
||||
}
|
||||
catch (e) {
|
||||
@@ -105,16 +118,40 @@ export async function installServe(installVersion: string, ignoreError?: boolean
|
||||
}
|
||||
|
||||
export async function serveMain(installVersion?: string) {
|
||||
let install = !!installVersion;
|
||||
|
||||
const { installDir, volume } = cwdInstallDir();
|
||||
if (!fs.existsSync('node_modules/@scrypted/server')) {
|
||||
install = true;
|
||||
installVersion ||= 'latest';
|
||||
console.log('Package @scrypted/server not found. Installing.');
|
||||
if (!installVersion) {
|
||||
try {
|
||||
installVersion = fs.readFileSync(path.join(volume, VERSION_FILE)).toString().trim();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
if (install) {
|
||||
await installServe(installVersion, true);
|
||||
|
||||
const options = ((): { install: true; version: string } | { install: false } => {
|
||||
if (installVersion) {
|
||||
console.log(`Installing @scrypted/server@${installVersion}`);
|
||||
return {
|
||||
install: true,
|
||||
version: installVersion
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync('node_modules/@scrypted/server')) {
|
||||
console.log('Package @scrypted/server not found. Installing.');
|
||||
return {
|
||||
install: true,
|
||||
version: 'latest',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
install: false,
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
if (options.install) {
|
||||
await installServe(options.version, true);
|
||||
}
|
||||
|
||||
// todo: remove at some point after core lxc updater rolls out.
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "Node16",
|
||||
},
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": false,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
|
||||
28
packages/client/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.27",
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
@@ -84,9 +84,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.27.tgz",
|
||||
"integrity": "sha512-XNtlqzqt6rHyNYwWrz3iiickh1h9ACwcLC3rfwxUbFk/Vq/UbDZgp0kGyj9UW6eLVNHzWFSE2dKqyyDS6V2KAg=="
|
||||
"version": "0.3.40",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.40.tgz",
|
||||
"integrity": "sha512-NBjNEfoLp7zL5Tf0odzf191oReDh4FEmZexDmMj1JbKDUMB9S8xJys3vbhcFadU/aUrUkyK/FSbkXv1z87bxSw=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -268,14 +268,14 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"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==",
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.0.tgz",
|
||||
"integrity": "sha512-iBtCdW5Tk3CnMAnC44VO4LwxXnl+RIq9ua1qHvxf5KSq2rzFgQFdfCSSl6Yuz2hl899SWTkfaT3c+WZQ42dJ8A==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -738,15 +738,15 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.27",
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
|
||||
@@ -57,9 +57,9 @@ export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
|
||||
export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
userId?: string;
|
||||
username?: string;
|
||||
admin: boolean;
|
||||
disconnect(): void;
|
||||
onClose?: Function;
|
||||
version: string;
|
||||
rtcConnectionManagement?: RTCConnectionManagement;
|
||||
browserSignalingSession?: BrowserSignalingSession;
|
||||
address?: string;
|
||||
@@ -163,6 +163,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
token: body.token as string,
|
||||
addresses: body.addresses as string[],
|
||||
externalAddresses: body.externalAddresses as string[],
|
||||
hostname: body.hostname,
|
||||
// the cloud plugin will include this header.
|
||||
// should maybe move this into the cloud server itself.
|
||||
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
|
||||
@@ -225,6 +226,7 @@ export interface ScryptedClientLoginResult {
|
||||
scryptedCloud: boolean;
|
||||
directAddress: string;
|
||||
cloudAddress: string;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export class ScryptedClientLoginError extends Error {
|
||||
@@ -270,6 +272,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
let scryptedCloud: boolean;
|
||||
let directAddress: string;
|
||||
let cloudAddress: string;
|
||||
let hostname: string;
|
||||
let token: string;
|
||||
|
||||
console.log('@scrypted/client', packageJson.version);
|
||||
@@ -295,6 +298,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization = loginResult.authorization;
|
||||
queryToken = loginResult.queryToken;
|
||||
token = loginResult.token;
|
||||
hostname = loginResult.hostname;
|
||||
console.log('login result', Date.now() - start, loginResult);
|
||||
}
|
||||
else {
|
||||
@@ -367,6 +371,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization = loginCheck.authorization;
|
||||
queryToken = loginCheck.queryToken;
|
||||
token = loginCheck.token;
|
||||
hostname = loginCheck.hostname;
|
||||
console.log('login checked', Date.now() - start, loginCheck);
|
||||
}
|
||||
|
||||
@@ -518,7 +523,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -532,9 +537,10 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
});
|
||||
serializer.setupRpcPeer(upgradingPeer);
|
||||
|
||||
const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
check.on('close', () => reject(new Error('closed')))
|
||||
})
|
||||
// is this an issue?
|
||||
// const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
// check.on('close', () => reject(new Error('closed')))
|
||||
// })
|
||||
|
||||
upgradingPeer.params['session'] = session;
|
||||
|
||||
@@ -565,7 +571,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
dcSerializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
pc.close();
|
||||
}
|
||||
});
|
||||
@@ -666,7 +672,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
socket.on('message', data => {
|
||||
@@ -708,16 +714,16 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
});
|
||||
}
|
||||
|
||||
const [version, rtcConnectionManagement] = await Promise.all([
|
||||
const [admin, rtcConnectionManagement] = await Promise.all([
|
||||
(async () => {
|
||||
let version = 'unknown';
|
||||
try {
|
||||
// info is
|
||||
const info = await systemManager.getComponent('info');
|
||||
version = await info.getVersion();
|
||||
return !!info;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return version;
|
||||
return false;
|
||||
})(),
|
||||
(async () => {
|
||||
let rtcConnectionManagement: RTCConnectionManagement;
|
||||
@@ -734,7 +740,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
]);
|
||||
|
||||
console.log('api initialized', Date.now() - start);
|
||||
console.log('api queried, version:', version);
|
||||
|
||||
const userDevice = Object.keys(systemManager.getSystemState())
|
||||
.map(id => systemManager.getDeviceById(id))
|
||||
@@ -781,7 +786,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
serializer.setupRpcPeer(clusterPeer);
|
||||
@@ -846,7 +851,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
pluginRemoteAPI: undefined,
|
||||
address,
|
||||
connectionType,
|
||||
version,
|
||||
admin,
|
||||
systemManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
@@ -868,6 +873,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
queryToken,
|
||||
authorization,
|
||||
cloudAddress,
|
||||
hostname,
|
||||
},
|
||||
connectRPCObject,
|
||||
fork: undefined,
|
||||
|
||||
4
packages/deferred/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<details>
|
||||
<summary>Changelog</summary>
|
||||
|
||||
### 0.3.2
|
||||
|
||||
alexa: fix syncedDevices being undefined
|
||||
|
||||
|
||||
### 0.3.1
|
||||
|
||||
alexa/google-home: fix potential vulnerability. do not allow local network control using cloud tokens belonging to a different user. the plugins are now locked to a specific scrypted cloud account once paired.
|
||||
|
||||
4
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
82
plugins/amcrest/dumps/amcrest-face-detected.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"CfgRuleId": 1,
|
||||
"Class": "FaceDetection",
|
||||
"CountInGroup": 2,
|
||||
"DetectRegion": null,
|
||||
"EventID": 10360,
|
||||
"EventSeq": 6,
|
||||
"Faces": [
|
||||
{
|
||||
"BoundingBox": [
|
||||
1504,
|
||||
2336,
|
||||
1728,
|
||||
2704
|
||||
],
|
||||
"Center": [
|
||||
1616,
|
||||
2520
|
||||
],
|
||||
"ObjectID": 94,
|
||||
"ObjectType": "HumanFace",
|
||||
"RelativeID": 0
|
||||
}
|
||||
],
|
||||
"FrameSequence": 8251212,
|
||||
"GroupID": 6,
|
||||
"Mark": 0,
|
||||
"Name": "FaceDetection",
|
||||
"Object": {
|
||||
"Action": "Appear",
|
||||
"BoundingBox": [
|
||||
1504,
|
||||
2336,
|
||||
1728,
|
||||
2704
|
||||
],
|
||||
"Center": [
|
||||
1616,
|
||||
2520
|
||||
],
|
||||
"Confidence": 19,
|
||||
"FrameSequence": 8251212,
|
||||
"ObjectID": 94,
|
||||
"ObjectType": "HumanFace",
|
||||
"RelativeID": 0,
|
||||
"SerialUUID": "",
|
||||
"Source": 0.0,
|
||||
"Speed": 0,
|
||||
"SpeedTypeInternal": 0
|
||||
},
|
||||
"Objects": [
|
||||
{
|
||||
"Action": "Appear",
|
||||
"BoundingBox": [
|
||||
1504,
|
||||
2336,
|
||||
1728,
|
||||
2704
|
||||
],
|
||||
"Center": [
|
||||
1616,
|
||||
2520
|
||||
],
|
||||
"Confidence": 19,
|
||||
"FrameSequence": 8251212,
|
||||
"ObjectID": 94,
|
||||
"ObjectType": "HumanFace",
|
||||
"RelativeID": 0,
|
||||
"SerialUUID": "",
|
||||
"Source": 0.0,
|
||||
"Speed": 0,
|
||||
"SpeedTypeInternal": 0
|
||||
}
|
||||
],
|
||||
"PTS": 43774941350.0,
|
||||
"Priority": 0,
|
||||
"RuleID": 1,
|
||||
"RuleId": 1,
|
||||
"Source": -1280470024.0,
|
||||
"UTC": 947510337,
|
||||
"UTCMS": 0
|
||||
}
|
||||
62
plugins/amcrest/dumps/amcrest-human-detected.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"Action": "Cross",
|
||||
"Class": "Normal",
|
||||
"CountInGroup": 1,
|
||||
"DetectRegion": [
|
||||
[
|
||||
455,
|
||||
260
|
||||
],
|
||||
[
|
||||
3586,
|
||||
260
|
||||
],
|
||||
[
|
||||
3768,
|
||||
7580
|
||||
],
|
||||
[
|
||||
382,
|
||||
7451
|
||||
]
|
||||
],
|
||||
"Direction": "Enter",
|
||||
"EventID": 10181,
|
||||
"GroupID": 0,
|
||||
"Name": "Rule1",
|
||||
"Object": {
|
||||
"Action": "Appear",
|
||||
"BoundingBox": [
|
||||
2856,
|
||||
1280,
|
||||
3880,
|
||||
4880
|
||||
],
|
||||
"Center": [
|
||||
3368,
|
||||
3080
|
||||
],
|
||||
"Confidence": 0,
|
||||
"LowerBodyColor": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"MainColor": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ObjectID": 863,
|
||||
"ObjectType": "Human",
|
||||
"RelativeID": 0,
|
||||
"Speed": 0
|
||||
},
|
||||
"PTS": 43380319830.0,
|
||||
"RuleID": 2,
|
||||
"Track": [],
|
||||
"UTC": 1711446999,
|
||||
"UTCMS": 701
|
||||
}
|
||||
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.150",
|
||||
"version": "0.0.162",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.150",
|
||||
"version": "0.0.162",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.150",
|
||||
"version": "0.0.162",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -27,6 +27,8 @@
|
||||
"name": "Amcrest Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"ScryptedDeviceCreator",
|
||||
"DeviceProvider",
|
||||
"DeviceCreator"
|
||||
],
|
||||
|
||||
@@ -4,103 +4,10 @@ import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
|
||||
import contentType from 'content-type';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import { Destroyable } from '../../rtsp/src/rtsp';
|
||||
import { createRtspMediaStreamOptions, Destroyable, UrlMediaStreamOptions } from '../../rtsp/src/rtsp';
|
||||
import { getDeviceInfo } from './probe';
|
||||
import { Point } from '@scrypted/sdk';
|
||||
import { MediaStreamConfiguration, MediaStreamOptions, Point } from '@scrypted/sdk';
|
||||
|
||||
// Human
|
||||
// {
|
||||
// "Action" : "Cross",
|
||||
// "Class" : "Normal",
|
||||
// "CountInGroup" : 1,
|
||||
// "DetectRegion" : [
|
||||
// [ 455, 260 ],
|
||||
// [ 3586, 260 ],
|
||||
// [ 3768, 7580 ],
|
||||
// [ 382, 7451 ]
|
||||
// ],
|
||||
// "Direction" : "Enter",
|
||||
// "EventID" : 10181,
|
||||
// "GroupID" : 0,
|
||||
// "Name" : "Rule1",
|
||||
// "Object" : {
|
||||
// "Action" : "Appear",
|
||||
// "BoundingBox" : [ 2856, 1280, 3880, 4880 ],
|
||||
// "Center" : [ 3368, 3080 ],
|
||||
// "Confidence" : 0,
|
||||
// "LowerBodyColor" : [ 0, 0, 0, 0 ],
|
||||
// "MainColor" : [ 0, 0, 0, 0 ],
|
||||
// "ObjectID" : 863,
|
||||
// "ObjectType" : "Human",
|
||||
// "RelativeID" : 0,
|
||||
// "Speed" : 0
|
||||
// },
|
||||
// "PTS" : 43380319830.0,
|
||||
// "RuleID" : 2,
|
||||
// "Track" : [],
|
||||
// "UTC" : 1711446999,
|
||||
// "UTCMS" : 701
|
||||
// }
|
||||
|
||||
// Face
|
||||
// {
|
||||
// "CfgRuleId" : 1,
|
||||
// "Class" : "FaceDetection",
|
||||
// "CountInGroup" : 2,
|
||||
// "DetectRegion" : null,
|
||||
// "EventID" : 10360,
|
||||
// "EventSeq" : 6,
|
||||
// "Faces" : [
|
||||
// {
|
||||
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
|
||||
// "Center" : [ 1616, 2520 ],
|
||||
// "ObjectID" : 94,
|
||||
// "ObjectType" : "HumanFace",
|
||||
// "RelativeID" : 0
|
||||
// }
|
||||
// ],
|
||||
// "FrameSequence" : 8251212,
|
||||
// "GroupID" : 6,
|
||||
// "Mark" : 0,
|
||||
// "Name" : "FaceDetection",
|
||||
// "Object" : {
|
||||
// "Action" : "Appear",
|
||||
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
|
||||
// "Center" : [ 1616, 2520 ],
|
||||
// "Confidence" : 19,
|
||||
// "FrameSequence" : 8251212,
|
||||
// "ObjectID" : 94,
|
||||
// "ObjectType" : "HumanFace",
|
||||
// "RelativeID" : 0,
|
||||
// "SerialUUID" : "",
|
||||
// "Source" : 0.0,
|
||||
// "Speed" : 0,
|
||||
// "SpeedTypeInternal" : 0
|
||||
// },
|
||||
// "Objects" : [
|
||||
// {
|
||||
// "Action" : "Appear",
|
||||
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
|
||||
// "Center" : [ 1616, 2520 ],
|
||||
// "Confidence" : 19,
|
||||
// "FrameSequence" : 8251212,
|
||||
// "ObjectID" : 94,
|
||||
// "ObjectType" : "HumanFace",
|
||||
// "RelativeID" : 0,
|
||||
// "SerialUUID" : "",
|
||||
// "Source" : 0.0,
|
||||
// "Speed" : 0,
|
||||
// "SpeedTypeInternal" : 0
|
||||
// }
|
||||
// ],
|
||||
// "PTS" : 43774941350.0,
|
||||
// "Priority" : 0,
|
||||
// "RuleID" : 1,
|
||||
// "RuleId" : 1,
|
||||
// "Source" : -1280470024.0,
|
||||
// "UTC" : 947510337,
|
||||
// "UTCMS" : 0
|
||||
// }
|
||||
export interface AmcrestObjectDetails {
|
||||
Action: string;
|
||||
BoundingBox: Point;
|
||||
@@ -134,7 +41,7 @@ export interface AmcrestEventData {
|
||||
export enum AmcrestEvent {
|
||||
MotionStart = "Code=VideoMotion;action=Start",
|
||||
MotionStop = "Code=VideoMotion;action=Stop",
|
||||
MotionInfo = "Code=VideoMotionInfo;action=State",
|
||||
MotionInfo = "Code=VideoMotionInfo;action=State",
|
||||
AudioStart = "Code=AudioMutation;action=Start",
|
||||
AudioStop = "Code=AudioMutation;action=Stop",
|
||||
TalkInvite = "Code=_DoTalkAction_;action=Invite",
|
||||
@@ -174,6 +81,65 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
function findValue(blob: string, prefix: string, key: string) {
|
||||
const lines = blob.split('\n');
|
||||
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
const parts = value.split('=');
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
function fromAmcrestAudioCodec(audioCodec: string) {
|
||||
audioCodec = audioCodec?.trim();
|
||||
if (audioCodec === 'AAC')
|
||||
return 'aac';
|
||||
if (audioCodec === 'G.711A')
|
||||
return 'pcm_alaw';
|
||||
if (audioCodec === 'G.711Mu')
|
||||
return 'pcm_mulaw';
|
||||
}
|
||||
|
||||
function toAmcrestAudioCodec(audioCodec: string) {
|
||||
if (audioCodec === 'aac')
|
||||
return 'AAC';
|
||||
if (audioCodec === 'pcm_alaw')
|
||||
return 'G.711A';
|
||||
if (audioCodec === 'pcm_mulaw')
|
||||
return 'G.711Mu';
|
||||
}
|
||||
|
||||
function fromAmcrestVideoCodec(videoCodec: string) {
|
||||
videoCodec = videoCodec?.trim();
|
||||
if (videoCodec === 'H.264')
|
||||
videoCodec = 'h264';
|
||||
else if (videoCodec === 'H.265')
|
||||
videoCodec = 'h265';
|
||||
return videoCodec;
|
||||
}
|
||||
|
||||
const amcrestResolutions = {
|
||||
"1080P": [1920, 1080],
|
||||
"720P": [1280, 720],
|
||||
"D1": [704, 480],
|
||||
"HD1": [352, 480],
|
||||
"BCIF": [704, 240],
|
||||
"2CIF": [704, 240],
|
||||
"CIF": [352, 240],
|
||||
"QCIF": [176, 120],
|
||||
"NHD": [640, 360],
|
||||
"VGA": [640, 480],
|
||||
"QVGA": [320, 240]
|
||||
};
|
||||
|
||||
function fromAmcrestResolution(resolution: string) {
|
||||
const named = amcrestResolutions[resolution];
|
||||
if (named)
|
||||
return named;
|
||||
const parts = resolution.split('x');
|
||||
return [parseInt(parts[0]), parseInt(parts[1])];
|
||||
}
|
||||
|
||||
export class AmcrestCameraClient {
|
||||
credential: AuthFetchCredentialState;
|
||||
@@ -263,6 +229,8 @@ export class AmcrestCameraClient {
|
||||
// make content type parsable as content disposition filename
|
||||
const cd = contentType.parse(ct);
|
||||
let { boundary } = cd.parameters;
|
||||
// amcrest may send "--myboundary" or "-- myboundary" (with a space)
|
||||
const altBoundary = `-- ${boundary}`;
|
||||
boundary = `--${boundary}`;
|
||||
const boundaryEnd = `${boundary}--`;
|
||||
|
||||
@@ -286,7 +254,7 @@ export class AmcrestCameraClient {
|
||||
this.console.log('ignoring dahua http body', body);
|
||||
continue;
|
||||
}
|
||||
if (ignore !== boundary) {
|
||||
if (ignore !== boundary && ignore !== altBoundary) {
|
||||
this.console.error('expected boundary but found', ignore);
|
||||
this.console.error(response.headers);
|
||||
throw new Error('expected boundary');
|
||||
@@ -369,6 +337,7 @@ export class AmcrestCameraClient {
|
||||
|
||||
async unlock(): Promise<boolean> {
|
||||
const response = await this.request({
|
||||
// channel 1? this may fail through nvr.
|
||||
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=openDoor&channel=1&UserID=101&Type=Remote`,
|
||||
responseType: 'text',
|
||||
});
|
||||
@@ -377,9 +346,223 @@ export class AmcrestCameraClient {
|
||||
|
||||
async lock(): Promise<boolean> {
|
||||
const response = await this.request({
|
||||
// channel 1? this may fail through nvr.
|
||||
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=closeDoor&channel=1&UserID=101&Type=Remote`,
|
||||
responseType: 'text',
|
||||
});
|
||||
return response.body.includes('OK');
|
||||
}
|
||||
|
||||
|
||||
async resetMotionDetection(cameraNumber: number) {
|
||||
const params = new URLSearchParams();
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].Enable`, 'true');
|
||||
|
||||
// from amcrest docs:
|
||||
// basically a 22x18 binary grid.
|
||||
// so a full cell block is 4194303.
|
||||
|
||||
// Currently, a region is divided into 18 lines and 22 blocks per line.
|
||||
// A bit describes a block in the line.
|
||||
// Bit = 1: motion in this block is monitored.
|
||||
// Example:
|
||||
// MotionDetect [0].Region [0] = 4194303 (0x3FFFFF): the 22 blocks in
|
||||
// channel 0 line 0 is monitored.
|
||||
// MotionDetect [0].Region [1] =0: the 22 blocks in channel 0 line 1 is
|
||||
// not monitored.
|
||||
// MotionDetect [0].Region [17] = 3: the left two blocks in the last line o
|
||||
// channel 0 is monitored.
|
||||
|
||||
// there are 4 configurable motion windows, will use the first one, index 0.
|
||||
// each window is 18 lines, 22 blocks per line.
|
||||
|
||||
// not sure what this first line is.
|
||||
|
||||
// table.MotionDetect[0].Level=3
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Id=0
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Name=Region1
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[0]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[1]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[2]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[3]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[4]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[5]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[6]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[7]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[8]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[9]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[10]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[11]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[12]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[13]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[14]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[15]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[16]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[17]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Sensitive=60
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Threshold=5
|
||||
|
||||
// doesn't seem to be able to be renamed.
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Name`, 'Scrypted');
|
||||
|
||||
for (let i = 0; i < 18; i++) {
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Region[${i}]`, '4194303');
|
||||
}
|
||||
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Sensitive`, '60');
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Threshold`, '5');
|
||||
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reset motion result', response.body);
|
||||
}
|
||||
|
||||
async configureCodecs(cameraNumber: number, options: MediaStreamConfiguration) {
|
||||
if (!options.id?.startsWith('channel'))
|
||||
throw new Error('invalid id');
|
||||
|
||||
const capsResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/encode.cgi?action=getConfigCaps&channel=${cameraNumber}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
this.console.log(capsResponse.body);
|
||||
|
||||
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
|
||||
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
|
||||
const encode = `Encode[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
const params = new URLSearchParams();
|
||||
if (options.video?.bitrate) {
|
||||
let bitrate = options?.video?.bitrate;
|
||||
bitrate = Math.round(bitrate / 1000);
|
||||
params.set(`${encode}.Video.BitRate`, bitrate.toString());
|
||||
}
|
||||
if (options.video?.codec === 'h264') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.264');
|
||||
params.set(`${encode}.VideoEnable`, 'true');
|
||||
}
|
||||
if (options.video?.profile) {
|
||||
let profile = 'Main';
|
||||
if (options.video.profile === 'high')
|
||||
profile = 'High';
|
||||
else if (options.video.profile === 'baseline')
|
||||
profile = 'Baseline';
|
||||
params.set(`${encode}.Video.Profile`, profile);
|
||||
|
||||
}
|
||||
if (options.video?.codec === 'h265') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.265');
|
||||
}
|
||||
if (options.video?.width && options.video?.height) {
|
||||
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
|
||||
}
|
||||
if (options.video?.fps) {
|
||||
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
|
||||
}
|
||||
if (options.video?.keyframeInterval) {
|
||||
params.set(`${encode}.Video.GOP`, options.video?.keyframeInterval.toString());
|
||||
}
|
||||
if (options.video?.bitrateControl) {
|
||||
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'constant' ? 'CBR' : 'VBR');
|
||||
}
|
||||
|
||||
if (options.audio?.codec) {
|
||||
params.set(`${encode}.Audio.Compression`, toAmcrestAudioCodec(options.audio.codec));
|
||||
params.set(`${encode}.AudioEnable`, 'true');
|
||||
}
|
||||
|
||||
// nothing else audio related seems configurable.
|
||||
|
||||
if ([...params.keys()].length) {
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reconfigure result', response.body);
|
||||
}
|
||||
|
||||
const caps = `caps[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
const singleCaps = `caps.${format}[${formatNumber}]`;
|
||||
|
||||
const findCaps = (key: string) => {
|
||||
const found = findValue(capsResponse.body, caps, key);
|
||||
if (found)
|
||||
return found;
|
||||
// ad410 doesnt return a camera number if accessed directly
|
||||
if (cameraNumber - 1 === 0)
|
||||
return findValue(capsResponse.body, singleCaps, key);
|
||||
}
|
||||
|
||||
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
|
||||
const bitrates = findCaps('Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
|
||||
const fpsMax = parseInt(findCaps('Video.FPSMax'));
|
||||
const vso: MediaStreamConfiguration = {
|
||||
id: options.id,
|
||||
video: {},
|
||||
};
|
||||
vso.video.resolutions = resolutions;
|
||||
vso.video.bitrateRange = [bitrates[0], bitrates[bitrates.length - 1]];
|
||||
vso.video.fpsRange = [1, fpsMax];
|
||||
return vso;
|
||||
}
|
||||
|
||||
async getCodecs(cameraNumber: number): Promise<UrlMediaStreamOptions[]> {
|
||||
const masResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
});
|
||||
const mas = masResponse.body.split('=')[1].trim();
|
||||
|
||||
// amcrest reports more streams than are acually available in its responses,
|
||||
// so checking the max extra streams prevents usage of invalid streams.
|
||||
const maxExtraStreams = parseInt(mas) || 1;
|
||||
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => createRtspMediaStreamOptions(undefined, subtype));
|
||||
|
||||
const encodeResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
let encName: string;
|
||||
if (i === 0) {
|
||||
encName = `table.Encode[${cameraNumber - 1}].MainFormat[0]`;
|
||||
}
|
||||
else {
|
||||
encName = `table.Encode[${cameraNumber - 1}].ExtraFormat[${i - 1}]`;
|
||||
}
|
||||
|
||||
const videoCodec = fromAmcrestVideoCodec(findValue(encodeResponse.body, encName, 'Video.Compression'));
|
||||
const audioCodec = fromAmcrestAudioCodec(findValue(encodeResponse.body, encName, 'Audio.Compression'));
|
||||
|
||||
if (vso.audio)
|
||||
vso.audio.codec = audioCodec;
|
||||
vso.video.codec = videoCodec;
|
||||
|
||||
const width = findValue(encodeResponse.body, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.body, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
|
||||
if (videoEnable?.trim() === 'false') {
|
||||
this.console.warn('Video stream is disabled and should likely be enabled:', encName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
vso.video.bitrate = parseInt(encodeOptions) * 1000;
|
||||
}
|
||||
|
||||
return vsos;
|
||||
}
|
||||
}
|
||||
|
||||
24
plugins/amcrest/src/amcrest-configure.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AudioStreamConfiguration, Setting } from '@scrypted/sdk';
|
||||
import { autoconfigureCodecs as ac } from '../../../common/src/autoconfigure-codecs';
|
||||
import { AmcrestCameraClient } from "./amcrest-api";
|
||||
|
||||
export const amcrestAutoConfigureSettings: Setting = {
|
||||
key: 'amcrest-autoconfigure',
|
||||
type: 'html',
|
||||
value: 'Amcrest autoconfiguration will configure the camera codecs and the motion sensor.',
|
||||
};
|
||||
|
||||
export async function autoconfigureSettings(client: AmcrestCameraClient, cameraNumber: number) {
|
||||
const audioOptions: AudioStreamConfiguration = {
|
||||
codec: 'aac',
|
||||
sampleRate: 8000,
|
||||
};
|
||||
|
||||
await client.resetMotionDetection(cameraNumber);
|
||||
|
||||
return ac(
|
||||
() => client.getCodecs(cameraNumber),
|
||||
options => client.configureCodecs(cameraNumber, options),
|
||||
audioOptions,
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,27 @@
|
||||
import { automaticallyConfigureSettings, checkPluginNeedsAutoConfigure } from "@scrypted/common/src/autoconfigure-codecs";
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { PassThrough, Readable, Stream } from "stream";
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { createRtspMediaStreamOptions, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api";
|
||||
import { amcrestAutoConfigureSettings, autoconfigureSettings } from "./amcrest-configure";
|
||||
import { group } from "console";
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
const AMCREST_DOORBELL_TYPE = 'Amcrest Doorbell';
|
||||
const DAHUA_DOORBELL_TYPE = 'Dahua Doorbell';
|
||||
|
||||
function findValue(blob: string, prefix: string, key: string) {
|
||||
const lines = blob.split('\n');
|
||||
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
const parts = value.split('=');
|
||||
return parts[1];
|
||||
}
|
||||
const rtspChannelSetting: Setting = {
|
||||
subgroup: 'Advanced',
|
||||
key: 'rtspChannel',
|
||||
title: 'Channel Number Override',
|
||||
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
|
||||
placeholder: '1',
|
||||
};
|
||||
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, Lock, VideoRecorder, Reboot, ObjectDetector {
|
||||
eventStream: Stream;
|
||||
@@ -110,48 +111,10 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.info = deviceInfo;
|
||||
}
|
||||
|
||||
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
|
||||
if (!options.id?.startsWith('channel'))
|
||||
throw new Error('invalid id');
|
||||
async setVideoStreamOptions(options: MediaStreamOptions) {
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
const formatNumber = parseInt(options.id?.substring('channel'.length)) - 1;
|
||||
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
|
||||
const encode = `Encode[${channel - 1}].${format}[${formatNumber}]`;
|
||||
const params = new URLSearchParams();
|
||||
if (options.video?.bitrate) {
|
||||
let bitrate = options?.video?.bitrate;
|
||||
if (!bitrate)
|
||||
return;
|
||||
bitrate = Math.round(bitrate / 1000);
|
||||
params.set(`${encode}.Video.BitRate`, bitrate.toString());
|
||||
}
|
||||
if (options.video?.codec === 'h264') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.264');
|
||||
}
|
||||
if (options.video?.codec === 'h265') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.265');
|
||||
}
|
||||
if (options.video?.width && options.video?.height) {
|
||||
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
|
||||
}
|
||||
if (options.video?.fps) {
|
||||
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
|
||||
if (options.video?.idrIntervalMillis) {
|
||||
params.set(`${encode}.Video.GOP`, (options.video.fps * options.video?.idrIntervalMillis / 1000).toString());
|
||||
}
|
||||
}
|
||||
if (options.video?.bitrateControl) {
|
||||
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'variable' ? 'VBR' : 'CBR');
|
||||
}
|
||||
|
||||
if (![...params.keys()].length)
|
||||
return;
|
||||
|
||||
const response = await this.getClient().request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reconfigure result', response.body);
|
||||
const client = this.getClient();
|
||||
return client.configureCodecs(channel, options);
|
||||
}
|
||||
|
||||
getClient() {
|
||||
@@ -196,8 +159,9 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
else if (event === AmcrestEvent.MotionInfo) {
|
||||
// this seems to be a motion pulse
|
||||
if (this.motionDetected)
|
||||
resetMotionTimeout();
|
||||
if (!this.motionDetected)
|
||||
this.motionDetected = true;
|
||||
resetMotionTimeout();
|
||||
}
|
||||
else if (event === AmcrestEvent.MotionStop) {
|
||||
// use resetMotionTimeout
|
||||
@@ -283,6 +247,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
const ret = await super.getOtherSettings();
|
||||
ret.push(
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
title: 'Doorbell Type',
|
||||
choices: [
|
||||
'Not a Doorbell',
|
||||
@@ -353,6 +318,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
ret.push(
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
title: 'Two Way Audio',
|
||||
value: twoWayAudio,
|
||||
key: 'twoWayAudio',
|
||||
@@ -369,8 +335,18 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
// },
|
||||
);
|
||||
|
||||
return ret;
|
||||
const ac = {
|
||||
...automaticallyConfigureSettings,
|
||||
subgroup: 'Advanced',
|
||||
};
|
||||
ac.type = 'button';
|
||||
ret.push(ac);
|
||||
ret.push({
|
||||
...amcrestAutoConfigureSettings,
|
||||
subgroup: 'Advanced',
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async takeSmartCameraPicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
||||
@@ -378,15 +354,12 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
async getUrlSettings() {
|
||||
const rtspChannel = {
|
||||
...rtspChannelSetting,
|
||||
value: this.storage.getItem('rtspChannel'),
|
||||
};
|
||||
return [
|
||||
{
|
||||
key: 'rtspChannel',
|
||||
title: 'Channel Number Override',
|
||||
subgroup: 'Advanced',
|
||||
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
|
||||
placeholder: '1',
|
||||
value: this.storage.getItem('rtspChannel'),
|
||||
},
|
||||
rtspChannel,
|
||||
...await super.getUrlSettings(),
|
||||
]
|
||||
}
|
||||
@@ -396,7 +369,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
createRtspMediaStreamOptions(url: string, index: number) {
|
||||
const ret = super.createRtspMediaStreamOptions(url, index);
|
||||
const ret = createRtspMediaStreamOptions(url, index);
|
||||
ret.tool = 'scrypted';
|
||||
return ret;
|
||||
}
|
||||
@@ -404,98 +377,38 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
||||
const client = this.getClient();
|
||||
|
||||
if (!this.videoStreamOptions) {
|
||||
this.videoStreamOptions = (async () => {
|
||||
let mas: string;
|
||||
if (this.videoStreamOptions)
|
||||
return this.videoStreamOptions;
|
||||
|
||||
this.videoStreamOptions = (async () => {
|
||||
const cameraNumber = parseInt(this.getRtspChannel()) || 1;
|
||||
try {
|
||||
let vsos: UrlMediaStreamOptions[];
|
||||
try {
|
||||
const response = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
})
|
||||
mas = response.body.split('=')[1].trim();
|
||||
this.storage.setItem('maxExtraStreams', mas.toString());
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving max extra streams', e);
|
||||
mas = this.storage.getItem('maxExtraStreams');
|
||||
}
|
||||
|
||||
const maxExtraStreams = parseInt(mas) || 1;
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
|
||||
|
||||
try {
|
||||
const capResponse = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(capResponse.body);
|
||||
const encodeResponse = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
let capName: string;
|
||||
let encName: string;
|
||||
if (i === 0) {
|
||||
capName = `caps[${channel - 1}].MainFormat[0]`;
|
||||
encName = `table.Encode[${channel - 1}].MainFormat[0]`;
|
||||
}
|
||||
else {
|
||||
capName = `caps[${channel - 1}].ExtraFormat[${i - 1}]`;
|
||||
encName = `table.Encode[${channel - 1}].ExtraFormat[${i - 1}]`;
|
||||
}
|
||||
|
||||
const videoCodec = findValue(encodeResponse.body, encName, 'Video.Compression')
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
let audioCodec = findValue(encodeResponse.body, encName, 'Audio.Compression')
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
if (audioCodec?.includes('aac'))
|
||||
audioCodec = 'aac';
|
||||
else if (audioCodec?.includes('g711a'))
|
||||
audioCodec = 'pcm_alaw';
|
||||
else if (audioCodec?.includes('g711u'))
|
||||
audioCodec = 'pcm_mulaw';
|
||||
else if (audioCodec?.includes('g711'))
|
||||
audioCodec = 'pcm';
|
||||
|
||||
if (vso.audio)
|
||||
vso.audio.codec = audioCodec;
|
||||
vso.video.codec = videoCodec;
|
||||
|
||||
const width = findValue(encodeResponse.body, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.body, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const bitrateOptions = findValue(capResponse.body, capName, 'Video.BitRateOptions');
|
||||
if (!bitrateOptions)
|
||||
continue;
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
const [min, max] = bitrateOptions.split(',');
|
||||
if (!min || !max)
|
||||
continue;
|
||||
vso.video.bitrate = parseInt(encodeOptions) * 1000;
|
||||
vso.video.maxBitrate = parseInt(max) * 1000;
|
||||
vso.video.minBitrate = parseInt(min) * 1000;
|
||||
}
|
||||
vsos = await client.getCodecs(cameraNumber);
|
||||
this.storage.setItem('vsosJSON', JSON.stringify(vsos));
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving stream configurations', e);
|
||||
vsos = JSON.parse(this.storage.getItem('vsosJSON')) as UrlMediaStreamOptions[];
|
||||
}
|
||||
|
||||
for (const [index, vso] of vsos.entries()) {
|
||||
vso.tool = 'scrypted';
|
||||
vso.url = `rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${index}`;
|
||||
}
|
||||
return vsos;
|
||||
})();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.videoStreamOptions = undefined;
|
||||
const vsos = [...Array(2).keys()].map(subtype => {
|
||||
const ret = createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${subtype}`, subtype);
|
||||
ret.tool = 'scrypted';
|
||||
return ret;
|
||||
});
|
||||
return vsos;
|
||||
}
|
||||
})();
|
||||
|
||||
return this.videoStreamOptions;
|
||||
}
|
||||
@@ -534,6 +447,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
if (key === automaticallyConfigureSettings.key) {
|
||||
const client = this.getClient();
|
||||
autoconfigureSettings(client, parseInt(this.getRtspChannel()) || 1)
|
||||
.then(() => {
|
||||
this.log.a('Successfully configured settings.');
|
||||
})
|
||||
.catch(e => {
|
||||
this.log.a('There was an error automatically configuring settings. More information can be viewed in the console.');
|
||||
this.console.error('error autoconfiguring', e);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'continuousRecording') {
|
||||
if (value === 'true') {
|
||||
try {
|
||||
@@ -575,7 +501,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
// not sure if this all works, since i don't actually have a doorbell.
|
||||
// good luck!
|
||||
const channel = this.getRtspChannel() || '1';
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
|
||||
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
|
||||
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
|
||||
@@ -693,6 +619,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
class AmcrestProvider extends RtspProvider {
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
super(nativeId);
|
||||
checkPluginNeedsAutoConfigure(this);
|
||||
}
|
||||
|
||||
getAdditionalInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.Reboot,
|
||||
@@ -703,6 +634,9 @@ class AmcrestProvider extends RtspProvider {
|
||||
];
|
||||
}
|
||||
|
||||
getScryptedDeviceCreator(): string {
|
||||
return 'Amcrest Camera';
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
||||
const httpAddress = `${settings.ip}:${settings.httpPort || 80}`;
|
||||
@@ -712,8 +646,14 @@ class AmcrestProvider extends RtspProvider {
|
||||
const password = settings.password?.toString();
|
||||
const skipValidate = settings.skipValidate?.toString() === 'true';
|
||||
let twoWayAudio: string;
|
||||
|
||||
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
|
||||
if (settings.autoconfigure) {
|
||||
const cameraNumber = parseInt(settings.rtspChannel as string) || 1;
|
||||
await autoconfigureSettings(api, cameraNumber);
|
||||
}
|
||||
|
||||
if (!skipValidate) {
|
||||
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
|
||||
try {
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
|
||||
@@ -744,8 +684,10 @@ class AmcrestProvider extends RtspProvider {
|
||||
device.info = info;
|
||||
device.putSetting('username', username);
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
if (settings.rtspChannel)
|
||||
device.putSetting('rtspChannel', settings.rtspChannel as string);
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
device.updateDeviceInfo();
|
||||
@@ -768,13 +710,18 @@ class AmcrestProvider extends RtspProvider {
|
||||
title: 'IP Address',
|
||||
placeholder: '192.168.2.222',
|
||||
},
|
||||
rtspChannelSetting,
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
key: 'httpPort',
|
||||
title: 'HTTP Port',
|
||||
description: 'Optional: Override the HTTP Port from the default value of 80',
|
||||
description: 'Optional: Override the HTTP Port from the default value of 80.',
|
||||
placeholder: '80',
|
||||
},
|
||||
automaticallyConfigureSettings,
|
||||
amcrestAutoConfigureSettings,
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
key: 'skipValidate',
|
||||
title: 'Skip Validation',
|
||||
description: 'Add the device without verifying the credentials and network settings.',
|
||||
@@ -786,6 +733,7 @@ class AmcrestProvider extends RtspProvider {
|
||||
createCamera(nativeId: string) {
|
||||
return new AmcrestCamera(nativeId, this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AmcrestProvider;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# BTicino C300X Plugin for Scrypted
|
||||
# BTicino Intercom Plugin for Scrypted
|
||||
|
||||
The C300X Plugin for Scrypted allows viewing your C300X intercom with incoming video/audio.
|
||||
|
||||
|
||||
1167
plugins/bticino/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.15",
|
||||
"version": "0.0.18",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -20,9 +20,11 @@
|
||||
"sip"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "BTicino SIP Plugin",
|
||||
"name": "BTicino Intercom Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"ScryptedDeviceCreator",
|
||||
"DeviceProvider",
|
||||
"DeviceCreator"
|
||||
],
|
||||
@@ -32,16 +34,14 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@slyoldfox/sip": "^0.0.6-1",
|
||||
"sdp": "^3.0.3",
|
||||
"stun": "^2.1.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@slyoldfox/sip": "^0.0.6-1",
|
||||
"sdp": "^3.0.3",
|
||||
"stun": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"cross-env": "^7.0.3",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import { VoicemailHandler } from "./bticino-voicemailHandler";
|
||||
|
||||
export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
|
||||
private timeout : NodeJS.Timeout
|
||||
private voicemailHandler : VoicemailHandler
|
||||
|
||||
constructor(private camera: BticinoSipCamera, private voicemailHandler : VoicemailHandler) {
|
||||
constructor(private camera: BticinoSipCamera) {
|
||||
super( camera.nativeId + "-aswm-switch")
|
||||
this.voicemailHandler = new VoicemailHandler(camera)
|
||||
camera.requestHandlers.add(this.voicemailHandler)
|
||||
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
|
||||
}
|
||||
|
||||
@@ -29,6 +32,7 @@ export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, Http
|
||||
if( this.timeout ) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.voicemailHandler?.cancelTimer()
|
||||
}
|
||||
|
||||
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
|
||||