Compare commits
462 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33cacd15d | ||
|
|
e225b746c4 | ||
|
|
1e1fda6b9a | ||
|
|
e23c9c22dc | ||
|
|
28f1ce9d4e | ||
|
|
5f4e2793ff | ||
|
|
1e1755fa7e | ||
|
|
ebd56b86e4 | ||
|
|
4607bec07c | ||
|
|
a26cdfea2a | ||
|
|
6d0da449ad | ||
|
|
ad15fe3324 | ||
|
|
fbe3e83884 | ||
|
|
0d4cf34930 | ||
|
|
3a3e15cd74 | ||
|
|
8b9cfebbfa | ||
|
|
b3087058a7 | ||
|
|
4e923a78da | ||
|
|
b5593d6251 | ||
|
|
40b7b621a0 | ||
|
|
7104ad6378 | ||
|
|
51f893ef63 | ||
|
|
29c5dfd73b | ||
|
|
1615f79a0b | ||
|
|
dc5a6126b9 | ||
|
|
0fbe3e4686 | ||
|
|
c9641568f8 | ||
|
|
dceea38eb8 | ||
|
|
cd1fce71e2 | ||
|
|
3b6454f107 | ||
|
|
d238d8d4ba | ||
|
|
a24d46f3d2 | ||
|
|
df7deef4aa | ||
|
|
e94cea0236 | ||
|
|
4794a6dbf3 | ||
|
|
57439634e5 | ||
|
|
89e6e50b12 | ||
|
|
c21ef069bd | ||
|
|
5d41bb38da | ||
|
|
b289024083 | ||
|
|
56572bcec9 | ||
|
|
3e78209817 | ||
|
|
299204313f | ||
|
|
88b134f4b9 | ||
|
|
e8bd72a329 | ||
|
|
214dbc8153 | ||
|
|
3f0c706154 | ||
|
|
013131e816 | ||
|
|
0342bf91f6 | ||
|
|
6fb98c7e84 | ||
|
|
f4168ff4eb | ||
|
|
a5febd7ca0 | ||
|
|
31428b4c28 | ||
|
|
3e0dfc6bda | ||
|
|
4290eb0abb | ||
|
|
fd2d7e9485 | ||
|
|
a57cf3b1e6 | ||
|
|
fad485c0d7 | ||
|
|
9e3cf83b07 | ||
|
|
ebe0b6ea7f | ||
|
|
4ce0ecaaa2 | ||
|
|
44264fb50b | ||
|
|
ab188bfe80 | ||
|
|
83d32da7f1 | ||
|
|
e68b3f401f | ||
|
|
43a73c6d89 | ||
|
|
6c6613d841 | ||
|
|
479ef136a6 | ||
|
|
b42abf377b | ||
|
|
5713935ccc | ||
|
|
09fc609c7f | ||
|
|
e44ba222b8 | ||
|
|
e34a5a7c3d | ||
|
|
e490225c4a | ||
|
|
e769e8ea98 | ||
|
|
ec5c164552 | ||
|
|
64a213424d | ||
|
|
e81c454c1e | ||
|
|
776307bcbc | ||
|
|
95c97e3eb2 | ||
|
|
08926a35a9 | ||
|
|
c037548ffb | ||
|
|
462189efc2 | ||
|
|
a4fed9c7ad | ||
|
|
3c8f94ab2f | ||
|
|
fe3391c89c | ||
|
|
270b43bed6 | ||
|
|
c0fe12827f | ||
|
|
3bbda53107 | ||
|
|
0d7ee00485 | ||
|
|
c605c3ddb5 | ||
|
|
099ba4f081 | ||
|
|
c14487ac27 | ||
|
|
991c31dda8 | ||
|
|
3865efd1f7 | ||
|
|
9c5ce45c1e | ||
|
|
4ab7bc1298 | ||
|
|
0ebbc5ea8f | ||
|
|
86dcb66e6a | ||
|
|
11831e5d87 | ||
|
|
6b3dc8c1ae | ||
|
|
a49256f073 | ||
|
|
ea408377ec | ||
|
|
2da762dfc2 | ||
|
|
bfbc6ba6ce | ||
|
|
c2747c80dc | ||
|
|
2b58de196e | ||
|
|
731744afbc | ||
|
|
f4d88493b1 | ||
|
|
265fc4b481 | ||
|
|
40a300cff1 | ||
|
|
c410907c58 | ||
|
|
199f333fc1 | ||
|
|
e39bc8c5e6 | ||
|
|
a55099de12 | ||
|
|
17900f0589 | ||
|
|
075d8bc4ab | ||
|
|
28fce1bb8b | ||
|
|
14a2790825 | ||
|
|
c2cd491c96 | ||
|
|
6301089620 | ||
|
|
c591a87015 | ||
|
|
544700ac12 | ||
|
|
2c54dc07ae | ||
|
|
0dad6eaa76 | ||
|
|
d18b8e0694 | ||
|
|
a4130ed047 | ||
|
|
1f89dcb34c | ||
|
|
e86ed47533 | ||
|
|
5fb75e351d | ||
|
|
8ef3fe7a24 | ||
|
|
5c8f034d7c | ||
|
|
561852bc15 | ||
|
|
d14c592d55 | ||
|
|
bc439d6b7c | ||
|
|
aedb4212fe | ||
|
|
7b780a0eb9 | ||
|
|
6aaaccaece | ||
|
|
0e42b71e4b | ||
|
|
71a6f9d6a6 | ||
|
|
68c77aaca4 | ||
|
|
4b73c168b3 | ||
|
|
8f4b67dc5c | ||
|
|
8a8bee33c1 | ||
|
|
2b353cf4c8 | ||
|
|
6d9cd45936 | ||
|
|
0bc6fadb23 | ||
|
|
407f573a29 | ||
|
|
39674ef9b6 | ||
|
|
ffaed01dc3 | ||
|
|
9a144a9a05 | ||
|
|
76e63120f0 | ||
|
|
154bc6fef7 | ||
|
|
8302c564c2 | ||
|
|
affda9e94f | ||
|
|
30148f453c | ||
|
|
259313c454 | ||
|
|
4f82d49f15 | ||
|
|
a0b7fc54de | ||
|
|
5501093ff9 | ||
|
|
b2622d92f2 | ||
|
|
957bf742d9 | ||
|
|
b06a3ac55f | ||
|
|
9c195594ea | ||
|
|
d32efd6500 | ||
|
|
03f1957739 | ||
|
|
94f564a218 | ||
|
|
286aa61a18 | ||
|
|
43d30a91f9 | ||
|
|
7be81ab7e2 | ||
|
|
31c7c9d86a | ||
|
|
820c66311d | ||
|
|
84cbe28a47 | ||
|
|
cad1cdd5ce | ||
|
|
2a624a95e5 | ||
|
|
c6d8333402 | ||
|
|
96dbb1776c | ||
|
|
b43a002650 | ||
|
|
a58d66d484 | ||
|
|
bcc9be62e9 | ||
|
|
38dd9e2ee2 | ||
|
|
3033cd9626 | ||
|
|
12273b7d1e | ||
|
|
afe1421c39 | ||
|
|
a9fdb71402 | ||
|
|
9cac4cfd18 | ||
|
|
fbaf9b97aa | ||
|
|
0a55732919 | ||
|
|
ece0ecbd8f | ||
|
|
d2743465c3 | ||
|
|
e41af930c5 | ||
|
|
582b5182e6 | ||
|
|
2b85aa0f27 | ||
|
|
e74e0b7e21 | ||
|
|
70d6813938 | ||
|
|
c1d84d6e08 | ||
|
|
5c69c70013 | ||
|
|
6379aa89ef | ||
|
|
c2756a3a4a | ||
|
|
303ced735a | ||
|
|
bc70803cc0 | ||
|
|
171b04f267 | ||
|
|
1cd5c194cc | ||
|
|
c15fe4281e | ||
|
|
1bd7f37041 | ||
|
|
170bc5f6ad | ||
|
|
8daf74e6db | ||
|
|
b84adf514e | ||
|
|
947aa151a5 | ||
|
|
12c734fe1b | ||
|
|
724b9332f4 | ||
|
|
a1f90607af | ||
|
|
1a954cc232 | ||
|
|
22cb23a075 | ||
|
|
608f82cf81 | ||
|
|
2f4608e697 | ||
|
|
c08ce3115a | ||
|
|
470c28d6ef | ||
|
|
7ea1d8e85d | ||
|
|
1669438312 | ||
|
|
d4b77cac66 | ||
|
|
f1bebd0612 | ||
|
|
003e1f79f0 | ||
|
|
97702da9ef | ||
|
|
35cf9f9352 | ||
|
|
5225823e8b | ||
|
|
2569e7c823 | ||
|
|
aa5c4d5064 | ||
|
|
40ff2a8315 | ||
|
|
bf150712a0 | ||
|
|
92531ff675 | ||
|
|
6919c26311 | ||
|
|
f19c09f239 | ||
|
|
7cadb8ffac | ||
|
|
ea204b24a6 | ||
|
|
45799362ce | ||
|
|
2836e10262 | ||
|
|
a04d463e0f | ||
|
|
3ce8a57daa | ||
|
|
1647c73375 | ||
|
|
b7980b7cbf | ||
|
|
0a6114cc60 | ||
|
|
149675cfb3 | ||
|
|
b8eec159bc | ||
|
|
ddb93b28cd | ||
|
|
4ed6d1a9fd | ||
|
|
bd60e39b24 | ||
|
|
b6636b10f0 | ||
|
|
8c8beea2eb | ||
|
|
3592a98f2a | ||
|
|
56f8418d13 | ||
|
|
5bb8ea0f86 | ||
|
|
daddf10035 | ||
|
|
2ac8e1509d | ||
|
|
3cd3208183 | ||
|
|
be217021a2 | ||
|
|
a2bbb67670 | ||
|
|
465189f4b8 | ||
|
|
173f7fa4f6 | ||
|
|
d405232d81 | ||
|
|
673fd95bbd | ||
|
|
25b2a663c8 | ||
|
|
962ecf2cae | ||
|
|
4c3f1c43fa | ||
|
|
82dfa96699 | ||
|
|
83d3add41a | ||
|
|
54db7dc64e | ||
|
|
4c04e9e403 | ||
|
|
de44217f65 | ||
|
|
3ae6079615 | ||
|
|
3f3409ef1b | ||
|
|
fd90b8d5f0 | ||
|
|
782c9930da | ||
|
|
d0b46c35a9 | ||
|
|
6c7671dc21 | ||
|
|
bdc3c204d4 | ||
|
|
2013830677 | ||
|
|
95906a9ed5 | ||
|
|
6fdd4bd0f4 | ||
|
|
37cf7aada0 | ||
|
|
dfdc41626b | ||
|
|
237b3ce0d9 | ||
|
|
05745bf3c5 | ||
|
|
8a6eaa5389 | ||
|
|
7ed298139d | ||
|
|
82908b82c0 | ||
|
|
946d88236c | ||
|
|
1aa1df885d | ||
|
|
7c94ed9b50 | ||
|
|
f7dbff4753 | ||
|
|
00b5e762a7 | ||
|
|
e1440eb76f | ||
|
|
4adb8e4202 | ||
|
|
e870370d0c | ||
|
|
f944b76c1f | ||
|
|
62543bdfcf | ||
|
|
447a321eb7 | ||
|
|
d094934bd9 | ||
|
|
c4f8959072 | ||
|
|
d682bd2ebb | ||
|
|
437ab70cd9 | ||
|
|
15031cde1f | ||
|
|
e88008552c | ||
|
|
fd49deefb8 | ||
|
|
1f2973abd2 | ||
|
|
317cd7671f | ||
|
|
9556efc224 | ||
|
|
2f9db83868 | ||
|
|
c627832ebd | ||
|
|
7d2df3af42 | ||
|
|
e9288bd4a1 | ||
|
|
191620b55b | ||
|
|
90b6fc1e49 | ||
|
|
cd3c748dd0 | ||
|
|
34dbc7930e | ||
|
|
112633a776 | ||
|
|
56416109b1 | ||
|
|
a889abae98 | ||
|
|
dcb50ba3ff | ||
|
|
3c61ddb806 | ||
|
|
2a9aba1df8 | ||
|
|
450acbdcb1 | ||
|
|
80aff3199a | ||
|
|
834eff20c7 | ||
|
|
6314f5e45a | ||
|
|
5b3793e810 | ||
|
|
9a2bff48c5 | ||
|
|
aa9903b328 | ||
|
|
c1f4ae96fa | ||
|
|
d5995d93e2 | ||
|
|
f13844cf3e | ||
|
|
6d7add8272 | ||
|
|
983a683d54 | ||
|
|
f3e5cf2a8b | ||
|
|
40db551799 | ||
|
|
fdb9e03656 | ||
|
|
48976b2947 | ||
|
|
3ee022c2be | ||
|
|
ab24a61fd3 | ||
|
|
8d2237b26f | ||
|
|
8ad05bbd5b | ||
|
|
7499e79dc7 | ||
|
|
7132278204 | ||
|
|
60fa494ed0 | ||
|
|
815f204136 | ||
|
|
fe80fab811 | ||
|
|
2699ecd93d | ||
|
|
5d634f5876 | ||
|
|
9979789e08 | ||
|
|
79d2d4e366 | ||
|
|
90b4bcfec9 | ||
|
|
49df286cfa | ||
|
|
20edf8a622 | ||
|
|
bb76102171 | ||
|
|
e71f9b585c | ||
|
|
1effc45f18 | ||
|
|
2e5e5b7be0 | ||
|
|
1df5cfefd0 | ||
|
|
5d1e2663b8 | ||
|
|
59441c414b | ||
|
|
419d007445 | ||
|
|
90bca27bde | ||
|
|
0050624880 | ||
|
|
c4ea7938d1 | ||
|
|
b5d58455b6 | ||
|
|
82993df715 | ||
|
|
555a688c16 | ||
|
|
0241a5fb93 | ||
|
|
db7351e7d4 | ||
|
|
891e9792f8 | ||
|
|
97e7333415 | ||
|
|
dc4dd07ced | ||
|
|
937f615c8c | ||
|
|
7578cf092e | ||
|
|
3041207177 | ||
|
|
46d66122aa | ||
|
|
d05e3a92f3 | ||
|
|
4a4b077132 | ||
|
|
cf5e010faf | ||
|
|
46616467f4 | ||
|
|
3dcb36adf9 | ||
|
|
855940fb03 | ||
|
|
1f25e1a308 | ||
|
|
232298d7f4 | ||
|
|
fa9b4f1a1c | ||
|
|
355c2719fd | ||
|
|
dfb18ce882 | ||
|
|
07187d058b | ||
|
|
5060b5f8c7 | ||
|
|
50c628a25e | ||
|
|
7bf4609d3d | ||
|
|
548a8eb321 | ||
|
|
627f9e7a0a | ||
|
|
4faf85c988 | ||
|
|
259c6434da | ||
|
|
321d5b364f | ||
|
|
e56491ec27 | ||
|
|
8fe5d1bace | ||
|
|
4efa58ee8b | ||
|
|
8249a5efa1 | ||
|
|
08b0717407 | ||
|
|
c277833332 | ||
|
|
37d9f2870d | ||
|
|
cc71d1292b | ||
|
|
3ca6841ea2 | ||
|
|
c81cdd0df1 | ||
|
|
bd0cbe5e97 | ||
|
|
fdd4eebd96 | ||
|
|
34eeaf5cce | ||
|
|
09c38e427a | ||
|
|
fca2773282 | ||
|
|
c138cc81c0 | ||
|
|
91be95e158 | ||
|
|
e172b45047 | ||
|
|
0a6c07551f | ||
|
|
fa33f850f7 | ||
|
|
605513d165 | ||
|
|
d635ab8662 | ||
|
|
4862705dcd | ||
|
|
4d471eb285 | ||
|
|
470d315eaf | ||
|
|
4e267e3de9 | ||
|
|
bd28cd1766 | ||
|
|
f5c324bd68 | ||
|
|
b7bf995303 | ||
|
|
68516817aa | ||
|
|
9dc5f2a063 | ||
|
|
d2564efe46 | ||
|
|
696e97914d | ||
|
|
cafc5da8bf | ||
|
|
a24b6432c2 | ||
|
|
68668c1b91 | ||
|
|
460441abd2 | ||
|
|
3875afd002 | ||
|
|
f769c1fbec | ||
|
|
644df95f21 | ||
|
|
95f1e618f9 | ||
|
|
03e6cf1070 | ||
|
|
f01a207166 | ||
|
|
1795996825 | ||
|
|
375f7bcc09 | ||
|
|
76f10ced5f | ||
|
|
37c791f147 | ||
|
|
9a8034eb4c | ||
|
|
ff70ed301e | ||
|
|
3f66594821 | ||
|
|
f2cd0218fd | ||
|
|
028d601674 | ||
|
|
e06d012875 | ||
|
|
5995400414 | ||
|
|
91fbc2fdf8 | ||
|
|
6b00324c74 | ||
|
|
1369197a11 | ||
|
|
a30580f3b8 | ||
|
|
fc93a85e21 | ||
|
|
5351d869d4 | ||
|
|
a61d9af25c | ||
|
|
2111413704 | ||
|
|
a2781f9af2 | ||
|
|
09eeae3802 | ||
|
|
0408b7e23d | ||
|
|
ea606de22f |
6
.github/workflows/build-sdk.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
pull_request:
|
||||
paths: ["sdk/**"]
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
@@ -15,11 +15,11 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./sdk
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22.4.1
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
44
.github/workflows/static-sites.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["sites/static/**", ".github/workflows/static-sites.yml"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './sites/static'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
6
.gitmodules
vendored
@@ -1,9 +1,6 @@
|
||||
[submodule "plugins/unifi-protect/src/unifi-protect"]
|
||||
path = external/unifi-protect
|
||||
url = ../../koush/unifi-protect.git
|
||||
[submodule "plugins/myq/src/myq"]
|
||||
path = plugins/myq/src/myq
|
||||
url = ../../koush/myq.git
|
||||
[submodule "external/ring-client-api"]
|
||||
path = external/ring-client-api
|
||||
url = ../../koush/ring
|
||||
@@ -14,9 +11,6 @@
|
||||
[submodule "external/werift"]
|
||||
path = external/werift
|
||||
url = ../../koush/werift-webrtc
|
||||
[submodule "plugins/zwave/file-stream-rotator"]
|
||||
path = plugins/zwave/file-stream-rotator
|
||||
url = ../../koush/file-stream-rotator.git
|
||||
[submodule "sdk/developer.scrypted.app"]
|
||||
path = sdk/developer.scrypted.app
|
||||
url = ../../koush/developer.scrypted.app
|
||||
|
||||
108
common/package-lock.json
generated
@@ -10,12 +10,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.29",
|
||||
"version": "0.3.45",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -111,39 +111,40 @@
|
||||
},
|
||||
"../server": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.106.0",
|
||||
"version": "0.115.0",
|
||||
"extraneous": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
||||
"@scrypted/node-pty": "^1.0.10",
|
||||
"@scrypted/types": "^0.3.28",
|
||||
"adm-zip": "^0.5.12",
|
||||
"@scrypted/node-pty": "^1.0.18",
|
||||
"@scrypted/types": "^0.3.33",
|
||||
"adm-zip": "^0.5.14",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"engine.io": "^6.5.4",
|
||||
"engine.io": "^6.6.0",
|
||||
"express": "^4.19.2",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^2.0.1",
|
||||
"level": "^8.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nan": "^2.19.0",
|
||||
"nan": "^2.20.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^10.1.0",
|
||||
"py": "npm:@bjia56/portable-python@^0.1.31",
|
||||
"py": "npm:@bjia56/portable-python@^0.1.54",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.33.3",
|
||||
"sharp": "^0.33.4",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^7.1.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"tar": "^7.4.0",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.17.0"
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-serve": "bin/scrypted-serve"
|
||||
@@ -155,7 +156,7 @@
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.17.1",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
@@ -205,10 +206,6 @@
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/server": {
|
||||
"resolved": "../server",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -301,6 +298,12 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
|
||||
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -345,9 +348,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -442,53 +445,6 @@
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"@scrypted/server": {
|
||||
"version": "file:../server",
|
||||
"requires": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build1",
|
||||
"@scrypted/node-pty": "^1.0.10",
|
||||
"@scrypted/types": "^0.3.28",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.17.1",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"adm-zip": "^0.5.12",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.19.2",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^2.0.1",
|
||||
"level": "^8.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nan": "^2.19.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^10.1.0",
|
||||
"py": "npm:@bjia56/portable-python@^0.1.31",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.33.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^7.1.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.17.0"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
@@ -566,6 +522,12 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.50.0",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
|
||||
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -588,9 +550,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
|
||||
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export function createAsyncQueue<T>() {
|
||||
}
|
||||
catch (e) {
|
||||
// the yield above may raise an error, and the queue should be ended.
|
||||
end(e);
|
||||
end(e as Error);
|
||||
if (e instanceof EndError)
|
||||
return;
|
||||
throw e;
|
||||
@@ -155,6 +155,23 @@ export function createAsyncQueue<T>() {
|
||||
}
|
||||
}
|
||||
|
||||
export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
const q = createAsyncQueue<T>();
|
||||
(async() => {
|
||||
try {
|
||||
for await (const i of generator) {
|
||||
q.submit(i);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
q.end(e as Error);
|
||||
}
|
||||
q.end();
|
||||
})();
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
// async function testSlowEnqueue() {
|
||||
// const asyncQueue = createAsyncQueue<number>();
|
||||
|
||||
|
||||
209
common/src/autoconfigure-codecs.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import sdk, { AudioStreamOptions, MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, ScryptedDeviceBase, Setting } from "@scrypted/sdk";
|
||||
|
||||
export const automaticallyConfigureSettings: Setting = {
|
||||
key: 'autoconfigure',
|
||||
title: 'Automatically Configure Settings',
|
||||
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
};
|
||||
|
||||
export const onvifAutoConfigureSettings: Setting = {
|
||||
key: 'onvif-autoconfigure',
|
||||
type: 'html',
|
||||
value: 'ONVIF autoconfiguration will configure the camera codecs. <b>The camera motion sensor must still be <a target="_blank" href="https://docs.scrypted.app/camera-preparation.html#motion-sensor-setup">configured manually</a>.</b>',
|
||||
};
|
||||
|
||||
const MEGABIT = 1024 * 1000;
|
||||
|
||||
function getBitrateForResolution(resolution: number) {
|
||||
if (resolution >= 3840 * 2160)
|
||||
return 8 * MEGABIT;
|
||||
if (resolution >= 2688 * 1520)
|
||||
return 3 * MEGABIT;
|
||||
if (resolution >= 1920 * 1080)
|
||||
return 2 * MEGABIT;
|
||||
if (resolution >= 1280 * 720)
|
||||
return MEGABIT;
|
||||
if (resolution >= 640 * 480)
|
||||
return MEGABIT / 2;
|
||||
return MEGABIT / 4;
|
||||
}
|
||||
|
||||
export async function checkPluginNeedsAutoConfigure(plugin: ScryptedDeviceBase, extraDevices = 0) {
|
||||
if (plugin.storage.getItem('autoconfigure') === 'true')
|
||||
return;
|
||||
|
||||
plugin.storage.setItem('autoconfigure', 'true');
|
||||
if (sdk.deviceManager.getNativeIds().length <= 1 + extraDevices)
|
||||
return;
|
||||
plugin.log.a(`${plugin.name} now has support for automatic camera configuration for optimal performance. Cameras can be autoconfigured in their respective settings.`);
|
||||
}
|
||||
|
||||
export async function autoconfigureCodecs(
|
||||
getCodecs: () => Promise<MediaStreamOptions[]>,
|
||||
configureCodecs: (options: MediaStreamOptions) => Promise<MediaStreamConfiguration>,
|
||||
audioOptions?: AudioStreamOptions,
|
||||
) {
|
||||
audioOptions ||= {
|
||||
codec: 'pcm_mulaw',
|
||||
bitrate: 64000,
|
||||
sampleRate: 8000,
|
||||
};
|
||||
|
||||
const codecs = await getCodecs();
|
||||
const configurable: MediaStreamConfiguration[] = [];
|
||||
for (const codec of codecs) {
|
||||
const config = await configureCodecs({
|
||||
id: codec.id,
|
||||
});
|
||||
configurable.push(config);
|
||||
}
|
||||
|
||||
const used: MediaStreamConfiguration[] = [];
|
||||
|
||||
for (const _ of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
|
||||
// find stream with the highest configurable resolution.
|
||||
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
|
||||
for (const codec of configurable) {
|
||||
if (used.includes(codec))
|
||||
continue;
|
||||
for (const resolution of codec.video.resolutions) {
|
||||
if (resolution[0] * resolution[1] > highest[1]) {
|
||||
highest = [codec, resolution[0] * resolution[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = highest[0];
|
||||
if (!config)
|
||||
break;
|
||||
|
||||
used.push(config);
|
||||
}
|
||||
|
||||
const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
|
||||
let diff = 999999999;
|
||||
let ret: [number, number];
|
||||
|
||||
const targetArea = width * height;
|
||||
for (const res of config.video.resolutions) {
|
||||
const actualArea = res[0] * res[1];
|
||||
const diffArea = Math.abs(targetArea - actualArea);
|
||||
if (diffArea < diff) {
|
||||
diff = diffArea;
|
||||
ret = res;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// find the highest resolution
|
||||
const l = used[0];
|
||||
const resolution = findResolutionTarget(l, 8192, 8192);
|
||||
|
||||
// get the fps of 20 or highest available
|
||||
let fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
|
||||
let errors = '';
|
||||
|
||||
const logConfigureCodecs = async (config: MediaStreamConfiguration) => {
|
||||
try {
|
||||
await configureCodecs(config);
|
||||
}
|
||||
catch (e) {
|
||||
errors += e;
|
||||
}
|
||||
}
|
||||
|
||||
await logConfigureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: resolution[0],
|
||||
height: resolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
|
||||
if (used.length === 3) {
|
||||
// find remote and low
|
||||
const r = used[1];
|
||||
const l = used[2];
|
||||
|
||||
const rResolution = findResolutionTarget(r, 1280, 720);
|
||||
const lResolution = findResolutionTarget(l, 640, 360);
|
||||
|
||||
fps = Math.min(20, Math.max(...r.video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: r.id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: 1 * MEGABIT,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
|
||||
fps = Math.min(20, Math.max(...l.video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: l.id,
|
||||
video: {
|
||||
width: lResolution[0],
|
||||
height: lResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: MEGABIT / 2,
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
}
|
||||
else if (used.length == 2) {
|
||||
let target: [number, number];
|
||||
if (resolution[0] * resolution[1] > 1920 * 1080)
|
||||
target = [1280, 720];
|
||||
else
|
||||
target = [640, 360];
|
||||
|
||||
const rResolution = findResolutionTarget(used[1], target[0], target[1]);
|
||||
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
|
||||
await logConfigureCodecs({
|
||||
id: used[1].id,
|
||||
video: {
|
||||
width: rResolution[0],
|
||||
height: rResolution[1],
|
||||
bitrateControl: 'variable',
|
||||
codec: 'h264',
|
||||
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
|
||||
fps,
|
||||
keyframeInterval: fps * 4,
|
||||
quality: 5,
|
||||
profile: 'main',
|
||||
},
|
||||
audio: audioOptions,
|
||||
});
|
||||
}
|
||||
else if (used.length === 1) {
|
||||
// no nop
|
||||
}
|
||||
|
||||
if (errors)
|
||||
throw new Error(errors);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export class Deferred<T> {
|
||||
finished = false;
|
||||
resolve!: (value: T|PromiseLike<T>) => this;
|
||||
reject!: (error: Error) => this;
|
||||
promise: Promise<T> = new Promise((resolve, reject) => {
|
||||
this.resolve = v => {
|
||||
this.finished = true;
|
||||
resolve(v);
|
||||
return this;
|
||||
};
|
||||
this.reject = e => {
|
||||
this.finished = true;
|
||||
reject(e);
|
||||
return this;
|
||||
};
|
||||
});
|
||||
}
|
||||
1
common/src/deferred.ts
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/src/deferred.ts
|
||||
96
common/src/eval/monaco-libs.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type * as monacoEditor from 'monaco-editor';
|
||||
|
||||
export interface StandardLibs {
|
||||
'@types/node/globals.d.ts': string,
|
||||
'@types/node/buffer.d.ts': string,
|
||||
'@types/node/process.d.ts': string,
|
||||
'@types/node/events.d.ts': string,
|
||||
'@types/node/stream.d.ts': string,
|
||||
'@types/node/fs.d.ts': string,
|
||||
'@types/node/net.d.ts': string,
|
||||
'@types/node/child_process.d.ts': string,
|
||||
}
|
||||
|
||||
export interface ScryptedLibs {
|
||||
'@types/sdk/settings-mixin.d.ts': string,
|
||||
'@types/sdk/storage-settings.d.ts': string,
|
||||
'@types/sdk/types.d.ts': string,
|
||||
'@types/sdk/index.d.ts': string,
|
||||
}
|
||||
|
||||
export function createMonacoEvalDefaultsWithLibs(standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
|
||||
// const libs = Object.assign(scryptedLibs, extraLibs);
|
||||
|
||||
function monacoEvalDefaultsFunction(monaco: typeof monacoEditor, standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
|
||||
{
|
||||
diagnosticCodesToIgnore: [1108, 1375, 1378],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
{
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const libs: any = {
|
||||
...scryptedLibs,
|
||||
...extraLibs,
|
||||
};
|
||||
|
||||
const catLibs = Object.values(libs).join('\n');
|
||||
const catlibsNoExport = Object.keys(libs)
|
||||
.map(lib => libs[lib]).map(lib =>
|
||||
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
|
||||
.join('\n');
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
|
||||
${catLibs}
|
||||
|
||||
declare global {
|
||||
${catlibsNoExport}
|
||||
|
||||
const log: Logger;
|
||||
|
||||
const deviceManager: DeviceManager;
|
||||
const endpointManager: EndpointManager;
|
||||
const mediaManager: MediaManager;
|
||||
const systemManager: SystemManager;
|
||||
|
||||
const eventSource: ScryptedDevice;
|
||||
const eventDetails: EventDetails;
|
||||
const eventData: any;
|
||||
}
|
||||
`,
|
||||
|
||||
"node_modules/@types/scrypted__sdk/types/index.d.ts"
|
||||
);
|
||||
|
||||
for (const lib of Object.keys(standardLibs)) {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
standardLibs[lib as keyof StandardLibs],
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `(function() {
|
||||
const standardLibs = ${JSON.stringify(standardLibs)};
|
||||
const scryptedLibs = ${JSON.stringify(scryptedLibs)};
|
||||
const extraLibs = ${JSON.stringify(extraLibs)};
|
||||
|
||||
return (monaco) => {
|
||||
(${monacoEvalDefaultsFunction})(monaco, standardLibs, scryptedLibs, extraLibs);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
|
||||
export interface ScriptDevice {
|
||||
/**
|
||||
* @deprecated Use the default export to specify the device handler.
|
||||
@@ -6,3 +8,5 @@ export interface ScriptDevice {
|
||||
handle<T>(handler?: T & object): void;
|
||||
handleTypes(...interfaces: string[]): void;
|
||||
}
|
||||
|
||||
export declare const device: ScryptedDeviceBase & ScriptDevice;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase } from "@scrypted/sdk/settings-mixin";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import fs from 'fs';
|
||||
import type { TranspileOptions } from "typescript";
|
||||
import vm from "vm";
|
||||
import { createMonacoEvalDefaultsWithLibs, ScryptedLibs, StandardLibs } from "./monaco-libs";
|
||||
import { ScriptDevice } from "./monaco/script-device";
|
||||
|
||||
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
|
||||
@@ -28,22 +29,18 @@ export function readFileAsString(f: string) {
|
||||
return fs.readFileSync(f).toString();;
|
||||
}
|
||||
|
||||
function getTypeDefs() {
|
||||
const settingsMixinDefs = readFileAsString('@types/sdk/settings-mixin.d.ts');
|
||||
const storageSettingsDefs = readFileAsString('@types/sdk/storage-settings.d.ts');
|
||||
const scryptedTypesDefs = readFileAsString('@types/sdk/types.d.ts');
|
||||
const scryptedIndexDefs = readFileAsString('@types/sdk/index.d.ts');
|
||||
function getScryptedLibs(): ScryptedLibs {
|
||||
return {
|
||||
settingsMixinDefs,
|
||||
storageSettingsDefs,
|
||||
scryptedIndexDefs,
|
||||
scryptedTypesDefs,
|
||||
};
|
||||
"@types/sdk/index.d.ts": readFileAsString('@types/sdk/index.d.ts'),
|
||||
"@types/sdk/settings-mixin.d.ts": readFileAsString('@types/sdk/settings-mixin.d.ts'),
|
||||
"@types/sdk/storage-settings.d.ts": readFileAsString('@types/sdk/storage-settings.d.ts'),
|
||||
"@types/sdk/types.d.ts": readFileAsString('@types/sdk/types.d.ts'),
|
||||
}
|
||||
}
|
||||
|
||||
export async function scryptedEval(device: ScryptedDeviceBase, script: string, extraLibs: { [lib: string]: string }, params: { [name: string]: any }) {
|
||||
const libs = Object.assign({
|
||||
types: getTypeDefs().scryptedTypesDefs,
|
||||
types: getScryptedLibs()['@types/sdk/types.d.ts'],
|
||||
}, extraLibs);
|
||||
const allScripts = Object.values(libs).join('\n').toString() + script;
|
||||
let compiled: string;
|
||||
@@ -117,102 +114,18 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
}
|
||||
|
||||
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
|
||||
const safeLibs: any = {};
|
||||
const standardlibs: StandardLibs = {
|
||||
"@types/node/globals.d.ts": readFileAsString('@types/node/globals.d.ts'),
|
||||
"@types/node/buffer.d.ts": readFileAsString('@types/node/buffer.d.ts'),
|
||||
"@types/node/process.d.ts": readFileAsString('@types/node/process.d.ts'),
|
||||
"@types/node/events.d.ts": readFileAsString('@types/node/events.d.ts'),
|
||||
"@types/node/stream.d.ts": readFileAsString('@types/node/stream.d.ts'),
|
||||
"@types/node/fs.d.ts": readFileAsString('@types/node/fs.d.ts'),
|
||||
"@types/node/net.d.ts": readFileAsString('@types/node/net.d.ts'),
|
||||
"@types/node/child_process.d.ts": readFileAsString('@types/node/child_process.d.ts'),
|
||||
};
|
||||
|
||||
for (const safeLib of [
|
||||
'@types/node/globals.d.ts',
|
||||
'@types/node/buffer.d.ts',
|
||||
'@types/node/process.d.ts',
|
||||
'@types/node/events.d.ts',
|
||||
'@types/node/stream.d.ts',
|
||||
'@types/node/fs.d.ts',
|
||||
'@types/node/net.d.ts',
|
||||
'@types/node/child_process.d.ts',
|
||||
]) {
|
||||
safeLibs[`node_modules/${safeLib}`] = readFileAsString(safeLib)
|
||||
}
|
||||
|
||||
const libs = Object.assign(getTypeDefs(), extraLibs);
|
||||
|
||||
function monacoEvalDefaultsFunction(monaco: any, safeLibs: any, libs: any) {
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
|
||||
{
|
||||
diagnosticCodesToIgnore: [1108, 1375, 1378],
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
|
||||
Object.assign(
|
||||
{},
|
||||
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
|
||||
{
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const catLibs = Object.values(libs).join('\n');
|
||||
const catlibsNoExport = Object.keys(libs).filter(lib => lib !== 'sdk')
|
||||
.map(lib => libs[lib]).map(lib =>
|
||||
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
|
||||
.join('\n');
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
|
||||
${catLibs}
|
||||
|
||||
declare global {
|
||||
${catlibsNoExport}
|
||||
|
||||
const log: Logger;
|
||||
|
||||
const deviceManager: DeviceManager;
|
||||
const endpointManager: EndpointManager;
|
||||
const mediaManager: MediaManager;
|
||||
const systemManager: SystemManager;
|
||||
const mqtt: MqttClient;
|
||||
const device: ScryptedDeviceBase & { pathname : string };
|
||||
}
|
||||
`,
|
||||
|
||||
"node_modules/@types/scrypted__sdk/types/index.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['settingsMixin'],
|
||||
"node_modules/@types/scrypted__sdk/settings-mixin.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['storageSettings'],
|
||||
"node_modules/@types/scrypted__sdk/storage-settings.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
libs['sdk'],
|
||||
"node_modules/@types/scrypted__sdk/index.d.ts"
|
||||
);
|
||||
|
||||
for (const lib of Object.keys(safeLibs)) {
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
safeLibs[lib],
|
||||
lib,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `(function() {
|
||||
const safeLibs = ${JSON.stringify(safeLibs)};
|
||||
const libs = ${JSON.stringify(libs)};
|
||||
|
||||
return (monaco) => {
|
||||
(${monacoEvalDefaultsFunction})(monaco, safeLibs, libs);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
return createMonacoEvalDefaultsWithLibs(standardlibs, getScryptedLibs(), extraLibs);
|
||||
}
|
||||
|
||||
export interface ScriptDeviceImpl extends ScriptDevice {
|
||||
|
||||
@@ -79,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "../../server/src/listen-zero";
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from '@scrypted/server/src/media-helpers';
|
||||
export { safeKillFFmpeg, ffmpegLogInitialOutput, safePrintFFmpegArguments } from '../../server/src/media-helpers';
|
||||
|
||||
@@ -54,18 +54,18 @@ export async function read16BELengthLoop(readable: Readable, options: {
|
||||
readable.on('readable', read);
|
||||
|
||||
await once(readable, 'end');
|
||||
throw new Error('stream ended');
|
||||
throw new StreamEndError('read16BELengthLoop');
|
||||
}
|
||||
|
||||
export class StreamEndError extends Error {
|
||||
constructor() {
|
||||
super('stream ended');
|
||||
constructor(where: string) {
|
||||
super(`stream ended: ${where}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
throw new StreamEndError();
|
||||
throw new StreamEndError('readLength start');
|
||||
|
||||
if (!length) {
|
||||
return Buffer.alloc(0);
|
||||
@@ -88,12 +88,12 @@ export async function readLength(readable: Readable, length: number): Promise<Bu
|
||||
}
|
||||
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
reject(new Error("stream ended during read"));
|
||||
reject(new StreamEndError('readLength readable'));
|
||||
};
|
||||
|
||||
const e = () => {
|
||||
cleanup();
|
||||
reject(new StreamEndError())
|
||||
reject(new StreamEndError('readLength end'));
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RpcPeer } from "@scrypted/server/src/rpc";
|
||||
import { createRpcSerializer } from "@scrypted/server/src/rpc-serializer";
|
||||
import { RpcPeer } from "../../server/src/rpc";
|
||||
import { createRpcSerializer } from "../../server/src/rpc-serializer";
|
||||
import type { RTCSignalingSession } from "@scrypted/sdk";
|
||||
|
||||
export async function createBrowserSignalingSession(ws: WebSocket, localName: string, remoteName: string) {
|
||||
|
||||
@@ -41,15 +41,15 @@ export function isPeerConnectionClosed(pc: RTCPeerConnection) {
|
||||
|| pc.iceConnectionState === 'closed';
|
||||
}
|
||||
|
||||
function silence() {
|
||||
let ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||
const dest = ctx.createMediaStreamDestination();
|
||||
oscillator.connect(dest);
|
||||
oscillator.start();
|
||||
const ret = dest.stream.getAudioTracks()[0];
|
||||
ret.enabled = false;
|
||||
return ret;
|
||||
}
|
||||
// function silence() {
|
||||
// let ctx = new AudioContext(), oscillator = ctx.createOscillator();
|
||||
// const dest = ctx.createMediaStreamDestination();
|
||||
// oscillator.connect(dest);
|
||||
// oscillator.start();
|
||||
// const ret = dest.stream.getAudioTracks()[0];
|
||||
// ret.enabled = false;
|
||||
// return ret;
|
||||
// }
|
||||
|
||||
function createOptions() {
|
||||
const options: RTCSignalingOptions = {
|
||||
|
||||
@@ -506,7 +506,7 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.client.destroy(e);
|
||||
this.client.destroy(e as Error);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -572,7 +572,8 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
deferred.reject(e);
|
||||
if (!deferred.finished)
|
||||
deferred.reject(e as Error);
|
||||
this.client.destroy();
|
||||
}
|
||||
};
|
||||
@@ -725,7 +726,10 @@ export class RtspClient extends RtspBase {
|
||||
Accept: 'application/sdp',
|
||||
});
|
||||
|
||||
this.contentBase = response.headers['content-base'] || response.headers['content-location'];;
|
||||
this.contentBase = response.headers['content-base'] || response.headers['content-location'];
|
||||
// content base may be a relative path? seems odd.
|
||||
if (this.contentBase)
|
||||
this.contentBase = new URL(this.contentBase, this.url).toString();
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1123,7 +1127,7 @@ export class RtspServer {
|
||||
}
|
||||
|
||||
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
|
||||
hostname?: string,
|
||||
hostname: string,
|
||||
pathToken?: string,
|
||||
createServer?(duplex: Duplex): T,
|
||||
}) {
|
||||
|
||||
@@ -227,6 +227,10 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
|
||||
codec = 'pcm_alaw';
|
||||
ffmpegEncoder = 'pcm_alaw';
|
||||
}
|
||||
else if (mline.payloadTypes?.includes(14)) {
|
||||
codec = 'mp3';
|
||||
ffmpegEncoder = 'mp3';
|
||||
}
|
||||
else {
|
||||
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
|
||||
// is this the default?
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "@scrypted/server/src/sleep"
|
||||
export { sleep } from "../../server/src/sleep";
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import sdk, { PluginFork } from '@scrypted/sdk';
|
||||
import worker_threads from 'worker_threads';
|
||||
import sdk, { ForkOptions, PluginFork } from '@scrypted/sdk';
|
||||
import { createAsyncQueue } from './async-queue';
|
||||
import os from 'os';
|
||||
|
||||
export type Zygote<T> = () => PluginFork<T>;
|
||||
|
||||
export function createZygote<T>(): Zygote<T> {
|
||||
if (!worker_threads.isMainThread)
|
||||
return;
|
||||
|
||||
let zygote = sdk.fork<T>();
|
||||
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
|
||||
let zygote = sdk.fork<T>(options);
|
||||
function* next() {
|
||||
while (true) {
|
||||
const cur = zygote;
|
||||
zygote = sdk.fork<T>();
|
||||
zygote = sdk.fork<T>(options);
|
||||
yield cur;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node16",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
|
||||
|
Before Width: | Height: | Size: 667 B |
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Scrypted Management Console</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,45 +0,0 @@
|
||||
{
|
||||
"name": "Scrypted Management Console",
|
||||
"short_name": "Scrypted",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#424242"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
2
external/unifi-protect
vendored
2
external/werift
vendored
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.111.0-jammy-full"
|
||||
version: "v0.116.0-jammy-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="jammy"
|
||||
ARG REPO="ubuntu"
|
||||
FROM ${REPO}:${BASE} as header
|
||||
FROM ubuntu:${BASE} as header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -72,8 +71,9 @@ RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
# intel opencl gpu and npu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -17,9 +17,6 @@ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg -
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
|
||||
@@ -1,38 +1,52 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
then
|
||||
# this script previvously apt install intel-media-va-driver-non-free, but that seems to no longer be necessary.
|
||||
|
||||
# the intel provided script is disabled since it does not work with the 6.8 kernel in Ubuntu 24.04 or Proxmox 8.2.
|
||||
# manual installation of the Intel graphics stuff is required.
|
||||
|
||||
# echo "Installing Intel graphics packages."
|
||||
# apt-get update && apt-get install -y gpg-agent &&
|
||||
# rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
# curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
# echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
# apt-get -y update &&
|
||||
# apt-get -y install intel-opencl-icd &&
|
||||
# apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
rm -rf /tmp/neo && mkdir -p /tmp/neo && cd /tmp/neo &&
|
||||
apt-get install -y ocl-icd-libopencl1 &&
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.16695.4/intel-igc-core_1.0.16695.4_amd64.deb &&
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.16695.4/intel-igc-opencl_1.0.16695.4_amd64.deb &&
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-level-zero-gpu-dbgsym_1.3.29377.6_amd64.ddeb &&
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-level-zero-gpu_1.3.29377.6_amd64.deb &&
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-opencl-icd-dbgsym_24.17.29377.6_amd64.ddeb &&
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-opencl-icd_24.17.29377.6_amd64.deb &&
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/libigdgmm12_22.3.19_amd64.deb &&
|
||||
dpkg -i *.deb &&
|
||||
cd /tmp && rm -rf /tmp/neo &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
exit $?
|
||||
else
|
||||
echo "Intel graphics will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 0
|
||||
# no errors beyond this point
|
||||
set -e
|
||||
|
||||
# the intel provided script is disabled since it does not work with the 6.8 kernel in Ubuntu 24.04 or Proxmox 8.2.
|
||||
# manual installation of the Intel graphics stuff is required.
|
||||
|
||||
# echo "Installing Intel graphics packages."
|
||||
# apt-get update && apt-get install -y gpg-agent &&
|
||||
# rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
# curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
# echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
# apt-get -y update &&
|
||||
# apt-get -y install intel-opencl-icd &&
|
||||
# apt-get -y dist-upgrade;
|
||||
|
||||
# need intel-media-va-driver-non-free, but all the other intel packages are installed from Intel github.
|
||||
echo "Installing Intel graphics packages."
|
||||
apt-get update && apt-get install -y gpg-agent &&
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
apt-get -y update &&
|
||||
apt-get -y install intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
apt-get install -y ocl-icd-libopencl1
|
||||
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu-dbgsym_1.3.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu_1.3.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb
|
||||
|
||||
dpkg -i *.deb
|
||||
|
||||
cd /tmp && rm -rf /tmp/gpu
|
||||
|
||||
apt-get -y dist-upgrade
|
||||
|
||||
69
install/docker/install-intel-npu.sh
Normal file
@@ -0,0 +1,69 @@
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
then
|
||||
echo "Intel NPU will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
then
|
||||
UBUNTU_22_04=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "Intel NPU will not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
|
||||
|
||||
# no errors beyond this point
|
||||
set -e
|
||||
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# different npu downloads for ubuntu versions
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-driver-compiler-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
|
||||
else
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
dpkg -i *.deb
|
||||
|
||||
cd /tmp && rm -rf /tmp/npu
|
||||
|
||||
apt-get -y dist-upgrade
|
||||
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
echo
|
||||
echo "###############################################################################"
|
||||
echo "Intel NPU firmware was installed. Reboot the host to complete the installation."
|
||||
echo "###############################################################################"
|
||||
fi
|
||||
@@ -128,7 +128,7 @@ then
|
||||
set -e
|
||||
removescryptedfstab
|
||||
mkdir -p /mnt/scrypted-nvr
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail 0 0" >> /etc/fstab
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail,noatime 0 0" >> /etc/fstab
|
||||
mount -a
|
||||
systemctl daemon-reload
|
||||
set +e
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
# intel opencl gpu and npu for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -10,13 +10,20 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.96.0
|
||||
SCRYPTED_VERSION=v0.116.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=10444
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
if [ ! -f "$SCRYPTED_TAR_ZST" ]
|
||||
then
|
||||
@@ -75,6 +82,27 @@ else
|
||||
echo "$CONF not found? Start on boot must be enabled manually."
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
# this copies the
|
||||
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
|
||||
|
||||
rm *.tar
|
||||
vzdump 10444 --dumpdir /tmp
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Moving data volume to backup..."
|
||||
pct restore $VMID *.tar $@
|
||||
|
||||
pct destroy 10444
|
||||
fi
|
||||
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
@@ -82,6 +110,7 @@ then
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"accel0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
|
||||
@@ -84,15 +84,33 @@ export function createAuthFetch<B, M>(
|
||||
if (initialHeader && !hasHeader(headers, 'Authorization'))
|
||||
setHeader(headers, 'Authorization', initialHeader);
|
||||
|
||||
|
||||
const controller = new AbortController();
|
||||
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
|
||||
|
||||
const initialResponse = await h({
|
||||
...options,
|
||||
ignoreStatusCode: true,
|
||||
signal: controller.signal,
|
||||
// need to intercept the status code to check for 401.
|
||||
// all other status codes will be handled according to the initial request options.
|
||||
checkStatusCode(statusCode) {
|
||||
// can handle a 401 if an credential is provided.
|
||||
// however, not providing a credential is also valid, and should
|
||||
// fall through to the normal response handling which may be interested
|
||||
// in the 401 response.
|
||||
if (statusCode === 401 && options.credential)
|
||||
return true;
|
||||
if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
|
||||
const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
|
||||
return checker(statusCode);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
responseType: 'readable',
|
||||
});
|
||||
|
||||
if (initialResponse.statusCode !== 401 || !options.credential) {
|
||||
if (!options?.ignoreStatusCode)
|
||||
checkStatus(initialResponse.statusCode);
|
||||
// if it's not a 401, just return the response.
|
||||
if (initialResponse.statusCode !== 401) {
|
||||
return {
|
||||
...initialResponse,
|
||||
body: await parser(initialResponse.body, options.responseType),
|
||||
|
||||
2
packages/cli/.vscode/launch.json
vendored
@@ -21,7 +21,7 @@
|
||||
],
|
||||
"preLaunchTask": "npm: build",
|
||||
"args": [
|
||||
"login",
|
||||
"serve",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
|
||||
4
packages/cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.16",
|
||||
"version": "1.3.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.3.16",
|
||||
"version": "1.3.20",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.3.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.16",
|
||||
"version": "1.3.20",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
|
||||
@@ -10,6 +10,7 @@ import semver from 'semver';
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
import { installServe, serveMain } from './service';
|
||||
import { connectShell } from './shell';
|
||||
import { convertRtspToMp4, printRtspUsage } from './rtsp-file';
|
||||
|
||||
if (!semver.gte(process.version, '16.0.0')) {
|
||||
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
||||
@@ -173,6 +174,14 @@ async function main() {
|
||||
});
|
||||
sdk.disconnect();
|
||||
}
|
||||
else if (process.argv[2] === 'rtsp') {
|
||||
if (!process.argv[3]) {
|
||||
printRtspUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await convertRtspToMp4(process.argv[3], process.argv[4]);
|
||||
}
|
||||
else if (process.argv[2] === 'create-cert-json' && process.argv.length === 5) {
|
||||
const key = fs.readFileSync(process.argv[3]).toString();
|
||||
const cert = fs.readFileSync(process.argv[4]).toString();
|
||||
|
||||
72
packages/cli/src/rtsp-file.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { listenSingleRtspClient } from '../../../common/src/rtsp-server';
|
||||
import { parseSdp } from '../../../common/src/sdp-utils';
|
||||
import { once } from 'events';
|
||||
|
||||
export async function convertRtspToMp4(rtspFile: string, sessionFile?: string) {
|
||||
// rtsp file will be in roughly:
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/[hour-timestamp]/[segment-timestamp].rtsp
|
||||
|
||||
// sdp can be found in
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/session.json
|
||||
// or legacy:
|
||||
// /nvr/scrypted-[id]/[session-timestamp]/session.sdp
|
||||
|
||||
const sessionDir = path.dirname(path.dirname(rtspFile));
|
||||
let sdp: string;
|
||||
let sessionJson = path.join(sessionDir, 'session.json');
|
||||
if (!fs.existsSync(sessionJson) && sessionFile)
|
||||
sessionJson = sessionFile.endsWith('.json') && sessionFile;
|
||||
|
||||
let sessionSdp = path.join(sessionDir, 'session.sdp');
|
||||
if (!fs.existsSync(sessionSdp) && sessionFile)
|
||||
sessionSdp = sessionFile.endsWith('.sdp') && sessionFile;
|
||||
|
||||
if (fs.existsSync(sessionJson)) {
|
||||
sdp = JSON.parse(fs.readFileSync(sessionJson).toString()).sdp;
|
||||
}
|
||||
else if (fs.existsSync(sessionSdp)) {
|
||||
sdp = fs.readFileSync(sessionSdp).toString();
|
||||
}
|
||||
else {
|
||||
console.error('Could not find session sdp. Ensure the rtsp directory structure is intact or specify the path to the session file.');
|
||||
console.error();
|
||||
printRtspUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const parsedSdp = parseSdp(sdp);
|
||||
const hasAudio = parsedSdp.msections.some(msection => msection.type === 'audio');
|
||||
const rtspContents = fs.readFileSync(rtspFile);
|
||||
|
||||
const clientPromise = await listenSingleRtspClient();
|
||||
clientPromise.rtspServerPromise.then(async rtspServer => {
|
||||
rtspServer.sdp = sdp;
|
||||
await rtspServer.handlePlayback();
|
||||
console.log('playing')
|
||||
rtspServer.client.write(rtspContents);
|
||||
rtspServer.client.end();
|
||||
});
|
||||
|
||||
const mp4 = rtspFile + '.mp4';
|
||||
|
||||
const cp = child_process.spawn('ffmpeg', [
|
||||
'-y',
|
||||
'-i', clientPromise.url,
|
||||
'-vcodec', 'copy',
|
||||
...(hasAudio ? ['-acodec', 'aac'] : []),
|
||||
mp4,
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
await once(cp, 'exit');
|
||||
|
||||
console.log('mp4 written to:', mp4);
|
||||
}
|
||||
|
||||
export function printRtspUsage() {
|
||||
console.log('usage: npx rtsp /path/to/nvr/file.rtsp [/path/to/nvr/session.json | /path/to/nvr/session.sdp]');
|
||||
}
|
||||
@@ -12,6 +12,7 @@ async function sleep(ms: number) {
|
||||
|
||||
const EXIT_FILE = '.exit';
|
||||
const UPDATE_FILE = '.update';
|
||||
const VERSION_FILE = '.version';
|
||||
|
||||
async function runCommand(command: string, ...args: string[]) {
|
||||
if (os.platform() === 'win32') {
|
||||
@@ -117,6 +118,15 @@ export async function installServe(installVersion: string, ignoreError?: boolean
|
||||
}
|
||||
|
||||
export async function serveMain(installVersion?: string) {
|
||||
const { installDir, volume } = cwdInstallDir();
|
||||
if (!installVersion) {
|
||||
try {
|
||||
installVersion = fs.readFileSync(path.join(volume, VERSION_FILE)).toString().trim();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
const options = ((): { install: true; version: string } | { install: false } => {
|
||||
if (installVersion) {
|
||||
console.log(`Installing @scrypted/server@${installVersion}`);
|
||||
@@ -139,7 +149,6 @@ export async function serveMain(installVersion?: string) {
|
||||
}
|
||||
})();
|
||||
|
||||
const { installDir, volume } = cwdInstallDir();
|
||||
|
||||
if (options.install) {
|
||||
await installServe(options.version, true);
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
"inlineSources": true,
|
||||
"declaration": true,
|
||||
"moduleResolution": "Node16",
|
||||
"strict": true
|
||||
},
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": false,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
|
||||
28
packages/client/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.27",
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
@@ -84,9 +84,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.27.tgz",
|
||||
"integrity": "sha512-XNtlqzqt6rHyNYwWrz3iiickh1h9ACwcLC3rfwxUbFk/Vq/UbDZgp0kGyj9UW6eLVNHzWFSE2dKqyyDS6V2KAg=="
|
||||
"version": "0.3.40",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.40.tgz",
|
||||
"integrity": "sha512-NBjNEfoLp7zL5Tf0odzf191oReDh4FEmZexDmMj1JbKDUMB9S8xJys3vbhcFadU/aUrUkyK/FSbkXv1z87bxSw=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
@@ -268,14 +268,14 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.0.tgz",
|
||||
"integrity": "sha512-iBtCdW5Tk3CnMAnC44VO4LwxXnl+RIq9ua1qHvxf5KSq2rzFgQFdfCSSl6Yuz2hl899SWTkfaT3c+WZQ42dJ8A==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -738,15 +738,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.6",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.27",
|
||||
"@scrypted/types": "^0.3.40",
|
||||
"engine.io-client": "^6.5.3",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"rimraf": "^5.0.5"
|
||||
|
||||
@@ -57,9 +57,9 @@ export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
|
||||
export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
userId?: string;
|
||||
username?: string;
|
||||
admin: boolean;
|
||||
disconnect(): void;
|
||||
onClose?: Function;
|
||||
version: string;
|
||||
rtcConnectionManagement?: RTCConnectionManagement;
|
||||
browserSignalingSession?: BrowserSignalingSession;
|
||||
address?: string;
|
||||
@@ -163,6 +163,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
|
||||
token: body.token as string,
|
||||
addresses: body.addresses as string[],
|
||||
externalAddresses: body.externalAddresses as string[],
|
||||
hostname: body.hostname,
|
||||
// the cloud plugin will include this header.
|
||||
// should maybe move this into the cloud server itself.
|
||||
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
|
||||
@@ -225,6 +226,7 @@ export interface ScryptedClientLoginResult {
|
||||
scryptedCloud: boolean;
|
||||
directAddress: string;
|
||||
cloudAddress: string;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export class ScryptedClientLoginError extends Error {
|
||||
@@ -270,6 +272,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
let scryptedCloud: boolean;
|
||||
let directAddress: string;
|
||||
let cloudAddress: string;
|
||||
let hostname: string;
|
||||
let token: string;
|
||||
|
||||
console.log('@scrypted/client', packageJson.version);
|
||||
@@ -295,6 +298,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization = loginResult.authorization;
|
||||
queryToken = loginResult.queryToken;
|
||||
token = loginResult.token;
|
||||
hostname = loginResult.hostname;
|
||||
console.log('login result', Date.now() - start, loginResult);
|
||||
}
|
||||
else {
|
||||
@@ -367,6 +371,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
authorization = loginCheck.authorization;
|
||||
queryToken = loginCheck.queryToken;
|
||||
token = loginCheck.token;
|
||||
hostname = loginCheck.hostname;
|
||||
console.log('login checked', Date.now() - start, loginCheck);
|
||||
}
|
||||
|
||||
@@ -518,7 +523,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -532,9 +537,10 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
});
|
||||
serializer.setupRpcPeer(upgradingPeer);
|
||||
|
||||
const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
check.on('close', () => reject(new Error('closed')))
|
||||
})
|
||||
// is this an issue?
|
||||
// const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
// check.on('close', () => reject(new Error('closed')))
|
||||
// })
|
||||
|
||||
upgradingPeer.params['session'] = session;
|
||||
|
||||
@@ -565,7 +571,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
dcSerializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
pc.close();
|
||||
}
|
||||
});
|
||||
@@ -666,7 +672,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
socket.on('message', data => {
|
||||
@@ -708,16 +714,16 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
});
|
||||
}
|
||||
|
||||
const [version, rtcConnectionManagement] = await Promise.all([
|
||||
const [admin, rtcConnectionManagement] = await Promise.all([
|
||||
(async () => {
|
||||
let version = 'unknown';
|
||||
try {
|
||||
// info is
|
||||
const info = await systemManager.getComponent('info');
|
||||
version = await info.getVersion();
|
||||
return !!info;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return version;
|
||||
return false;
|
||||
})(),
|
||||
(async () => {
|
||||
let rtcConnectionManagement: RTCConnectionManagement;
|
||||
@@ -734,7 +740,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
]);
|
||||
|
||||
console.log('api initialized', Date.now() - start);
|
||||
console.log('api queried, version:', version);
|
||||
|
||||
const userDevice = Object.keys(systemManager.getSystemState())
|
||||
.map(id => systemManager.getDeviceById(id))
|
||||
@@ -781,7 +786,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e);
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
serializer.setupRpcPeer(clusterPeer);
|
||||
@@ -846,7 +851,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
pluginRemoteAPI: undefined,
|
||||
address,
|
||||
connectionType,
|
||||
version,
|
||||
admin,
|
||||
systemManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
@@ -868,6 +873,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
queryToken,
|
||||
authorization,
|
||||
cloudAddress,
|
||||
hostname,
|
||||
},
|
||||
connectRPCObject,
|
||||
fork: undefined,
|
||||
|
||||
4
packages/deferred/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<details>
|
||||
<summary>Changelog</summary>
|
||||
|
||||
### 0.3.2
|
||||
|
||||
alexa: fix syncedDevices being undefined
|
||||
|
||||
|
||||
### 0.3.1
|
||||
|
||||
alexa/google-home: fix potential vulnerability. do not allow local network control using cloud tokens belonging to a different user. the plugins are now locked to a specific scrypted cloud account once paired.
|
||||
|
||||
4
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
82
plugins/amcrest/dumps/amcrest-face-detected.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"CfgRuleId": 1,
|
||||
"Class": "FaceDetection",
|
||||
"CountInGroup": 2,
|
||||
"DetectRegion": null,
|
||||
"EventID": 10360,
|
||||
"EventSeq": 6,
|
||||
"Faces": [
|
||||
{
|
||||
"BoundingBox": [
|
||||
1504,
|
||||
2336,
|
||||
1728,
|
||||
2704
|
||||
],
|
||||
"Center": [
|
||||
1616,
|
||||
2520
|
||||
],
|
||||
"ObjectID": 94,
|
||||
"ObjectType": "HumanFace",
|
||||
"RelativeID": 0
|
||||
}
|
||||
],
|
||||
"FrameSequence": 8251212,
|
||||
"GroupID": 6,
|
||||
"Mark": 0,
|
||||
"Name": "FaceDetection",
|
||||
"Object": {
|
||||
"Action": "Appear",
|
||||
"BoundingBox": [
|
||||
1504,
|
||||
2336,
|
||||
1728,
|
||||
2704
|
||||
],
|
||||
"Center": [
|
||||
1616,
|
||||
2520
|
||||
],
|
||||
"Confidence": 19,
|
||||
"FrameSequence": 8251212,
|
||||
"ObjectID": 94,
|
||||
"ObjectType": "HumanFace",
|
||||
"RelativeID": 0,
|
||||
"SerialUUID": "",
|
||||
"Source": 0.0,
|
||||
"Speed": 0,
|
||||
"SpeedTypeInternal": 0
|
||||
},
|
||||
"Objects": [
|
||||
{
|
||||
"Action": "Appear",
|
||||
"BoundingBox": [
|
||||
1504,
|
||||
2336,
|
||||
1728,
|
||||
2704
|
||||
],
|
||||
"Center": [
|
||||
1616,
|
||||
2520
|
||||
],
|
||||
"Confidence": 19,
|
||||
"FrameSequence": 8251212,
|
||||
"ObjectID": 94,
|
||||
"ObjectType": "HumanFace",
|
||||
"RelativeID": 0,
|
||||
"SerialUUID": "",
|
||||
"Source": 0.0,
|
||||
"Speed": 0,
|
||||
"SpeedTypeInternal": 0
|
||||
}
|
||||
],
|
||||
"PTS": 43774941350.0,
|
||||
"Priority": 0,
|
||||
"RuleID": 1,
|
||||
"RuleId": 1,
|
||||
"Source": -1280470024.0,
|
||||
"UTC": 947510337,
|
||||
"UTCMS": 0
|
||||
}
|
||||
62
plugins/amcrest/dumps/amcrest-human-detected.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"Action": "Cross",
|
||||
"Class": "Normal",
|
||||
"CountInGroup": 1,
|
||||
"DetectRegion": [
|
||||
[
|
||||
455,
|
||||
260
|
||||
],
|
||||
[
|
||||
3586,
|
||||
260
|
||||
],
|
||||
[
|
||||
3768,
|
||||
7580
|
||||
],
|
||||
[
|
||||
382,
|
||||
7451
|
||||
]
|
||||
],
|
||||
"Direction": "Enter",
|
||||
"EventID": 10181,
|
||||
"GroupID": 0,
|
||||
"Name": "Rule1",
|
||||
"Object": {
|
||||
"Action": "Appear",
|
||||
"BoundingBox": [
|
||||
2856,
|
||||
1280,
|
||||
3880,
|
||||
4880
|
||||
],
|
||||
"Center": [
|
||||
3368,
|
||||
3080
|
||||
],
|
||||
"Confidence": 0,
|
||||
"LowerBodyColor": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"MainColor": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"ObjectID": 863,
|
||||
"ObjectType": "Human",
|
||||
"RelativeID": 0,
|
||||
"Speed": 0
|
||||
},
|
||||
"PTS": 43380319830.0,
|
||||
"RuleID": 2,
|
||||
"Track": [],
|
||||
"UTC": 1711446999,
|
||||
"UTCMS": 701
|
||||
}
|
||||
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.151",
|
||||
"version": "0.0.162",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.151",
|
||||
"version": "0.0.162",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.151",
|
||||
"version": "0.0.162",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -27,6 +27,8 @@
|
||||
"name": "Amcrest Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"ScryptedDeviceCreator",
|
||||
"DeviceProvider",
|
||||
"DeviceCreator"
|
||||
],
|
||||
|
||||
@@ -4,103 +4,10 @@ import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
|
||||
import contentType from 'content-type';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import { Destroyable } from '../../rtsp/src/rtsp';
|
||||
import { createRtspMediaStreamOptions, Destroyable, UrlMediaStreamOptions } from '../../rtsp/src/rtsp';
|
||||
import { getDeviceInfo } from './probe';
|
||||
import { Point } from '@scrypted/sdk';
|
||||
import { MediaStreamConfiguration, MediaStreamOptions, Point } from '@scrypted/sdk';
|
||||
|
||||
// Human
|
||||
// {
|
||||
// "Action" : "Cross",
|
||||
// "Class" : "Normal",
|
||||
// "CountInGroup" : 1,
|
||||
// "DetectRegion" : [
|
||||
// [ 455, 260 ],
|
||||
// [ 3586, 260 ],
|
||||
// [ 3768, 7580 ],
|
||||
// [ 382, 7451 ]
|
||||
// ],
|
||||
// "Direction" : "Enter",
|
||||
// "EventID" : 10181,
|
||||
// "GroupID" : 0,
|
||||
// "Name" : "Rule1",
|
||||
// "Object" : {
|
||||
// "Action" : "Appear",
|
||||
// "BoundingBox" : [ 2856, 1280, 3880, 4880 ],
|
||||
// "Center" : [ 3368, 3080 ],
|
||||
// "Confidence" : 0,
|
||||
// "LowerBodyColor" : [ 0, 0, 0, 0 ],
|
||||
// "MainColor" : [ 0, 0, 0, 0 ],
|
||||
// "ObjectID" : 863,
|
||||
// "ObjectType" : "Human",
|
||||
// "RelativeID" : 0,
|
||||
// "Speed" : 0
|
||||
// },
|
||||
// "PTS" : 43380319830.0,
|
||||
// "RuleID" : 2,
|
||||
// "Track" : [],
|
||||
// "UTC" : 1711446999,
|
||||
// "UTCMS" : 701
|
||||
// }
|
||||
|
||||
// Face
|
||||
// {
|
||||
// "CfgRuleId" : 1,
|
||||
// "Class" : "FaceDetection",
|
||||
// "CountInGroup" : 2,
|
||||
// "DetectRegion" : null,
|
||||
// "EventID" : 10360,
|
||||
// "EventSeq" : 6,
|
||||
// "Faces" : [
|
||||
// {
|
||||
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
|
||||
// "Center" : [ 1616, 2520 ],
|
||||
// "ObjectID" : 94,
|
||||
// "ObjectType" : "HumanFace",
|
||||
// "RelativeID" : 0
|
||||
// }
|
||||
// ],
|
||||
// "FrameSequence" : 8251212,
|
||||
// "GroupID" : 6,
|
||||
// "Mark" : 0,
|
||||
// "Name" : "FaceDetection",
|
||||
// "Object" : {
|
||||
// "Action" : "Appear",
|
||||
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
|
||||
// "Center" : [ 1616, 2520 ],
|
||||
// "Confidence" : 19,
|
||||
// "FrameSequence" : 8251212,
|
||||
// "ObjectID" : 94,
|
||||
// "ObjectType" : "HumanFace",
|
||||
// "RelativeID" : 0,
|
||||
// "SerialUUID" : "",
|
||||
// "Source" : 0.0,
|
||||
// "Speed" : 0,
|
||||
// "SpeedTypeInternal" : 0
|
||||
// },
|
||||
// "Objects" : [
|
||||
// {
|
||||
// "Action" : "Appear",
|
||||
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
|
||||
// "Center" : [ 1616, 2520 ],
|
||||
// "Confidence" : 19,
|
||||
// "FrameSequence" : 8251212,
|
||||
// "ObjectID" : 94,
|
||||
// "ObjectType" : "HumanFace",
|
||||
// "RelativeID" : 0,
|
||||
// "SerialUUID" : "",
|
||||
// "Source" : 0.0,
|
||||
// "Speed" : 0,
|
||||
// "SpeedTypeInternal" : 0
|
||||
// }
|
||||
// ],
|
||||
// "PTS" : 43774941350.0,
|
||||
// "Priority" : 0,
|
||||
// "RuleID" : 1,
|
||||
// "RuleId" : 1,
|
||||
// "Source" : -1280470024.0,
|
||||
// "UTC" : 947510337,
|
||||
// "UTCMS" : 0
|
||||
// }
|
||||
export interface AmcrestObjectDetails {
|
||||
Action: string;
|
||||
BoundingBox: Point;
|
||||
@@ -174,6 +81,65 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
function findValue(blob: string, prefix: string, key: string) {
|
||||
const lines = blob.split('\n');
|
||||
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
const parts = value.split('=');
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
function fromAmcrestAudioCodec(audioCodec: string) {
|
||||
audioCodec = audioCodec?.trim();
|
||||
if (audioCodec === 'AAC')
|
||||
return 'aac';
|
||||
if (audioCodec === 'G.711A')
|
||||
return 'pcm_alaw';
|
||||
if (audioCodec === 'G.711Mu')
|
||||
return 'pcm_mulaw';
|
||||
}
|
||||
|
||||
function toAmcrestAudioCodec(audioCodec: string) {
|
||||
if (audioCodec === 'aac')
|
||||
return 'AAC';
|
||||
if (audioCodec === 'pcm_alaw')
|
||||
return 'G.711A';
|
||||
if (audioCodec === 'pcm_mulaw')
|
||||
return 'G.711Mu';
|
||||
}
|
||||
|
||||
function fromAmcrestVideoCodec(videoCodec: string) {
|
||||
videoCodec = videoCodec?.trim();
|
||||
if (videoCodec === 'H.264')
|
||||
videoCodec = 'h264';
|
||||
else if (videoCodec === 'H.265')
|
||||
videoCodec = 'h265';
|
||||
return videoCodec;
|
||||
}
|
||||
|
||||
const amcrestResolutions = {
|
||||
"1080P": [1920, 1080],
|
||||
"720P": [1280, 720],
|
||||
"D1": [704, 480],
|
||||
"HD1": [352, 480],
|
||||
"BCIF": [704, 240],
|
||||
"2CIF": [704, 240],
|
||||
"CIF": [352, 240],
|
||||
"QCIF": [176, 120],
|
||||
"NHD": [640, 360],
|
||||
"VGA": [640, 480],
|
||||
"QVGA": [320, 240]
|
||||
};
|
||||
|
||||
function fromAmcrestResolution(resolution: string) {
|
||||
const named = amcrestResolutions[resolution];
|
||||
if (named)
|
||||
return named;
|
||||
const parts = resolution.split('x');
|
||||
return [parseInt(parts[0]), parseInt(parts[1])];
|
||||
}
|
||||
|
||||
export class AmcrestCameraClient {
|
||||
credential: AuthFetchCredentialState;
|
||||
@@ -371,6 +337,7 @@ export class AmcrestCameraClient {
|
||||
|
||||
async unlock(): Promise<boolean> {
|
||||
const response = await this.request({
|
||||
// channel 1? this may fail through nvr.
|
||||
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=openDoor&channel=1&UserID=101&Type=Remote`,
|
||||
responseType: 'text',
|
||||
});
|
||||
@@ -379,9 +346,223 @@ export class AmcrestCameraClient {
|
||||
|
||||
async lock(): Promise<boolean> {
|
||||
const response = await this.request({
|
||||
// channel 1? this may fail through nvr.
|
||||
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=closeDoor&channel=1&UserID=101&Type=Remote`,
|
||||
responseType: 'text',
|
||||
});
|
||||
return response.body.includes('OK');
|
||||
}
|
||||
|
||||
|
||||
async resetMotionDetection(cameraNumber: number) {
|
||||
const params = new URLSearchParams();
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].Enable`, 'true');
|
||||
|
||||
// from amcrest docs:
|
||||
// basically a 22x18 binary grid.
|
||||
// so a full cell block is 4194303.
|
||||
|
||||
// Currently, a region is divided into 18 lines and 22 blocks per line.
|
||||
// A bit describes a block in the line.
|
||||
// Bit = 1: motion in this block is monitored.
|
||||
// Example:
|
||||
// MotionDetect [0].Region [0] = 4194303 (0x3FFFFF): the 22 blocks in
|
||||
// channel 0 line 0 is monitored.
|
||||
// MotionDetect [0].Region [1] =0: the 22 blocks in channel 0 line 1 is
|
||||
// not monitored.
|
||||
// MotionDetect [0].Region [17] = 3: the left two blocks in the last line o
|
||||
// channel 0 is monitored.
|
||||
|
||||
// there are 4 configurable motion windows, will use the first one, index 0.
|
||||
// each window is 18 lines, 22 blocks per line.
|
||||
|
||||
// not sure what this first line is.
|
||||
|
||||
// table.MotionDetect[0].Level=3
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Id=0
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Name=Region1
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[0]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[1]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[2]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[3]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[4]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[5]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[6]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[7]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[8]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[9]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[10]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[11]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[12]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[13]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[14]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[15]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[16]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Region[17]=4194303
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Sensitive=60
|
||||
// table.MotionDetect[0].MotionDetectWindow[0].Threshold=5
|
||||
|
||||
// doesn't seem to be able to be renamed.
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Name`, 'Scrypted');
|
||||
|
||||
for (let i = 0; i < 18; i++) {
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Region[${i}]`, '4194303');
|
||||
}
|
||||
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Sensitive`, '60');
|
||||
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Threshold`, '5');
|
||||
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reset motion result', response.body);
|
||||
}
|
||||
|
||||
async configureCodecs(cameraNumber: number, options: MediaStreamConfiguration) {
|
||||
if (!options.id?.startsWith('channel'))
|
||||
throw new Error('invalid id');
|
||||
|
||||
const capsResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/encode.cgi?action=getConfigCaps&channel=${cameraNumber}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
this.console.log(capsResponse.body);
|
||||
|
||||
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
|
||||
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
|
||||
const encode = `Encode[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
const params = new URLSearchParams();
|
||||
if (options.video?.bitrate) {
|
||||
let bitrate = options?.video?.bitrate;
|
||||
bitrate = Math.round(bitrate / 1000);
|
||||
params.set(`${encode}.Video.BitRate`, bitrate.toString());
|
||||
}
|
||||
if (options.video?.codec === 'h264') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.264');
|
||||
params.set(`${encode}.VideoEnable`, 'true');
|
||||
}
|
||||
if (options.video?.profile) {
|
||||
let profile = 'Main';
|
||||
if (options.video.profile === 'high')
|
||||
profile = 'High';
|
||||
else if (options.video.profile === 'baseline')
|
||||
profile = 'Baseline';
|
||||
params.set(`${encode}.Video.Profile`, profile);
|
||||
|
||||
}
|
||||
if (options.video?.codec === 'h265') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.265');
|
||||
}
|
||||
if (options.video?.width && options.video?.height) {
|
||||
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
|
||||
}
|
||||
if (options.video?.fps) {
|
||||
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
|
||||
}
|
||||
if (options.video?.keyframeInterval) {
|
||||
params.set(`${encode}.Video.GOP`, options.video?.keyframeInterval.toString());
|
||||
}
|
||||
if (options.video?.bitrateControl) {
|
||||
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'constant' ? 'CBR' : 'VBR');
|
||||
}
|
||||
|
||||
if (options.audio?.codec) {
|
||||
params.set(`${encode}.Audio.Compression`, toAmcrestAudioCodec(options.audio.codec));
|
||||
params.set(`${encode}.AudioEnable`, 'true');
|
||||
}
|
||||
|
||||
// nothing else audio related seems configurable.
|
||||
|
||||
if ([...params.keys()].length) {
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reconfigure result', response.body);
|
||||
}
|
||||
|
||||
const caps = `caps[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
const singleCaps = `caps.${format}[${formatNumber}]`;
|
||||
|
||||
const findCaps = (key: string) => {
|
||||
const found = findValue(capsResponse.body, caps, key);
|
||||
if (found)
|
||||
return found;
|
||||
// ad410 doesnt return a camera number if accessed directly
|
||||
if (cameraNumber - 1 === 0)
|
||||
return findValue(capsResponse.body, singleCaps, key);
|
||||
}
|
||||
|
||||
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
|
||||
const bitrates = findCaps('Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
|
||||
const fpsMax = parseInt(findCaps('Video.FPSMax'));
|
||||
const vso: MediaStreamConfiguration = {
|
||||
id: options.id,
|
||||
video: {},
|
||||
};
|
||||
vso.video.resolutions = resolutions;
|
||||
vso.video.bitrateRange = [bitrates[0], bitrates[bitrates.length - 1]];
|
||||
vso.video.fpsRange = [1, fpsMax];
|
||||
return vso;
|
||||
}
|
||||
|
||||
async getCodecs(cameraNumber: number): Promise<UrlMediaStreamOptions[]> {
|
||||
const masResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
});
|
||||
const mas = masResponse.body.split('=')[1].trim();
|
||||
|
||||
// amcrest reports more streams than are acually available in its responses,
|
||||
// so checking the max extra streams prevents usage of invalid streams.
|
||||
const maxExtraStreams = parseInt(mas) || 1;
|
||||
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => createRtspMediaStreamOptions(undefined, subtype));
|
||||
|
||||
const encodeResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
let encName: string;
|
||||
if (i === 0) {
|
||||
encName = `table.Encode[${cameraNumber - 1}].MainFormat[0]`;
|
||||
}
|
||||
else {
|
||||
encName = `table.Encode[${cameraNumber - 1}].ExtraFormat[${i - 1}]`;
|
||||
}
|
||||
|
||||
const videoCodec = fromAmcrestVideoCodec(findValue(encodeResponse.body, encName, 'Video.Compression'));
|
||||
const audioCodec = fromAmcrestAudioCodec(findValue(encodeResponse.body, encName, 'Audio.Compression'));
|
||||
|
||||
if (vso.audio)
|
||||
vso.audio.codec = audioCodec;
|
||||
vso.video.codec = videoCodec;
|
||||
|
||||
const width = findValue(encodeResponse.body, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.body, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
|
||||
if (videoEnable?.trim() === 'false') {
|
||||
this.console.warn('Video stream is disabled and should likely be enabled:', encName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
vso.video.bitrate = parseInt(encodeOptions) * 1000;
|
||||
}
|
||||
|
||||
return vsos;
|
||||
}
|
||||
}
|
||||
|
||||
24
plugins/amcrest/src/amcrest-configure.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AudioStreamConfiguration, Setting } from '@scrypted/sdk';
|
||||
import { autoconfigureCodecs as ac } from '../../../common/src/autoconfigure-codecs';
|
||||
import { AmcrestCameraClient } from "./amcrest-api";
|
||||
|
||||
export const amcrestAutoConfigureSettings: Setting = {
|
||||
key: 'amcrest-autoconfigure',
|
||||
type: 'html',
|
||||
value: 'Amcrest autoconfiguration will configure the camera codecs and the motion sensor.',
|
||||
};
|
||||
|
||||
export async function autoconfigureSettings(client: AmcrestCameraClient, cameraNumber: number) {
|
||||
const audioOptions: AudioStreamConfiguration = {
|
||||
codec: 'aac',
|
||||
sampleRate: 8000,
|
||||
};
|
||||
|
||||
await client.resetMotionDetection(cameraNumber);
|
||||
|
||||
return ac(
|
||||
() => client.getCodecs(cameraNumber),
|
||||
options => client.configureCodecs(cameraNumber, options),
|
||||
audioOptions,
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,27 @@
|
||||
import { automaticallyConfigureSettings, checkPluginNeedsAutoConfigure } from "@scrypted/common/src/autoconfigure-codecs";
|
||||
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
|
||||
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { PassThrough, Readable, Stream } from "stream";
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { createRtspMediaStreamOptions, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api";
|
||||
import { amcrestAutoConfigureSettings, autoconfigureSettings } from "./amcrest-configure";
|
||||
import { group } from "console";
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
const AMCREST_DOORBELL_TYPE = 'Amcrest Doorbell';
|
||||
const DAHUA_DOORBELL_TYPE = 'Dahua Doorbell';
|
||||
|
||||
function findValue(blob: string, prefix: string, key: string) {
|
||||
const lines = blob.split('\n');
|
||||
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
|
||||
if (!value)
|
||||
return;
|
||||
|
||||
const parts = value.split('=');
|
||||
return parts[1];
|
||||
}
|
||||
const rtspChannelSetting: Setting = {
|
||||
subgroup: 'Advanced',
|
||||
key: 'rtspChannel',
|
||||
title: 'Channel Number Override',
|
||||
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
|
||||
placeholder: '1',
|
||||
};
|
||||
|
||||
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, Lock, VideoRecorder, Reboot, ObjectDetector {
|
||||
eventStream: Stream;
|
||||
@@ -110,48 +111,10 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.info = deviceInfo;
|
||||
}
|
||||
|
||||
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
|
||||
if (!options.id?.startsWith('channel'))
|
||||
throw new Error('invalid id');
|
||||
async setVideoStreamOptions(options: MediaStreamOptions) {
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
const formatNumber = parseInt(options.id?.substring('channel'.length)) - 1;
|
||||
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
|
||||
const encode = `Encode[${channel - 1}].${format}[${formatNumber}]`;
|
||||
const params = new URLSearchParams();
|
||||
if (options.video?.bitrate) {
|
||||
let bitrate = options?.video?.bitrate;
|
||||
if (!bitrate)
|
||||
return;
|
||||
bitrate = Math.round(bitrate / 1000);
|
||||
params.set(`${encode}.Video.BitRate`, bitrate.toString());
|
||||
}
|
||||
if (options.video?.codec === 'h264') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.264');
|
||||
}
|
||||
if (options.video?.codec === 'h265') {
|
||||
params.set(`${encode}.Video.Compression`, 'H.265');
|
||||
}
|
||||
if (options.video?.width && options.video?.height) {
|
||||
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
|
||||
}
|
||||
if (options.video?.fps) {
|
||||
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
|
||||
if (options.video?.idrIntervalMillis) {
|
||||
params.set(`${encode}.Video.GOP`, (options.video.fps * options.video?.idrIntervalMillis / 1000).toString());
|
||||
}
|
||||
}
|
||||
if (options.video?.bitrateControl) {
|
||||
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'variable' ? 'VBR' : 'CBR');
|
||||
}
|
||||
|
||||
if (![...params.keys()].length)
|
||||
return;
|
||||
|
||||
const response = await this.getClient().request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reconfigure result', response.body);
|
||||
const client = this.getClient();
|
||||
return client.configureCodecs(channel, options);
|
||||
}
|
||||
|
||||
getClient() {
|
||||
@@ -196,8 +159,9 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
else if (event === AmcrestEvent.MotionInfo) {
|
||||
// this seems to be a motion pulse
|
||||
if (this.motionDetected)
|
||||
resetMotionTimeout();
|
||||
if (!this.motionDetected)
|
||||
this.motionDetected = true;
|
||||
resetMotionTimeout();
|
||||
}
|
||||
else if (event === AmcrestEvent.MotionStop) {
|
||||
// use resetMotionTimeout
|
||||
@@ -283,6 +247,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
const ret = await super.getOtherSettings();
|
||||
ret.push(
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
title: 'Doorbell Type',
|
||||
choices: [
|
||||
'Not a Doorbell',
|
||||
@@ -353,6 +318,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
ret.push(
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
title: 'Two Way Audio',
|
||||
value: twoWayAudio,
|
||||
key: 'twoWayAudio',
|
||||
@@ -369,8 +335,18 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
// },
|
||||
);
|
||||
|
||||
return ret;
|
||||
const ac = {
|
||||
...automaticallyConfigureSettings,
|
||||
subgroup: 'Advanced',
|
||||
};
|
||||
ac.type = 'button';
|
||||
ret.push(ac);
|
||||
ret.push({
|
||||
...amcrestAutoConfigureSettings,
|
||||
subgroup: 'Advanced',
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async takeSmartCameraPicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
||||
@@ -378,15 +354,12 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
async getUrlSettings() {
|
||||
const rtspChannel = {
|
||||
...rtspChannelSetting,
|
||||
value: this.storage.getItem('rtspChannel'),
|
||||
};
|
||||
return [
|
||||
{
|
||||
key: 'rtspChannel',
|
||||
title: 'Channel Number Override',
|
||||
subgroup: 'Advanced',
|
||||
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
|
||||
placeholder: '1',
|
||||
value: this.storage.getItem('rtspChannel'),
|
||||
},
|
||||
rtspChannel,
|
||||
...await super.getUrlSettings(),
|
||||
]
|
||||
}
|
||||
@@ -396,7 +369,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
createRtspMediaStreamOptions(url: string, index: number) {
|
||||
const ret = super.createRtspMediaStreamOptions(url, index);
|
||||
const ret = createRtspMediaStreamOptions(url, index);
|
||||
ret.tool = 'scrypted';
|
||||
return ret;
|
||||
}
|
||||
@@ -404,98 +377,38 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
||||
const client = this.getClient();
|
||||
|
||||
if (!this.videoStreamOptions) {
|
||||
this.videoStreamOptions = (async () => {
|
||||
let mas: string;
|
||||
if (this.videoStreamOptions)
|
||||
return this.videoStreamOptions;
|
||||
|
||||
this.videoStreamOptions = (async () => {
|
||||
const cameraNumber = parseInt(this.getRtspChannel()) || 1;
|
||||
try {
|
||||
let vsos: UrlMediaStreamOptions[];
|
||||
try {
|
||||
const response = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
})
|
||||
mas = response.body.split('=')[1].trim();
|
||||
this.storage.setItem('maxExtraStreams', mas.toString());
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving max extra streams', e);
|
||||
mas = this.storage.getItem('maxExtraStreams');
|
||||
}
|
||||
|
||||
const maxExtraStreams = parseInt(mas) || 1;
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
|
||||
|
||||
try {
|
||||
const capResponse = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(capResponse.body);
|
||||
const encodeResponse = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
let capName: string;
|
||||
let encName: string;
|
||||
if (i === 0) {
|
||||
capName = `caps[${channel - 1}].MainFormat[0]`;
|
||||
encName = `table.Encode[${channel - 1}].MainFormat[0]`;
|
||||
}
|
||||
else {
|
||||
capName = `caps[${channel - 1}].ExtraFormat[${i - 1}]`;
|
||||
encName = `table.Encode[${channel - 1}].ExtraFormat[${i - 1}]`;
|
||||
}
|
||||
|
||||
const videoCodec = findValue(encodeResponse.body, encName, 'Video.Compression')
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
let audioCodec = findValue(encodeResponse.body, encName, 'Audio.Compression')
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
if (audioCodec?.includes('aac'))
|
||||
audioCodec = 'aac';
|
||||
else if (audioCodec?.includes('g711a'))
|
||||
audioCodec = 'pcm_alaw';
|
||||
else if (audioCodec?.includes('g711u'))
|
||||
audioCodec = 'pcm_mulaw';
|
||||
else if (audioCodec?.includes('g711'))
|
||||
audioCodec = 'pcm';
|
||||
|
||||
if (vso.audio)
|
||||
vso.audio.codec = audioCodec;
|
||||
vso.video.codec = videoCodec;
|
||||
|
||||
const width = findValue(encodeResponse.body, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.body, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const bitrateOptions = findValue(capResponse.body, capName, 'Video.BitRateOptions');
|
||||
if (!bitrateOptions)
|
||||
continue;
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
const [min, max] = bitrateOptions.split(',');
|
||||
if (!min || !max)
|
||||
continue;
|
||||
vso.video.bitrate = parseInt(encodeOptions) * 1000;
|
||||
vso.video.maxBitrate = parseInt(max) * 1000;
|
||||
vso.video.minBitrate = parseInt(min) * 1000;
|
||||
}
|
||||
vsos = await client.getCodecs(cameraNumber);
|
||||
this.storage.setItem('vsosJSON', JSON.stringify(vsos));
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error retrieving stream configurations', e);
|
||||
vsos = JSON.parse(this.storage.getItem('vsosJSON')) as UrlMediaStreamOptions[];
|
||||
}
|
||||
|
||||
for (const [index, vso] of vsos.entries()) {
|
||||
vso.tool = 'scrypted';
|
||||
vso.url = `rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${index}`;
|
||||
}
|
||||
return vsos;
|
||||
})();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.videoStreamOptions = undefined;
|
||||
const vsos = [...Array(2).keys()].map(subtype => {
|
||||
const ret = createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${subtype}`, subtype);
|
||||
ret.tool = 'scrypted';
|
||||
return ret;
|
||||
});
|
||||
return vsos;
|
||||
}
|
||||
})();
|
||||
|
||||
return this.videoStreamOptions;
|
||||
}
|
||||
@@ -534,6 +447,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
if (key === automaticallyConfigureSettings.key) {
|
||||
const client = this.getClient();
|
||||
autoconfigureSettings(client, parseInt(this.getRtspChannel()) || 1)
|
||||
.then(() => {
|
||||
this.log.a('Successfully configured settings.');
|
||||
})
|
||||
.catch(e => {
|
||||
this.log.a('There was an error automatically configuring settings. More information can be viewed in the console.');
|
||||
this.console.error('error autoconfiguring', e);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'continuousRecording') {
|
||||
if (value === 'true') {
|
||||
try {
|
||||
@@ -575,7 +501,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
// not sure if this all works, since i don't actually have a doorbell.
|
||||
// good luck!
|
||||
const channel = this.getRtspChannel() || '1';
|
||||
const channel = parseInt(this.getRtspChannel()) || 1;
|
||||
|
||||
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
|
||||
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
|
||||
@@ -693,6 +619,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
}
|
||||
|
||||
class AmcrestProvider extends RtspProvider {
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
super(nativeId);
|
||||
checkPluginNeedsAutoConfigure(this);
|
||||
}
|
||||
|
||||
getAdditionalInterfaces() {
|
||||
return [
|
||||
ScryptedInterface.Reboot,
|
||||
@@ -703,6 +634,9 @@ class AmcrestProvider extends RtspProvider {
|
||||
];
|
||||
}
|
||||
|
||||
getScryptedDeviceCreator(): string {
|
||||
return 'Amcrest Camera';
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
||||
const httpAddress = `${settings.ip}:${settings.httpPort || 80}`;
|
||||
@@ -712,8 +646,14 @@ class AmcrestProvider extends RtspProvider {
|
||||
const password = settings.password?.toString();
|
||||
const skipValidate = settings.skipValidate?.toString() === 'true';
|
||||
let twoWayAudio: string;
|
||||
|
||||
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
|
||||
if (settings.autoconfigure) {
|
||||
const cameraNumber = parseInt(settings.rtspChannel as string) || 1;
|
||||
await autoconfigureSettings(api, cameraNumber);
|
||||
}
|
||||
|
||||
if (!skipValidate) {
|
||||
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
|
||||
try {
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
|
||||
@@ -744,8 +684,10 @@ class AmcrestProvider extends RtspProvider {
|
||||
device.info = info;
|
||||
device.putSetting('username', username);
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
if (settings.rtspChannel)
|
||||
device.putSetting('rtspChannel', settings.rtspChannel as string);
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
device.setIPAddress(settings.ip?.toString());
|
||||
if (twoWayAudio)
|
||||
device.putSetting('twoWayAudio', twoWayAudio);
|
||||
device.updateDeviceInfo();
|
||||
@@ -768,13 +710,18 @@ class AmcrestProvider extends RtspProvider {
|
||||
title: 'IP Address',
|
||||
placeholder: '192.168.2.222',
|
||||
},
|
||||
rtspChannelSetting,
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
key: 'httpPort',
|
||||
title: 'HTTP Port',
|
||||
description: 'Optional: Override the HTTP Port from the default value of 80',
|
||||
description: 'Optional: Override the HTTP Port from the default value of 80.',
|
||||
placeholder: '80',
|
||||
},
|
||||
automaticallyConfigureSettings,
|
||||
amcrestAutoConfigureSettings,
|
||||
{
|
||||
subgroup: 'Advanced',
|
||||
key: 'skipValidate',
|
||||
title: 'Skip Validation',
|
||||
description: 'Add the device without verifying the credentials and network settings.',
|
||||
@@ -786,6 +733,7 @@ class AmcrestProvider extends RtspProvider {
|
||||
createCamera(nativeId: string) {
|
||||
return new AmcrestCamera(nativeId, this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AmcrestProvider;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# BTicino C300X Plugin for Scrypted
|
||||
# BTicino Intercom Plugin for Scrypted
|
||||
|
||||
The C300X Plugin for Scrypted allows viewing your C300X intercom with incoming video/audio.
|
||||
|
||||
|
||||
993
plugins/bticino/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.18",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -20,9 +20,11 @@
|
||||
"sip"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "BTicino SIP Plugin",
|
||||
"name": "BTicino Intercom Plugin",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"ScryptedDeviceCreator",
|
||||
"DeviceProvider",
|
||||
"DeviceCreator"
|
||||
],
|
||||
@@ -32,14 +34,14 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@slyoldfox/sip": "^0.0.6-1",
|
||||
"sdp": "^3.0.3",
|
||||
"stun": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6",
|
||||
"@types/node": "^20.11.30",
|
||||
"cross-env": "^7.0.3",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import { VoicemailHandler } from "./bticino-voicemailHandler";
|
||||
|
||||
export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
|
||||
private timeout : NodeJS.Timeout
|
||||
private voicemailHandler : VoicemailHandler
|
||||
|
||||
constructor(private camera: BticinoSipCamera, private voicemailHandler : VoicemailHandler) {
|
||||
constructor(private camera: BticinoSipCamera) {
|
||||
super( camera.nativeId + "-aswm-switch")
|
||||
this.voicemailHandler = new VoicemailHandler(camera)
|
||||
camera.requestHandlers.add(this.voicemailHandler)
|
||||
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
|
||||
}
|
||||
|
||||
@@ -29,6 +32,7 @@ export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, Http
|
||||
if( this.timeout ) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
this.voicemailHandler?.cancelTimer()
|
||||
}
|
||||
|
||||
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
|
||||
@@ -2,13 +2,12 @@ import { createBindUdp, listenZeroSingleClient } from '@scrypted/common/src/list
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import { RtspServer } from '@scrypted/common/src/rtsp-server';
|
||||
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, MotionSensor, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, MotionSensor, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
|
||||
import { SipCallSession } from '../../sip/src/sip-call-session';
|
||||
import { RtpDescription, getPayloadType, getSequenceNumber, isRtpMessagePayloadType, isStunMessage } from '../../sip/src/rtp-utils';
|
||||
import { VoicemailHandler } from './bticino-voicemailHandler';
|
||||
import { CompositeSipMessageHandler } from '../../sip/src/compositeSipMessageHandler';
|
||||
import { SipHelper } from './sip-helper';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import child_process from 'child_process';
|
||||
import { BticinoStorageSettings } from './storage-settings';
|
||||
import { BticinoSipPlugin } from './main';
|
||||
import { BticinoSipLock } from './bticino-lock';
|
||||
@@ -29,7 +28,6 @@ import { ControllerApi } from './c300x-controller-api';
|
||||
import { BticinoAswmSwitch } from './bticino-aswm-switch';
|
||||
import { BticinoMuteSwitch } from './bticino-mute-switch';
|
||||
|
||||
const STREAM_TIMEOUT = 65000;
|
||||
const { mediaManager } = sdk;
|
||||
const BTICINO_CLIPS = path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'bticino-clips');
|
||||
|
||||
@@ -38,16 +36,14 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
private session: SipCallSession
|
||||
private remoteRtpDescription: Promise<RtpDescription>
|
||||
private forwarder
|
||||
private refreshTimeout: NodeJS.Timeout
|
||||
public requestHandlers: CompositeSipMessageHandler = new CompositeSipMessageHandler()
|
||||
public incomingCallRequest : SipRequest
|
||||
private settingsStorage: BticinoStorageSettings = new BticinoStorageSettings( this )
|
||||
private voicemailHandler : VoicemailHandler = new VoicemailHandler(this)
|
||||
private inviteHandler : InviteHandler = new InviteHandler(this)
|
||||
private controllerApi : ControllerApi = new ControllerApi(this)
|
||||
private muteSwitch : BticinoMuteSwitch
|
||||
private aswmSwitch : BticinoAswmSwitch
|
||||
private deferredCleanup
|
||||
private deferredCleanup: () => void
|
||||
private currentMediaObject : Promise<MediaObject>
|
||||
private lastImageRefresh : number
|
||||
//TODO: randomize this
|
||||
@@ -60,7 +56,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
|
||||
constructor(nativeId: string, public provider: BticinoSipPlugin) {
|
||||
super(nativeId)
|
||||
this.requestHandlers.add( this.voicemailHandler ).add( this.inviteHandler )
|
||||
this.requestHandlers.add( this.inviteHandler )
|
||||
this.persistentSipManager = new PersistentSipManager( this );
|
||||
(async() => {
|
||||
this.doorbellWebhookUrl = await this.doorbellWebhookEndpoint()
|
||||
@@ -149,7 +145,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
}).on('error', (error) => {
|
||||
this.console.error(error)
|
||||
reject(error)
|
||||
} ).end(); ;
|
||||
} ).end();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,8 +279,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
// get a proxy object to make sure we pass prebuffer when already watching a stream
|
||||
let cam : VideoCamera = sdk.systemManager.getDeviceById<VideoCamera>(this.id)
|
||||
let vs : MediaObject = await cam.getVideoStream()
|
||||
let buf : Buffer = await mediaManager.convertMediaObjectToBuffer(vs, 'image/jpeg');
|
||||
this.cachedImage = buf
|
||||
this.cachedImage = await mediaManager.convertMediaObjectToBuffer(vs, 'image/jpeg');
|
||||
this.lastImageRefresh = new Date().getTime()
|
||||
this.console.log(`Camera picture updated and cached: ${this.lastImageRefresh} + cache time: ${thumbnailCacheTime} < ${now}`)
|
||||
|
||||
@@ -349,19 +344,13 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
this.forwarder = undefined
|
||||
}
|
||||
|
||||
resetStreamTimeout() {
|
||||
this.log.d('starting/refreshing stream')
|
||||
clearTimeout(this.refreshTimeout)
|
||||
this.refreshTimeout = setTimeout(() => this.stopSession(), STREAM_TIMEOUT)
|
||||
}
|
||||
|
||||
hasActiveCall() {
|
||||
return this.session;
|
||||
}
|
||||
|
||||
stopSession() {
|
||||
if (this.session) {
|
||||
this.log.d('ending sip session')
|
||||
this.console.log('ending sip session')
|
||||
this.session.stop()
|
||||
this.session = undefined
|
||||
}
|
||||
@@ -406,7 +395,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
if (this.session === sip)
|
||||
this.session = undefined
|
||||
try {
|
||||
this.log.d('cleanup(): stopping sip session.')
|
||||
this.console.log('cleanup(): stopping sip session.')
|
||||
sip?.stop()
|
||||
this.currentMediaObject = undefined
|
||||
}
|
||||
@@ -479,7 +468,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
|
||||
this.session = sip
|
||||
|
||||
videoSplitter.server.on('message', (message, rinfo) => {
|
||||
videoSplitter.server.on('message', (message:Buffer) => {
|
||||
if ( !isStunMessage(message)) {
|
||||
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
|
||||
if (!isRtpMessage)
|
||||
@@ -498,7 +487,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
}
|
||||
});
|
||||
|
||||
audioSplitter.server.on('message', (message, rinfo ) => {
|
||||
audioSplitter.server.on('message', (message:Buffer) => {
|
||||
if ( !isStunMessage(message)) {
|
||||
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
|
||||
if (!isRtpMessage)
|
||||
@@ -617,13 +606,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
|
||||
async getDevice(nativeId: string) : Promise<any> {
|
||||
if( nativeId && nativeId.endsWith('-aswm-switch')) {
|
||||
this.aswmSwitch = new BticinoAswmSwitch(this, this.voicemailHandler)
|
||||
this.aswmSwitch = new BticinoAswmSwitch(this)
|
||||
this.aswmSwitch.info = this.info
|
||||
return this.aswmSwitch
|
||||
} else if( nativeId && nativeId.endsWith('-mute-switch') ) {
|
||||
this.muteSwitch = new BticinoMuteSwitch(this)
|
||||
this.muteSwitch.info = this.info
|
||||
return this.muteSwitch
|
||||
}
|
||||
return new BticinoSipLock(this)
|
||||
const lock = new BticinoSipLock(this)
|
||||
lock.info = this.info
|
||||
return lock
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
@@ -633,7 +626,6 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
|
||||
this.muteSwitch.cancelTimer()
|
||||
} else {
|
||||
this.stopIntercom()
|
||||
this.voicemailHandler.cancelTimer()
|
||||
this.persistentSipManager.cancelTimer()
|
||||
this.controllerApi.cancelTimer()
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ export class ControllerApi {
|
||||
res.on("end", () => {
|
||||
try {
|
||||
let parsedBody = JSON.parse( body )
|
||||
if( !parsedBody["model"] ) {
|
||||
reject( new Error("Cannot determine model, update your c300x-controller.") )
|
||||
}
|
||||
if( parsedBody["errors"].length > 0 ) {
|
||||
reject( new Error( parsedBody["errors"][0] ) )
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, LockState, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'
|
||||
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, LockState, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { BticinoSipCamera } from './bticino-camera'
|
||||
import { ControllerApi } from './c300x-controller-api';
|
||||
import { SipHelper } from './sip-helper';
|
||||
|
||||
const { systemManager, deviceManager } = sdk
|
||||
|
||||
@@ -9,6 +10,13 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
|
||||
devices = new Map<string, BticinoSipCamera>()
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.systemDevice = {
|
||||
deviceCreator: 'Bticino Doorbell',
|
||||
};
|
||||
}
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
@@ -24,6 +32,18 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
]
|
||||
}
|
||||
|
||||
deviceInfo(setupData) : DeviceInformation {
|
||||
return {
|
||||
model: setupData["model"].toLocaleUpperCase(),
|
||||
manufacturer: `Bticino (c300x-controller v${setupData["version"]})`,
|
||||
version: setupData["version"],
|
||||
firmware: setupData["firmware"],
|
||||
ip: setupData["ipAddress"],
|
||||
mac: setupData["macAddress"],
|
||||
managementUrl: 'http://' + setupData["ipAddress"] + ':8080'
|
||||
}
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
|
||||
if( !settings.ip ) {
|
||||
throw new Error('IP address is required!')
|
||||
@@ -34,16 +54,12 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
return validate.then( async (setupData) => {
|
||||
const nativeId = randomBytes(4).toString('hex')
|
||||
const name = settings.newCamera?.toString() === undefined ? "Doorbell" : settings.newCamera?.toString()
|
||||
await this.updateDevice(nativeId, name)
|
||||
const deviceInfo : DeviceInformation = this.deviceInfo(setupData)
|
||||
await this.updateDevice(nativeId, name, deviceInfo)
|
||||
|
||||
const lockDevice: Device = {
|
||||
providerNativeId: nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
manufacturer: 'BticinoPlugin',
|
||||
//firmware: camera.data.firmware_version,
|
||||
//serialNumber: camera.data.device_id
|
||||
},
|
||||
info: deviceInfo,
|
||||
nativeId: nativeId + '-lock',
|
||||
name: name + ' Lock',
|
||||
type: ScryptedDeviceType.Lock,
|
||||
@@ -52,12 +68,7 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
|
||||
const aswmSwitchDevice: Device = {
|
||||
providerNativeId: nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
manufacturer: 'BticinoPlugin',
|
||||
//firmware: camera.data.firmware_version,
|
||||
//serialNumber: camera.data.device_id
|
||||
},
|
||||
info: deviceInfo,
|
||||
nativeId: nativeId + '-aswm-switch',
|
||||
name: name + ' Voicemail',
|
||||
type: ScryptedDeviceType.Switch,
|
||||
@@ -66,27 +77,23 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
|
||||
const muteSwitchDevice: Device = {
|
||||
providerNativeId: nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
manufacturer: 'BticinoPlugin',
|
||||
//firmware: camera.data.firmware_version,
|
||||
//serialNumber: camera.data.device_id
|
||||
},
|
||||
info: deviceInfo,
|
||||
nativeId: nativeId + '-mute-switch',
|
||||
name: name + ' Muted',
|
||||
type: ScryptedDeviceType.Switch,
|
||||
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.HttpRequestHandler],
|
||||
}
|
||||
}
|
||||
const devices = setupData["model"] === 'c100x' ? [lockDevice, muteSwitchDevice] : [lockDevice, aswmSwitchDevice, muteSwitchDevice]
|
||||
|
||||
await deviceManager.onDevicesChanged({
|
||||
providerNativeId: nativeId,
|
||||
devices: [lockDevice, aswmSwitchDevice, muteSwitchDevice],
|
||||
devices: devices
|
||||
})
|
||||
|
||||
let sipCamera : BticinoSipCamera = await this.getDevice(nativeId)
|
||||
|
||||
sipCamera.putSetting("sipfrom", "scrypted-" + sipCamera.id + "@127.0.0.1")
|
||||
sipCamera.putSetting("sipto", "c300x@" + setupData["ipAddress"] )
|
||||
sipCamera.putSetting("sipto", setupData["model"] + "@" + setupData["ipAddress"] )
|
||||
sipCamera.putSetting("sipdomain", setupData["domain"])
|
||||
sipCamera.putSetting("sipdebug", true )
|
||||
|
||||
@@ -99,15 +106,10 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
})
|
||||
}
|
||||
|
||||
updateDevice(nativeId: string, name: string) {
|
||||
updateDevice(nativeId: string, name: string, deviceInfo) {
|
||||
return deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
info: {
|
||||
//model: `${camera.model} (${camera.data.kind})`,
|
||||
manufacturer: 'BticinoSipPlugin',
|
||||
//firmware: camera.data.firmware_version,
|
||||
//serialNumber: camera.data.device_id
|
||||
},
|
||||
info: deviceInfo,
|
||||
name,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
@@ -128,6 +130,9 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
if (!this.devices.has(nativeId)) {
|
||||
const camera = new BticinoSipCamera(nativeId, this)
|
||||
ControllerApi.validate(SipHelper.getIntercomIp(camera)).then( async (setupData) => {
|
||||
camera.info = this.deviceInfo(setupData)
|
||||
} )
|
||||
this.devices.set(nativeId, camera)
|
||||
}
|
||||
return this.devices.get(nativeId)
|
||||
|
||||
@@ -49,11 +49,23 @@ export class BticinoStorageSettings {
|
||||
description: 'Enable SIP debugging',
|
||||
placeholder: 'true or false',
|
||||
},
|
||||
DEVADDR: {
|
||||
title: 'Device address (DEVADDR)',
|
||||
type: 'string',
|
||||
description: 'Only specify if this is different than 20. For c100x this is a UUID, see: tcpdump -i lo port 5060',
|
||||
defaultValue: '20',
|
||||
placeholder: '20',
|
||||
},
|
||||
notifyVoicemail: {
|
||||
title: 'Notify on new voicemail messages',
|
||||
type: 'boolean',
|
||||
description: 'Enable voicemail alerts',
|
||||
placeholder: 'true or false',
|
||||
onGet: async () => {
|
||||
return {
|
||||
hide: this.storageSettings.values.sipto.indexOf('c100x') == 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
doorbellWebhookUrl: {
|
||||
title: 'Doorbell Sensor Webhook',
|
||||
|
||||
14
plugins/cloud/.vscode/launch.json
vendored
@@ -4,6 +4,20 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Test Cloudflared",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${workspaceFolder}/test/test-cloudflared.ts"
|
||||
],
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
|
||||
2
plugins/cloud/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
4243
plugins/cloud/package-lock.json
generated
@@ -30,28 +30,28 @@
|
||||
"realfs": true,
|
||||
"interfaces": [
|
||||
"SystemSettings",
|
||||
"BufferConverter",
|
||||
"MediaConverter",
|
||||
"OauthClient",
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"HttpRequestHandler"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^3.1.5",
|
||||
"@eneris/push-receiver": "^4.2.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"bpmux": "^8.2.1",
|
||||
"cloudflared": "^0.5.2",
|
||||
"cloudflared": "^0.5.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"nat-upnp": "file:./external/node-nat-upnp"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/http-proxy": "^1.17.14",
|
||||
"@types/http-proxy": "^1.17.15",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/nat-upnp": "^1.1.5",
|
||||
"@types/node": "^20.14.6"
|
||||
"@types/node": "^22.5.2",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"version": "0.2.15"
|
||||
"version": "0.2.37"
|
||||
}
|
||||
|
||||
28
plugins/cloud/src/cloudflared-install.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as cloudflared from 'cloudflared';
|
||||
import { once } from 'events';
|
||||
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
|
||||
export async function installCloudflared() {
|
||||
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
|
||||
const version = 5;
|
||||
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`, `${process.platform}-${process.arch}`);
|
||||
const bin = path.join(cloudflareD, cloudflared.bin);
|
||||
|
||||
if (!fs.existsSync(bin)) {
|
||||
for (let i = 0; i <= version; i++) {
|
||||
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`);
|
||||
rmSync(cloudflareD, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
await cloudflared.install(bin);
|
||||
}
|
||||
|
||||
return {
|
||||
bin,
|
||||
cloudflareD,
|
||||
};
|
||||
}
|
||||
128
plugins/cloud/src/cloudflared-local-managed.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import * as cloudflared from 'cloudflared';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
import child_process from 'child_process';
|
||||
import { once } from 'events';
|
||||
import { timeoutPromise } from '@scrypted/common/src/promise-utils';
|
||||
|
||||
function extractJsonFilePath(message: string): string | null {
|
||||
const regex = /Tunnel credentials written to (.+?\.json)/;
|
||||
const match = message.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function runLog(bin: string, args: string[]) {
|
||||
const cp = child_process.spawn(bin, args, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
cp.stdio[1].on('data', (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
cp.stdio[2].on('data', (data) => {
|
||||
console.error(data.toString());
|
||||
});
|
||||
|
||||
return cp;
|
||||
}
|
||||
|
||||
async function runLogWait(bin: string, args: string[], timeout: number, signal?: AbortSignal, outputChanged?: (output: string) => void) {
|
||||
const cp = runLog(bin, args);
|
||||
|
||||
signal?.addEventListener('abort', () => {
|
||||
cp.kill();
|
||||
});
|
||||
|
||||
let output: string = '';
|
||||
cp.stdio[1].on('data', (data) => {
|
||||
output += data.toString();
|
||||
outputChanged?.(output);
|
||||
});
|
||||
cp.stdio[2].on('data', (data) => {
|
||||
output += data.toString();
|
||||
outputChanged?.(output);
|
||||
});
|
||||
|
||||
await timeoutPromise(timeout, once(cp, 'exit'));
|
||||
if (cp.exitCode !== 0)
|
||||
throw new Error(`failed: cloudflared ${args.join(' ')}`);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function login(bin: string, signal?: AbortSignal, urlCallback?: (url: string) => void) {
|
||||
const userHome = process.env.HOME || process.env.USERPROFILE;
|
||||
const certPem = path.join(userHome, '.cloudflared', 'cert.pem');
|
||||
rmSync(certPem, { force: true, recursive: true });
|
||||
|
||||
await runLogWait(bin, ['tunnel', 'login'], 300000, signal, output => {
|
||||
const match = output.match(/Please open the following URL and log in with your Cloudflare account:(?<url>.*?)Leave/s);
|
||||
if (match) {
|
||||
const url = match.groups.url.trim();
|
||||
if (url)
|
||||
urlCallback(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createTunnel(bin: string, domain: string) {
|
||||
await runLogWait(bin, ['tunnel', 'cleanup', domain], 30000).catch(() => { });
|
||||
await runLogWait(bin, ['tunnel', 'delete', domain], 30000).catch(() => { });
|
||||
return runLogWait(bin, ['tunnel', 'create', domain], 30000);
|
||||
}
|
||||
|
||||
async function routeDns(bin: string, tunnelId: string, domain: string) {
|
||||
return runLogWait(bin, ['tunnel', 'route', "dns", "-f", tunnelId, domain], 30000);
|
||||
}
|
||||
|
||||
export async function runLocallyManagedTunnel(jsonContents: any, url: string, workDir: string, bin?: string) {
|
||||
bin = await ensureBin(bin);
|
||||
|
||||
const { TunnelID } = jsonContents;
|
||||
const credentialsJson = path.join(workDir, `${TunnelID}.json`);
|
||||
writeFileSync(credentialsJson, JSON.stringify(jsonContents));
|
||||
|
||||
const configYml =
|
||||
`url: ${url}
|
||||
tunnel: ${TunnelID}
|
||||
credentials-file: ${workDir}/${TunnelID}.json
|
||||
`;
|
||||
|
||||
const configYmlPath = path.join(workDir, `${TunnelID}.yml`);
|
||||
writeFileSync(configYmlPath, configYml);
|
||||
|
||||
|
||||
return runLog(bin, ['tunnel', '--config', configYmlPath, 'run', TunnelID]);
|
||||
}
|
||||
|
||||
async function ensureBin(bin: string) {
|
||||
if (bin)
|
||||
return bin;
|
||||
const dir = path.join(tmpdir(), 'cloudflared');
|
||||
bin = path.join(dir, 'cloudflared');
|
||||
if (!existsSync(bin)) {
|
||||
try {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
const b = await cloudflared.install(bin);
|
||||
console.warn(b);
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
|
||||
export async function createLocallyManagedTunnel(domain: string, bin?: string, signal?: AbortSignal, urlCallback?: (url: string) => void) {
|
||||
bin = await ensureBin(bin);
|
||||
|
||||
await login(bin, signal, urlCallback);
|
||||
const createOutput = await createTunnel(bin, domain);
|
||||
const jsonFilePath = extractJsonFilePath(createOutput);
|
||||
|
||||
const jsonContents = JSON.parse(readFileSync(jsonFilePath).toString());
|
||||
|
||||
const { TunnelID } = jsonContents;
|
||||
await routeDns(bin, TunnelID, domain);
|
||||
return jsonContents;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Deferred } from "@scrypted/common/src/deferred";
|
||||
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
|
||||
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MediaConverter, MediaObject, MediaObjectOptions, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import bpmux from 'bpmux';
|
||||
import { ChildProcess } from "child_process";
|
||||
import * as cloudflared from 'cloudflared';
|
||||
import crypto from 'crypto';
|
||||
import { once } from 'events';
|
||||
import { backOff } from "exponential-backoff";
|
||||
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
|
||||
import http from 'http';
|
||||
import HttpProxy from 'http-proxy';
|
||||
import https from 'https';
|
||||
@@ -17,13 +17,14 @@ import path from 'path';
|
||||
import { Duplex } from 'stream';
|
||||
import tls from 'tls';
|
||||
import { readLine } from '../../../common/src/read-stream';
|
||||
import { sleep } from '../../../common/src/sleep';
|
||||
import { createSelfSignedCertificate } from '../../../server/src/cert';
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
import { installCloudflared } from "./cloudflared-install";
|
||||
import { createLocallyManagedTunnel, runLocallyManagedTunnel } from "./cloudflared-local-managed";
|
||||
import { PushManager } from './push';
|
||||
import { qsparse, qsstringify } from "./qs";
|
||||
|
||||
// import { registerDuckDns } from "./greenlock";
|
||||
|
||||
const { deviceManager, endpointManager, systemManager } = sdk;
|
||||
|
||||
export const DEFAULT_SENDER_ID = '827888101440';
|
||||
@@ -31,32 +32,16 @@ const SCRYPTED_SERVER = localStorage.getItem('scrypted-server') || 'home.scrypte
|
||||
|
||||
const SCRYPTED_CLOUD_MESSAGE_PATH = '/_punch/cloudmessage';
|
||||
|
||||
class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
|
||||
constructor(public cloud: ScryptedCloud) {
|
||||
super('push');
|
||||
|
||||
this.fromMimeType = ScryptedMimeTypes.PushEndpoint;
|
||||
this.toMimeType = ScryptedMimeTypes.Url;
|
||||
}
|
||||
|
||||
async convert(data: Buffer | string, fromMimeType: string): Promise<Buffer> {
|
||||
const validDomain = this.cloud.getSSLHostname();
|
||||
if (validDomain)
|
||||
return Buffer.from(`https://${validDomain}${await this.cloud.getCloudMessagePath()}/${data}`);
|
||||
|
||||
const url = `http://127.0.0.1/push/${data}`;
|
||||
return this.cloud.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.cloud.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
|
||||
}
|
||||
}
|
||||
|
||||
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter, DeviceProvider, HttpRequestHandler {
|
||||
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, MediaConverter, HttpRequestHandler {
|
||||
cloudflareTunnel: string;
|
||||
cloudflared: Awaited<ReturnType<typeof cloudflared.tunnel>>;
|
||||
cloudflared: {
|
||||
url: Promise<string>;
|
||||
child: ChildProcess;
|
||||
};
|
||||
manager = new PushManager(DEFAULT_SENDER_ID);
|
||||
server: http.Server;
|
||||
secureServer: https.Server;
|
||||
proxy: HttpProxy;
|
||||
push: ScryptedPush;
|
||||
whitelisted = new Map<string, string>();
|
||||
reregisterTimer: NodeJS.Timeout;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
@@ -79,15 +64,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
persistedDefaultValue: crypto.randomBytes(8).toString('hex'),
|
||||
},
|
||||
forwardingMode: {
|
||||
title: "Port Forwarding Mode",
|
||||
description: "The port forwarding mode used to expose the HTTPS port. If port forwarding is disabled or unavailable, Scrypted Cloud will fall back to push to initiate connections with this Scrypted server. Port Forwarding and UPNP are optional but will significantly speed up cloud connections.",
|
||||
title: "Connection Mode",
|
||||
description: "The connection mode that exposes this server to the internet.",
|
||||
choices: [
|
||||
"Default",
|
||||
"UPNP",
|
||||
"Router Forward",
|
||||
"Custom Domain",
|
||||
"Disabled",
|
||||
],
|
||||
defaultValue: 'UPNP',
|
||||
defaultValue: 'Default',
|
||||
onPut: () => this.scheduleRefreshPortForward(),
|
||||
},
|
||||
hostname: {
|
||||
@@ -170,6 +156,39 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
onPut: () => {
|
||||
this.cloudflared?.child.kill();
|
||||
},
|
||||
// this has been deprecated in favor of locally managed tunnels.
|
||||
hide: true,
|
||||
},
|
||||
cloudflaredTunnelCredentials: {
|
||||
group: 'Cloudflare',
|
||||
json: true,
|
||||
hide: true,
|
||||
},
|
||||
cloudflaredTunnelCustomDomain: {
|
||||
group: 'Cloudflare',
|
||||
title: 'Cloudflare Tunnel Custom Domain',
|
||||
placeholder: 'scrypted.example.com',
|
||||
description: 'Optional: Host a custom domain with Cloudflare. After setting the domain, complete the Cloudflare browser login link shown in Scrypted Cloud Plugin Console.',
|
||||
mapPut: (ov, nv) => {
|
||||
try {
|
||||
const url = new URL(nv);
|
||||
return url.hostname;
|
||||
}
|
||||
catch (e) {
|
||||
return nv;
|
||||
}
|
||||
},
|
||||
onPut: (_, nv) => {
|
||||
if (!nv)
|
||||
this.storageSettings.values.cloudflaredTunnelCredentials = undefined;
|
||||
this.doCloudflaredLogin(nv);
|
||||
},
|
||||
},
|
||||
cloudflaredTunnelLoginUrl: {
|
||||
group: 'Cloudflare',
|
||||
type: 'html',
|
||||
title: 'Cloudflare Tunnel Login',
|
||||
hide: true,
|
||||
},
|
||||
cloudflaredTunnelUrl: {
|
||||
group: 'Cloudflare',
|
||||
@@ -219,9 +238,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
upnpInterval: NodeJS.Timeout;
|
||||
upnpClient = upnp.createClient();
|
||||
upnpStatus = 'Starting';
|
||||
securePort: number;
|
||||
randomBytes = crypto.randomBytes(16).toString('base64');
|
||||
reverseConnections = new Set<Duplex>();
|
||||
cloudflaredLoginController?: AbortController;
|
||||
|
||||
get portForwardingDisabled() {
|
||||
return this.storageSettings.values.forwardingMode === 'Disabled' || this.storageSettings.values.forwardingMode === 'Default';
|
||||
}
|
||||
|
||||
get cloudflareTunnelHost() {
|
||||
if (!this.cloudflareTunnel)
|
||||
@@ -232,6 +255,17 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.converters = [
|
||||
[ScryptedMimeTypes.LocalUrl, ScryptedMimeTypes.Url],
|
||||
[ScryptedMimeTypes.PushEndpoint, ScryptedMimeTypes.Url],
|
||||
];
|
||||
// legacy cleanup
|
||||
this.fromMimeType = undefined;
|
||||
this.toMimeType = undefined;
|
||||
deviceManager.onDevicesChanged({
|
||||
devices: [],
|
||||
});
|
||||
|
||||
this.storageSettings.settings.register.onPut = async () => {
|
||||
await this.sendRegistrationId(await this.manager.registrationId);
|
||||
}
|
||||
@@ -259,9 +293,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
};
|
||||
|
||||
this.storageSettings.settings.securePort.onGet = async () => {
|
||||
const hide = this.portForwardingDisabled;
|
||||
return {
|
||||
group: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare' : undefined,
|
||||
title: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare Port' : 'Forward Port',
|
||||
hide,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -271,42 +305,28 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
};
|
||||
|
||||
// this.storageSettings.settings.duckDnsToken.onGet = async () => {
|
||||
// return {
|
||||
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
// || this.storageSettings.values.forwardingMode === 'Disabled',
|
||||
// }
|
||||
// };
|
||||
|
||||
// this.storageSettings.settings.duckDnsHostname.onGet = async () => {
|
||||
// return {
|
||||
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
|
||||
// || this.storageSettings.values.forwardingMode === 'Disabled',
|
||||
// }
|
||||
// };
|
||||
|
||||
this.storageSettings.settings.cloudflaredTunnelToken.onGet =
|
||||
this.storageSettings.settings.cloudflaredTunnelCustomDomain.onGet =
|
||||
this.storageSettings.settings.cloudflaredTunnelUrl.onGet = async () => {
|
||||
return {
|
||||
hide: !this.storageSettings.values.cloudflareEnabled,
|
||||
}
|
||||
};
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
this.storageSettings.settings.securePort.onPut = (ov, nv) => {
|
||||
if (ov && ov !== nv)
|
||||
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
|
||||
};
|
||||
|
||||
this.fromMimeType = ScryptedMimeTypes.LocalUrl;
|
||||
this.toMimeType = ScryptedMimeTypes.Url;
|
||||
|
||||
if (!this.storageSettings.values.certificate)
|
||||
this.storageSettings.values.certificate = createSelfSignedCertificate();
|
||||
|
||||
if (this.storageSettings.values.cloudflaredTunnelCustomDomain && !this.storageSettings.values.cloudflaredTunnelCredentials)
|
||||
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
const proxy = this.setupProxyServer();
|
||||
this.setupCloudPush();
|
||||
this.updateCors();
|
||||
|
||||
const observeRegistrations = () => {
|
||||
@@ -405,7 +425,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
if (this.storageSettings.values.forwardingMode === 'Custom Domain' || this.cloudflareTunnelHost)
|
||||
upnpPort = 443;
|
||||
|
||||
this.console.log(`Scrypted Cloud mapped https://${ip}:${upnpPort} to https://127.0.0.1:${this.securePort}`);
|
||||
this.console.log(`Scrypted Cloud routing to https://${ip}:${upnpPort}`);
|
||||
|
||||
// the ip is not sent, but should be checked to see if it changed.
|
||||
if (this.storageSettings.values.lastPersistedUpnpPort !== upnpPort || ip !== this.storageSettings.values.lastPersistedIp) {
|
||||
@@ -424,7 +444,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
async testPortForward() {
|
||||
try {
|
||||
if (this.storageSettings.values.forwardingMode === 'Disabled')
|
||||
if (this.portForwardingDisabled)
|
||||
throw new Error('Port forwarding is disabled.');
|
||||
|
||||
const pluginPath = await endpointManager.getPath(undefined, {
|
||||
@@ -459,7 +479,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
if (!upnpPort)
|
||||
upnpPort = Math.round(Math.random() * 20000 + 40000);
|
||||
|
||||
if (this.storageSettings.values.forwardingMode === 'Disabled') {
|
||||
if (this.portForwardingDisabled) {
|
||||
this.updatePortForward(upnpPort);
|
||||
return;
|
||||
}
|
||||
@@ -491,7 +511,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
},
|
||||
private: {
|
||||
host: localAddress,
|
||||
port: this.securePort,
|
||||
port: this.storageSettings.values.securePort,
|
||||
},
|
||||
ttl: 1800,
|
||||
}, async err => {
|
||||
@@ -556,7 +576,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
try {
|
||||
endpointManager.setAccessControlAllowOrigin({
|
||||
origins: [
|
||||
`http://${SCRYPTED_SERVER}`,
|
||||
'https://manage.scrypted.app',
|
||||
`https://${SCRYPTED_SERVER}`,
|
||||
...this.storageSettings.values.additionalCorsOrigins,
|
||||
],
|
||||
@@ -581,10 +601,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
|
||||
getAuthority() {
|
||||
const { forwardingMode } = this.storageSettings.values;
|
||||
if (forwardingMode === 'Disabled')
|
||||
if (this.portForwardingDisabled)
|
||||
return {};
|
||||
|
||||
const { forwardingMode } = this.storageSettings.values;
|
||||
|
||||
const upnp_port = forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
|
||||
const hostname = forwardingMode === 'Custom Domain'
|
||||
? this.storageSettings.values.hostname
|
||||
@@ -666,18 +687,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
}
|
||||
|
||||
async setupCloudPush() {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
name: 'Cloud Push Endpoint',
|
||||
type: ScryptedDeviceType.API,
|
||||
nativeId: 'push',
|
||||
interfaces: [ScryptedInterface.BufferConverter],
|
||||
},
|
||||
);
|
||||
this.push = new ScryptedPush(this);
|
||||
}
|
||||
|
||||
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
if (request.url.endsWith('/testPortForward')) {
|
||||
response.send(this.randomBytes);
|
||||
@@ -704,16 +713,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
return this.push;
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
getSSLHostname() {
|
||||
const validDomain = (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|
||||
|| (this.storageSettings.values.cloudflaredTunnelToken && this.cloudflareTunnelHost)
|
||||
|| (this.storageSettings.values.cloudflaredTunnelCredentials && this.cloudflareTunnelHost)
|
||||
|| (this.storageSettings.values.duckDnsCertValid && this.storageSettings.values.duckDnsHostname && this.storageSettings.values.upnpPort && `${this.storageSettings.values.duckDnsHostname}:${this.storageSettings.values.upnpPort}`);
|
||||
return validDomain;
|
||||
}
|
||||
@@ -722,19 +728,34 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
return this.getSSLHostname() || SCRYPTED_SERVER;
|
||||
}
|
||||
|
||||
async convert(data: Buffer, fromMimeType: string, toMimeType: string): Promise<Buffer> {
|
||||
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
|
||||
// short lived urls.
|
||||
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
|
||||
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
|
||||
if (params.get('short-lived') === 'true') {
|
||||
const u = new URL(data.toString(), this.cloudflareTunnel);
|
||||
u.host = this.cloudflareTunnelHost;
|
||||
u.port = '';
|
||||
return Buffer.from(u.toString());
|
||||
async convertMedia(data: string | Buffer | any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<MediaObject | Buffer | any> {
|
||||
if (!toMimeType.startsWith(ScryptedMimeTypes.Url))
|
||||
throw new Error('unsupported cloud url conversion');
|
||||
|
||||
if (fromMimeType.startsWith(ScryptedMimeTypes.LocalUrl)) {
|
||||
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
|
||||
// short lived urls.
|
||||
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
|
||||
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
|
||||
if (params.get('short-lived') === 'true') {
|
||||
const u = new URL(data.toString(), this.cloudflareTunnel);
|
||||
u.host = this.cloudflareTunnelHost;
|
||||
u.port = '';
|
||||
return Buffer.from(u.toString());
|
||||
}
|
||||
}
|
||||
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
|
||||
}
|
||||
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
|
||||
else if (fromMimeType.startsWith(ScryptedMimeTypes.PushEndpoint)) {
|
||||
const validDomain = this.getSSLHostname();
|
||||
if (validDomain)
|
||||
return Buffer.from(`https://${validDomain}${await this.getCloudMessagePath()}/${data}`);
|
||||
|
||||
const url = `http://127.0.0.1/push/${data}`;
|
||||
return this.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
|
||||
}
|
||||
|
||||
throw new Error('unsupported cloud url conversion');
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
@@ -867,16 +888,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.server.listen(0, '127.0.0.1');
|
||||
await once(this.server, 'listening');
|
||||
const port = (this.server.address() as any).port;
|
||||
|
||||
this.secureServer = https.createServer({
|
||||
key: this.storageSettings.values.certificate.serviceKey,
|
||||
cert: this.storageSettings.values.certificate.certificate,
|
||||
}, handler);
|
||||
this.secureServer.on('upgrade', wsHandler)
|
||||
// this is the direct connection port
|
||||
this.secureServer.listen(this.storageSettings.values.securePort, '0.0.0.0');
|
||||
await once(this.secureServer, 'listening');
|
||||
this.storageSettings.values.securePort = this.securePort = (this.secureServer.address() as any).port;
|
||||
this.console.log('scrypted cloud server listening on', port);
|
||||
|
||||
const agent = new http.Agent({ maxSockets: Number.MAX_VALUE, keepAlive: true });
|
||||
this.proxy = HttpProxy.createProxy({
|
||||
@@ -888,7 +900,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.proxy.on('proxyRes', (res, req) => {
|
||||
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
|
||||
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
|
||||
res.headers['X-Scrypted-Cloud-Address'] = this.cloudflareTunnel;
|
||||
let domain = this.cloudflareTunnel;
|
||||
if (!domain && this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|
||||
domain = `https://${this.storageSettings.values.hostname}`;
|
||||
res.headers['X-Scrypted-Cloud-Address'] = domain;
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address, X-Scrypted-Cloud-Address';
|
||||
});
|
||||
|
||||
@@ -945,13 +960,36 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
socket.pipe(local).pipe(socket);
|
||||
});
|
||||
mux.on('error', () => {
|
||||
client.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.startCloudflared();
|
||||
this.startCloudflared(port);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
this.secureServer = https.createServer({
|
||||
key: this.storageSettings.values.certificate.serviceKey,
|
||||
cert: this.storageSettings.values.certificate.certificate,
|
||||
}, handler);
|
||||
this.secureServer.on('upgrade', wsHandler)
|
||||
// this is the direct connection port
|
||||
this.secureServer.listen(this.storageSettings.values.securePort, '0.0.0.0');
|
||||
await once(this.secureServer, 'listening');
|
||||
this.storageSettings.values.securePort = (this.secureServer.address() as any).port;
|
||||
break;
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
this.console.log('error starting secure server. retrying.', e);
|
||||
await sleep(60000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async startCloudflared() {
|
||||
async startCloudflared(quickTunnelPort: number) {
|
||||
while (true) {
|
||||
try {
|
||||
if (!this.storageSettings.values.cloudflareEnabled) {
|
||||
@@ -961,61 +999,50 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|
||||
this.console.log('starting cloudflared');
|
||||
this.cloudflared = await backOff(async () => {
|
||||
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
|
||||
const version = 2;
|
||||
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`, `${process.platform}-${process.arch}`);
|
||||
const bin = path.join(cloudflareD, cloudflared.bin);
|
||||
const { cloudflareD, bin } = await installCloudflared();
|
||||
|
||||
if (!fs.existsSync(bin)) {
|
||||
for (let i = 0; i <= version; i++) {
|
||||
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`);
|
||||
rmSync(cloudflareD, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
||||
const bin = path.join(cloudflareD, cloudflared.bin);
|
||||
mkdirSync(path.dirname(bin), {
|
||||
recursive: true,
|
||||
});
|
||||
const tmp = `${bin}.tmp`;
|
||||
if (this.storageSettings.values.cloudflaredTunnelCredentials && this.storageSettings.values.cloudflaredTunnelCustomDomain) {
|
||||
const tunnelUrl = `http://127.0.0.1:${quickTunnelPort}`;
|
||||
const url = this.cloudflareTunnel = `https://${this.storageSettings.values.cloudflaredTunnelCustomDomain}`;
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
|
||||
|
||||
const stream = await httpFetch({
|
||||
url: 'https://github.com/scryptedapp/cloudflared/releases/download/2023.8.2/cloudflared-darwin-arm64',
|
||||
responseType: 'readable',
|
||||
});
|
||||
const write = stream.body.pipe(fs.createWriteStream(tmp));
|
||||
await once(write, 'close');
|
||||
renameSync(tmp, bin);
|
||||
fs.chmodSync(bin, 0o0755)
|
||||
}
|
||||
else {
|
||||
await cloudflared.install(bin);
|
||||
const ret = await runLocallyManagedTunnel(this.storageSettings.values.cloudflaredTunnelCredentials, tunnelUrl, cloudflareD, bin);
|
||||
return {
|
||||
child: ret,
|
||||
url: Promise.resolve(url),
|
||||
}
|
||||
}
|
||||
|
||||
// npm cloudflared package kinda sucks.
|
||||
process.chdir(cloudflareD);
|
||||
|
||||
const secureUrl = `https://127.0.0.1:${this.securePort}`;
|
||||
let tunnelUrl: string
|
||||
|
||||
const args: any = {};
|
||||
if (this.storageSettings.values.cloudflaredTunnelToken) {
|
||||
this.log.a('Cloudflare tunnel tokens are no longer supported. Please use the new Cloudflare Tunnel Custom Domain option.');
|
||||
tunnelUrl = `https://127.0.0.1:${this.storageSettings.values.securePort}`;
|
||||
args['run'] = null;
|
||||
args['--token'] = this.storageSettings.values.cloudflaredTunnelToken;
|
||||
}
|
||||
else {
|
||||
args['--no-tls-verify'] = null;
|
||||
args['--url'] = secureUrl;
|
||||
tunnelUrl = `http://127.0.0.1:${quickTunnelPort}`;
|
||||
args['--url'] = tunnelUrl;
|
||||
}
|
||||
|
||||
const deferred = new Deferred<string>();
|
||||
const cloudflareTunnel = cloudflared.tunnel(args);
|
||||
cloudflareTunnel.child.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cloudflareTunnel.child.stderr.on('data', data => {
|
||||
const string: string = data.toString();
|
||||
|
||||
const processData = (string: string) => {
|
||||
this.console.error(string);
|
||||
|
||||
const lines = string.split('\n');
|
||||
for (const line of lines) {
|
||||
if ((line.includes('Unregistered tunnel connection') || line.includes('Register tunnel error'))
|
||||
&& deferred.finished) {
|
||||
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');
|
||||
cloudflareTunnel.child.kill();
|
||||
}
|
||||
if (line.includes('hostname'))
|
||||
this.console.log(line);
|
||||
const match = /config=(".*?}")/gm.exec(line)
|
||||
@@ -1037,7 +1064,19 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cloudflareTunnel.child.stdout.on('data', data => {
|
||||
const d = data.toString();
|
||||
this.console.log(d);
|
||||
processData(d);
|
||||
});
|
||||
cloudflareTunnel.child.stderr.on('data', data => {
|
||||
const d = data.toString();
|
||||
this.console.error(d);
|
||||
processData(d);
|
||||
});
|
||||
|
||||
cloudflareTunnel.child.on('exit', () => deferred.resolve(undefined));
|
||||
try {
|
||||
this.cloudflareTunnel = await Promise.any([deferred.promise, cloudflareTunnel.url]);
|
||||
@@ -1049,7 +1088,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.console.error('cloudflared error', e);
|
||||
throw e;
|
||||
}
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${secureUrl}`);
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
|
||||
return cloudflareTunnel;
|
||||
}, {
|
||||
startingDelay: 60000,
|
||||
@@ -1173,6 +1212,35 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
}
|
||||
}
|
||||
|
||||
async doCloudflaredLogin(domain: string) {
|
||||
if (!domain) {
|
||||
this.cloudflared?.child.kill();
|
||||
return;
|
||||
}
|
||||
|
||||
// this.log.a('Visit the URL printed in the Scrypted Cloud plugin console to log into Cloudflare.');
|
||||
const customDomain = this.storageSettings.values.cloudflaredTunnelCustomDomain;
|
||||
try {
|
||||
this.cloudflaredLoginController?.abort();
|
||||
this.cloudflaredLoginController = new AbortController();
|
||||
const { bin } = await installCloudflared();
|
||||
const jsonContents = await createLocallyManagedTunnel(domain, bin, this.cloudflaredLoginController.signal, url => {
|
||||
this.console.warn('Cloudflare login URL:', url);
|
||||
this.storageSettings.values.cloudflaredTunnelLoginUrl = `<div style="padding-bottom: 16px"><a href="${url}" target="_blank" >Click here to log into Cloudflare</a></div>`;
|
||||
this.storageSettings.settings.cloudflaredTunnelLoginUrl.hide = false;
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
});
|
||||
this.storageSettings.values.cloudflaredTunnelCredentials = jsonContents;
|
||||
this.storageSettings.values.cloudflaredTunnelToken = undefined;
|
||||
this.cloudflared?.child.kill();
|
||||
}
|
||||
catch (e) {
|
||||
if (customDomain)
|
||||
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
|
||||
this.console.error('cloudflared login error', e);
|
||||
this.log.a('Cloudflare login error. See console logs.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ScryptedCloud;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import PushReceiver, { Types } from '@eneris/push-receiver';
|
||||
import { PushReceiver } from '@eneris/push-receiver';
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import type { Types } from '@eneris/push-receiver/dist/client';
|
||||
|
||||
export declare interface PushManager {
|
||||
on(event: 'message', listener: (data: any) => void): this;
|
||||
@@ -27,7 +28,15 @@ export class PushManager extends EventEmitter {
|
||||
|
||||
const instance = new PushReceiver({
|
||||
...savedConfig,
|
||||
senderId,
|
||||
firebase: {
|
||||
apiKey: "AIzaSyDI0bgFuVPIqKZoNpB-iTOU7ijIeepxOXE",
|
||||
authDomain: "scrypted-app.firebaseapp.com",
|
||||
databaseURL: "https://scrypted-app.firebaseio.com",
|
||||
projectId: "scrypted-app",
|
||||
storageBucket: "scrypted-app.appspot.com",
|
||||
messagingSenderId: "827888101440",
|
||||
appId: "1:827888101440:web:6ff9f8ada107e9cc0097a5"
|
||||
},
|
||||
heartbeatIntervalMs: 15 * 60 * 1000,
|
||||
});
|
||||
|
||||
@@ -51,7 +60,12 @@ export class PushManager extends EventEmitter {
|
||||
this.emit('message', message.data);
|
||||
});
|
||||
|
||||
await instance.connect();
|
||||
try {
|
||||
await instance.connect();
|
||||
}
|
||||
catch (e) {
|
||||
console.error('failed to connect to push server', e);
|
||||
}
|
||||
|
||||
return savedConfig.credentials?.fcm?.token || deferred.promise;
|
||||
})();
|
||||
|
||||
8
plugins/cloud/test/test-cloudflared.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createLocallyManagedTunnel, runLocallyManagedTunnel } from '../src/cloudflared-local-managed';
|
||||
|
||||
async function main() {
|
||||
const jsonContents = await createLocallyManagedTunnel('test.scrypted.io')
|
||||
await runLocallyManagedTunnel(jsonContents, 'http://127.0.0.1:49725', '/tmp/work');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||