mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
806 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
711eb222ed | ||
|
|
19f8bfb74a | ||
|
|
08a8428d6e | ||
|
|
4feeeda904 | ||
|
|
753373a691 | ||
|
|
2f3529b822 | ||
|
|
2501d1460b | ||
|
|
e063637100 | ||
|
|
5ec0bf4bf3 | ||
|
|
0c05b59121 | ||
|
|
cbbfa0b525 | ||
|
|
28835b1ccc | ||
|
|
0585e7bbaf | ||
|
|
b2040ea2c8 | ||
|
|
2fd2151b4f | ||
|
|
4c7974519d | ||
|
|
d91c919558 | ||
|
|
7a297761bc | ||
|
|
c15e10e5cf | ||
|
|
3494106857 | ||
|
|
7d3dfb16f0 | ||
|
|
63fc223036 | ||
|
|
6736379858 | ||
|
|
7a811b2b22 | ||
|
|
dd5cb432c9 | ||
|
|
ab3a71ab49 | ||
|
|
b5c9382180 | ||
|
|
81682678ac | ||
|
|
dec184629e | ||
|
|
f33bb53138 | ||
|
|
2d3957e086 | ||
|
|
d16ed9e54f | ||
|
|
d7e8052498 | ||
|
|
48cd3830a5 | ||
|
|
ce138d1a17 | ||
|
|
7b4919fba9 | ||
|
|
0b3dee3a03 | ||
|
|
4cef09540b | ||
|
|
92583e568a | ||
|
|
67aaa08c31 | ||
|
|
2e9f618f6f | ||
|
|
bf4d39d6af | ||
|
|
c31e68f720 | ||
|
|
6d8b3c1ce7 | ||
|
|
106fef95b4 | ||
|
|
488d68ee1c | ||
|
|
f7e35fb1ee | ||
|
|
b1bf897bdb | ||
|
|
8eb533c220 | ||
|
|
f10cdfbced | ||
|
|
8f5e9e5a8c | ||
|
|
cc0283ef39 | ||
|
|
5c7b67c973 | ||
|
|
d1be0f1b4c | ||
|
|
55d58d1e44 | ||
|
|
d9dccf36a3 | ||
|
|
33477fdf80 | ||
|
|
e6ece3aa3e | ||
|
|
6a4126191b | ||
|
|
e9f999b911 | ||
|
|
1fef31a081 | ||
|
|
659f99c33d | ||
|
|
a9deff0046 | ||
|
|
7a56cefe2a | ||
|
|
a06c6e9568 | ||
|
|
56f127a203 | ||
|
|
2ffe67b2db | ||
|
|
44dc648398 | ||
|
|
7807cc4bc6 | ||
|
|
81fb690089 | ||
|
|
8b15617f6e | ||
|
|
fd8aa70352 | ||
|
|
be888d215d | ||
|
|
ce5f568a5d | ||
|
|
336220559f | ||
|
|
8014060a54 | ||
|
|
7f4c8997b9 | ||
|
|
9f73b92dbd | ||
|
|
381892fca6 | ||
|
|
a28df23032 | ||
|
|
dc5456d36f | ||
|
|
3a23e8ed26 | ||
|
|
e0db86cb41 | ||
|
|
37ccefebd1 | ||
|
|
0076c4827f | ||
|
|
c5c07d8169 | ||
|
|
2372acc796 | ||
|
|
6b9c3e4aa0 | ||
|
|
d5b652da8c | ||
|
|
2b9a0f082d | ||
|
|
b10b4d047e | ||
|
|
74cd23bd88 | ||
|
|
ef742bdb23 | ||
|
|
6f7fa54f24 | ||
|
|
d9a575cb5a | ||
|
|
29094afa4d | ||
|
|
62a92fe083 | ||
|
|
9b8bde556c | ||
|
|
326ef11760 | ||
|
|
92a0b4a863 | ||
|
|
9fd3641455 | ||
|
|
2918cf9ae1 | ||
|
|
6f004db859 | ||
|
|
367d741c5f | ||
|
|
8f83894e49 | ||
|
|
ea6e33d159 | ||
|
|
1b5565b5b2 | ||
|
|
19692d02c6 | ||
|
|
4179698c12 | ||
|
|
1eea3a87d0 | ||
|
|
ec89a77955 | ||
|
|
443158286e | ||
|
|
b168ca52c6 | ||
|
|
fe01d3a1ba | ||
|
|
18cad22627 | ||
|
|
c67c9a028c | ||
|
|
0cff8ad5ed | ||
|
|
0269959cf3 | ||
|
|
1b6de42eca | ||
|
|
39342d5d46 | ||
|
|
c4b5af46d0 | ||
|
|
a46235d095 | ||
|
|
848d490a66 | ||
|
|
87fbb95157 | ||
|
|
c036da9ae0 | ||
|
|
1688fcc126 | ||
|
|
99cae0ba31 | ||
|
|
a7b00b9e91 | ||
|
|
3f2a62c6f2 | ||
|
|
3fc318a370 | ||
|
|
aed8575aa0 | ||
|
|
2e28b50588 | ||
|
|
2e87cc380f | ||
|
|
1fdd2d4b01 | ||
|
|
53b23b2ca8 | ||
|
|
54016a9c78 | ||
|
|
d207a3b824 | ||
|
|
e72a74d008 | ||
|
|
d1b907e45b | ||
|
|
4a4c47ffe2 | ||
|
|
f6baf99935 | ||
|
|
b5cc138e2b | ||
|
|
40738a74cf | ||
|
|
d2b1f104ca | ||
|
|
6cb4f589c0 | ||
|
|
5cf2b26630 | ||
|
|
e7f16af04c | ||
|
|
6287b9deaa | ||
|
|
b9b5fdb712 | ||
|
|
c85af9c8a5 | ||
|
|
069f765507 | ||
|
|
0e587abc79 | ||
|
|
47770c0a8d | ||
|
|
82d1c3afe5 | ||
|
|
1c9b52ce4f | ||
|
|
adcd9fa537 | ||
|
|
91e2c2870b | ||
|
|
1fc892815d | ||
|
|
38ed1acc15 | ||
|
|
3bdc9ab930 | ||
|
|
bfa6346333 | ||
|
|
fcbb308cb8 | ||
|
|
f137edcc8c | ||
|
|
53e6f083b9 | ||
|
|
0f96fdb4bc | ||
|
|
96ea3f3b27 | ||
|
|
a31d6482af | ||
|
|
be16bf7858 | ||
|
|
1dad0126bc | ||
|
|
9292ebbe48 | ||
|
|
0b3a1a1998 | ||
|
|
b5d58b6899 | ||
|
|
215a56f70e | ||
|
|
c593701e72 | ||
|
|
46351f2fd7 | ||
|
|
9bce4acd14 | ||
|
|
cba20ec887 | ||
|
|
7c41516cce | ||
|
|
1f209072ba | ||
|
|
8978bff8a9 | ||
|
|
04c500b855 | ||
|
|
8b4859579c | ||
|
|
90deaf1161 | ||
|
|
de56a8c653 | ||
|
|
a5215ae92b | ||
|
|
73cd40b540 | ||
|
|
93556dd404 | ||
|
|
125b436cb6 | ||
|
|
0a4ea032f5 | ||
|
|
c658cee5c9 | ||
|
|
6589176c8b | ||
|
|
6c4c83f655 | ||
|
|
8d4124adda | ||
|
|
b7cda86df7 | ||
|
|
6622e13e51 | ||
|
|
cbc45da679 | ||
|
|
e7d06c66af | ||
|
|
ea02bc3b6f | ||
|
|
2b43cb7d15 | ||
|
|
f3c0362e18 | ||
|
|
817ae42250 | ||
|
|
8043f83f20 | ||
|
|
d33ab5dbcf | ||
|
|
2b1674bea8 | ||
|
|
f045e59258 | ||
|
|
9125aafc07 | ||
|
|
6f5244ec9f | ||
|
|
f1eb2f988a | ||
|
|
1f659d9a72 | ||
|
|
dd98f12f2a | ||
|
|
2063e3822a | ||
|
|
f7495a7a76 | ||
|
|
fddb9c655f | ||
|
|
297e7a7b4f | ||
|
|
29e080f6b6 | ||
|
|
c72ea24794 | ||
|
|
ada80796de | ||
|
|
1ebcf32998 | ||
|
|
79765ba58e | ||
|
|
ff4665520c | ||
|
|
be5b810335 | ||
|
|
fdc99b7fa6 | ||
|
|
f730d13cbd | ||
|
|
af02753cef | ||
|
|
9334d1c2a4 | ||
|
|
71ecc07e2b | ||
|
|
5310dd5ff6 | ||
|
|
adf1a10659 | ||
|
|
2ecc26c914 | ||
|
|
9a49416831 | ||
|
|
f0eff01898 | ||
|
|
edd071739f | ||
|
|
ab81c568bc | ||
|
|
62470df0af | ||
|
|
19b83eb056 | ||
|
|
b75d4cbfd4 | ||
|
|
8c0bb7b205 | ||
|
|
ef64515e56 | ||
|
|
302272e437 | ||
|
|
80e433f6ef | ||
|
|
60786aba2b | ||
|
|
256fde46f6 | ||
|
|
e1a7dd367e | ||
|
|
8612ba3462 | ||
|
|
ab638f26be | ||
|
|
02b881a2d2 | ||
|
|
35475b03e2 | ||
|
|
0b55c777f8 | ||
|
|
68f86d214c | ||
|
|
2abea2d25b | ||
|
|
1c2f17b9f9 | ||
|
|
e3d4800e4f | ||
|
|
d2f175715b | ||
|
|
93c1a699f1 | ||
|
|
41570e9134 | ||
|
|
3ef75854c2 | ||
|
|
c88a638f4e | ||
|
|
793c4da33a | ||
|
|
68f071660e | ||
|
|
8ea5b6aca6 | ||
|
|
2f13c77444 | ||
|
|
981ad183f5 | ||
|
|
8748be82ef | ||
|
|
a347fc2b73 | ||
|
|
002bf3b52c | ||
|
|
72abcd79ec | ||
|
|
86e5b824c7 | ||
|
|
43f6f176f0 | ||
|
|
bc543aa28e | ||
|
|
e90db378e8 | ||
|
|
f2907532aa | ||
|
|
866706505a | ||
|
|
59db3b622c | ||
|
|
7451b9903a | ||
|
|
aded2e43b1 | ||
|
|
031a7527e1 | ||
|
|
2aca568707 | ||
|
|
6b22d34831 | ||
|
|
429d9ec5a6 | ||
|
|
b426668146 | ||
|
|
8bce14f834 | ||
|
|
7511abf768 | ||
|
|
180c12e8cc | ||
|
|
1ed7d03a20 | ||
|
|
9e7b57f154 | ||
|
|
205fdb0222 | ||
|
|
d8f3edee1e | ||
|
|
90c9efc8a6 | ||
|
|
3893ccd776 | ||
|
|
1b154f14bc | ||
|
|
2e3eba4350 | ||
|
|
450f05910a | ||
|
|
22505c9226 | ||
|
|
7120bf86ff | ||
|
|
b49742204f | ||
|
|
6fda76a5e8 | ||
|
|
08bd785d45 | ||
|
|
aa9ddb35aa | ||
|
|
7997c07179 | ||
|
|
a67e24d5dc | ||
|
|
0d4da0dd06 | ||
|
|
993e903f3b | ||
|
|
fbb11a5312 | ||
|
|
ea72d2159b | ||
|
|
1892fdb529 | ||
|
|
1e16793b20 | ||
|
|
2f6c577b47 | ||
|
|
212306449b | ||
|
|
16445bc38e | ||
|
|
b6e9e15d4f | ||
|
|
39abd49ea0 | ||
|
|
05b9b49732 | ||
|
|
1857acac66 | ||
|
|
fedf184847 | ||
|
|
d2afac0dd6 | ||
|
|
6844b55983 | ||
|
|
379dabc182 | ||
|
|
df3c751f2d | ||
|
|
da714d1f94 | ||
|
|
34ee29b7b4 | ||
|
|
4c48f50e01 | ||
|
|
81a5a4349c | ||
|
|
8526c92dcc | ||
|
|
73fefeec26 | ||
|
|
6060b50856 | ||
|
|
d29cd7e421 | ||
|
|
8589283135 | ||
|
|
837dae5f02 | ||
|
|
c26aa2d01e | ||
|
|
c98eca23ab | ||
|
|
eb5d1ac4f6 | ||
|
|
37b0e46dd0 | ||
|
|
042dd84520 | ||
|
|
62d5c145c2 | ||
|
|
1ea3774849 | ||
|
|
9d8345e901 | ||
|
|
9ed850e327 | ||
|
|
957d27b8ef | ||
|
|
b74a957ecb | ||
|
|
debaedfd8c | ||
|
|
0123a97e3c | ||
|
|
a32d47e192 | ||
|
|
90ed8bd3f5 | ||
|
|
c4f4002f55 | ||
|
|
1ea2828e78 | ||
|
|
eb864456df | ||
|
|
51af4f07ff | ||
|
|
f6201acf2a | ||
|
|
96ac479c73 | ||
|
|
0c08875de3 | ||
|
|
bd05fc1b5d | ||
|
|
5a0d325718 | ||
|
|
015794c1d1 | ||
|
|
02d5b429b7 | ||
|
|
e169d154e7 | ||
|
|
01c7b5674a | ||
|
|
a7a1aed0dc | ||
|
|
6bb3f0fd19 | ||
|
|
7828de9d50 | ||
|
|
ea77bb29d0 | ||
|
|
bb184247d0 | ||
|
|
dbc45173ae | ||
|
|
95a23b2882 | ||
|
|
212883e84b | ||
|
|
1200537d62 | ||
|
|
5f6adc9449 | ||
|
|
7d17236ca7 | ||
|
|
028401362a | ||
|
|
69927be4f4 | ||
|
|
ffee1c5cc2 | ||
|
|
ebc3a03e2c | ||
|
|
4246e3c476 | ||
|
|
3fce0838f1 | ||
|
|
2609e301fe | ||
|
|
f4737bf2ac | ||
|
|
fc102aa526 | ||
|
|
9ef33e156f | ||
|
|
881865a0cb | ||
|
|
be5643cc53 | ||
|
|
7e6eba1596 | ||
|
|
27dde776a6 | ||
|
|
b24159a22a | ||
|
|
b6c242b9d5 | ||
|
|
2fbaa12caa | ||
|
|
eb5a497e82 | ||
|
|
66a0ea08ec | ||
|
|
0527baf14a | ||
|
|
c7c5c6eed5 | ||
|
|
143c950c19 | ||
|
|
8d0bb0fa97 | ||
|
|
964274e50c | ||
|
|
e9844528aa | ||
|
|
0609fc8986 | ||
|
|
9331b71433 | ||
|
|
21f8239db7 | ||
|
|
86042ec3fe | ||
|
|
cdb87fb268 | ||
|
|
63dcd35b17 | ||
|
|
951c3b9be6 | ||
|
|
ed642bb3fe | ||
|
|
8093cdd3d9 | ||
|
|
fcbfc3a73f | ||
|
|
94945a48bd | ||
|
|
e360ede5cb | ||
|
|
bc9ec73567 | ||
|
|
cd7e570508 | ||
|
|
1b06c9c11d | ||
|
|
154ab42d15 | ||
|
|
1929f6e8ed | ||
|
|
58bfa17cfe | ||
|
|
38c7006055 | ||
|
|
b5e16b45a9 | ||
|
|
9c13668812 | ||
|
|
a1ca724d6b | ||
|
|
1b032d669c | ||
|
|
c492c15081 | ||
|
|
ee7076384b | ||
|
|
717cac721a | ||
|
|
af41c853bc | ||
|
|
109b716753 | ||
|
|
07930508fe | ||
|
|
a291abe375 | ||
|
|
f4f34b2da8 | ||
|
|
3b4de526ba | ||
|
|
5de67fca86 | ||
|
|
98dc0b1b6d | ||
|
|
a05595ecc7 | ||
|
|
87be4648f1 | ||
|
|
60e51adb41 | ||
|
|
ace7720fe1 | ||
|
|
b9eb74d403 | ||
|
|
fb7353383d | ||
|
|
bee119b486 | ||
|
|
0b6ffc2b87 | ||
|
|
3863527b4d | ||
|
|
51c48f4a1c | ||
|
|
4c138e9b4c | ||
|
|
e762c305a3 | ||
|
|
5bce335288 | ||
|
|
8201e9883a | ||
|
|
74e5884285 | ||
|
|
9cffd9ffbe | ||
|
|
d8b617f2ae | ||
|
|
aeb564aa5d | ||
|
|
45f672883a | ||
|
|
c0ff857a1b | ||
|
|
64f7e31f54 | ||
|
|
6b55f8876e | ||
|
|
718a31f2c5 | ||
|
|
c1e1d50fa5 | ||
|
|
75c4a1939f | ||
|
|
0d703c2aff | ||
|
|
0a6e4fda75 | ||
|
|
4c2de9e443 | ||
|
|
b8a4fedf1a | ||
|
|
79d9f1d4a1 | ||
|
|
983213c578 | ||
|
|
7dd3d71ebd | ||
|
|
493f8deeef | ||
|
|
b29f2d5ee1 | ||
|
|
96bda10123 | ||
|
|
3294700d31 | ||
|
|
0cf77d4c76 | ||
|
|
953841e3a5 | ||
|
|
393c1017df | ||
|
|
f50176d14a | ||
|
|
7f2bf0b542 | ||
|
|
9e3990400c | ||
|
|
95eed80735 | ||
|
|
be43d0c017 | ||
|
|
386ea9a98a | ||
|
|
9b40978f61 | ||
|
|
f0ee435cd0 | ||
|
|
30748784ef | ||
|
|
8310e33719 | ||
|
|
1d18697161 | ||
|
|
d500b3fd6c | ||
|
|
95ae916b6c | ||
|
|
ec3e16f20f | ||
|
|
30d28f543c | ||
|
|
e0cce24999 | ||
|
|
409b25f8b0 | ||
|
|
8f278abec8 | ||
|
|
d6179dab82 | ||
|
|
ed186e2142 | ||
|
|
3c021bb2c8 | ||
|
|
c522edc622 | ||
|
|
022a103bcb | ||
|
|
efd125b6e4 | ||
|
|
19f7688a65 | ||
|
|
7f18e4629c | ||
|
|
dfe2c937a1 | ||
|
|
47d7a23a3d | ||
|
|
0ea609c80c | ||
|
|
71ee5727f1 | ||
|
|
2383f16112 | ||
|
|
7d5defd736 | ||
|
|
cbf4cf0579 | ||
|
|
422dd94e5c | ||
|
|
076f5e27f1 | ||
|
|
645de2e5fd | ||
|
|
dcf24a77d7 | ||
|
|
7065365a47 | ||
|
|
b82520776e | ||
|
|
638c1f77fd | ||
|
|
73a489ea37 | ||
|
|
77d69f025a | ||
|
|
3bc14ad248 | ||
|
|
03e5a9dec1 | ||
|
|
57b790c332 | ||
|
|
ce2ea63be7 | ||
|
|
2dd4721b7f | ||
|
|
667075dfad | ||
|
|
7abdb06b66 | ||
|
|
43e5822c93 | ||
|
|
bc579514e7 | ||
|
|
825100f94e | ||
|
|
803bfc1560 | ||
|
|
b2013a54ed | ||
|
|
f252407935 | ||
|
|
516f2a2a7b | ||
|
|
c1677ce691 | ||
|
|
5028fb812d | ||
|
|
2db4e2579f | ||
|
|
b339ca6cd2 | ||
|
|
f100999cb1 | ||
|
|
2863756bd6 | ||
|
|
cc408850a0 | ||
|
|
ed1ceeda51 | ||
|
|
df09d8e92a | ||
|
|
298ac960d1 | ||
|
|
62d4d55aae | ||
|
|
a2121c0dc5 | ||
|
|
9b5ea27c0b | ||
|
|
0b0e90fc04 | ||
|
|
d8aff609bf | ||
|
|
d8283c261a | ||
|
|
e3aca964be | ||
|
|
a96025c45f | ||
|
|
6afd4b4579 | ||
|
|
f97669949d | ||
|
|
0a0a31574f | ||
|
|
90fb751a22 | ||
|
|
b8d06fada5 | ||
|
|
2cecb1686f | ||
|
|
db03775530 | ||
|
|
cccbc33f1a | ||
|
|
5f23873366 | ||
|
|
e43accae67 | ||
|
|
b3a0cda6f9 | ||
|
|
58c3348282 | ||
|
|
a9e6d76e99 | ||
|
|
3b58936387 | ||
|
|
3a14ab81c8 | ||
|
|
291178a7b5 | ||
|
|
b65faf1a79 | ||
|
|
9d8a1353c0 | ||
|
|
b29d793178 | ||
|
|
d8e406d415 | ||
|
|
4529872fd6 | ||
|
|
fa86c31340 | ||
|
|
94ded75d40 | ||
|
|
887b61cd7a | ||
|
|
48e3d30987 | ||
|
|
02dba3cd71 | ||
|
|
195769034d | ||
|
|
39c08aa378 | ||
|
|
fa8056d38e | ||
|
|
145f116c68 | ||
|
|
15b6f336e4 | ||
|
|
8b46f0a466 | ||
|
|
a20cc5cd89 | ||
|
|
3d068929fd | ||
|
|
928f9b7579 | ||
|
|
c1c5a42645 | ||
|
|
12643cdde2 | ||
|
|
0bff96a6e6 | ||
|
|
4e7e67de54 | ||
|
|
65c4a30004 | ||
|
|
309a1dc11f | ||
|
|
b7904b73b2 | ||
|
|
9e9ddbc5f3 | ||
|
|
ceda54f91b | ||
|
|
1d4052b839 | ||
|
|
6a5d6e6617 | ||
|
|
f55cc6066f | ||
|
|
527714e434 | ||
|
|
8a1633ffa3 | ||
|
|
56b2ab9c4f | ||
|
|
d330e2eb9d | ||
|
|
b55e7cacb3 | ||
|
|
c70375db06 | ||
|
|
2c23021d40 | ||
|
|
84a4ef4539 | ||
|
|
7f3db0549b | ||
|
|
de0e1784a3 | ||
|
|
5a8798638e | ||
|
|
14da49728c | ||
|
|
55423b2d09 | ||
|
|
596106247b | ||
|
|
5472d90368 | ||
|
|
fcf58413fc | ||
|
|
0d03b91753 | ||
|
|
2fd088e4d6 | ||
|
|
c6933198b2 | ||
|
|
210e684a22 | ||
|
|
53cc4b6ef3 | ||
|
|
d58d138a68 | ||
|
|
c0199a2b76 | ||
|
|
badb1905ce | ||
|
|
735c2dce7b | ||
|
|
ffae3f246f | ||
|
|
31b424f89f | ||
|
|
3b7acc3a90 | ||
|
|
7e66d1ac7f | ||
|
|
a613da069e | ||
|
|
40b73c6589 | ||
|
|
ef16ca83a2 | ||
|
|
76bf1d0d3f | ||
|
|
3d5ccf25d1 | ||
|
|
36fcb713d9 | ||
|
|
e306631850 | ||
|
|
17400fa886 | ||
|
|
c6dc628616 | ||
|
|
f974653e73 | ||
|
|
b83880a8a3 | ||
|
|
ee4d8f52df | ||
|
|
3854b75c6e | ||
|
|
07c3173506 | ||
|
|
2894ab1b96 | ||
|
|
99995ea882 | ||
|
|
d6560fbbe4 | ||
|
|
7205583104 | ||
|
|
8479a16d3d | ||
|
|
409aad4794 | ||
|
|
a29d009e5c | ||
|
|
6772419ccf | ||
|
|
38746ee743 | ||
|
|
c5cb3ffa90 | ||
|
|
e119056267 | ||
|
|
590ad3de37 | ||
|
|
6cd412de88 | ||
|
|
33ca0242b1 | ||
|
|
68d3f10888 | ||
|
|
7a844aac84 | ||
|
|
6f2bb9fd9e | ||
|
|
12e47993a4 | ||
|
|
b0396b77bd | ||
|
|
07c2314376 | ||
|
|
cee140e49f | ||
|
|
a3963af6e7 | ||
|
|
8ff28418b3 | ||
|
|
08a5c2f2b3 | ||
|
|
286bd5b19e | ||
|
|
59f3c2e3ad | ||
|
|
ea1b394061 | ||
|
|
5dc1af76e8 | ||
|
|
771bbd834b | ||
|
|
418724f860 | ||
|
|
2ecf48bc60 | ||
|
|
d19b942d2c | ||
|
|
08e724759d | ||
|
|
80031bc80b | ||
|
|
beb53c672c | ||
|
|
0dc75bf737 | ||
|
|
59008fb964 | ||
|
|
b119e5ee00 | ||
|
|
01d0f4c72a | ||
|
|
9fe3f1a4db | ||
|
|
60bf112ebd | ||
|
|
45aa443889 | ||
|
|
08f4922860 | ||
|
|
899970405a | ||
|
|
b4a3960e43 | ||
|
|
0514e62d78 | ||
|
|
3621e58d4c | ||
|
|
506b24026f | ||
|
|
98b67f5d56 | ||
|
|
33c95aa0e8 | ||
|
|
7d8f86bb6c | ||
|
|
d6717cc58b | ||
|
|
673f8e3b2a | ||
|
|
cae87ba414 | ||
|
|
13362fd53e | ||
|
|
d9f2ba0665 | ||
|
|
64a0f90a9a | ||
|
|
88300910a2 | ||
|
|
7face43d54 | ||
|
|
6a9f35ce2a | ||
|
|
effe76f251 | ||
|
|
58d5539cb8 | ||
|
|
d956ee06d0 | ||
|
|
8ddf91d13b | ||
|
|
3f65cd4f6d | ||
|
|
3ffdbf9d2b | ||
|
|
a51754b0e3 | ||
|
|
e8ee21e567 | ||
|
|
420f070035 | ||
|
|
c78cbc04d3 | ||
|
|
dddf565fbe | ||
|
|
0516ca810d | ||
|
|
fac67696a9 | ||
|
|
c62d4bd3fd | ||
|
|
877e1d4992 | ||
|
|
35b5cddd95 | ||
|
|
a86fb128d9 | ||
|
|
983daae971 | ||
|
|
9b687e3286 | ||
|
|
abfd0ffe35 | ||
|
|
407afa1d8c | ||
|
|
9bafe97ef6 | ||
|
|
cb151e79d8 | ||
|
|
7e6230d7b0 | ||
|
|
7d95de389a | ||
|
|
2ce187bc98 | ||
|
|
100671265e | ||
|
|
965d5af631 | ||
|
|
a19f356a66 | ||
|
|
a520357a23 | ||
|
|
d92d130a7c | ||
|
|
c8dd7d2f04 | ||
|
|
b85b589675 | ||
|
|
9b4cbed28f | ||
|
|
6b1794d32f | ||
|
|
aefe4b6849 | ||
|
|
a68395174a | ||
|
|
8a56e789b7 | ||
|
|
06ef146c5b | ||
|
|
4121cbd400 | ||
|
|
2d3bb8798d | ||
|
|
b7b6ac0f87 | ||
|
|
e5fb65d75e | ||
|
|
290b73f3d9 | ||
|
|
f717e87306 | ||
|
|
b80ac7c60d | ||
|
|
997a4732ec | ||
|
|
6e08f11578 | ||
|
|
87c4814e6f | ||
|
|
2e0e009719 | ||
|
|
77399038e9 | ||
|
|
fae66619fb | ||
|
|
d979b9ec0c | ||
|
|
975319a65d | ||
|
|
7b5aa4ba2d | ||
|
|
670739c82b | ||
|
|
8511bd15a8 | ||
|
|
06d3c89274 | ||
|
|
e13f3eb2f1 | ||
|
|
001918d613 | ||
|
|
c859c3aa40 | ||
|
|
2bce019677 | ||
|
|
6ba3386157 | ||
|
|
51e66d98f9 | ||
|
|
6484804649 | ||
|
|
b2a05c099d | ||
|
|
898331da4c | ||
|
|
9044e782b2 | ||
|
|
aedb985941 | ||
|
|
9ba22e4058 | ||
|
|
ab0afb61ae | ||
|
|
bf00ba0adc | ||
|
|
d564cf1b62 | ||
|
|
544dfb3b24 | ||
|
|
21eeab6c3c | ||
|
|
cf9af910be | ||
|
|
e2e65f93af | ||
|
|
b271567428 | ||
|
|
a88a295d9a | ||
|
|
38ba31ca7d | ||
|
|
1c8ff2493b | ||
|
|
5c9f62e6b6 | ||
|
|
6fd8018c52 | ||
|
|
d900ddf5f1 | ||
|
|
e3a8d311ce | ||
|
|
8bbc3d5470 | ||
|
|
00cf987cec | ||
|
|
7e5dcae64a | ||
|
|
cb67237d7c | ||
|
|
4be848c440 | ||
|
|
b33422b066 | ||
|
|
77418684da | ||
|
|
08cf9f7774 | ||
|
|
9f2fabf9c0 | ||
|
|
e2e1c7be44 | ||
|
|
ba030ba197 | ||
|
|
a4f37bdc16 | ||
|
|
f6c7b00562 | ||
|
|
b951614f7c | ||
|
|
f1dfdb3494 | ||
|
|
ffbd25b13b | ||
|
|
4f03fe2420 | ||
|
|
ffdb386afa | ||
|
|
9eeeaa79d0 | ||
|
|
4163142d1e | ||
|
|
71cddc67e0 | ||
|
|
2cbc4eb54f | ||
|
|
fc94fb4221 | ||
|
|
85ed41c590 | ||
|
|
59f889a200 | ||
|
|
7dc476fe02 | ||
|
|
f5070f1ff1 | ||
|
|
18c6edd310 | ||
|
|
a1d7a0d9ca | ||
|
|
5d5078534d | ||
|
|
537a968e2e | ||
|
|
4520d1d29f |
50
.github/workflows/docker-HEAD.yml
vendored
50
.github/workflows/docker-HEAD.yml
vendored
@@ -1,50 +0,0 @@
|
||||
name: Publish Scrypted (git HEAD)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16-bullseye"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image (scrypted)
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: BASE=${{ matrix.node }}
|
||||
context: .
|
||||
file: docker/Dockerfile.HEAD
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted:HEAD
|
||||
ghcr.io/koush/scrypted:HEAD
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
57
.github/workflows/docker-common.yml
vendored
57
.github/workflows/docker-common.yml
vendored
@@ -2,54 +2,71 @@ name: Publish Scrypted Common
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
schedule:
|
||||
# publish the common base once a month.
|
||||
- cron: '30 8 2 * *'
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: ["18"]
|
||||
BUILDPACK_DEPS_BASE: ["bullseye"]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite", "thin"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.124
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.119
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# with:
|
||||
# platforms: linux/arm64,linux/armhf
|
||||
# append: |
|
||||
# - endpoint: ssh://koush@192.168.2.124
|
||||
# # platforms: linux/arm64
|
||||
# platforms: linux/arm64
|
||||
# # - endpoint: ssh://koush@192.168.2.119
|
||||
# # platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
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@v2
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: NODE_VERSION=${{ matrix.NODE_VERSION }}
|
||||
context: docker/
|
||||
file: docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
build-args: |
|
||||
NODE_VERSION=${{ matrix.NODE_VERSION }}
|
||||
BASE=${{ matrix.BASE }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/armhf,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BUILDPACK_DEPS_BASE }}-${{ matrix.FLAVOR }}
|
||||
# ${{ matrix.NODE_VERSION == '16-bullseye' && 'koush/scrypted-common:latest' || '' }}
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
89
.github/workflows/docker.yml
vendored
89
.github/workflows/docker.yml
vendored
@@ -1,48 +1,67 @@
|
||||
name: Publish Scrypted
|
||||
name: Publish Scrypted Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
docker_tag:
|
||||
description: 'Docker Tag'
|
||||
tag:
|
||||
description: "The npm tag used to build the Docker image. The tag will be resolved as a specific version on npm, and that will be used to version the docker image."
|
||||
required: true
|
||||
package_version:
|
||||
description: 'Package Version'
|
||||
publish_tag:
|
||||
description: "The versioned tag for the published Docker image. NPM will use the minor version, Docker should only specify a patch version."
|
||||
required: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["18-bullseye-full", "18-bullseye-lite", "18-bullseye-thin"]
|
||||
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin"]
|
||||
SUPERVISOR: ["", ".s6"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@master
|
||||
|
||||
- name: NPM Package Request
|
||||
id: npm-request
|
||||
uses: fjogeleit/http-request-action@v1
|
||||
with:
|
||||
path: server
|
||||
url: 'https://registry.npmjs.org/@scrypted/server'
|
||||
method: 'GET'
|
||||
|
||||
- name: Print Version
|
||||
run: echo "Version ${{ github.event.inputs.package_version || steps.package-version.outputs.current-version }}"
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Set NPM Version
|
||||
id: package-version
|
||||
run: echo "NPM_VERSION=${{ fromJson(steps.npm-request.outputs.response)['dist-tags'][ github.event.inputs.tag] }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.124
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.119
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# with:
|
||||
# platforms: linux/arm64,linux/armhf
|
||||
# append: |
|
||||
# - endpoint: ssh://koush@192.168.2.124
|
||||
# # platforms: linux/arm64
|
||||
# platforms: linux/arm64
|
||||
# # - endpoint: ssh://koush@192.168.2.119
|
||||
# # platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -56,25 +75,31 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image (scrypted)
|
||||
uses: docker/build-push-action@v3
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
BASE=${{ matrix.BASE }}
|
||||
SCRYPTED_INSTALL_VERSION=${{ github.event.inputs.package_version }}
|
||||
context: docker/
|
||||
file: docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
|
||||
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:thin-s6' || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
|
||||
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
|
||||
${{ 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 == '18-jammy-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:thin-s6' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
60
.github/workflows/test.yml
vendored
Normal file
60
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["install/**", ".github/workflows/test.yml"]
|
||||
pull_request:
|
||||
paths: ["install/**", ".github/workflows/test.yml"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test_linux_local:
|
||||
name: Test Linux local installation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run install script
|
||||
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/
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
.DS_Store
|
||||
__pycache__
|
||||
venv
|
||||
.venv
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,6 +1,3 @@
|
||||
[submodule "plugins/homekit/HAP-NodeJS"]
|
||||
path = external/HAP-NodeJS
|
||||
url = ../../koush/HAP-NodeJS
|
||||
[submodule "plugins/unifi-protect/src/unifi-protect"]
|
||||
path = external/unifi-protect
|
||||
url = ../../koush/unifi-protect.git
|
||||
@@ -35,9 +32,6 @@
|
||||
[submodule "plugins/sample-cameraprovider"]
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
[submodule "plugins/tensorflow-lite/sort_oh"]
|
||||
path = plugins/sort-tracker/sort_oh
|
||||
url = ../../koush/sort_oh.git
|
||||
[submodule "plugins/cloud/node-nat-upnp"]
|
||||
path = plugins/cloud/node-nat-upnp
|
||||
url = ../../koush/node-nat-upnp.git
|
||||
|
||||
58
README.md
58
README.md
@@ -1,58 +1,20 @@
|
||||
# Scrypted
|
||||
|
||||
Scrypted is a high performance home video integration and automation platform.
|
||||
* Video load instantly, everywhere: [Demo](https://www.reddit.com/r/homebridge/comments/r34k6b/if_youre_using_homebridge_for_cameras_ditch_it/)
|
||||
* [HomeKit Secure Video Support](https://github.com/koush/scrypted/wiki/HomeKit-Secure-Video-Setup)
|
||||
* Google Home support: "Ok Google, Stream Backyard"
|
||||
* Alexa Support: Streaming to Alexa app on iOS/Android and Echo Show.
|
||||
Scrypted is a high performance home video integration platform and NVR with smart detections. [Instant, low latency, streaming](https://streamable.com/xbxn7z) to HomeKit, Google Home, and Alexa. Supports most cameras. [Learn more](https://docs.scrypted.app).
|
||||
|
||||
<img width="400" alt="Scrypted_Management_Console" src="https://user-images.githubusercontent.com/73924/185666320-ae972867-6c2c-488a-8413-fd8a215e9fee.png">
|
||||
<img src="https://github.com/koush/scrypted/assets/73924/57e1d556-cd3d-4448-81f9-a6c51b6513de">
|
||||
|
||||
# Installation
|
||||
## Installation and Documentation
|
||||
|
||||
Select the appropriate guide. After installation is finished, remember to visit [HomeKit Secure Video Setup](https://github.com/koush/scrypted/wiki/HomeKit-Secure-Video-Setup).
|
||||
Installation and camera onboarding instructions can be found in the [docs](https://docs.scrypted.app).
|
||||
|
||||
* [Raspberry Pi](https://github.com/koush/scrypted/wiki/Installation:-Raspberry-Pi)
|
||||
* Linux
|
||||
* [Docker Compose](https://github.com/koush/scrypted/wiki/Installation:-Docker-Compose-Linux) - This is the recommended method. Local installation may interfere with other server software.
|
||||
* [Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Linux) - Use Docker Compose. This is a reference documentation.
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Linux) - Use this if Docker scares you or whatever.
|
||||
* Mac
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Mac)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* Windows
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Windows)
|
||||
* [WSL2 Installation](https://github.com/koush/scrypted/wiki/Installation:-WSL2-Windows)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* [ReadyNAS: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-ReadyNAS)
|
||||
* [Synology: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Synology-NAS)
|
||||
* [QNAP: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-QNAP-NAS)
|
||||
* [Unraid: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Unraid)
|
||||
|
||||
## Discord
|
||||
|
||||
Chat on Discord for support, tips, announcements, and bug reporting. There is an active and helpful community.
|
||||
|
||||
[Join Scrypted Discord](https://discord.gg/DcFzmBHYGq)
|
||||
|
||||
## Wiki
|
||||
|
||||
There are many topics covered in the [Scrypted Wiki](https://github.com/koush/scrypted/wiki) sidebar. Review them for documented support, tips, and guides before asking for assistance on GitHub or Discord.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
* Google Home
|
||||
* Apple HomeKit
|
||||
* Amazon Alexa
|
||||
|
||||
Supported accessories:
|
||||
* Camera and Core Plugins: https://github.com/koush/scrypted/tree/main/plugins
|
||||
* Community Plugins: https://github.com/orgs/scryptedapp/repositories
|
||||
## Community
|
||||
|
||||
Scrypted has active communities on [Discord](https://discord.gg/DcFzmBHYGq), [Reddit](https://reddit.com/r/scrypted), and [Github](https://github.com/koush/scrypted). Check them out if you have questions!
|
||||
|
||||
## Development
|
||||
|
||||
## Debug Scrypted Plugins in VSCode
|
||||
## Debug Scrypted Plugins in VS Code
|
||||
|
||||
```sh
|
||||
# this is an example for homekit.
|
||||
@@ -65,7 +27,7 @@ cd scrypted
|
||||
code plugins/homekit
|
||||
```
|
||||
|
||||
You can now launch (using the Start Debugging play button) the HomeKit Plugin in VSCode. Please be aware that you do *not* need to restart the Scrypted Server if you make changes to a plugin. Edit the plugin, launch, and the updated plugin will deploy on the running server.
|
||||
You can now launch (using the Start Debugging play button) the HomeKit Plugin in VS Code. Please be aware that you do *not* need to restart the Scrypted Server if you make changes to a plugin. Edit the plugin, launch, and the updated plugin will deploy on the running server.
|
||||
|
||||
If you do not want to set up VS Code, you can also run build and install the plugin directly from the command line:
|
||||
|
||||
@@ -79,7 +41,7 @@ npm run build && npm run scrypted-deploy 127.0.0.1
|
||||
Want to write your own plugin? Full documentation is available here: https://developer.scrypted.app
|
||||
|
||||
|
||||
## Debug the Scrypted Server in VSCode
|
||||
## Debug the Scrypted Server in VS Code
|
||||
|
||||
Debugging the server should not be necessary, as the server only provides the hosting and RPC mechanism for plugins. The following is for reference purpose. Most development can be done by debugging the relevant plugin.
|
||||
|
||||
@@ -93,4 +55,4 @@ cd scrypted
|
||||
code server
|
||||
```
|
||||
|
||||
You can now launch the Scrypted Server in VSCode.
|
||||
You can now launch the Scrypted Server in VS Code.
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
export class Deferred<T> {
|
||||
finished = false;
|
||||
resolve!: (value: T|PromiseLike<T>) => void;
|
||||
reject!: (error: Error) => void;
|
||||
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;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import { EventEmitter } from 'events';
|
||||
import { Server } from 'net';
|
||||
import { Duplex } from 'stream';
|
||||
import { cloneDeep } from './clone-deep';
|
||||
import { Deferred } from "./deferred";
|
||||
import { listenZeroSingleClient } from './listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from './media-helpers';
|
||||
import { createRtspParser } from "./rtsp-server";
|
||||
import { parseSdp } from "./sdp-utils";
|
||||
import { StreamChunk, StreamParser } from './stream-parser';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
@@ -57,9 +60,13 @@ export async function parseResolution(cp: ChildProcess) {
|
||||
}
|
||||
|
||||
async function parseInputToken(cp: ChildProcess, token: string) {
|
||||
let processed = 0;
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
cp.on('exit', () => reject(new Error('ffmpeg exited while waiting to parse stream information: ' + token)));
|
||||
const parser = (data: Buffer) => {
|
||||
processed += data.length;
|
||||
if (processed > 10000)
|
||||
return resolve(undefined);
|
||||
const stdout: string = data.toString().split('Output ')[0];
|
||||
const idx = stdout.lastIndexOf(`${token}: `);
|
||||
if (idx !== -1) {
|
||||
@@ -77,7 +84,11 @@ async function parseInputToken(cp: ChildProcess, token: string) {
|
||||
};
|
||||
cp.stdout.on('data', parser);
|
||||
cp.stderr.on('data', parser);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
cp.stdout.removeAllListeners('data');
|
||||
cp.stderr.removeAllListeners('data');
|
||||
});
|
||||
}
|
||||
|
||||
export async function parseVideoCodec(cp: ChildProcess) {
|
||||
@@ -158,8 +169,6 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
|
||||
const args = ffmpegInput.inputArguments.slice();
|
||||
|
||||
let needSdp = false;
|
||||
|
||||
const ensureActive = (killed: () => void) => {
|
||||
if (!isActive) {
|
||||
killed();
|
||||
@@ -211,11 +220,6 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
}
|
||||
}
|
||||
|
||||
if (needSdp) {
|
||||
args.push('-sdp_file', `pipe:${pipeCount++}`);
|
||||
stdio.push('pipe');
|
||||
}
|
||||
|
||||
// start ffmpeg process with child process pipes
|
||||
args.unshift('-hide_banner');
|
||||
safePrintFFmpegArguments(console, args);
|
||||
@@ -225,20 +229,7 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
ffmpegLogInitialOutput(console, cp, undefined, options?.storage);
|
||||
cp.on('exit', () => kill(new Error('ffmpeg exited')));
|
||||
|
||||
let sdp: Promise<Buffer[]>;
|
||||
if (needSdp) {
|
||||
sdp = new Promise<Buffer[]>(resolve => {
|
||||
const ret: Buffer[] = [];
|
||||
cp.stdio[pipeCount - 1].on('data', buffer => {
|
||||
ret.push(buffer);
|
||||
resolve(ret);
|
||||
});
|
||||
})
|
||||
}
|
||||
else {
|
||||
sdp = Promise.resolve([]);
|
||||
}
|
||||
|
||||
const deferredStart = new Deferred<void>();
|
||||
// now parse the created pipes
|
||||
const start = () => {
|
||||
for (const p of startParsers) {
|
||||
@@ -257,6 +248,7 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
const { resetActivityTimer } = setupActivityTimer(container, kill, events, options?.timeout);
|
||||
|
||||
for await (const chunk of parser.parse(pipe as any, parseInt(inputVideoResolution?.[2]), parseInt(inputVideoResolution?.[3]))) {
|
||||
await deferredStart.promise;
|
||||
events.emit(container, chunk);
|
||||
resetActivityTimer();
|
||||
}
|
||||
@@ -268,14 +260,26 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
});
|
||||
};
|
||||
|
||||
// tbh parsing stdout is super sketchy way of doing this.
|
||||
parseAudioCodec(cp).then(result => inputAudioCodec = result);
|
||||
parseResolution(cp).then(result => inputVideoResolution = result);
|
||||
await parseVideoCodec(cp).then(result => inputVideoCodec = result);
|
||||
const rtsp = (options.parsers as any).rtsp as ReturnType<typeof createRtspParser>;
|
||||
rtsp.sdp.then(sdp => {
|
||||
const parsed = parseSdp(sdp);
|
||||
const audio = parsed.msections.find(msection => msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection => msection.type === 'video');
|
||||
inputVideoCodec = video?.codec;
|
||||
inputAudioCodec = audio?.codec;
|
||||
});
|
||||
|
||||
const sdp = new Deferred<Buffer[]>();
|
||||
rtsp.sdp.then(r => sdp.resolve([Buffer.from(r)]));
|
||||
killed.then(() => sdp.reject(new Error("ffmpeg killed before sdp could be parsed")));
|
||||
|
||||
start();
|
||||
|
||||
return {
|
||||
start,
|
||||
sdp,
|
||||
start() {
|
||||
deferredStart.resolve();
|
||||
},
|
||||
sdp: sdp.promise,
|
||||
get inputAudioCodec() {
|
||||
return inputAudioCodec;
|
||||
},
|
||||
@@ -361,8 +365,7 @@ export interface RebroadcasterOptions {
|
||||
},
|
||||
}
|
||||
|
||||
export async function handleRebroadcasterClient(duplex: Promise<Duplex> | Duplex, options?: RebroadcasterOptions) {
|
||||
const socket = await duplex;
|
||||
export function handleRebroadcasterClient(socket: Duplex, options?: RebroadcasterOptions) {
|
||||
const firstWriteData = (data: StreamChunk) => {
|
||||
if (data.startStream) {
|
||||
socket.write(data.startStream)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import net from 'net';
|
||||
import { once } from 'events';
|
||||
import dgram, { SocketType } from 'dgram';
|
||||
import { once } from 'events';
|
||||
import net from 'net';
|
||||
|
||||
export async function closeQuiet(socket: dgram.Socket | net.Server) {
|
||||
if (!socket)
|
||||
@@ -37,6 +37,23 @@ export async function createBindZero(socketType?: SocketType) {
|
||||
return createBindUdp(0, socketType);
|
||||
}
|
||||
|
||||
export async function createSquentialBindZero(socketType?: SocketType) {
|
||||
let attempts = 0;
|
||||
while (true) {
|
||||
const rtpServer = await createBindZero(socketType);
|
||||
try {
|
||||
const rtcpServer = await createBindUdp(rtpServer.port + 1, socketType);
|
||||
return [rtpServer, rtcpServer];
|
||||
}
|
||||
catch (e) {
|
||||
attempts++;
|
||||
closeQuiet(rtpServer.server);
|
||||
}
|
||||
if (attempts === 10)
|
||||
throw new Error('unable to reserve sequential udp ports')
|
||||
}
|
||||
}
|
||||
|
||||
export async function reserveUdpPort() {
|
||||
const udp = await createBindZero();
|
||||
await new Promise(resolve => udp.server.close(() => resolve(undefined)));
|
||||
@@ -62,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export { listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
|
||||
@@ -250,7 +250,8 @@ export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
function logSendCandidate(console: Console, type: string, session: RTCSignalingSession): RTCSignalingSendIceCandidate {
|
||||
return async (candidate) => {
|
||||
try {
|
||||
console.log(`${type} trickled candidate:`, candidate.sdpMLineIndex, candidate.candidate);
|
||||
if (localStorage.getItem('debugLog') === 'true')
|
||||
console.log(`${type} trickled candidate:`, candidate.sdpMLineIndex, candidate.candidate);
|
||||
await session.addIceCandidate(candidate);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -297,7 +298,7 @@ export async function connectRTCSignalingClients(
|
||||
if (offerOptions?.offer && answerOptions?.offer)
|
||||
throw new Error('Both RTC clients have offers and can not negotiate. Consider implementing this in @scrypted/webrtc.');
|
||||
|
||||
if (offerOptions?.requiresOffer && answerOptions.requiresOffer)
|
||||
if (offerOptions?.requiresOffer && answerOptions?.requiresOffer)
|
||||
throw new Error('Both RTC clients require offers and can not negotiate.');
|
||||
|
||||
offerSetup.type = 'offer';
|
||||
@@ -308,11 +309,13 @@ export async function connectRTCSignalingClients(
|
||||
|
||||
const offer = await offerClient.createLocalDescription('offer', offerSetup as RTCAVSignalingSetup,
|
||||
disableTrickle ? undefined : answerQueue.queueSendCandidate);
|
||||
console.log('offer sdp', offer.sdp);
|
||||
if (localStorage.getItem('debugLog') === 'true')
|
||||
console.log('offer sdp', offer.sdp);
|
||||
await answerClient.setRemoteDescription(offer, answerSetup as RTCAVSignalingSetup);
|
||||
const answer = await answerClient.createLocalDescription('answer', answerSetup as RTCAVSignalingSetup,
|
||||
disableTrickle ? undefined : offerQueue.queueSendCandidate);
|
||||
console.log('answer sdp', answer.sdp);
|
||||
if (localStorage.getItem('debugLog') === 'true')
|
||||
console.log('answer sdp', answer.sdp);
|
||||
await offerClient.setRemoteDescription(answer, offerSetup as RTCAVSignalingSetup);
|
||||
offerQueue.flush();
|
||||
answerQueue.flush();
|
||||
|
||||
@@ -6,8 +6,9 @@ import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
|
||||
import net from 'net';
|
||||
import { Duplex, Readable, Writable } from 'stream';
|
||||
import tls from 'tls';
|
||||
import { URL } from 'url';
|
||||
import { Deferred } from './deferred';
|
||||
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { closeQuiet, createBindZero, createSquentialBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { timeoutPromise } from './promise-utils';
|
||||
import { readLength, readLine } from './read-stream';
|
||||
import { MSection, parseSdp } from './sdp-utils';
|
||||
@@ -128,6 +129,16 @@ export function getNaluTypes(streamChunk: StreamChunk) {
|
||||
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;
|
||||
@@ -184,48 +195,17 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
let foundIndex: number;
|
||||
let nonVideo: {
|
||||
[codec: string]: StreamChunk,
|
||||
} = {};
|
||||
|
||||
const createSyncFrame = () => {
|
||||
const ret = streamChunks.slice(foundIndex);
|
||||
// for (const nv of Object.values(nonVideo)) {
|
||||
// ret.unshift(nv);
|
||||
// }
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
nonVideo[streamChunk.type] = streamChunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS))
|
||||
foundIndex = prebufferIndex;
|
||||
}
|
||||
|
||||
if (foundIndex !== undefined)
|
||||
return createSyncFrame();
|
||||
|
||||
nonVideo = {};
|
||||
// some streams don't contain codec info, so find an idr frame instead.
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
nonVideo[streamChunk.type] = streamChunk;
|
||||
continue;
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS) || findH264NaluType(streamChunk, H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_IDR))
|
||||
foundIndex = prebufferIndex;
|
||||
}
|
||||
|
||||
if (foundIndex !== undefined)
|
||||
return createSyncFrame();
|
||||
|
||||
// oh well!
|
||||
},
|
||||
sdp: new Promise<string>(r => resolve = r),
|
||||
@@ -580,7 +560,7 @@ export class RtspClient extends RtspBase {
|
||||
const username = decodeURIComponent(authedUrl.username);
|
||||
const password = decodeURIComponent(authedUrl.password);
|
||||
|
||||
const strippedUrl = new URL(url);
|
||||
const strippedUrl = new URL(url.toString());
|
||||
strippedUrl.username = '';
|
||||
strippedUrl.password = '';
|
||||
|
||||
@@ -670,7 +650,7 @@ export class RtspClient extends RtspBase {
|
||||
});
|
||||
}
|
||||
|
||||
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions) {
|
||||
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions, headers?: Headers) {
|
||||
const protocol = options.type === 'udp' ? '' : '/TCP';
|
||||
const client = options.type === 'udp' ? 'client_port' : 'interleaved';
|
||||
let port: number;
|
||||
@@ -686,9 +666,9 @@ export class RtspClient extends RtspBase {
|
||||
port = options.dgram.address().port;
|
||||
options.dgram.on('message', data => options.onRtp(undefined, data));
|
||||
}
|
||||
const headers: any = {
|
||||
headers = Object.assign({
|
||||
Transport: `RTP/AVP${protocol};unicast;${client}=${port}-${port + 1}`,
|
||||
};
|
||||
}, headers);
|
||||
const response = await this.request('SETUP', headers, options.path);
|
||||
let interleaved: {
|
||||
begin: number;
|
||||
@@ -953,8 +933,7 @@ export class RtspServer {
|
||||
const match = transport.match(/.*?client_port=([0-9]+)-([0-9]+)/);
|
||||
const [_, rtp, rtcp] = match;
|
||||
|
||||
const rtpServer = await createBindZero();
|
||||
const rtcpServer = await createBindUdp(rtpServer.port + 1);
|
||||
const [rtpServer, rtcpServer] = await createSquentialBindZero();
|
||||
this.client.on('close', () => closeQuiet(rtpServer.server));
|
||||
this.client.on('close', () => closeQuiet(rtcpServer.server));
|
||||
this.setupTracks[msection.control] = {
|
||||
|
||||
@@ -217,14 +217,12 @@ const acontrol = 'a=control:';
|
||||
const artpmap = 'a=rtpmap:';
|
||||
export function parseMSection(msection: string[]) {
|
||||
const control = msection.find(line => line.startsWith(acontrol))?.substring(acontrol.length);
|
||||
const rtpmapFirst = msection.find(line => line.startsWith(artpmap));
|
||||
const mline = parseMLine(msection[0]);
|
||||
|
||||
let codec = parseRtpMap(mline.type, rtpmapFirst).codec;
|
||||
|
||||
const rtpmaps = msection.filter(line => line.startsWith(artpmap)).map(line => parseRtpMap(mline.type, line));
|
||||
|
||||
const rawRtpmaps = msection.filter(line => line.startsWith(artpmap));
|
||||
const rtpmaps = rawRtpmaps.map(line => parseRtpMap(mline.type, line));
|
||||
const codec = parseRtpMap(mline.type, rawRtpmaps[0]).codec;
|
||||
let direction: string;
|
||||
|
||||
for (const checkDirection of ['sendonly', 'sendrecv', 'recvonly', 'inactive']) {
|
||||
const found = msection.find(line => line === 'a=' + checkDirection);
|
||||
if (found) {
|
||||
|
||||
56
common/test/rtsp-proxy.ts
Normal file
56
common/test/rtsp-proxy.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import net from 'net';
|
||||
import { listenZero } from '../src/listen-cluster';
|
||||
import { RtspClient, RtspServer } from '../src/rtsp-server';
|
||||
|
||||
async function main() {
|
||||
const server = net.createServer(async serverSocket => {
|
||||
const client = new RtspClient('rtsp://localhost:57594/911db962087f904d');
|
||||
await client.options();
|
||||
const describeResponse = await client.describe();
|
||||
const sdp = describeResponse.body.toString();
|
||||
const server = new RtspServer(serverSocket, sdp, true);
|
||||
const setupResponse = await server.handlePlayback();
|
||||
if (setupResponse !== 'play') {
|
||||
serverSocket.destroy();
|
||||
client.client.destroy();
|
||||
return;
|
||||
}
|
||||
console.log('playback handled');
|
||||
|
||||
let channel = 0;
|
||||
for (const track of Object.keys(server.setupTracks)) {
|
||||
const setupTrack = server.setupTracks[track];
|
||||
await client.setup({
|
||||
// type: 'udp',
|
||||
|
||||
type: 'tcp',
|
||||
port: channel,
|
||||
|
||||
path: setupTrack.control,
|
||||
onRtp(rtspHeader, rtp) {
|
||||
server.sendTrack(setupTrack.control, rtp, false);
|
||||
},
|
||||
});
|
||||
|
||||
channel += 2;
|
||||
}
|
||||
|
||||
|
||||
await client.play();
|
||||
console.log('client playing');
|
||||
await client.readLoop();
|
||||
});
|
||||
|
||||
let port: number;
|
||||
if (false) {
|
||||
port = await listenZero(server);
|
||||
}
|
||||
else {
|
||||
port = 5555;
|
||||
server.listen(5555)
|
||||
}
|
||||
|
||||
console.log(`rtsp://127.0.0.1:${port}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,81 +0,0 @@
|
||||
################################################################
|
||||
# THIS FILE IS GENERATED. DO NOT EDIT.
|
||||
################################################################
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.header
|
||||
# This common file will be used by both Docker and the linux
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install libedgetpu1-std
|
||||
|
||||
RUN apt-get -y install software-properties-common apt-utils
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y upgrade
|
||||
|
||||
# base development stuff
|
||||
RUN apt-get -y install \
|
||||
build-essential \
|
||||
cmake \
|
||||
gcc \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-gi \
|
||||
python3-gst-1.0 \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-skimage \
|
||||
python3-wheel
|
||||
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
ENV SCRYPTED_DOCKER_SERVE="true"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
@@ -1,45 +0,0 @@
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y upgrade
|
||||
RUN apt-get -y install software-properties-common apt-utils
|
||||
RUN apt-get -y update
|
||||
|
||||
# base development stuff
|
||||
RUN apt-get -y install \
|
||||
build-essential \
|
||||
gcc \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-gi \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
|
||||
|
||||
ENV SCRYPTED_DOCKER_SERVE="true"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
@@ -1,30 +0,0 @@
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y upgrade
|
||||
RUN apt-get -y install software-properties-common apt-utils
|
||||
RUN apt-get -y update
|
||||
|
||||
# base development stuff
|
||||
RUN apt-get -y install \
|
||||
build-essential \
|
||||
gcc \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
|
||||
|
||||
ENV SCRYPTED_DOCKER_SERVE="true"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$SCRYPTED_DOCKER_AVAHI" ]
|
||||
then
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
done
|
||||
fi
|
||||
|
||||
until [ -e /var/run/dbus/system_bus_socket ]; do
|
||||
echo "Waiting for dbus..."
|
||||
sleep 1s
|
||||
done
|
||||
echo "Starting Avahi daemon..."
|
||||
exec avahi-daemon --no-chroot -f /etc/avahi/avahi-daemon.conf
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Starting dbus..."
|
||||
exec dbus-daemon --system --nofork
|
||||
@@ -1,13 +0,0 @@
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
ENV SCRYPTED_DOCKER_SERVE="true"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
@@ -1,65 +0,0 @@
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.header
|
||||
# This common file will be used by both Docker and the linux
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BUILDPACK_DEPS_BASE="bullseye"
|
||||
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y install libedgetpu1-std
|
||||
|
||||
RUN apt-get -y install software-properties-common apt-utils
|
||||
RUN apt-get -y update
|
||||
RUN apt-get -y upgrade
|
||||
|
||||
# base development stuff
|
||||
RUN apt-get -y install \
|
||||
build-essential \
|
||||
cmake \
|
||||
gcc \
|
||||
libgirepository1.0-dev \
|
||||
libglib2.0-dev \
|
||||
pkg-config
|
||||
|
||||
# ffmpeg
|
||||
RUN apt-get -y install \
|
||||
ffmpeg
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-gi \
|
||||
python3-gst-1.0 \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-skimage \
|
||||
python3-wheel
|
||||
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
1
external/HAP-NodeJS
vendored
1
external/HAP-NodeJS
vendored
Submodule external/HAP-NodeJS deleted from 3fe1f920f5
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: d571cdfc00...4e95093f76
2
external/unifi-protect
vendored
2
external/unifi-protect
vendored
Submodule external/unifi-protect updated: 1f40c63e7f...3759ba334f
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 140faa891d...9815d03344
48
install/config.yaml
Executable file
48
install/config.yaml
Executable file
@@ -0,0 +1,48 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-jammy-full.s6-v0.39.4"
|
||||
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
|
||||
panel_icon: mdi:memory
|
||||
hassio_api: true
|
||||
homeassistant_api: true
|
||||
ingress_stream: true
|
||||
host_network: true
|
||||
gpio: true
|
||||
usb: true
|
||||
uart: true
|
||||
video: true
|
||||
image: "ghcr.io/koush/scrypted"
|
||||
environment:
|
||||
SCRYPTED_INSTALL_PLUGIN: "@scrypted/homeassistant"
|
||||
SCRYPTED_VOLUME: "/data/scrypted_data"
|
||||
SCRYPTED_NVR_VOLUME: "/data/scrypted_nvr"
|
||||
SCRYPTED_ADMIN_ADDRESS: "172.30.32.2"
|
||||
SCRYPTED_ADMIN_USERNAME: "homeassistant"
|
||||
SCRYPTED_INSTALL_ENVIRONMENT: "ha"
|
||||
backup_exclude:
|
||||
- '/server/**'
|
||||
- '/data/scrypted_nvr/**'
|
||||
- '/data/scrypted_data/plugins/**'
|
||||
map:
|
||||
- config:rw
|
||||
- media:rw
|
||||
devices:
|
||||
- /dev/mem
|
||||
- /dev/dri/renderD128
|
||||
- /dev/apex_0
|
||||
- /dev/apex_1
|
||||
- /dev/apex_2
|
||||
- /dev/apex_3
|
||||
- /dev/dri/card0
|
||||
- /dev/vchiq
|
||||
- /dev/video10
|
||||
- /dev/video0
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG BASE="18-bullseye-full"
|
||||
ARG BASE="18-jammy-full"
|
||||
FROM koush/scrypted-common:${BASE}
|
||||
|
||||
WORKDIR /
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG BASE="16-bullseye"
|
||||
ARG BASE="16-jammy"
|
||||
FROM koush/scrypted-common:${BASE}
|
||||
|
||||
WORKDIR /
|
||||
135
install/docker/Dockerfile.full
Normal file
135
install/docker/Dockerfile.full
Normal file
@@ -0,0 +1,135 @@
|
||||
################################################################
|
||||
# THIS FILE IS GENERATED. DO NOT EDIT.
|
||||
################################################################
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.header
|
||||
# This common file will be used by both Docker and the linux
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="jammy"
|
||||
FROM ubuntu:${BASE} as header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN apt-get -y install \
|
||||
libjpeg-dev zlib1g-dev
|
||||
|
||||
# plugins support fallback to pillow, but vips is faster.
|
||||
RUN apt-get -y install \
|
||||
libvips
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
# which causes weird behavior in python which looks at the arch version
|
||||
# which still reports 64bit, even if running in 32bit docker.
|
||||
# this scenario is not supported and will be reported at runtime.
|
||||
# this bit is not necessary on amd64, but leaving it for consistency.
|
||||
RUN apt-get -y install \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# python pip
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
then \
|
||||
apt-get update && apt-get install -y gpg-agent && \
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg && \
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
47
install/docker/Dockerfile.lite
Normal file
47
install/docker/Dockerfile.lite
Normal file
@@ -0,0 +1,47 @@
|
||||
ARG BASE="jammy"
|
||||
FROM ubuntu:${BASE} as header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# python pip
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
22
install/docker/Dockerfile.nvidia
Normal file
22
install/docker/Dockerfile.nvidia
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM koush/18-jammy-full.s6
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Install miniconda
|
||||
ENV CONDA_DIR /opt/conda
|
||||
RUN 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 install -c conda-forge cudatoolkit=11.2.2 cudnn=8.1.0
|
||||
ENV CONDA_PREFIX=/opt/conda
|
||||
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/
|
||||
|
||||
# this is a copy pasta, seems to need a reinstall.
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
@@ -1,23 +1,25 @@
|
||||
ARG BASE="18-bullseye-full"
|
||||
ARG BASE="18-jammy-full"
|
||||
FROM koush/scrypted-common:${BASE}
|
||||
|
||||
# avahi advertiser support
|
||||
RUN apt-get -y install \
|
||||
RUN apt-get update && apt-get -y install \
|
||||
libnss-mdns \
|
||||
avahi-discover \
|
||||
libavahi-compat-libdnssd-dev
|
||||
libavahi-compat-libdnssd-dev \
|
||||
xz-utils
|
||||
|
||||
# copy configurations and scripts
|
||||
COPY fs /
|
||||
|
||||
# s6 process supervisor
|
||||
ARG S6_OVERLAY_VERSION=3.1.1.2
|
||||
ARG S6_OVERLAY_VERSION=3.1.5.0
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
||||
ENV S6_KEEP_ENV=1
|
||||
RUN case "$(uname -m)" in \
|
||||
x86_64) S6_ARCH='x86_64';; \
|
||||
armv7l) S6_ARCH='armhf';; \
|
||||
aarch64) S6_ARCH='aarch64';; \
|
||||
ARG TARGETARCH
|
||||
RUN case "${TARGETARCH}" in \
|
||||
amd64) S6_ARCH='x86_64';; \
|
||||
arm) S6_ARCH='armhf';; \
|
||||
arm64) S6_ARCH='aarch64';; \
|
||||
*) echo "Your system architecture isn't supported."; exit 1 ;; \
|
||||
esac \
|
||||
&& cd /tmp \
|
||||
25
install/docker/Dockerfile.thin
Normal file
25
install/docker/Dockerfile.thin
Normal file
@@ -0,0 +1,25 @@
|
||||
ARG BASE="jammy"
|
||||
FROM ubuntu:${BASE} as header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -y update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install curl software-properties-common apt-utils ffmpeg
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && apt-get update && apt-get install -y nodejs
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="thin"
|
||||
3
install/docker/docker-build-nvidia.sh
Executable file
3
install/docker/docker-build-nvidia.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
./docker-build.sh
|
||||
|
||||
docker build -t koush/scrypted:18-jammy-full.nvidia -f Dockerfile.nvidia
|
||||
@@ -3,15 +3,16 @@
|
||||
set -x
|
||||
|
||||
NODE_VERSION=18
|
||||
BUILDPACK_DEPS_BASE=bullseye
|
||||
SCRYPTED_INSTALL_VERSION=beta
|
||||
IMAGE_BASE=jammy
|
||||
FLAVOR=full
|
||||
BASE=$NODE_VERSION-$BUILDPACK_DEPS_BASE-$FLAVOR
|
||||
BASE=$NODE_VERSION-$IMAGE_BASE-$FLAVOR
|
||||
echo $BASE
|
||||
SUPERVISOR=.s6
|
||||
SUPERVISOR_BASE=$BASE$SUPERVISOR
|
||||
|
||||
docker build -t koush/scrypted-common:$BASE -f Dockerfile.$FLAVOR \
|
||||
--build-arg NODE_VERSION=$NODE_VERSION --build-arg BUILDPACK_DEPS_BASE=$BUILDPACK_DEPS_BASE . && \
|
||||
--build-arg NODE_VERSION=$NODE_VERSION --build-arg BASE=$IMAGE_BASE . && \
|
||||
\
|
||||
docker build -t koush/scrypted:$SUPERVISOR_BASE -f Dockerfile$SUPERVISOR \
|
||||
--build-arg BASE=$BASE .
|
||||
--build-arg BASE=$BASE --build-arg SCRYPTED_INSTALL_VERSION=$SCRYPTED_INSTALL_VERSION .
|
||||
@@ -3,9 +3,10 @@ version: "3.5"
|
||||
# The Scrypted docker-compose.yml file typically resides at:
|
||||
# ~/.scrypted/docker-compose.yml
|
||||
|
||||
|
||||
# Scrypted NVR Storage (Optional Network Volume: Part 1 of 3)
|
||||
# Example volumes SMB (CIFS) and NFS.
|
||||
# Uncomment only one.
|
||||
|
||||
# volumes:
|
||||
# nvr:
|
||||
# driver_opts:
|
||||
@@ -20,40 +21,69 @@ version: "3.5"
|
||||
|
||||
services:
|
||||
scrypted:
|
||||
image: koush/scrypted
|
||||
environment:
|
||||
# Scrypted NVR Storage (Part 2 of 3)
|
||||
|
||||
# Uncomment the next line to configure the NVR plugin to store recordings
|
||||
# use the /nvr directory within the container. This can also be configured
|
||||
# within the plugin manually.
|
||||
# The drive or network share will ALSO need to be configured in the volumes
|
||||
# section below.
|
||||
# - SCRYPTED_NVR_VOLUME=/nvr
|
||||
|
||||
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
|
||||
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
|
||||
# nvidia support
|
||||
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
# runtime: nvidia
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
# uncomment this and a line below as needed.
|
||||
# devices:
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# intel hardware accelerated video decoding
|
||||
# - /dev/dri:/dev/dri
|
||||
# Uncomment next line to run avahi-daemon inside the container
|
||||
# Don't use if dbus and avahi run on the host and are bind-mounted
|
||||
# (see below under "volumes")
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
# runtime: nvidia
|
||||
|
||||
volumes:
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
# modify and add the additional volume for Scrypted NVR
|
||||
# the following example would mount the /mnt/sda/video path on the host
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
# The following example would mount the /mnt/sda/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/sda/video:/nvr
|
||||
|
||||
# or use a network mount from one of the examples above
|
||||
# Or use a network mount from one of the CIFS/NFS examples at the top of this file.
|
||||
# - type: volume
|
||||
# source: nvr
|
||||
# target: /nvr
|
||||
# volume:
|
||||
# nocopy: true
|
||||
|
||||
# uncomment the following lines to expose Avahi, an mDNS advertiser.
|
||||
# make sure Avahi is running on the host machine, otherwise this will not work.
|
||||
# not compatible with Avahi enabled via SCRYPTED_DOCKER_AVAHI=true
|
||||
# - /var/run/dbus:/var/run/dbus
|
||||
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
|
||||
|
||||
# Default volume for the Scrypted database. Typically should not be changed.
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
devices:
|
||||
# all usb devices, such as coral tpu
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
# - /dev/dri:/dev/dri
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# coral PCI devices
|
||||
# - /dev/apex_0:/dev/apex_0
|
||||
# - /dev/apex_1:/dev/apex_1
|
||||
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
image: koush/scrypted
|
||||
|
||||
# logging is noisy and will unnecessarily wear on flash storage.
|
||||
# scrypted has per device in memory logging that is preferred.
|
||||
logging:
|
||||
@@ -85,4 +115,4 @@ services:
|
||||
# Must match the port in the auto update url above.
|
||||
- 10444:8080
|
||||
# check for updates once an hour (interval is in seconds)
|
||||
command: --interval 3600 --cleanup
|
||||
command: --interval 3600 --cleanup --scope scrypted
|
||||
@@ -1,7 +1,7 @@
|
||||
[server]
|
||||
#host-name=
|
||||
use-ipv4=yes
|
||||
use-ipv6=no
|
||||
use-ipv6=yes
|
||||
enable-dbus=yes
|
||||
ratelimit-interval-usec=1000000
|
||||
ratelimit-burst=1000
|
||||
@@ -14,4 +14,4 @@ rlimit-core=0
|
||||
rlimit-data=4194304
|
||||
rlimit-fsize=0
|
||||
rlimit-nofile=768
|
||||
rlimit-stack=4194304
|
||||
rlimit-stack=4194304
|
||||
16
install/docker/fs/etc/s6-overlay/s6-rc.d/avahi/run
Executable file
16
install/docker/fs/etc/s6-overlay/s6-rc.d/avahi/run
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, not starting avahi-daemon" >/dev/stderr
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
done
|
||||
fi
|
||||
|
||||
until [ -e /var/run/dbus/system_bus_socket ]; do
|
||||
echo "Waiting for dbus..."
|
||||
sleep 1s
|
||||
done
|
||||
echo "Starting Avahi daemon..."
|
||||
exec avahi-daemon --no-chroot -f /etc/avahi/avahi-daemon.conf
|
||||
12
install/docker/fs/etc/s6-overlay/s6-rc.d/dbus/run
Executable file
12
install/docker/fs/etc/s6-overlay/s6-rc.d/dbus/run
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, not starting dbus-daemon" >/dev/stderr
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Starting dbus..."
|
||||
exec dbus-daemon --system --nofork
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, won't manage dbus nor avahi-daemon" >/dev/stderr
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -qE " ((/var)?/run/dbus|(/var)?/run/avahi-daemon(/socket)?) " /proc/mounts; then
|
||||
echo "dbus and/or avahi-daemon volumes are bind mounted, won't touch them" >/dev/stderr
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# make run folders
|
||||
mkdir -p /var/run/dbus
|
||||
mkdir -p /var/run/avahi-daemon
|
||||
@@ -22,4 +32,4 @@ if [ ! -z "$DSM_HOSTNAME" ]; then
|
||||
sed -i "s/.*host-name.*/host-name=${DSM_HOSTNAME}/" /etc/avahi/avahi-daemon.conf
|
||||
else
|
||||
sed -i "s/.*host-name.*/#host-name=/" /etc/avahi/avahi-daemon.conf
|
||||
fi
|
||||
fi
|
||||
@@ -42,7 +42,11 @@ fi
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
|
||||
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
|
||||
echo "Created $DOCKER_COMPOSE_YML"
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum)"/g > $DOCKER_COMPOSE_YML
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
|
||||
if [ -d /dev/dri ]
|
||||
then
|
||||
sed -i 's/'#' - \/dev\/dri/- \/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME
|
||||
45
install/docker/template/Dockerfile.full.footer
Normal file
45
install/docker/template/Dockerfile.full.footer
Normal file
@@ -0,0 +1,45 @@
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
then \
|
||||
apt-get update && apt-get install -y gpg-agent && \
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg && \
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
87
install/docker/template/Dockerfile.full.header
Normal file
87
install/docker/template/Dockerfile.full.header
Normal file
@@ -0,0 +1,87 @@
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.header
|
||||
# This common file will be used by both Docker and the linux
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="jammy"
|
||||
FROM ubuntu:${BASE} as header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN apt-get -y install \
|
||||
libjpeg-dev zlib1g-dev
|
||||
|
||||
# plugins support fallback to pillow, but vips is faster.
|
||||
RUN apt-get -y install \
|
||||
libvips
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
# which causes weird behavior in python which looks at the arch version
|
||||
# which still reports 64bit, even if running in 32bit docker.
|
||||
# this scenario is not supported and will be reported at runtime.
|
||||
# this bit is not necessary on amd64, but leaving it for consistency.
|
||||
RUN apt-get -y install \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# python pip
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
BIN
install/icon.png
Normal file
BIN
install/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -45,10 +45,10 @@ ARG() {
|
||||
}
|
||||
|
||||
ENV() {
|
||||
echo "ignoring ENV $1"
|
||||
export $@
|
||||
}
|
||||
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/template/Dockerfile.full.header)
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.header)
|
||||
|
||||
if [ -z "$SERVICE_USER" ]
|
||||
then
|
||||
@@ -40,48 +40,29 @@ echo "Installing Scrypted dependencies..."
|
||||
RUN_IGNORE xcode-select --install
|
||||
RUN brew update
|
||||
RUN_IGNORE brew install node@18
|
||||
# needed by scrypted-ffmpeg
|
||||
RUN_IGNORE brew install sdl2
|
||||
# gstreamer plugins
|
||||
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly
|
||||
# gst python bindings
|
||||
RUN_IGNORE brew install gst-python
|
||||
# python image library
|
||||
RUN_IGNORE brew install pillow
|
||||
# snapshot plugin and others
|
||||
RUN brew install libvips
|
||||
# dlib
|
||||
RUN brew install cmake
|
||||
|
||||
### HACK WORKAROUND
|
||||
### https://github.com/koush/scrypted/issues/544
|
||||
|
||||
brew unpin gstreamer
|
||||
brew unpin gst-python
|
||||
brew unpin gst-plugins-ugly
|
||||
brew unpin gst-plugins-good
|
||||
brew unpin gst-plugins-base
|
||||
brew unpin gst-plugins-good
|
||||
brew unpin gst-plugins-bad
|
||||
|
||||
brew unlink gstreamer
|
||||
brew unlink gst-python
|
||||
brew unlink gst-plugins-ugly
|
||||
brew unlink gst-plugins-good
|
||||
brew unlink gst-plugins-base
|
||||
brew unlink gst-plugins-bad
|
||||
|
||||
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gstreamer.rb && brew install ./gstreamer.rb
|
||||
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-python.rb && brew install ./gst-python.rb
|
||||
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-ugly.rb && brew install ./gst-plugins-ugly.rb
|
||||
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-good.rb && brew install ./gst-plugins-good.rb
|
||||
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-base.rb && brew install ./gst-plugins-base.rb
|
||||
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-bad.rb && brew install ./gst-plugins-bad.rb
|
||||
|
||||
brew pin gstreamer
|
||||
brew pin gst-python
|
||||
brew pin gst-plugins-ugly
|
||||
brew pin gst-plugins-good
|
||||
brew pin gst-plugins-base
|
||||
brew pin gst-plugins-bad
|
||||
brew unpin gst-plugins-ugly
|
||||
brew unpin gst-libav
|
||||
brew unpin gst-python
|
||||
|
||||
### END HACK WORKAROUND
|
||||
|
||||
# gstreamer plugins
|
||||
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav
|
||||
# gst python bindings
|
||||
RUN_IGNORE brew install gst-python
|
||||
|
||||
ARCH=$(arch)
|
||||
if [ "$ARCH" = "arm64" ]
|
||||
then
|
||||
@@ -102,7 +83,11 @@ then
|
||||
fi
|
||||
|
||||
RUN python$PYTHON_VERSION -m pip install --upgrade pip
|
||||
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions typing opencv-python psutil
|
||||
if [ "$PYTHON_VERSION" != "3.10" ]
|
||||
then
|
||||
RUN python$PYTHON_VERSION -m pip install typing
|
||||
fi
|
||||
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python psutil
|
||||
|
||||
echo "Installing Scrypted Launch Agent..."
|
||||
|
||||
@@ -20,7 +20,7 @@ $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 aiofiles debugpy typing_extensions typing opencv-python
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
|
||||
|
||||
npx -y scrypted@latest install-server
|
||||
|
||||
BIN
install/logo.png
Normal file
BIN
install/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -27,13 +27,6 @@ echo "sdk > npm run build"
|
||||
npm run build
|
||||
popd
|
||||
|
||||
pushd external/HAP-NodeJS
|
||||
echo "external/HAP-NodeJS > npm install"
|
||||
npm install
|
||||
echo "external/HAP-NodeJS > npm run build"
|
||||
npm run build
|
||||
popd
|
||||
|
||||
pushd external/werift
|
||||
echo "external/werift > npm install"
|
||||
npm install
|
||||
|
||||
@@ -23,10 +23,19 @@ async function example() {
|
||||
if (!backyard)
|
||||
throw new Error('Device not found');
|
||||
|
||||
backyard.listen(ScryptedInterface.ObjectDetector, (source, details, data) => {
|
||||
backyard.listen(ScryptedInterface.ObjectDetector, async (source, details, data) => {
|
||||
const results = data as ObjectsDetected;
|
||||
console.log(results);
|
||||
})
|
||||
console.log('detection results', results);
|
||||
// detections that are flagged for retention will have a detectionId.
|
||||
// tf etc won't retain automatically, and this requires a wrapping detector like Scrypted NVR Object Detection
|
||||
// to decide which frames to keep. Otherwise saving all images would be extremely poor performance.
|
||||
if (!results.detectionId)
|
||||
return;
|
||||
|
||||
const media = await backyard.getDetectionInput(results.detectionId);
|
||||
const jpeg = await sdk.mediaManager.convertMediaObjectToBuffer(media, 'image/jpeg');
|
||||
// do something with the buffer like save to disk or send to a service.
|
||||
});
|
||||
}
|
||||
|
||||
example();
|
||||
|
||||
12
packages/client/package-lock.json
generated
12
packages/client/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.43",
|
||||
"version": "1.1.54",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.43",
|
||||
"version": "1.1.54",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.76",
|
||||
"@scrypted/types": "^0.2.94",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
@@ -21,9 +21,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.76",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.76.tgz",
|
||||
"integrity": "sha512-/7n8ICkXj8TGba4cHvckLCgSNsOmOGQ8I+Jd8fX9sxkthgsZhF5At8PHhHdkCDS+yfSmfXHkcqluZZOfYPkpAg=="
|
||||
"version": "0.2.94",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.94.tgz",
|
||||
"integrity": "sha512-615C6lLnJGk0qhp+Y72B3xeD2CS9p/h8JUmFDjKh4H4IjL6zlV10tZVAXWQt3Q5rmy1WAaS3nScR6NgxZ5woOA=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.43",
|
||||
"version": "1.1.54",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -17,7 +17,7 @@
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.76",
|
||||
"@scrypted/types": "^0.2.94",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
|
||||
@@ -7,6 +7,7 @@ import { timeoutPromise } from "../../../common/src/promise-utils";
|
||||
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
|
||||
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
|
||||
import type { IOSocket } from '../../../server/src/io';
|
||||
import { MediaObject } from '../../../server/src/plugin/mediaobject';
|
||||
import type { MediaObjectRemote } from '../../../server/src/plugin/plugin-api';
|
||||
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
|
||||
import { RpcPeer } from '../../../server/src/rpc';
|
||||
@@ -77,25 +78,48 @@ export interface ScryptedClientOptions extends Partial<ScryptedLoginOptions> {
|
||||
transports?: string[];
|
||||
}
|
||||
|
||||
function isInstalledApp() {
|
||||
return globalThis.navigator?.userAgent.includes('InstalledApp');
|
||||
}
|
||||
|
||||
function isRunningStandalone() {
|
||||
return globalThis.matchMedia?.('(display-mode: standalone)').matches || globalThis.navigator?.userAgent.includes('InstalledApp');
|
||||
return globalThis.matchMedia?.('(display-mode: standalone)').matches || isInstalledApp();
|
||||
}
|
||||
|
||||
export async function logoutScryptedClient(baseUrl?: string) {
|
||||
const url = baseUrl ? new URL('/logout', baseUrl).toString() : '/logout';
|
||||
const url = combineBaseUrl(baseUrl, 'logout');
|
||||
const response = await axios(url, {
|
||||
withCredentials: true,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function getCurrentBaseUrl() {
|
||||
// an endpoint within scrypted will be served at /endpoint/[org/][id]
|
||||
// find the endpoint prefix and anything prior to that will be the server base url.
|
||||
const url = new URL(window.location.href);
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
let endpointPath = window.location.pathname;
|
||||
const parts = endpointPath.split('/');
|
||||
const index = parts.findIndex(p => p === 'endpoint');
|
||||
if (index === -1) {
|
||||
// console.warn('path not recognized, does not contain the segment "endpoint".')
|
||||
return undefined;
|
||||
}
|
||||
const keep = parts.slice(0, index);
|
||||
keep.push('');
|
||||
url.pathname = keep.join('/');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
let { baseUrl, username, password, change_password, maxAge } = options;
|
||||
// pwa should stay logged in for a year.
|
||||
if (!maxAge && isRunningStandalone())
|
||||
maxAge = 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const url = `${baseUrl || ''}/login`;
|
||||
const url = combineBaseUrl(baseUrl, 'login');
|
||||
const response = await axios.post(url, {
|
||||
username,
|
||||
password,
|
||||
@@ -128,7 +152,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
|
||||
export async function checkScryptedClientLogin(options?: ScryptedConnectionOptions) {
|
||||
let { baseUrl } = options || {};
|
||||
const url = `${baseUrl || ''}/login`;
|
||||
const url = combineBaseUrl(baseUrl, 'login');
|
||||
const response = await axios.get(url, {
|
||||
withCredentials: true,
|
||||
...options?.axiosConfig,
|
||||
@@ -137,6 +161,7 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
|
||||
const directAddress = response.headers['x-scrypted-direct-address'];
|
||||
|
||||
return {
|
||||
hostname: response.data.hostname as string,
|
||||
redirect: response.data.redirect as string,
|
||||
username: response.data.username as string,
|
||||
expiration: response.data.expiration as number,
|
||||
@@ -144,6 +169,7 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
|
||||
error: response.data.error as string,
|
||||
authorization: response.data.authorization as string,
|
||||
queryToken: response.data.queryToken as any,
|
||||
token: response.data.token as string,
|
||||
addresses: response.data.addresses as string[],
|
||||
scryptedCloud,
|
||||
directAddress,
|
||||
@@ -174,9 +200,12 @@ export function redirectScryptedLogin(options?: {
|
||||
globalThis.location.href = redirect_uri;
|
||||
}
|
||||
|
||||
export function combineBaseUrl(baseUrl: string, rootPath: string) {
|
||||
return baseUrl ? new URL(rootPath, baseUrl).toString() : '/' + rootPath;
|
||||
}
|
||||
|
||||
export async function redirectScryptedLogout(baseUrl?: string) {
|
||||
baseUrl = baseUrl || '';
|
||||
globalThis.location.href = `${baseUrl}/logout`;
|
||||
globalThis.location.href = combineBaseUrl(baseUrl, 'logout');
|
||||
}
|
||||
|
||||
export async function connectScryptedClient(options: ScryptedClientOptions): Promise<ScryptedClientStatic> {
|
||||
@@ -218,9 +247,10 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
}
|
||||
|
||||
let socket: IOClientSocket;
|
||||
const endpointPath = `/endpoint/${pluginId}`;
|
||||
const eioPath = `endpoint/${pluginId}/engine.io/api`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
const eioOptions: Partial<SocketOptions> = {
|
||||
path: `${endpointPath}/engine.io/api`,
|
||||
path: eioEndpoint,
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
@@ -237,14 +267,15 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
// if the cert has been accepted. Other browsers seem fine.
|
||||
// So the default is not to connect to IP addresses on Chrome, but do so on other browsers.
|
||||
const isChrome = globalThis.navigator?.userAgent.includes('Chrome');
|
||||
const isNotChromeOrIsInstalledApp = !isChrome || isInstalledApp();
|
||||
|
||||
const addresses: string[] = [];
|
||||
const localAddressDefault = !isChrome;
|
||||
const localAddressDefault = isNotChromeOrIsInstalledApp;
|
||||
if (((scryptedCloud && options.local === undefined && localAddressDefault) || options.local) && localAddresses) {
|
||||
addresses.push(...localAddresses);
|
||||
}
|
||||
|
||||
const directAddressDefault = directAddress && (!isChrome || !isIPAddress(directAddress));
|
||||
const directAddressDefault = directAddress && (isNotChromeOrIsInstalledApp || !isIPAddress(directAddress));
|
||||
if (((scryptedCloud && options.direct === undefined && directAddressDefault) || options.direct) && directAddress) {
|
||||
addresses.push(directAddress);
|
||||
}
|
||||
@@ -505,22 +536,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
console.log('api attached', Date.now() - start);
|
||||
|
||||
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
|
||||
const mo: MediaObjectRemote & {
|
||||
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
|
||||
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
|
||||
} = {
|
||||
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
|
||||
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: {
|
||||
mimeType,
|
||||
sourceId: options?.sourceId,
|
||||
},
|
||||
mimeType,
|
||||
sourceId: options?.sourceId,
|
||||
async getData() {
|
||||
return data;
|
||||
},
|
||||
};
|
||||
return mo as any;
|
||||
return new MediaObject(mimeType, data, options) as any;
|
||||
}
|
||||
|
||||
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
|
||||
|
||||
4
packages/h264-repacketizer/package-lock.json
generated
4
packages/h264-repacketizer/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/h264-packetizer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/h264-repacketizer",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
1
packages/python-client/.gitignore
vendored
Normal file
1
packages/python-client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.venv
|
||||
16
packages/python-client/.vscode/launch.json
vendored
Normal file
16
packages/python-client/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Current File",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/test.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
1
packages/python-client/plugin_remote.py
Symbolic link
1
packages/python-client/plugin_remote.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/plugin_remote.py
|
||||
3
packages/python-client/requirements.txt
Normal file
3
packages/python-client/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
python-engineio[asyncio_client]
|
||||
aiohttp
|
||||
aiodns
|
||||
1
packages/python-client/rpc.py
Symbolic link
1
packages/python-client/rpc.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/rpc.py
|
||||
1
packages/python-client/rpc_reader.py
Symbolic link
1
packages/python-client/rpc_reader.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/rpc_reader.py
|
||||
1
packages/python-client/scrypted_python
Symbolic link
1
packages/python-client/scrypted_python
Symbolic link
@@ -0,0 +1 @@
|
||||
../../sdk/types/scrypted_python
|
||||
151
packages/python-client/test.py
Normal file
151
packages/python-client/test.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import nullcontext
|
||||
|
||||
import aiohttp
|
||||
import engineio
|
||||
|
||||
import plugin_remote
|
||||
import rpc_reader
|
||||
from plugin_remote import DeviceManager, MediaManager, SystemManager
|
||||
from scrypted_python.scrypted_sdk import ScryptedInterface, ScryptedStatic
|
||||
|
||||
|
||||
class EioRpcTransport(rpc_reader.RpcTransport):
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||
super().__init__()
|
||||
self.eio = engineio.AsyncClient(ssl_verify=False)
|
||||
self.loop = loop
|
||||
self.write_error: Exception = None
|
||||
self.read_queue = asyncio.Queue()
|
||||
self.write_queue = asyncio.Queue()
|
||||
|
||||
@self.eio.on("message")
|
||||
def on_message(data):
|
||||
self.read_queue.put_nowait(data)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(self.send_loop(), self.loop)
|
||||
|
||||
async def read(self):
|
||||
return await self.read_queue.get()
|
||||
|
||||
async def send_loop(self):
|
||||
while True:
|
||||
data = await self.write_queue.get()
|
||||
try:
|
||||
await self.eio.send(data)
|
||||
except Exception as e:
|
||||
self.write_error = e
|
||||
self.write_queue = None
|
||||
break
|
||||
|
||||
def writeBuffer(self, buffer, reject):
|
||||
async def send():
|
||||
try:
|
||||
if self.write_error:
|
||||
raise self.write_error
|
||||
self.write_queue.put_nowait(buffer)
|
||||
except Exception as e:
|
||||
reject(e)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(send(), self.loop)
|
||||
|
||||
def writeJSON(self, json, reject):
|
||||
return self.writeBuffer(json, reject)
|
||||
|
||||
|
||||
async def connect_scrypted_client(
|
||||
transport: EioRpcTransport,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
plugin_id: str = "@scrypted/core",
|
||||
session: aiohttp.ClientSession | None = None,
|
||||
) -> ScryptedStatic:
|
||||
login_url = f"{base_url}/login"
|
||||
login_body = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
if session:
|
||||
cm = nullcontext(session)
|
||||
else:
|
||||
cm = aiohttp.ClientSession()
|
||||
|
||||
async with cm as _session:
|
||||
async with _session.post(
|
||||
login_url, verify_ssl=False, json=login_body
|
||||
) as response:
|
||||
login_response = await response.json()
|
||||
|
||||
headers = {"Authorization": login_response["authorization"]}
|
||||
|
||||
await transport.eio.connect(
|
||||
base_url,
|
||||
headers=headers,
|
||||
engineio_path=f"/endpoint/{plugin_id}/engine.io/api/",
|
||||
)
|
||||
|
||||
ret = asyncio.Future[ScryptedStatic](loop=transport.loop)
|
||||
peer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
|
||||
transport.loop, transport
|
||||
)
|
||||
peer.params["print"] = print
|
||||
|
||||
def callback(api, pluginId, hostInfo):
|
||||
remote = plugin_remote.PluginRemote(
|
||||
peer, api, pluginId, hostInfo, transport.loop
|
||||
)
|
||||
wrapped = remote.setSystemState
|
||||
|
||||
async def remoteSetSystemState(systemState):
|
||||
await wrapped(systemState)
|
||||
|
||||
async def resolve():
|
||||
sdk = ScryptedStatic()
|
||||
sdk.api = api
|
||||
sdk.remote = remote
|
||||
sdk.systemManager = SystemManager(api, remote.systemState)
|
||||
sdk.deviceManager = DeviceManager(
|
||||
remote.nativeIds, sdk.systemManager
|
||||
)
|
||||
sdk.mediaManager = MediaManager(await api.getMediaManager())
|
||||
ret.set_result(sdk)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(resolve(), transport.loop)
|
||||
|
||||
remote.setSystemState = remoteSetSystemState
|
||||
return remote
|
||||
|
||||
peer.params["getRemote"] = callback
|
||||
asyncio.run_coroutine_threadsafe(peerReadLoop(), transport.loop)
|
||||
|
||||
sdk = await ret
|
||||
return sdk
|
||||
|
||||
|
||||
async def main():
|
||||
transport = EioRpcTransport(asyncio.get_event_loop())
|
||||
sdk = await connect_scrypted_client(
|
||||
transport,
|
||||
"https://localhost:10443",
|
||||
os.environ["SCRYPTED_USERNAME"],
|
||||
os.environ["SCRYPTED_PASSWORD"],
|
||||
)
|
||||
|
||||
for id in sdk.systemManager.getSystemState():
|
||||
device = sdk.systemManager.getDeviceById(id)
|
||||
print(device.name)
|
||||
if ScryptedInterface.OnOff.value in device.interfaces:
|
||||
print(f"OnOff: device is {device.on}")
|
||||
|
||||
await transport.eio.disconnect()
|
||||
os._exit(0)
|
||||
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.run_coroutine_threadsafe(main(), loop)
|
||||
loop.run_forever()
|
||||
2
plugins/alexa/.vscode/settings.json
vendored
2
plugins/alexa/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "10.10.0.50",
|
||||
"scrypted.debugHost": "koushik-ubuntu",
|
||||
}
|
||||
2837
plugins/alexa/package-lock.json
generated
2837
plugins/alexa/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.6",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -39,6 +39,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.4.2",
|
||||
"@scrypted/sdk": "^0.2.70"
|
||||
"@scrypted/sdk": "../../sdk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ const includeToken = 4;
|
||||
|
||||
export let DEBUG = false;
|
||||
|
||||
function debug(...args: any[]) {
|
||||
if (DEBUG)
|
||||
console.debug(...args);
|
||||
}
|
||||
|
||||
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
tokenInfo: {
|
||||
@@ -34,6 +39,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
|
||||
type: 'string',
|
||||
readonly: true
|
||||
},
|
||||
debug: {
|
||||
title: 'Debug Events',
|
||||
description: 'Log all events to the console. This will be very noisy and should not be left enabled.',
|
||||
type: 'boolean',
|
||||
onPut(oldValue: boolean, newValue: boolean) {
|
||||
DEBUG = newValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,6 +57,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
DEBUG = this.storageSettings.values.debug ?? false;
|
||||
|
||||
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
|
||||
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
|
||||
|
||||
@@ -141,12 +156,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (!supportedType)
|
||||
return;
|
||||
|
||||
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
|
||||
let report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
|
||||
|
||||
if (!report && eventDetails.eventInterface === ScryptedInterface.Online) {
|
||||
report = {};
|
||||
}
|
||||
|
||||
if (!report && eventDetails.eventInterface === ScryptedInterface.Battery) {
|
||||
report = {};
|
||||
}
|
||||
|
||||
if (!report) {
|
||||
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
debug("event", eventDetails.eventInterface, eventDetails.property, eventSource.type);
|
||||
|
||||
let data = {
|
||||
"event": {
|
||||
"header": {
|
||||
@@ -234,7 +260,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const endpoint = await this.getAlexaEndpoint();
|
||||
const self = this;
|
||||
|
||||
this.console.assert(!DEBUG, `event:`, data);
|
||||
debug("send event to alexa", data);
|
||||
|
||||
return axios.post(`https://${endpoint}/v3/events`, data, {
|
||||
headers: {
|
||||
@@ -570,6 +596,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const { authorization } = request.headers;
|
||||
if (!this.validAuths.has(authorization)) {
|
||||
try {
|
||||
debug("making authorization request to Scrypted");
|
||||
|
||||
await axios.get('https://home.scrypted.app/_punch/getcookie', {
|
||||
headers: {
|
||||
'Authorization': authorization,
|
||||
@@ -590,11 +618,11 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const { directive } = body;
|
||||
const { namespace, name } = directive.header;
|
||||
|
||||
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
|
||||
|
||||
const mapName = `${namespace}/${name}`;
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
|
||||
debug("received directive from alexa", mapName, body);
|
||||
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive]);
|
||||
|
||||
@@ -641,7 +669,7 @@ class HttpResponseLoggingImpl implements AlexaHttpResponse {
|
||||
if (options.code !== 200)
|
||||
this.console.error(`response error ${options.code}:`, body);
|
||||
else
|
||||
this.console.assert(!DEBUG, `response ${options.code}:`, body);
|
||||
debug("response to alexa directive", options.code, body);
|
||||
|
||||
if (typeof body === 'object')
|
||||
body = JSON.stringify(body);
|
||||
|
||||
@@ -17,6 +17,13 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
sdp: this.directive.payload.offer.value,
|
||||
},
|
||||
disableTrickle: true,
|
||||
disableTurn: true,
|
||||
// this could be a low resolution screen, no way of knowning, so never send a 1080p stream
|
||||
screen: {
|
||||
devicePixelRatio: 1,
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ import { supportedTypes } from ".";
|
||||
supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
let capabilities: any[] = [];
|
||||
let category: DisplayCategory = 'DOORBELL';
|
||||
const displayCategories: DisplayCategory[] = ['DOORBELL'];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
|
||||
capabilities = await getCameraCapabilities(device);
|
||||
category = 'CAMERA';
|
||||
displayCategories.push('CAMERA');
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
|
||||
@@ -25,7 +25,7 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
}
|
||||
|
||||
return {
|
||||
displayCategories: [category],
|
||||
displayCategories,
|
||||
capabilities
|
||||
};
|
||||
},
|
||||
|
||||
199
plugins/amcrest/package-lock.json
generated
199
plugins/amcrest/package-lock.json
generated
@@ -1,26 +1,25 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.119",
|
||||
"lockfileVersion": 2,
|
||||
"version": "0.0.123",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.119",
|
||||
"version": "0.0.123",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
"multiparty": "^4.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.0"
|
||||
"@types/node": "^18.16.18"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -35,8 +34,7 @@
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.68",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -71,9 +69,6 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
@@ -100,9 +95,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
|
||||
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
|
||||
"version": "18.16.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
|
||||
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw=="
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
@@ -120,15 +115,15 @@
|
||||
"node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -145,15 +140,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
|
||||
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -165,11 +160,11 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/multiparty": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
|
||||
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz",
|
||||
"integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==",
|
||||
"dependencies": {
|
||||
"http-errors": "~1.8.0",
|
||||
"http-errors": "~1.8.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "2.1.5"
|
||||
},
|
||||
@@ -180,7 +175,7 @@
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -212,15 +207,15 @@
|
||||
"node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
@@ -236,147 +231,5 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"requires": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@types/multiparty": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz",
|
||||
"integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
|
||||
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
|
||||
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"multiparty": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
|
||||
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
|
||||
"requires": {
|
||||
"http-errors": "~1.8.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "2.1.5"
|
||||
}
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"requires": {
|
||||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.119",
|
||||
"version": "0.0.123",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -36,12 +36,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
"multiparty": "^4.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.0"
|
||||
"@types/node": "^18.16.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,16 @@ export class AmcrestCameraClient {
|
||||
});
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=reboot`,
|
||||
});
|
||||
return response.data as string;
|
||||
}
|
||||
|
||||
async checkTwoWayAudio() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
@@ -61,6 +71,7 @@ export class AmcrestCameraClient {
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, Reboot, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, 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";
|
||||
@@ -23,7 +23,7 @@ function findValue(blob: string, prefix: string, key: string) {
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, VideoRecorder {
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, VideoRecorder, Reboot {
|
||||
eventStream: Stream;
|
||||
cp: ChildProcess;
|
||||
client: AmcrestCameraClient;
|
||||
@@ -37,9 +37,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.storage.removeItem('amcrestDoorbell');
|
||||
}
|
||||
|
||||
this.updateDevice();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
const client = this.getClient();
|
||||
await client.reboot();
|
||||
}
|
||||
|
||||
getRecordingStreamCurrentTime(recordingStream: MediaObject): Promise<number> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
@@ -440,6 +446,29 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
return this.videoStreamOptions;
|
||||
}
|
||||
|
||||
updateDevice() {
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const isDoorbell = doorbellType === AMCREST_DOORBELL_TYPE || doorbellType === DAHUA_DOORBELL_TYPE;
|
||||
// true is the legacy value before onvif was added.
|
||||
const twoWayAudio = this.storage.getItem('twoWayAudio') === 'true'
|
||||
|| this.storage.getItem('twoWayAudio') === 'ONVIF'
|
||||
|| this.storage.getItem('twoWayAudio') === 'Amcrest';
|
||||
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type: ScryptedDeviceType = undefined;
|
||||
if (isDoorbell) {
|
||||
type = ScryptedDeviceType.Doorbell;
|
||||
interfaces.push(ScryptedInterface.BinarySensor)
|
||||
}
|
||||
if (isDoorbell || twoWayAudio) {
|
||||
interfaces.push(ScryptedInterface.Intercom);
|
||||
}
|
||||
const continuousRecording = this.storage.getItem('continuousRecording') === 'true';
|
||||
if (continuousRecording)
|
||||
interfaces.push(ScryptedInterface.VideoRecorder);
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
if (key === 'continuousRecording') {
|
||||
if (value === 'true') {
|
||||
@@ -461,27 +490,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.videoStreamOptions = undefined;
|
||||
|
||||
super.putSetting(key, value);
|
||||
const doorbellType = this.storage.getItem('doorbellType');
|
||||
const isDoorbell = doorbellType === AMCREST_DOORBELL_TYPE || doorbellType === DAHUA_DOORBELL_TYPE;
|
||||
// true is the legacy value before onvif was added.
|
||||
const twoWayAudio = this.storage.getItem('twoWayAudio') === 'true'
|
||||
|| this.storage.getItem('twoWayAudio') === 'ONVIF'
|
||||
|| this.storage.getItem('twoWayAudio') === 'Amcrest';
|
||||
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type: ScryptedDeviceType = undefined;
|
||||
if (isDoorbell) {
|
||||
type = ScryptedDeviceType.Doorbell;
|
||||
interfaces.push(ScryptedInterface.BinarySensor)
|
||||
}
|
||||
if (isDoorbell || twoWayAudio) {
|
||||
interfaces.push(ScryptedInterface.Intercom);
|
||||
}
|
||||
const continuousRecording = this.storage.getItem('continuousRecording') === 'true';
|
||||
if (continuousRecording)
|
||||
interfaces.push(ScryptedInterface.VideoRecorder);
|
||||
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
|
||||
|
||||
|
||||
this.updateDevice();
|
||||
this.updateDeviceInfo();
|
||||
}
|
||||
|
||||
@@ -576,6 +586,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
class AmcrestProvider extends RtspProvider {
|
||||
getAdditionalInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.Reboot,
|
||||
ScryptedInterface.VideoCameraConfiguration,
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.AudioSensor,
|
||||
@@ -616,7 +627,7 @@ class AmcrestProvider extends RtspProvider {
|
||||
this.console.warn('Error probing two way audio', e);
|
||||
}
|
||||
}
|
||||
settings.newCamera ||= 'Hikvision Camera';
|
||||
settings.newCamera ||= 'Amcrest Camera';
|
||||
|
||||
nativeId = await super.createDevice(settings, nativeId);
|
||||
|
||||
|
||||
2
plugins/arlo/.vscode/settings.json
vendored
2
plugins/arlo/.vscode/settings.json
vendored
@@ -22,6 +22,6 @@
|
||||
//"scrypted.volumeRoot": "${config:scrypted.serverRoot}/volume",
|
||||
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/scrypted_python"
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
}
|
||||
@@ -1,15 +1,41 @@
|
||||
# Arlo Plugin for Scrypted
|
||||
|
||||
The Arlo Plugin connects Scrypted to Arlo cloud, allowing you to access all of your Arlo cameras in Scrypted.
|
||||
The Arlo Plugin connects Scrypted to Arlo Cloud, allowing you to access all of your Arlo cameras in Scrypted.
|
||||
|
||||
It is highly recommended to create a dedicated Arlo account for use with this plugin and share your cameras from your main account, as Arlo only permits one connection to their servers per account. Using a separate account allows you to use the Arlo app or website simultaneously with this plugin.
|
||||
It is highly recommended to create a dedicated Arlo account for use with this plugin and share your cameras from your main account, as Arlo only permits one active login to their servers per account. Using a separate account allows you to use the Arlo app or website simultaneously with this plugin, otherwise logging in from one place will log you out from all other devices.
|
||||
|
||||
The account you use for this plugin must have either SMS or email set as the default 2FA option. Once you enter your username and password on the plugin settings page, you should receive a 2FA code through your default 2FA option. Enter that code into the provided box, and your cameras will appear in Scrypted. Or, see below for configuring IMAP to auto-login with 2FA.
|
||||
|
||||
If you experience any trouble logging in, clear the username and password boxes, reload the plugin, and try again.
|
||||
|
||||
If you are unable to see shared cameras in your separate Arlo account, ensure that both your primary and secondary accounts are upgraded according to this [forum post](https://web.archive.org/web/20230710141914/https://community.arlo.com/t5/Arlo-Secure/Invited-friend-cannot-see-devices-on-their-dashboard-Arlo-Pro-2/m-p/1889396#M1813). Verify the sharing worked by logging in via the Arlo web dashboard.
|
||||
|
||||
## General Setup Notes
|
||||
|
||||
* Ensure that your Arlo account's default 2FA option is set to either SMS or email.
|
||||
* Motion events notifications should be turned on in the Arlo app. If you are receiving motion push notifications, Scrypted will also receive motion events.
|
||||
* Disable smart detection and any cloud/local recording in the Arlo app. Arlo Cloud only permits one active stream per camera, so any smart detection or recording features may prevent downstream plugins (e.g. Homekit) from successfully pulling the video feed after a motion event.
|
||||
* It is highly recommended to enable the Rebroadcast plugin to allow multiple downstream plugins to pull the video feed within Scrypted.
|
||||
* If there is no audio on your camera, switch to the `FFmpeg (TCP)` parser under the `Cloud RTSP` settings.
|
||||
* Prebuffering should only be enabled if the camera is wired to a persistent power source, such as a wall outlet. Prebuffering will only work if your camera does not have a battery or `Plugged In to External Power` is selected.
|
||||
* The plugin supports pulling RTSP or DASH streams from Arlo Cloud. It is recommended to use RTSP for the lowest latency streams. DASH is inconsistent in reliability, and may return finicky codecs that require additional FFmpeg output arguments, e.g. `-vcodec h264`. *Note that both RTSP and DASH will ultimately pull the same video stream feed from your camera, and they cannot both be used at the same time due to the single stream limitation.*
|
||||
|
||||
Note that streaming cameras uses extra Internet bandwidth, since video and audio packets will need to travel from the camera through your network, out to Arlo Cloud, and then back to your network and into Scrypted.
|
||||
|
||||
## IMAP 2FA
|
||||
|
||||
The Arlo Plugin supports using the IMAP protocol to check an email mailbox for Arlo 2FA codes. This requires you to specify an email 2FA option as the default in your Arlo account settings.
|
||||
|
||||
The plugin should work with any mailbox that supports IMAP, but so far has been tested with Gmail. To configure a Gmail mailbox, see [here](https://support.google.com/mail/answer/7126229?hl=en) to see the Gmail IMAP settings, and [here](https://support.google.com/accounts/answer/185833?hl=en) to create an App Password. Enter the App Password in place of your normal Gmail password.
|
||||
The plugin should work with any mailbox that supports IMAP, but so far has been tested with Gmail. To configure a Gmail mailbox, see [here](https://support.google.com/mail/answer/7126229?hl=en) to see the Gmail IMAP settings, and [here](https://support.google.com/accounts/answer/185833?hl=en) to create an App Password. Enter the App Password in place of your normal Gmail password.
|
||||
|
||||
The plugin searches for emails sent by Arlo's `do_not_reply@arlo.com` address when looking for 2FA codes. If you are using a service to forward emails to the mailbox registered with this plugin (e.g. a service like iCloud's Hide My Email), it is possible that Arlo's email sender address has been overwritten by the mail forwarder. Check the email registered with this plugin to see what address the mail forwarder uses to replace Arlo's sender address, and update that in the IMAP 2FA settings.
|
||||
|
||||
## Virtual Security System for Arlo Sirens
|
||||
|
||||
In external integrations like Homekit, sirens are exposed as simple on-off switches. This makes it easy to accidentally hit the switch when using the Home app. The Arlo Plugin creates a "virtual" security system device per siren to allow Scrypted to arm or disarm the siren switch to protect against accidental triggers. This fake security system device will be synced into Homekit as a separate accessory from the camera, with the siren itself merged into the security system accessory.
|
||||
|
||||
Note that the virtual security system is NOT tied to your Arlo account at all, and will not make any changes such as switching your device's motion alert armed/disarmed modes. For more information, please see the README on the virtual security system device in Scrypted.
|
||||
|
||||
## Video Clips
|
||||
|
||||
The Arlo Plugin will show video clips available in Arlo Cloud for cameras with cloud recording enabled. These clips are not downloaded onto your Scrypted server, but rather streamed on-demand. Deleting clips is not available in Scrypted and should be done through the Arlo app or the Arlo web dashboard.
|
||||
7
plugins/arlo/package-lock.json
generated
7
plugins/arlo/package-lock.json
generated
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.7",
|
||||
"version": "0.8.11",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.7",
|
||||
"version": "0.8.11",
|
||||
"license": "Apache",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.78",
|
||||
"version": "0.2.103",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.7",
|
||||
"version": "0.8.11",
|
||||
"description": "Arlo Plugin for Scrypted",
|
||||
"license": "Apache",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
|
||||
@@ -24,12 +24,15 @@ limitations under the License.
|
||||
# Import helper classes that are part of this library.
|
||||
|
||||
from .request import Request
|
||||
from .host_picker import pick_host
|
||||
from .mqtt_stream_async import MQTTStream
|
||||
from .sse_stream_async import EventStream
|
||||
from .logging import logger
|
||||
|
||||
# Import all of the other stuff.
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from cachetools import cached, TTLCache
|
||||
import scrypted_arlo_go
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
@@ -37,6 +40,8 @@ import base64
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
stream_class = MQTTStream
|
||||
|
||||
@@ -70,20 +75,33 @@ USER_AGENTS = {
|
||||
"Gecko/20100101 Firefox/85.0",
|
||||
"linux":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
|
||||
|
||||
# extracted from cloudscraper as a working UA for cloudflare
|
||||
"android":
|
||||
"Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; PACM00 Build/O11019) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/8.8 Mobile Safari/537.36"
|
||||
}
|
||||
|
||||
# user agents for media players, e.g. the android app
|
||||
MEDIA_USER_AGENTS = {
|
||||
"android": "ijkplayer-android-4.5_28538"
|
||||
}
|
||||
|
||||
|
||||
class Arlo(object):
|
||||
BASE_URL = 'my.arlo.com'
|
||||
AUTH_URL = 'ocapi-app.arlo.com'
|
||||
BACKUP_AUTH_HOSTS = ['NTIuMjEwLjMuMTIx', 'MzQuMjU1LjkyLjIxMg==', 'MzQuMjUxLjE3Ny45MA==', 'NTQuMjQ2LjE3MS4x']
|
||||
TRANSID_PREFIX = 'web'
|
||||
|
||||
random.shuffle(BACKUP_AUTH_HOSTS)
|
||||
|
||||
def __init__(self, username, password):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.event_stream = None
|
||||
self.request = Request()
|
||||
self.request = None
|
||||
|
||||
def to_timestamp(self, dt):
|
||||
if sys.version[0] == '2':
|
||||
@@ -132,27 +150,48 @@ class Arlo(object):
|
||||
self.user_id = user_id
|
||||
headers['Content-Type'] = 'application/json; charset=UTF-8'
|
||||
headers['User-Agent'] = USER_AGENTS['arlo']
|
||||
self.request = Request(mode="cloudscraper")
|
||||
self.request.session.headers.update(headers)
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
|
||||
def LoginMFA(self):
|
||||
self.request = Request()
|
||||
|
||||
device_id = str(uuid.uuid4())
|
||||
headers = {
|
||||
'DNT': '1',
|
||||
'schemaVersion': '1',
|
||||
'Auth-Version': '2',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': USER_AGENTS['arlo'],
|
||||
'Origin': f'https://{self.BASE_URL}',
|
||||
'Referer': f'https://{self.BASE_URL}/',
|
||||
'Source': 'arloCamWeb',
|
||||
'TE': 'Trailers',
|
||||
'x-user-device-id': device_id,
|
||||
'x-user-device-automation-name': 'QlJPV1NFUg==',
|
||||
'x-user-device-type': 'BROWSER',
|
||||
'Host': self.AUTH_URL,
|
||||
}
|
||||
|
||||
self.request = Request()
|
||||
try:
|
||||
auth_host = self.AUTH_URL
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
logger.info("Using primary authentication host")
|
||||
except Exception as e:
|
||||
# in case cloudflare rejects our auth request...
|
||||
logger.warning(f"Using fallback authentication host due to: {e}")
|
||||
|
||||
auth_host = pick_host([
|
||||
base64.b64decode(h.encode("utf-8")).decode("utf-8")
|
||||
for h in self.BACKUP_AUTH_HOSTS
|
||||
], self.AUTH_URL, "/api/auth")
|
||||
logger.debug(f"Selected backup authentication host {auth_host}")
|
||||
|
||||
self.request = Request(mode="ip")
|
||||
|
||||
# Authenticate
|
||||
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
|
||||
auth_body = self.request.post(
|
||||
f'https://{self.AUTH_URL}/api/auth',
|
||||
f'https://{auth_host}/api/auth',
|
||||
params={
|
||||
'email': self.username,
|
||||
'password': str(base64.b64encode(self.password.encode('utf-8')), 'utf-8'),
|
||||
@@ -167,21 +206,26 @@ class Arlo(object):
|
||||
|
||||
# Retrieve MFA factor id
|
||||
factors_body = self.request.get(
|
||||
f'https://{self.AUTH_URL}/api/getFactors',
|
||||
f'https://{auth_host}/api/getFactors',
|
||||
params={'data': auth_body['data']['issued']},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
factor_id = next(
|
||||
i for i in factors_body['data']['items']
|
||||
if (i['factorType'] == 'EMAIL' or i['factorType'] == 'SMS')
|
||||
and i['factorRole'] == "PRIMARY"
|
||||
)['factorId']
|
||||
iter([
|
||||
i for i in factors_body['data']['items']
|
||||
if (i['factorType'] == 'EMAIL' or i['factorType'] == 'SMS')
|
||||
and i['factorRole'] == "PRIMARY"
|
||||
]),
|
||||
{}
|
||||
).get('factorId')
|
||||
if not factor_id:
|
||||
raise Exception("Could not find valid 2FA method - is the primary 2FA set to either Email or SMS?")
|
||||
|
||||
# Start factor auth
|
||||
start_auth_body = self.request.post(
|
||||
f'https://{self.AUTH_URL}/api/startAuth',
|
||||
{'factorId': factor_id},
|
||||
f'https://{auth_host}/api/startAuth',
|
||||
params={'factorId': factor_id},
|
||||
headers=headers,
|
||||
raw=True
|
||||
)
|
||||
@@ -191,8 +235,8 @@ class Arlo(object):
|
||||
nonlocal self, factor_auth_code, headers
|
||||
|
||||
finish_auth_body = self.request.post(
|
||||
f'https://{self.AUTH_URL}/api/finishAuth',
|
||||
{
|
||||
f'https://{auth_host}/api/finishAuth',
|
||||
params={
|
||||
'factorAuthCode': factor_auth_code,
|
||||
'otp': code
|
||||
},
|
||||
@@ -200,6 +244,11 @@ class Arlo(object):
|
||||
raw=True
|
||||
)
|
||||
|
||||
if finish_auth_body.get('data', {}).get('token') is None:
|
||||
raise Exception("Could not complete 2FA, maybe invalid token? If the error persists, please try reloading the plugin and logging in again.")
|
||||
|
||||
self.request = Request(mode="cloudscraper")
|
||||
|
||||
# Update Authorization code with new code
|
||||
headers = {
|
||||
'Auth-Version': '2',
|
||||
@@ -253,17 +302,25 @@ class Arlo(object):
|
||||
cameras[camera['deviceId']] = camera
|
||||
|
||||
# filter out cameras without basestation, where they are their own basestations
|
||||
# for now, keep doorbells and sirens in the list so they get pings
|
||||
proper_basestations = {}
|
||||
# this is so battery-powered devices do not drain due to pings
|
||||
# for wired devices, keep doorbells, sirens, and arloq in the list so they get pings
|
||||
# we also add arlo baby devices (abc1000, abc1000a) since they are standalone-only
|
||||
# and seem to want pings
|
||||
devices_to_ping = {}
|
||||
for basestation in basestations.values():
|
||||
if basestation['deviceId'] == basestation.get('parentId') and basestation['deviceType'] not in ['doorbell', 'siren']:
|
||||
if basestation['deviceId'] == basestation.get('parentId') and \
|
||||
basestation['deviceType'] not in ['doorbell', 'siren', 'arloq', 'arloqs'] and \
|
||||
basestation['modelId'].lower() not in ['abc1000', 'abc1000a']:
|
||||
continue
|
||||
proper_basestations[basestation['deviceId']] = basestation
|
||||
# avd2001 is the battery doorbell, and we don't want to drain its battery, so disable pings
|
||||
if basestation['modelId'].lower().startswith('avd2001'):
|
||||
continue
|
||||
devices_to_ping[basestation['deviceId']] = basestation
|
||||
|
||||
logger.info(f"Will send heartbeat to the following basestations: {list(proper_basestations.keys())}")
|
||||
logger.info(f"Will send heartbeat to the following devices: {list(devices_to_ping.keys())}")
|
||||
|
||||
# start heartbeat loop with only basestations
|
||||
asyncio.get_event_loop().create_task(heartbeat(self, list(proper_basestations.values())))
|
||||
# start heartbeat loop with only pingable devices
|
||||
asyncio.get_event_loop().create_task(heartbeat(self, list(devices_to_ping.values())))
|
||||
|
||||
# subscribe to all camera topics
|
||||
topics = [
|
||||
@@ -348,19 +405,19 @@ class Arlo(object):
|
||||
body['from'] = self.user_id+'_web'
|
||||
body['to'] = basestation_id
|
||||
|
||||
self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/notify/'+body['to'], body, headers={"xcloudId":basestation.get('xCloudId')})
|
||||
self.request.post(f'https://{self.BASE_URL}/hmsweb/users/devices/notify/'+body['to'], params=body, headers={"xcloudId":basestation.get('xCloudId')})
|
||||
return body.get('transId')
|
||||
|
||||
def Ping(self, basestation):
|
||||
basestation_id = basestation.get('deviceId')
|
||||
return self.Notify(basestation, {"action":"set","resource":"subscriptions/"+self.user_id+"_web","publishResponse":False,"properties":{"devices":[basestation_id]}})
|
||||
|
||||
def SubscribeToMotionEvents(self, basestation, camera, callback):
|
||||
def SubscribeToErrorEvents(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to motion events. You must provide a callback function which will get called once per motion event.
|
||||
Use this method to subscribe to error events. You must provide a callback function which will get called once per error event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(code, message)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -369,17 +426,121 @@ class Arlo(object):
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
# Note: It looks like sometimes a message is returned as an 'is' action
|
||||
# where a 'stateChangeReason' property contains the error message. This is
|
||||
# a bit of a hack but we will listen to both events with an 'error' key as
|
||||
# well as 'stateChangeReason' events.
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get('properties', {})
|
||||
stop = None
|
||||
if 'motionDetected' in properties:
|
||||
stop = callback(properties['motionDetected'])
|
||||
if 'error' in event:
|
||||
error = event['error']
|
||||
elif 'properties' in event:
|
||||
error = event['properties'].get('stateChangeReason', {})
|
||||
else:
|
||||
return None
|
||||
message = error.get('message')
|
||||
code = error.get('code')
|
||||
stop = callback(code, message)
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
|
||||
self.HandleEvents(basestation, resource, ['error', ('is', 'stateChangeReason')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToMotionEvents(self, basestation, camera, callback, logger) -> asyncio.Task:
|
||||
"""
|
||||
Use this method to subscribe to motion events. You must provide a callback function which will get called once per motion event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
return self._subscribe_to_motion_or_audio_events(basestation, camera, callback, logger, "motionDetected")
|
||||
|
||||
def SubscribeToAudioEvents(self, basestation, camera, callback, logger):
|
||||
"""
|
||||
Use this method to subscribe to audio events. You must provide a callback function which will get called once per audio event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
return self._subscribe_to_motion_or_audio_events(basestation, camera, callback, logger, "audioDetected")
|
||||
|
||||
def _subscribe_to_motion_or_audio_events(self, basestation, camera, callback, logger, event_key) -> asyncio.Task:
|
||||
"""
|
||||
Helper class to implement force reset of events (when event end signal is dropped) and delay of end
|
||||
of event signals (when the sensor turns off and on quickly)
|
||||
|
||||
event_key is either motionDetected or audioDetected
|
||||
"""
|
||||
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
# if we somehow miss the *Detected = False event, this task
|
||||
# is used to force the caller to register the end of the event
|
||||
force_reset_event_task: asyncio.Task = None
|
||||
|
||||
# when we receive a normal *Detected = False event, this
|
||||
# task is used to delay the delivery in case the sensor
|
||||
# registers an event immediately afterwards
|
||||
delayed_event_end_task: asyncio.Task = None
|
||||
|
||||
async def reset_event(sleep_duration: float) -> None:
|
||||
nonlocal force_reset_event_task, delayed_event_end_task
|
||||
await asyncio.sleep(sleep_duration)
|
||||
|
||||
logger.debug(f"{event_key}: delivering False")
|
||||
callback(False)
|
||||
|
||||
force_reset_event_task = None
|
||||
delayed_event_end_task = None
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
nonlocal force_reset_event_task, delayed_event_end_task
|
||||
properties = event.get('properties', {})
|
||||
|
||||
stop = None
|
||||
if event_key in properties:
|
||||
event_detected = properties[event_key]
|
||||
delivery_delay = 10
|
||||
|
||||
logger.debug(f"{event_key}: {event_detected} {'will delay delivery by ' + str(delivery_delay) + 's' if not event_detected else ''}".rstrip())
|
||||
|
||||
if force_reset_event_task:
|
||||
logger.debug(f"{event_key}: cancelling previous force reset task")
|
||||
force_reset_event_task.cancel()
|
||||
force_reset_event_task = None
|
||||
if delayed_event_end_task:
|
||||
logger.debug(f"{event_key}: cancelling previous delay event task")
|
||||
delayed_event_end_task.cancel()
|
||||
delayed_event_end_task = None
|
||||
|
||||
if event_detected:
|
||||
stop = callback(event_detected)
|
||||
|
||||
# schedule a callback to reset the sensor
|
||||
# if we somehow miss the *Detected = False event
|
||||
force_reset_event_task = asyncio.get_event_loop().create_task(reset_event(60))
|
||||
else:
|
||||
delayed_event_end_task = asyncio.get_event_loop().create_task(reset_event(delivery_delay))
|
||||
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', event_key)], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToBatteryEvents(self, basestation, camera, callback):
|
||||
@@ -387,7 +548,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to battery events. You must provide a callback function which will get called once per battery event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -414,7 +575,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to doorbell events. You must provide a callback function which will get called once per doorbell event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -450,7 +611,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to pushToTalk SDP answer events. You must provide a callback function which will get called once per SDP event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -478,7 +639,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to pushToTalk ICE candidate answer events. You must provide a callback function which will get called once per candidate event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -573,7 +734,7 @@ class Arlo(object):
|
||||
If you pass in a valid device type, as a string or a list, this method will return an array of just those devices that match that type. An example would be ['basestation', 'camera']
|
||||
To filter provisioned or unprovisioned devices pass in a True/False value for filter_provisioned. By default both types are returned.
|
||||
"""
|
||||
devices = self.request.get(f'https://{self.BASE_URL}/hmsweb/v2/users/devices')
|
||||
devices = self._getDevicesImpl()
|
||||
if device_type:
|
||||
devices = [ device for device in devices if device.get('deviceType') in device_type]
|
||||
|
||||
@@ -585,23 +746,45 @@ class Arlo(object):
|
||||
|
||||
return devices
|
||||
|
||||
async def StartStream(self, basestation, camera):
|
||||
@cached(cache=TTLCache(maxsize=1, ttl=60))
|
||||
def _getDevicesImpl(self):
|
||||
devices = self.request.get(f'https://{self.BASE_URL}/hmsweb/v2/users/devices')
|
||||
return devices
|
||||
|
||||
def GetDeviceCapabilities(self, device: dict) -> dict:
|
||||
return self._getDeviceCapabilitiesImpl(device['modelId'].lower(), device['interfaceVersion'])
|
||||
|
||||
@cached(cache=TTLCache(maxsize=64, ttl=60))
|
||||
def _getDeviceCapabilitiesImpl(self, model_id: str, interface_version: str) -> dict:
|
||||
return self.request.get(
|
||||
f'https://{self.BASE_URL}/resources/capabilities/{model_id}/{model_id}_{interface_version}.json',
|
||||
raw=True
|
||||
)
|
||||
|
||||
async def StartStream(self, basestation, camera, mode="rtsp"):
|
||||
"""
|
||||
This function returns the url of the rtsp video stream.
|
||||
This stream needs to be called within 30 seconds or else it becomes invalid.
|
||||
It can be streamed with: ffmpeg -re -i 'rtsps://<url>' -acodec copy -vcodec copy test.mp4
|
||||
The request to /users/devices/startStream returns: { url:rtsp://<url>:443/vzmodulelive?egressToken=b<xx>&userAgent=iOS&cameraId=<camid>}
|
||||
|
||||
If mode is set to "dash", returns the url to the mpd file for DASH streaming. Note that DASH
|
||||
has very specific header requirements - see GetMPDHeaders()
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
if mode not in ["rtsp", "dash"]:
|
||||
raise ValueError("mode must be 'rtsp' or 'dash'")
|
||||
|
||||
# nonlocal variable hack for Python 2.x.
|
||||
class nl:
|
||||
stream_url_dict = None
|
||||
|
||||
def trigger(self):
|
||||
ua = USER_AGENTS['arlo'] if mode == "rtsp" else USER_AGENTS["firefox"]
|
||||
nl.stream_url_dict = self.request.post(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/devices/startStream',
|
||||
{
|
||||
params={
|
||||
"to": camera.get('parentId'),
|
||||
"from": self.user_id + "_web",
|
||||
"resource": "cameras/" + camera.get('deviceId'),
|
||||
@@ -614,14 +797,19 @@ class Arlo(object):
|
||||
"cameraId": camera.get('deviceId')
|
||||
}
|
||||
},
|
||||
headers={"xcloudId":camera.get('xCloudId')}
|
||||
headers={"xcloudId":camera.get('xCloudId'), 'User-Agent': ua}
|
||||
)
|
||||
|
||||
def callback(self, event):
|
||||
#return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
if "error" in event:
|
||||
return None
|
||||
properties = event.get("properties", {})
|
||||
if properties.get("activityState") == "userStreamActive":
|
||||
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
if mode == "rtsp":
|
||||
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
else:
|
||||
return nl.stream_url_dict['url'].replace(":80", "")
|
||||
return None
|
||||
|
||||
return await self.TriggerAndHandleEvent(
|
||||
@@ -632,6 +820,37 @@ class Arlo(object):
|
||||
callback,
|
||||
)
|
||||
|
||||
def GetMPDHeaders(self, url: str) -> dict:
|
||||
parsed = urlparse(url)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
headers = {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Connection": "keep-alive",
|
||||
"DNT": "1",
|
||||
"Egress-Token": query['egressToken'][0], # this is very important
|
||||
"Origin": "https://my.arlo.com",
|
||||
"Referer": "https://my.arlo.com/",
|
||||
"User-Agent": USER_AGENTS["firefox"],
|
||||
}
|
||||
return headers
|
||||
|
||||
def GetSIPInfo(self):
|
||||
resp = self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/sipInfo')
|
||||
return resp
|
||||
|
||||
def GetSIPInfoV2(self, camera):
|
||||
resp = self.request.get(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/devices/sipInfo/v2',
|
||||
headers={
|
||||
"xcloudId": camera.get('xCloudId'),
|
||||
"cameraId": camera.get('deviceId'),
|
||||
}
|
||||
)
|
||||
return resp
|
||||
|
||||
def StartPushToTalk(self, basestation, camera):
|
||||
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
|
||||
resp = self.request.get(url)
|
||||
@@ -674,7 +893,7 @@ class Arlo(object):
|
||||
def trigger(self):
|
||||
self.request.post(
|
||||
f"https://{self.BASE_URL}/hmsweb/users/devices/fullFrameSnapshot",
|
||||
{
|
||||
params={
|
||||
"to": camera.get("parentId"),
|
||||
"from": self.user_id + "_web",
|
||||
"resource": "cameras/" + camera.get("deviceId"),
|
||||
@@ -689,6 +908,8 @@ class Arlo(object):
|
||||
)
|
||||
|
||||
def callback(self, event):
|
||||
if "error" in event:
|
||||
return None
|
||||
properties = event.get("properties", {})
|
||||
url = properties.get("presignedFullFrameSnapshotUrl")
|
||||
if url:
|
||||
@@ -709,3 +930,191 @@ class Arlo(object):
|
||||
trigger,
|
||||
callback,
|
||||
)
|
||||
|
||||
def SirenOn(self, basestation, camera=None):
|
||||
if camera is not None:
|
||||
resource = f"siren/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "on",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": "siren",
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "on",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
|
||||
def SirenOff(self, basestation, camera=None):
|
||||
if camera is not None:
|
||||
resource = f"siren/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "off",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": "siren",
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"sirenState": "off",
|
||||
"duration": 300,
|
||||
"volume": 8,
|
||||
"pattern": "alarm"
|
||||
}
|
||||
})
|
||||
|
||||
def SpotlightOn(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"spotlight": {
|
||||
"enabled": True,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def SpotlightOff(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"spotlight": {
|
||||
"enabled": False,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def FloodlightOn(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"floodlight": {
|
||||
"on": True,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def FloodlightOff(self, basestation, camera):
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"floodlight": {
|
||||
"on": False,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
def NightlightOn(self, basestation):
|
||||
resource = f"cameras/{basestation.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"nightLight": {
|
||||
"enabled": True
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def NightlightOff(self, basestation):
|
||||
resource = f"cameras/{basestation.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"nightLight": {
|
||||
"enabled": False
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def GetLibrary(self, device, from_date: datetime, to_date: datetime):
|
||||
"""
|
||||
This call returns the following:
|
||||
presignedContentUrl is a link to the actual video in Amazon AWS.
|
||||
presignedThumbnailUrl is a link to the thumbnail .jpg of the actual video in Amazon AWS.
|
||||
[
|
||||
{
|
||||
"mediaDurationSecond": 30,
|
||||
"contentType": "video/mp4",
|
||||
"name": "XXXXXXXXXXXXX",
|
||||
"presignedContentUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX.mp4?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"lastModified": 1472881430181,
|
||||
"localCreatedDate": XXXXXXXXXXXXX,
|
||||
"presignedThumbnailUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX_thumb.jpg?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"reason": "motionRecord",
|
||||
"deviceId": "XXXXXXXXXXXXX",
|
||||
"createdBy": "XXXXXXXXXXXXX",
|
||||
"createdDate": "20160903",
|
||||
"timeZone": "America/Chicago",
|
||||
"ownerId": "XXX-XXXXXXX",
|
||||
"utcCreatedDate": XXXXXXXXXXXXX,
|
||||
"currentState": "new",
|
||||
"mediaDuration": "00:00:30"
|
||||
}
|
||||
]
|
||||
"""
|
||||
# give the query range a bit of buffer
|
||||
from_date_internal = from_date - timedelta(days=1)
|
||||
to_date_internal = to_date + timedelta(days=1)
|
||||
|
||||
return [
|
||||
result for result in
|
||||
self._getLibraryCached(from_date_internal.strftime("%Y%m%d"), to_date_internal.strftime("%Y%m%d"))
|
||||
if result["deviceId"] == device["deviceId"]
|
||||
and datetime.fromtimestamp(int(result["name"]) / 1000.0) <= to_date
|
||||
and datetime.fromtimestamp(int(result["name"]) / 1000.0) >= from_date
|
||||
]
|
||||
|
||||
@cached(cache=TTLCache(maxsize=512, ttl=60))
|
||||
def _getLibraryCached(self, from_date: str, to_date: str):
|
||||
logger.debug(f"Library cache miss for {from_date}, {to_date}")
|
||||
return self.request.post(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/library',
|
||||
params={
|
||||
'dateFrom': from_date,
|
||||
'dateTo': to_date
|
||||
}
|
||||
)
|
||||
|
||||
def GetSmartFeatures(self, device) -> dict:
|
||||
smart_features = self._getSmartFeaturesCached()
|
||||
key = f"{device['owner']['ownerId']}_{device['deviceId']}"
|
||||
return smart_features["features"].get(key, {})
|
||||
|
||||
@cached(cache=TTLCache(maxsize=1, ttl=60))
|
||||
def _getSmartFeaturesCached(self) -> dict:
|
||||
return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/subscription/smart/features')
|
||||
31
plugins/arlo/src/arlo_plugin/arlo/host_picker.py
Normal file
31
plugins/arlo/src/arlo_plugin/arlo/host_picker.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import ssl
|
||||
from socket import setdefaulttimeout
|
||||
import requests
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
import scrypted_arlo_go
|
||||
|
||||
from .logging import logger
|
||||
|
||||
|
||||
setdefaulttimeout(15)
|
||||
|
||||
|
||||
def pick_host(hosts, hostname_to_match, endpoint_to_test):
|
||||
setdefaulttimeout(5)
|
||||
|
||||
try:
|
||||
session = requests.Session()
|
||||
session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
c = ssl.get_server_certificate((host, 443))
|
||||
scrypted_arlo_go.VerifyCertHostname(c, hostname_to_match)
|
||||
r = session.post(f"https://{host}{endpoint_to_test}", headers={"Host": hostname_to_match})
|
||||
r.raise_for_status()
|
||||
return host
|
||||
except Exception as e:
|
||||
logger.warning(f"{host} is invalid: {e}")
|
||||
raise Exception("no valid hosts found!")
|
||||
finally:
|
||||
setdefaulttimeout(15)
|
||||
@@ -9,7 +9,7 @@ logger.setLevel(logging.INFO)
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
# log formatting
|
||||
fmt = logging.Formatter("[Arlo] %(message)s")
|
||||
fmt = logging.Formatter("[Arlo]: %(message)s")
|
||||
ch.setFormatter(fmt)
|
||||
|
||||
# configure handler to logger
|
||||
|
||||
@@ -14,11 +14,23 @@
|
||||
# limitations under the License.
|
||||
##
|
||||
|
||||
from functools import partialmethod
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
import cloudscraper
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .logging import logger
|
||||
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as curl_cffi_requests
|
||||
HAS_CURL_CFFI = True
|
||||
except:
|
||||
HAS_CURL_CFFI = False
|
||||
|
||||
#from requests_toolbelt.utils import dump
|
||||
#def print_raw_http(response):
|
||||
# data = dump.dump_all(response, request_prefix=b'', response_prefix=b'')
|
||||
@@ -27,8 +39,21 @@ import uuid
|
||||
class Request(object):
|
||||
"""HTTP helper class"""
|
||||
|
||||
def __init__(self, timeout=5):
|
||||
self.session = requests.Session()
|
||||
def __init__(self, timeout=5, mode="curl" if HAS_CURL_CFFI else "cloudscraper"):
|
||||
if mode == "curl":
|
||||
logger.debug("HTTP helper using curl_cffi")
|
||||
self.session = curl_cffi_requests.Session(impersonate="chrome110")
|
||||
elif mode == "cloudscraper":
|
||||
logger.debug("HTTP helper using cloudscraper")
|
||||
from .arlo_async import USER_AGENTS
|
||||
self.session = cloudscraper.CloudScraper(browser={"custom": USER_AGENTS["android"]})
|
||||
elif mode == "ip":
|
||||
logger.debug("HTTP helper using requests with HostHeaderSSLAdapter")
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
else:
|
||||
logger.debug("HTTP helper using requests")
|
||||
self.session = requests.Session()
|
||||
self.timeout = timeout
|
||||
|
||||
def gen_event_id(self):
|
||||
@@ -37,7 +62,7 @@ class Request(object):
|
||||
def get_time(self):
|
||||
return int(time.time_ns() / 1_000_000)
|
||||
|
||||
def _request(self, url, method='GET', params={}, headers={}, stream=False, raw=False):
|
||||
def _request(self, url, method='GET', params={}, headers={}, raw=False, skip_event_id=False):
|
||||
|
||||
## uncomment for debug logging
|
||||
"""
|
||||
@@ -51,14 +76,13 @@ class Request(object):
|
||||
req_log.propagate = True
|
||||
#"""
|
||||
|
||||
url = f'{url}?eventId={self.gen_event_id()}&time={self.get_time()}'
|
||||
if not skip_event_id:
|
||||
url = f'{url}?eventId={self.gen_event_id()}&time={self.get_time()}'
|
||||
|
||||
if method == 'GET':
|
||||
#print('COOKIES: ', self.session.cookies.get_dict())
|
||||
r = self.session.get(url, params=params, headers=headers, stream=stream, timeout=self.timeout)
|
||||
r = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
if stream is True:
|
||||
return r
|
||||
elif method == 'PUT':
|
||||
r = self.session.put(url, json=params, headers=headers, timeout=self.timeout)
|
||||
r.raise_for_status()
|
||||
@@ -81,14 +105,14 @@ class Request(object):
|
||||
else:
|
||||
raise HTTPError('Request ({0} {1}) failed: {2}'.format(method, url, r.json()), response=r)
|
||||
|
||||
def get(self, url, params={}, headers={}, stream=False, raw=False):
|
||||
return self._request(url, 'GET', params=params, headers=headers, stream=stream, raw=raw)
|
||||
def get(self, url, **kwargs):
|
||||
return self._request(url, 'GET', **kwargs)
|
||||
|
||||
def put(self, url, params={}, headers={}, raw=False):
|
||||
return self._request(url, 'PUT', params=params, headers=headers, raw=raw)
|
||||
def put(self, url, **kwargs):
|
||||
return self._request(url, 'PUT', **kwargs)
|
||||
|
||||
def post(self, url, params={}, headers={}, raw=False):
|
||||
return self._request(url, 'POST', params=params, headers=headers, raw=raw)
|
||||
def post(self, url, **kwargs):
|
||||
return self._request(url, 'POST', **kwargs)
|
||||
|
||||
def options(self, url, headers={}, raw=False):
|
||||
return self._request(url, 'OPTIONS', headers=headers, raw=raw)
|
||||
def options(self, url, **kwargs):
|
||||
return self._request(url, 'OPTIONS', **kwargs)
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import sseclient
|
||||
import threading
|
||||
|
||||
from .stream_async import Stream
|
||||
from .stream_async import Stream
|
||||
from .logging import logger
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class EventStream(Stream):
|
||||
continue
|
||||
|
||||
try:
|
||||
response = json.loads(event.data)
|
||||
response = json.loads(event.data.strip())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
@@ -36,6 +36,7 @@ class EventStream(Stream):
|
||||
if self.event_stream_stop_event.is_set() or \
|
||||
self.shutting_down_stream is event_stream:
|
||||
logger.info(f"SSE {id(event_stream)} disconnected")
|
||||
self.shutting_down_stream = None
|
||||
return None
|
||||
elif response.get('status') == 'connected':
|
||||
if not self.connected:
|
||||
@@ -59,10 +60,10 @@ class EventStream(Stream):
|
||||
self.shutting_down_stream = self.event_stream
|
||||
self.event_stream = None
|
||||
await self.start()
|
||||
# give it an extra sleep to ensure any previous connections have disconnected properly
|
||||
# this is so we can mark reconnecting to False properly
|
||||
await asyncio.sleep(1)
|
||||
self.shutting_down_stream = None
|
||||
while self.shutting_down_stream is not None:
|
||||
# ensure any previous connections have disconnected properly
|
||||
# this is so we can mark reconnecting to False properly
|
||||
await asyncio.sleep(1)
|
||||
self.reconnecting = False
|
||||
|
||||
def subscribe(self, topics):
|
||||
|
||||
@@ -177,22 +177,25 @@ class Stream:
|
||||
|
||||
now = time.time()
|
||||
event = StreamEvent(response, now, now + self.expire)
|
||||
self._queue_impl(key, event)
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
# specialized setup for error responses
|
||||
if 'error' in response:
|
||||
key = f"{resource}/error"
|
||||
self._queue_impl(key, event)
|
||||
|
||||
# for optimized lookups, notify listeners of individual properties
|
||||
properties = response.get('properties', {})
|
||||
for property in properties.keys():
|
||||
key = f"{resource}/{action}/{property}"
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
self._queue_impl(key, event)
|
||||
|
||||
def _queue_impl(self, key, event):
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
|
||||
def requeue(self, event, resource, action, property=None):
|
||||
if not property:
|
||||
|
||||
76
plugins/arlo/src/arlo_plugin/base.py
Normal file
76
plugins/arlo/src/arlo_plugin/base.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from scrypted_sdk import ScryptedDeviceBase
|
||||
from scrypted_sdk.types import Device
|
||||
|
||||
from .logging import ScryptedDeviceLoggerMixin
|
||||
from .util import BackgroundTaskMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
|
||||
nativeId: str = None
|
||||
arlo_device: dict = None
|
||||
arlo_basestation: dict = None
|
||||
arlo_capabilities: dict = None
|
||||
provider: ArloProvider = None
|
||||
stop_subscriptions: bool = False
|
||||
|
||||
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
self.logger_name = nativeId
|
||||
|
||||
self.nativeId = nativeId
|
||||
self.arlo_device = arlo_device
|
||||
self.arlo_basestation = arlo_basestation
|
||||
self.provider = provider
|
||||
self.logger.setLevel(self.provider.get_current_log_level())
|
||||
|
||||
try:
|
||||
self.arlo_capabilities = self.provider.arlo.GetDeviceCapabilities(self.arlo_device)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not load device capabilities: {e}")
|
||||
self.arlo_capabilities = {}
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.stop_subscriptions = True
|
||||
self.cancel_pending_tasks()
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
"""Returns the list of Scrypted interfaces that applies to this device."""
|
||||
return []
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
"""Returns the Scrypted device type that applies to this device."""
|
||||
return ""
|
||||
|
||||
def get_device_manifest(self) -> Device:
|
||||
"""Returns the Scrypted device manifest representing this device."""
|
||||
parent = None
|
||||
if self.arlo_device.get("parentId") and self.arlo_device["parentId"] != self.arlo_device["deviceId"]:
|
||||
parent = self.arlo_device["parentId"]
|
||||
|
||||
return {
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
"manufacturer": "Arlo",
|
||||
"firmware": self.arlo_device.get("firmwareVersion"),
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": self.arlo_device["deviceId"],
|
||||
"name": self.arlo_device["deviceName"],
|
||||
"interfaces": self.get_applicable_interfaces(),
|
||||
"type": self.get_device_type(),
|
||||
"providerNativeId": parent,
|
||||
}
|
||||
|
||||
def get_builtin_child_device_manifests(self) -> List[Device]:
|
||||
"""Returns the list of child device manifests representing hardware features built into this device."""
|
||||
return []
|
||||
90
plugins/arlo/src/arlo_plugin/basestation.py
Normal file
90
plugins/arlo/src/arlo_plugin/basestation.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from scrypted_sdk import ScryptedDeviceBase
|
||||
from scrypted_sdk.types import Device, DeviceProvider, Setting, SettingValue, Settings, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .base import ArloDeviceBase
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloBasestation(ArloDeviceBase, DeviceProvider, Settings):
|
||||
MODELS_WITH_SIRENS = [
|
||||
"vmb4000",
|
||||
"vmb4500"
|
||||
]
|
||||
|
||||
vss: ArloSirenVirtualSecuritySystem = None
|
||||
|
||||
def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_basestation, arlo_basestation=arlo_basestation, provider=provider)
|
||||
|
||||
@property
|
||||
def has_siren(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS])
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
return [
|
||||
ScryptedInterface.DeviceProvider.value,
|
||||
ScryptedInterface.Settings.value,
|
||||
]
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
return ScryptedDeviceType.DeviceProvider.value
|
||||
|
||||
def get_builtin_child_device_manifests(self) -> List[Device]:
|
||||
if not self.has_siren:
|
||||
# this basestation has no builtin siren, so no manifests to return
|
||||
return []
|
||||
|
||||
vss = self.get_or_create_vss()
|
||||
return [
|
||||
{
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
"manufacturer": "Arlo",
|
||||
"firmware": self.arlo_device.get("firmwareVersion"),
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": vss.nativeId,
|
||||
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
|
||||
"interfaces": vss.get_applicable_interfaces(),
|
||||
"type": vss.get_device_type(),
|
||||
"providerNativeId": self.nativeId,
|
||||
},
|
||||
] + vss.get_builtin_child_device_manifests()
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ScryptedDeviceBase:
|
||||
if not nativeId.startswith(self.nativeId):
|
||||
# must be a camera, so get it from the provider
|
||||
return await self.provider.getDevice(nativeId)
|
||||
if not nativeId.endswith("vss"):
|
||||
return None
|
||||
return self.get_or_create_vss()
|
||||
|
||||
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
|
||||
vss_id = f'{self.arlo_device["deviceId"]}.vss'
|
||||
if not self.vss:
|
||||
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
return self.vss
|
||||
|
||||
async def getSettings(self) -> List[Setting]:
|
||||
return [
|
||||
{
|
||||
"group": "General",
|
||||
"key": "print_debug",
|
||||
"title": "Debug Info",
|
||||
"description": "Prints information about this device to console.",
|
||||
"type": "button",
|
||||
}
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
if key == "print_debug":
|
||||
self.logger.info(f"Device Capabilities: {self.arlo_capabilities}")
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,42 +3,74 @@ import subprocess
|
||||
import time
|
||||
import threading
|
||||
|
||||
import scrypted_arlo_go
|
||||
|
||||
|
||||
HEARTBEAT_INTERVAL = 5
|
||||
|
||||
|
||||
def multiprocess_main(name, child_conn, exe, args):
|
||||
print(f"[{name}] Child process starting")
|
||||
sp = subprocess.Popen([exe, *args])
|
||||
def multiprocess_main(name, logger_port, child_conn, exe, args):
|
||||
logger = scrypted_arlo_go.NewTCPLogger(logger_port, "HeartbeatChildProcess")
|
||||
|
||||
logger.Send(f"{name} starting\n")
|
||||
sp = subprocess.Popen([exe, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
# pull stdout and stderr from the subprocess and forward it over to
|
||||
# our tcp logger
|
||||
def logging_thread(stdstream):
|
||||
while True:
|
||||
line = stdstream.readline()
|
||||
if not line:
|
||||
break
|
||||
line = str(line, 'utf-8')
|
||||
logger.Send(line)
|
||||
stdout_t = threading.Thread(target=logging_thread, args=(sp.stdout,))
|
||||
stderr_t = threading.Thread(target=logging_thread, args=(sp.stderr,))
|
||||
stdout_t.start()
|
||||
stderr_t.start()
|
||||
|
||||
while True:
|
||||
has_data = child_conn.poll(HEARTBEAT_INTERVAL * 3)
|
||||
if not has_data:
|
||||
break
|
||||
|
||||
# check if the subprocess is still alive, if not then exit
|
||||
if sp.poll() is not None:
|
||||
break
|
||||
|
||||
keep_alive = child_conn.recv()
|
||||
if not keep_alive:
|
||||
break
|
||||
|
||||
logger.Send(f"{name} exiting\n")
|
||||
|
||||
sp.terminate()
|
||||
sp.wait()
|
||||
print(f"[{name}] Child process exiting")
|
||||
|
||||
stdout_t.join()
|
||||
stderr_t.join()
|
||||
|
||||
logger.Send(f"{name} exited\n")
|
||||
logger.Close()
|
||||
|
||||
|
||||
class HeartbeatChildProcess:
|
||||
"""Class to manage running a child process that gets cleaned up if the parent exits.
|
||||
|
||||
|
||||
When spawining subprocesses in Python, if the parent is forcibly killed (as is the case
|
||||
when Scrypted restarts plugins), subprocesses get orphaned. This approach uses parent-child
|
||||
heartbeats for the child to ensure that the parent process is still alive, and to cleanly
|
||||
exit the child if the parent has terminated.
|
||||
"""
|
||||
|
||||
def __init__(self, name, exe, *args):
|
||||
def __init__(self, name, logger_port, exe, *args):
|
||||
self.name = name
|
||||
self.logger_port = logger_port
|
||||
self.exe = exe
|
||||
self.args = args
|
||||
|
||||
self.parent_conn, self.child_conn = multiprocessing.Pipe()
|
||||
self.process = multiprocessing.Process(target=multiprocess_main, args=(name, self.child_conn, exe, args))
|
||||
self.process = multiprocessing.Process(target=multiprocess_main, args=(name, logger_port, self.child_conn, exe, args))
|
||||
self.process.daemon = True
|
||||
self._stop = False
|
||||
|
||||
@@ -55,4 +87,7 @@ class HeartbeatChildProcess:
|
||||
def heartbeat(self):
|
||||
while not self._stop:
|
||||
time.sleep(HEARTBEAT_INTERVAL)
|
||||
if not self.process.is_alive():
|
||||
self.stop()
|
||||
break
|
||||
self.parent_conn.send(True)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user