mirror of
https://github.com/koush/scrypted.git
synced 2026-02-06 15:32:19 +00:00
Compare commits
248 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde3dfb9a8 | ||
|
|
d751ac8871 | ||
|
|
d6afbcef26 | ||
|
|
457fbc594e | ||
|
|
aadb190c13 | ||
|
|
f9a1668e5d | ||
|
|
70672e2a87 | ||
|
|
cab0afaa53 | ||
|
|
e0764a54cc | ||
|
|
1e825b84bc | ||
|
|
946e8d3414 | ||
|
|
3043b058d7 | ||
|
|
65fa8dd7f9 | ||
|
|
c6a93cf245 | ||
|
|
911b3f6014 | ||
|
|
8b5d3eaeae | ||
|
|
8099df4a2a | ||
|
|
e703efc1aa | ||
|
|
e9dc5a4254 | ||
|
|
5ae0bb10ff | ||
|
|
da417f3d5c | ||
|
|
b00fd7e684 | ||
|
|
d5ce4e24c4 | ||
|
|
1e09b62795 | ||
|
|
dd256e7a39 | ||
|
|
f6457bf475 | ||
|
|
5008220c26 | ||
|
|
8504319b27 | ||
|
|
61cc544313 | ||
|
|
4e6066a7c9 | ||
|
|
22b790c7f5 | ||
|
|
65ab977d4f | ||
|
|
6e1f5cbfa7 | ||
|
|
4d7be52b98 | ||
|
|
0b0a43fefc | ||
|
|
1449debbd3 | ||
|
|
4e24e44246 | ||
|
|
e4d62668b7 | ||
|
|
a4a3731b94 | ||
|
|
f4a55ee76b | ||
|
|
29714f82d5 | ||
|
|
c2054fc7e0 | ||
|
|
50b312b290 | ||
|
|
db8a3ec40b | ||
|
|
ef07691eef | ||
|
|
8f1c5fdf3c | ||
|
|
f7ac2883ec | ||
|
|
1a87e0daa1 | ||
|
|
2e17e58060 | ||
|
|
c9b88e6d8f | ||
|
|
eaa6da005b | ||
|
|
ca855bb9a6 | ||
|
|
774f987a66 | ||
|
|
0b6ef28ae8 | ||
|
|
677e78e328 | ||
|
|
0f1f1c56fb | ||
|
|
27e7e4c9e2 | ||
|
|
b6b193bf80 | ||
|
|
25f52eb528 | ||
|
|
69d110b234 | ||
|
|
7240f328b3 | ||
|
|
7bf133745b | ||
|
|
77ba56cf38 | ||
|
|
ea6d404f12 | ||
|
|
40a1221f11 | ||
|
|
22444eb63d | ||
|
|
2a6f542e06 | ||
|
|
ec49e4630f | ||
|
|
9de2b480ff | ||
|
|
442e8d53f7 | ||
|
|
f718d435bd | ||
|
|
8bbd112f60 | ||
|
|
6c98fa62be | ||
|
|
2e56a7f7a9 | ||
|
|
8304c1d065 | ||
|
|
21d0ca99e6 | ||
|
|
fa14f4ca83 | ||
|
|
8ae0a33cbe | ||
|
|
dea55e4fcd | ||
|
|
9eab88572e | ||
|
|
427c3e2f7b | ||
|
|
98f97a51e8 | ||
|
|
529b4d30fb | ||
|
|
eaabd02bfe | ||
|
|
7a67c70ef7 | ||
|
|
b784995ebb | ||
|
|
d4da11bb2c | ||
|
|
f556ae7ff3 | ||
|
|
8bb999aa64 | ||
|
|
de99d59162 | ||
|
|
438a6d7fe9 | ||
|
|
b9b3a48a08 | ||
|
|
5c42740ab1 | ||
|
|
e988e5fb96 | ||
|
|
9c8cbc750a | ||
|
|
01e15fb070 | ||
|
|
7aa02d6e4a | ||
|
|
9c9be9db22 | ||
|
|
cc78c072ce | ||
|
|
48c489b898 | ||
|
|
2cbcc05428 | ||
|
|
58a722cfa8 | ||
|
|
2c16c4625e | ||
|
|
60613ee947 | ||
|
|
f69dd06513 | ||
|
|
d011419208 | ||
|
|
789be6bd57 | ||
|
|
45e1b7091e | ||
|
|
f2ab923c79 | ||
|
|
4c3d5133f6 | ||
|
|
d2edfc5ecc | ||
|
|
4c5ae94c7c | ||
|
|
f30efbecec | ||
|
|
4ecb1f3c85 | ||
|
|
3ca0234530 | ||
|
|
b784399afa | ||
|
|
0f16568edb | ||
|
|
7ecee115a6 | ||
|
|
34eb2be551 | ||
|
|
27ff0c8c80 | ||
|
|
51c5df6802 | ||
|
|
328bd78771 | ||
|
|
3d2ae6384f | ||
|
|
e1ba16f708 | ||
|
|
6f47e39bf3 | ||
|
|
e38c3c975f | ||
|
|
9c75b074b5 | ||
|
|
299d926eae | ||
|
|
22d0ce4f82 | ||
|
|
53c2b7cb58 | ||
|
|
86548f6fa4 | ||
|
|
0e1e641f8f | ||
|
|
58e0a748c4 | ||
|
|
b4a58df53a | ||
|
|
b83b7ff559 | ||
|
|
de2173567e | ||
|
|
9c931b21dc | ||
|
|
5291afad6a | ||
|
|
e1ac1ace87 | ||
|
|
1f6f1a82aa | ||
|
|
70af66a875 | ||
|
|
b7bab5b2e2 | ||
|
|
5d5686a9e7 | ||
|
|
1eb5012e9b | ||
|
|
3574e72e4f | ||
|
|
b7ff4dfd5e | ||
|
|
e0ed953963 | ||
|
|
930690a4ba | ||
|
|
1aa4d45caa | ||
|
|
28fb2b0853 | ||
|
|
4fae4fba3b | ||
|
|
b72c8f59eb | ||
|
|
369ad59324 | ||
|
|
51ac5a1042 | ||
|
|
200c107e97 | ||
|
|
35139abe30 | ||
|
|
dc7f305687 | ||
|
|
2a479dd38a | ||
|
|
d32f9bb07a | ||
|
|
a33bed0b44 | ||
|
|
f9847f6f72 | ||
|
|
add53d07f3 | ||
|
|
db21159299 | ||
|
|
6fa7f06852 | ||
|
|
58387e5046 | ||
|
|
1589908698 | ||
|
|
d0183c29a8 | ||
|
|
99dcdd12cf | ||
|
|
b1861e4630 | ||
|
|
193bfce979 | ||
|
|
5b7cc826a6 | ||
|
|
8484d75e82 | ||
|
|
e8fef925bb | ||
|
|
fa200e1bbf | ||
|
|
df0991b882 | ||
|
|
93ff686000 | ||
|
|
6ae9a5618d | ||
|
|
c882b9a04e | ||
|
|
af4269be49 | ||
|
|
61ad99a3f6 | ||
|
|
d71bbf1824 | ||
|
|
74674dab00 | ||
|
|
247f860a23 | ||
|
|
a801fe1f4e | ||
|
|
6744851256 | ||
|
|
10569731aa | ||
|
|
4965b1f99a | ||
|
|
510250c60b | ||
|
|
8e33775b0e | ||
|
|
1077bd1f56 | ||
|
|
a485d8ae69 | ||
|
|
17f42762e7 | ||
|
|
49943a5408 | ||
|
|
585c638220 | ||
|
|
6767892c63 | ||
|
|
289555c03e | ||
|
|
a563e17c56 | ||
|
|
54c317b217 | ||
|
|
0df9c31480 | ||
|
|
19c8436256 | ||
|
|
b73526674a | ||
|
|
fd863f4ba3 | ||
|
|
634b65c216 | ||
|
|
548086403b | ||
|
|
867432cd82 | ||
|
|
b3cc914772 | ||
|
|
b297a4d3d6 | ||
|
|
8144588bcf | ||
|
|
f3265f5fb6 | ||
|
|
f686812f01 | ||
|
|
552787e06b | ||
|
|
3c4de5af39 | ||
|
|
e08df29373 | ||
|
|
1efb624681 | ||
|
|
09afc6c96c | ||
|
|
666d2903e4 | ||
|
|
24eb60bce1 | ||
|
|
d1951687be | ||
|
|
3c3c2c1610 | ||
|
|
0f9106c639 | ||
|
|
ab00ade016 | ||
|
|
6cfc3db05c | ||
|
|
95aa58ce38 | ||
|
|
0d88b4746b | ||
|
|
8c4beeb3a0 | ||
|
|
4846cfaddf | ||
|
|
4e14f7fd6f | ||
|
|
266be72606 | ||
|
|
6a1970c075 | ||
|
|
0575d98424 | ||
|
|
cdf42fc1a2 | ||
|
|
fc1fabc49e | ||
|
|
4e08daecb2 | ||
|
|
58b27805ba | ||
|
|
b37c6bbd06 | ||
|
|
8eca02d819 | ||
|
|
0efdb34114 | ||
|
|
1a25100de2 | ||
|
|
51e0a8836d | ||
|
|
562d0839b7 | ||
|
|
e3df6accea | ||
|
|
03d159a89c | ||
|
|
4ead4726a9 | ||
|
|
b06ef623b3 | ||
|
|
8edb157e2a | ||
|
|
155a1ceb38 | ||
|
|
1cb6212fc6 | ||
|
|
ea628a7130 |
11
.github/workflows/docker-common.yml
vendored
11
.github/workflows/docker-common.yml
vendored
@@ -77,13 +77,14 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-nvidia:
|
||||
name: Push NVIDIA Docker image to Docker Hub
|
||||
build-vendor:
|
||||
name: Push Vendor Docker image to Docker Hub
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["noble"]
|
||||
VENDOR: ["nvidia", "intel", "amd"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
@@ -138,11 +139,11 @@ jobs:
|
||||
build-args: |
|
||||
BASE=ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-full
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.nvidia
|
||||
file: install/docker/Dockerfile.${{ matrix.VENDOR }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-nvidia
|
||||
koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.VENDOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.VENDOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
34
.github/workflows/docker.yml
vendored
34
.github/workflows/docker.yml
vendored
@@ -20,10 +20,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: [
|
||||
["noble-nvidia", ".s6", "noble-nvidia"],
|
||||
["noble-full", ".s6", "noble-full"],
|
||||
["noble-lite", "", "noble-lite"],
|
||||
# ["noble-lite", ".router", "noble-router"],
|
||||
["noble-nvidia", ".s6", "noble-nvidia", "nvidia"],
|
||||
["noble-intel", ".s6", "noble-intel", "intel"],
|
||||
["noble-amd", ".s6", "noble-amd", "amd"],
|
||||
["noble-full", ".s6", "noble-full", "full"],
|
||||
["noble-lite", "", "noble-lite", "lite"],
|
||||
["noble-lite", ".router", "noble-router", "router"],
|
||||
]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -94,19 +96,25 @@ jobs:
|
||||
file: install/docker/Dockerfile${{ matrix.BASE[1] }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
# when publishing a tag (beta or latest), platform and version, create some tags as follows.
|
||||
# using beta 0.0.1 as an example
|
||||
# koush/scrypted:v0.0.1-noble-full
|
||||
# koush/scrypted:beta
|
||||
# koush/scrypted:beta-nvidia|intel|full|router|lite
|
||||
|
||||
# using latest 0.0.2 as an example:
|
||||
# koush/scrypted:v0.0.2-noble-full
|
||||
# koush/scrypted:latest
|
||||
# koush/scrypted:nvidia|intel|full|router|lite
|
||||
tags: |
|
||||
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[2], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ format('koush/scrypted:v{0}-{1}', github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION, matrix.BASE[2]) }}
|
||||
${{ matrix.BASE[2] == 'noble-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-lite' && matrix.BASE[1] == '' && 'koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-router' && 'koush/scrypted:router' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && format('koush/scrypted:{0}', matrix.BASE[3]) || '' }}
|
||||
${{ github.event.inputs.tag != 'latest' && format('koush/scrypted:{0}-{1}', github.event.inputs.tag, matrix.BASE[3]) || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[2] == 'noble-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-lite' && matrix.BASE[1] == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[2] == 'noble-lite' && 'ghcr.io/koush/scrypted:router' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && format('ghcr.io/koush/scrypted:{0}', matrix.BASE[3]) || ''}}
|
||||
${{ github.event.inputs.tag != 'latest' && format('ghcr.io/koush/scrypted:{0}-{1}', github.event.inputs.tag, matrix.BASE[3]) || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
70
common/package-lock.json
generated
70
common/package-lock.json
generated
@@ -10,39 +10,41 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/node": "^20.19.11",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.5.3",
|
||||
"version": "0.5.39",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -55,9 +57,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"../sdk/node_modules/@ampproject/remapping": {
|
||||
@@ -3308,6 +3310,15 @@
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.5.27",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.27.tgz",
|
||||
"integrity": "sha512-1SAEa6Js1VeAzGtaCQXXpNc2Ty1ZB6aqqNLtsoPeeuNw+JlSdK42sX4wVnzKxkAOcS1WZiC1fj6DV9B/CNyGtA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openai": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"dev": true,
|
||||
@@ -3329,11 +3340,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.0",
|
||||
"version": "20.19.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz",
|
||||
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
@@ -3393,6 +3406,27 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-5.8.2.tgz",
|
||||
"integrity": "sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"dev": true,
|
||||
@@ -3447,7 +3481,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/node": "^20.19.11",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,16 @@ export function createAsyncQueue<T>() {
|
||||
const waiting: Deferred<T>[] = [];
|
||||
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
|
||||
|
||||
const wait = async (index: number) => {
|
||||
const q = queued[index];
|
||||
if (!q)
|
||||
return;
|
||||
if (!q.dequeued) {
|
||||
q.dequeued = new Deferred<void>();
|
||||
}
|
||||
return q.dequeued.promise;
|
||||
}
|
||||
|
||||
const dequeue = async () => {
|
||||
if (queued.length) {
|
||||
const { item, dequeued: enqueue } = queued.shift()!;
|
||||
@@ -66,7 +76,7 @@ export function createAsyncQueue<T>() {
|
||||
dequeued?.reject(new Error('abort'));
|
||||
};
|
||||
|
||||
dequeued?.promise.catch(() => {}).finally(() => signal.removeEventListener('abort', h));
|
||||
dequeued?.promise.catch(() => { }).finally(() => signal.removeEventListener('abort', h));
|
||||
signal.addEventListener('abort', h);
|
||||
|
||||
return true;
|
||||
@@ -154,13 +164,14 @@ export function createAsyncQueue<T>() {
|
||||
dequeue,
|
||||
get queue() {
|
||||
return queue();
|
||||
}
|
||||
},
|
||||
wait,
|
||||
}
|
||||
}
|
||||
|
||||
export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
const q = createAsyncQueue<T>();
|
||||
(async() => {
|
||||
(async () => {
|
||||
try {
|
||||
for await (const i of generator) {
|
||||
await q.enqueue(i);
|
||||
|
||||
5
common/src/devices.ts
Normal file
5
common/src/devices.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { SystemManager } from '@scrypted/types';
|
||||
|
||||
export function getAllDevices<T>(systemManager: SystemManager) {
|
||||
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById<T>(id));
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type * as monacoEditor from 'monaco-editor';
|
||||
|
||||
export interface StandardLibs {
|
||||
'@types/node/globals.d.ts': string,
|
||||
'@types/node/module.d.ts': string,
|
||||
'@types/node/buffer.d.ts': string,
|
||||
'@types/node/process.d.ts': string,
|
||||
'@types/node/events.d.ts': string,
|
||||
|
||||
@@ -116,6 +116,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
|
||||
const standardlibs: StandardLibs = {
|
||||
"@types/node/globals.d.ts": readFileAsString('@types/node/globals.d.ts'),
|
||||
"@types/node/module.d.ts": readFileAsString('@types/node/module.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'),
|
||||
|
||||
8
common/src/json.ts
Normal file
8
common/src/json.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export function safeParseJson(value: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
@@ -93,8 +93,12 @@ export const H265_NAL_TYPE_AGG = 48;
|
||||
export const H265_NAL_TYPE_VPS = 32;
|
||||
export const H265_NAL_TYPE_SPS = 33;
|
||||
export const H265_NAL_TYPE_PPS = 34;
|
||||
export const H265_NAL_TYPE_IDR_N = 19;
|
||||
export const H265_NAL_TYPE_IDR_W = 20;
|
||||
export const H265_NAL_TYPE_BLA_W_LP = 16;
|
||||
export const H265_NAL_TYPE_BLA_W_RADL = 17;
|
||||
export const H265_NAL_TYPE_BLA_N_LP = 18;
|
||||
export const H265_NAL_TYPE_IDR_W_RADL = 19;
|
||||
export const H265_NAL_TYPE_IDR_N_LP = 20;
|
||||
export const H265_NAL_TYPE_CRA_NUT = 21;
|
||||
export const H265_NAL_TYPE_FU = 49;
|
||||
export const H265_NAL_TYPE_SEI_PREFIX = 39;
|
||||
export const H265_NAL_TYPE_SEI_SUFFIX = 40;
|
||||
@@ -252,6 +256,26 @@ export function getNaluTypesInH265Nalu(nalu: Buffer, fuaRequireStart = false, fu
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function isH265KeyFrameRelatedInSet(naluTypes: Set<number>, allowCodecInfo = true) {
|
||||
if (naluTypes.has(H265_NAL_TYPE_IDR_N_LP)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_W_RADL)
|
||||
|| naluTypes.has(H265_NAL_TYPE_CRA_NUT)
|
||||
|| naluTypes.has(H265_NAL_TYPE_BLA_N_LP)
|
||||
|| naluTypes.has(H265_NAL_TYPE_BLA_W_LP)
|
||||
|| naluTypes.has(H265_NAL_TYPE_BLA_W_RADL)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowCodecInfo) {
|
||||
if (naluTypes.has(H265_NAL_TYPE_VPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_SPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_PPS))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
export function createRtspParser(options?: StreamParserOptions): RtspStreamParser {
|
||||
let resolve: any;
|
||||
|
||||
@@ -283,12 +307,7 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
else if (streamChunk.type === 'h265') {
|
||||
const naluTypes = getStartedH265NaluTypes(streamChunk);
|
||||
|
||||
if (naluTypes.has(H265_NAL_TYPE_VPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_SPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_PPS)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_N)
|
||||
|| naluTypes.has(H265_NAL_TYPE_IDR_W)
|
||||
) {
|
||||
if (isH265KeyFrameRelatedInSet(naluTypes)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
}
|
||||
@@ -670,9 +689,12 @@ export class RtspClient extends RtspBase {
|
||||
// @ts-ignore
|
||||
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
|
||||
|
||||
const authedUrl = new URL(this.url);
|
||||
const username = decodeURIComponent(authedUrl.username);
|
||||
const password = decodeURIComponent(authedUrl.password);
|
||||
|
||||
if (this.wwwAuthenticate.includes('Basic')) {
|
||||
const parsedUrl = new URL(this.url);
|
||||
const hash = BASIC.computeHash({ username: parsedUrl.username, password: parsedUrl.password });
|
||||
const hash = BASIC.computeHash({ username, password });
|
||||
return `Basic ${hash}`;
|
||||
}
|
||||
|
||||
@@ -692,10 +714,6 @@ export class RtspClient extends RtspBase {
|
||||
REQUIRED_WWW_AUTHENTICATE_KEYS,
|
||||
) as DigestWWWAuthenticateData;
|
||||
|
||||
const authedUrl = new URL(this.url);
|
||||
const username = decodeURIComponent(authedUrl.username);
|
||||
const password = decodeURIComponent(authedUrl.password);
|
||||
|
||||
const strippedUrl = new URL(url.toString());
|
||||
strippedUrl.username = '';
|
||||
strippedUrl.password = '';
|
||||
|
||||
@@ -175,6 +175,8 @@ export type RTPMap = ReturnType<typeof parseRtpMap>;
|
||||
export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string) {
|
||||
const mlineType = mline.type;
|
||||
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)(\/([\d]+))?/);
|
||||
let channels = parseInt(match?.[5]) || undefined;
|
||||
let payloadType = parseInt(match?.[1]);
|
||||
|
||||
rtpmap = rtpmap?.toLowerCase();
|
||||
|
||||
@@ -222,14 +224,20 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
|
||||
if (mline.payloadTypes?.includes(0)) {
|
||||
codec = 'pcm_mulaw';
|
||||
ffmpegEncoder = 'pcm_mulaw';
|
||||
payloadType = 0;
|
||||
channels = 1;
|
||||
}
|
||||
else if (mline.payloadTypes?.includes(8)) {
|
||||
codec = 'pcm_alaw';
|
||||
ffmpegEncoder = 'pcm_alaw';
|
||||
payloadType = 8;
|
||||
channels = 1;
|
||||
}
|
||||
else if (mline.payloadTypes?.includes(14)) {
|
||||
codec = 'mp3';
|
||||
ffmpegEncoder = 'mp3';
|
||||
payloadType = 14;
|
||||
channels = 2;
|
||||
}
|
||||
else {
|
||||
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
|
||||
@@ -239,17 +247,29 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
|
||||
// https://en.wikipedia.org/wiki/RTP_payload_formats
|
||||
codec = 'pcm_alaw';
|
||||
ffmpegEncoder = 'pcm_alaw';
|
||||
payloadType = 8;
|
||||
channels = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// assigned payload types do not need to provide a clock, there is a default.
|
||||
let clock = parseInt(match?.[3]);
|
||||
if (!clock) {
|
||||
clock = undefined;
|
||||
if (codec === 'pcm_mulaw' || codec === 'pcm_alaw')
|
||||
clock = 8000;
|
||||
else if (codec === 'pcm_s16be')
|
||||
clock = 16000;
|
||||
}
|
||||
|
||||
return {
|
||||
line: rtpmap,
|
||||
codec,
|
||||
ffmpegEncoder,
|
||||
rawCodec: match?.[2],
|
||||
clock: parseInt(match?.[3]),
|
||||
channels: parseInt(match?.[5]) || undefined,
|
||||
payloadType: parseInt(match?.[1]),
|
||||
clock,
|
||||
channels,
|
||||
payloadType,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Socket as DatagramSocket } from "dgram";
|
||||
import { once } from "events";
|
||||
import { Duplex } from "stream";
|
||||
import { FFMPEG_FRAGMENTED_MP4_OUTPUT_ARGS, MP4Atom, parseFragmentedMP4 } from "./ffmpeg-mp4-parser-session";
|
||||
import { readLength } from "./read-stream";
|
||||
|
||||
export interface StreamParser {
|
||||
container: string;
|
||||
@@ -25,59 +24,11 @@ export interface StreamParserOptions {
|
||||
export interface StreamChunk {
|
||||
startStream?: Buffer;
|
||||
chunks: Buffer[];
|
||||
type?: string;
|
||||
type: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// function checkTsPacket(pkt: Buffer) {
|
||||
// const pid = ((pkt[1] & 0x1F) << 8) | pkt[2];
|
||||
// if (pid == 256) {
|
||||
// // found video stream
|
||||
// if ((pkt[3] & 0x20) && (pkt[4] > 0)) {
|
||||
// // have AF
|
||||
// if (pkt[5] & 0x40) {
|
||||
// // found keyframe
|
||||
// console.log('keyframe');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
function createLengthParser(length: number, verify?: (concat: Buffer) => void) {
|
||||
async function* parse(socket: Duplex): AsyncGenerator<StreamChunk> {
|
||||
let pending: Buffer[] = [];
|
||||
let pendingSize = 0;
|
||||
while (true) {
|
||||
const data: Buffer = socket.read();
|
||||
if (!data) {
|
||||
await once(socket, 'readable');
|
||||
continue;
|
||||
}
|
||||
pending.push(data);
|
||||
pendingSize += data.length;
|
||||
if (pendingSize < length)
|
||||
continue;
|
||||
|
||||
const concat = Buffer.concat(pending);
|
||||
|
||||
verify?.(concat);
|
||||
|
||||
const remaining = concat.length % length;
|
||||
const left = concat.slice(0, concat.length - remaining);
|
||||
const right = concat.slice(concat.length - remaining);
|
||||
pending = [right];
|
||||
pendingSize = right.length;
|
||||
|
||||
yield {
|
||||
chunks: [left],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return parse;
|
||||
}
|
||||
|
||||
export function createDgramParser() {
|
||||
async function* parse(socket: DatagramSocket, width: number, height: number, type: string) {
|
||||
while (true) {
|
||||
@@ -91,65 +42,6 @@ export function createDgramParser() {
|
||||
return parse;
|
||||
}
|
||||
|
||||
export function createMpegTsParser(options?: StreamParserOptions): StreamParser {
|
||||
return {
|
||||
container: 'mpegts',
|
||||
outputArguments: [
|
||||
...(options?.vcodec || []),
|
||||
...(options?.acodec || []),
|
||||
'-f', 'mpegts',
|
||||
],
|
||||
parse: createLengthParser(188, concat => {
|
||||
if (concat[0] != 0x47) {
|
||||
throw new Error('Invalid sync byte in mpeg-ts packet. Terminating stream.')
|
||||
}
|
||||
}),
|
||||
findSyncFrame(streamChunks): StreamChunk[] {
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < streamChunk.chunks.length; chunkIndex++) {
|
||||
const chunk = streamChunk.chunks[chunkIndex];
|
||||
|
||||
let offset = 0;
|
||||
while (offset + 188 < chunk.length) {
|
||||
const pkt = chunk.subarray(offset, offset + 188);
|
||||
const pid = ((pkt[1] & 0x1F) << 8) | pkt[2];
|
||||
if (pid == 256) {
|
||||
// found video stream
|
||||
if ((pkt[3] & 0x20) && (pkt[4] > 0)) {
|
||||
// have AF
|
||||
if (pkt[5] & 0x40) {
|
||||
// we found the sync frame, but also need to send the pat and pmt
|
||||
// which might be at the start of this chunk before the keyframe.
|
||||
// yolo!
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
// const chunks = streamChunk.chunks.slice(chunkIndex + 1);
|
||||
// const take = chunk.subarray(offset);
|
||||
// chunks.unshift(take);
|
||||
|
||||
// const remainingChunks = streamChunks.slice(prebufferIndex + 1);
|
||||
// const ret = Object.assign({}, streamChunk);
|
||||
// ret.chunks = chunks;
|
||||
// return [
|
||||
// ret,
|
||||
// ...remainingChunks
|
||||
// ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += 188;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return findSyncFrame(streamChunks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function* parseMp4StreamChunks(parser: AsyncGenerator<MP4Atom>) {
|
||||
let ftyp: MP4Atom;
|
||||
let moov: MP4Atom;
|
||||
@@ -213,54 +105,3 @@ export const PIXEL_FORMAT_RGB24: RawVideoPixelFormat = {
|
||||
name: 'rgb24',
|
||||
computeLength: (width, height) => width * height * 3,
|
||||
}
|
||||
|
||||
export function createRawVideoParser(options: RawVideoParserOptions): StreamParser {
|
||||
const pixelFormat = options?.pixelFormat || PIXEL_FORMAT_YUV420P;
|
||||
let filter: string;
|
||||
const { size, everyNFrames } = options;
|
||||
if (size) {
|
||||
filter = `scale=${size.width}:${size.height}`;
|
||||
}
|
||||
if (everyNFrames && everyNFrames > 1) {
|
||||
if (filter)
|
||||
filter += ',';
|
||||
else
|
||||
filter = '';
|
||||
filter = filter + `select=not(mod(n\\,${everyNFrames}))`
|
||||
}
|
||||
|
||||
const inputArguments: string[] = [];
|
||||
if (options.size)
|
||||
inputArguments.push('-s', `${options.size.width}x${options.size.height}`);
|
||||
|
||||
inputArguments.push('-pix_fmt', pixelFormat.name);
|
||||
return {
|
||||
inputArguments,
|
||||
container: 'rawvideo',
|
||||
outputArguments: [
|
||||
'-s', `${options.size.width}x${options.size.height}`,
|
||||
'-an',
|
||||
'-vcodec', 'rawvideo',
|
||||
'-pix_fmt', pixelFormat.name,
|
||||
'-f', 'rawvideo',
|
||||
],
|
||||
async *parse(socket: Duplex, width: number, height: number): AsyncGenerator<StreamChunk> {
|
||||
width = size?.width || width;
|
||||
height = size?.height || height
|
||||
|
||||
if (!width || !height)
|
||||
throw new Error("error parsing rawvideo, unknown width and height");
|
||||
|
||||
const toRead = pixelFormat.computeLength(width, height);
|
||||
while (true) {
|
||||
const buffer = await readLength(socket, toRead);
|
||||
yield {
|
||||
chunks: [buffer],
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
},
|
||||
findSyncFrame,
|
||||
}
|
||||
}
|
||||
98
common/src/using-holder.ts
Normal file
98
common/src/using-holder.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
export abstract class AsyncUsingHolderBase<T> {
|
||||
constructor(private _value: T) {
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.release();
|
||||
}
|
||||
|
||||
abstract asyncDispose(value: T): Promise<void>;
|
||||
|
||||
detach() {
|
||||
const value = this._value;
|
||||
this._value = undefined;
|
||||
return value;
|
||||
}
|
||||
|
||||
async replace(value: T) {
|
||||
this.release();
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
async release() {
|
||||
const released = this.detach();
|
||||
if (released)
|
||||
await this.asyncDispose(released);
|
||||
}
|
||||
}
|
||||
export abstract class UsingHolderBase<T> {
|
||||
constructor(private _value: T) {
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this.release();
|
||||
}
|
||||
|
||||
abstract dispose(value: T): void;
|
||||
|
||||
detach() {
|
||||
const value = this._value;
|
||||
this._value = undefined;
|
||||
return value;
|
||||
}
|
||||
|
||||
replace(value: T) {
|
||||
this.release();
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
release() {
|
||||
const released = this.detach();
|
||||
if (released)
|
||||
this.dispose(released);
|
||||
}
|
||||
}
|
||||
|
||||
export class UsingHolder<T extends Disposable> extends UsingHolderBase<T> {
|
||||
dispose(value: T) {
|
||||
value?.[Symbol.dispose]();
|
||||
}
|
||||
|
||||
transferClosure<V>(closure: (value: UsingHolder<T>) => Promise<V>) {
|
||||
return (async () => {
|
||||
using attached = new UsingHolder(this.detach());
|
||||
return await closure(attached);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
export class AsyncUsingHolder<T extends AsyncDisposable> extends AsyncUsingHolderBase<T> {
|
||||
async asyncDispose(value: T) {
|
||||
value?.[Symbol.asyncDispose]();
|
||||
}
|
||||
|
||||
transferClosure<V>(closure: (value: AsyncUsingHolder<T>) => Promise<V>) {
|
||||
return (async () => {
|
||||
await using attached = new AsyncUsingHolder(this.detach());
|
||||
return await closure(attached);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
export class DisposableHolder<T> extends UsingHolderBase<T> {
|
||||
constructor(value: T, private _dispose: (value: T) => void) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
dispose(value: T) {
|
||||
this._dispose(value);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,12 @@ export function createZygote<T>(options?: ForkOptions): Zygote<T> {
|
||||
}
|
||||
|
||||
const gen = next();
|
||||
return () => gen.next().value as PluginFork<T>;
|
||||
return () => {
|
||||
const ret = gen.next();
|
||||
if (ret.done || !ret.value)
|
||||
throw new Error('Zygote exhausted');
|
||||
return ret.value;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"target": "esnext",
|
||||
"noImplicitAny": true,
|
||||
|
||||
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: d9f51b8b6d...516e96a24e
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: c317c6eb30...4aa96f291a
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.130.1-noble-full"
|
||||
version: "v0.141.0-noble-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
ARG BASE="20-jammy-full"
|
||||
ARG BASE="noble-full"
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
WORKDIR /
|
||||
|
||||
7
install/docker/Dockerfile.amd
Normal file
7
install/docker/Dockerfile.amd
Normal file
@@ -0,0 +1,7 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="amd"
|
||||
|
||||
# amd opencl
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
|
||||
@@ -7,7 +7,7 @@
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="noble"
|
||||
FROM ubuntu:${BASE} as header
|
||||
FROM ubuntu:${BASE} AS header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -61,7 +61,7 @@ RUN python3 -m pip install debugpy
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
FROM header AS base
|
||||
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
@@ -92,7 +92,7 @@ RUN python3.9 -m pip install debugpy
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/coral-edgetpu.gpg
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# set default shell to bash
|
||||
|
||||
9
install/docker/Dockerfile.intel
Normal file
9
install/docker/Dockerfile.intel
Normal file
@@ -0,0 +1,9 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="intel"
|
||||
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-oneapi.sh | bash
|
||||
# these paths must be updated if oneapi is updated via the install-intel-oneapi.sh script
|
||||
# note that the 2022.2 seems to be a typo in the intel script...?
|
||||
ENV LD_LIBRARY_PATH=/opt/intel/oneapi/tcm/1.4/lib:/opt/intel/oneapi/umf/0.11/lib:/opt/intel/oneapi/tbb/2022.2/env/../lib/intel64/gcc4.8:/opt/intel/oneapi/mkl/2025.2/lib:/opt/intel/oneapi/compiler/2025.2/opt/compiler/lib:/opt/intel/oneapi/compiler/2025.2/lib
|
||||
@@ -1,5 +1,7 @@
|
||||
ARG BASE="jammy"
|
||||
FROM ubuntu:${BASE} as header
|
||||
FROM ubuntu:${BASE} AS header
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -26,5 +28,3 @@ ENV SHELL="/bin/bash"
|
||||
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.12"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
ARG BASE="ghcr.io/koush/scrypted-common:20-jammy-full"
|
||||
FROM $BASE
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="nvidia"
|
||||
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES=all
|
||||
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
ARG BASE="noble-lite"
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="router"
|
||||
|
||||
# tools
|
||||
RUN apt -y update && apt -y install nano net-tools dnsutils dnsmasq vlan bridge-utils netplan.io nftables isc-dhcp-client
|
||||
RUN apt -y update && apt -y install nano net-tools dnsutils dnsmasq vlan bridge-utils netplan.io nftables isc-dhcp-client cron
|
||||
RUN rm -f /etc/systemd/system/multi-user.target.wants/dnsmasq.service
|
||||
RUN rm -f /etc/systemd/system/sysinit.target.wants/systemd-resolved.service
|
||||
|
||||
# go + caddy
|
||||
RUN apt -y install golang-go
|
||||
RUN GO_VERSION=1.25.1 && ARCH=$(dpkg --print-architecture) && \
|
||||
if [ "$ARCH" = "amd64" ]; then GOARCH="amd64"; \
|
||||
elif [ "$ARCH" = "arm64" ]; then GOARCH="arm64"; \
|
||||
elif [ "$ARCH" = "armhf" ]; then GOARCH="armv6l"; \
|
||||
else echo "Unsupported architecture: $ARCH" && exit 1; fi && \
|
||||
curl -LO "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" && \
|
||||
tar -C /usr/local -xzf "go${GO_VERSION}.linux-${GOARCH}.tar.gz" && \
|
||||
rm "go${GO_VERSION}.linux-${GOARCH}.tar.gz"
|
||||
ENV PATH=$PATH:/usr/local/go/bin
|
||||
RUN apt install -y debian-keyring debian-archive-keyring apt-transport-https
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
|
||||
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-xcaddy.list
|
||||
|
||||
@@ -8,6 +8,9 @@ RUN apt-get update && apt-get -y install \
|
||||
libavahi-compat-libdnssd-dev \
|
||||
xz-utils
|
||||
|
||||
# killall
|
||||
RUN apt -y install psmisc
|
||||
|
||||
# copy configurations and scripts
|
||||
COPY fs /
|
||||
|
||||
|
||||
45
install/docker/docker-compose-setup.py
Normal file
45
install/docker/docker-compose-setup.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
# Define the devices to check for
|
||||
devices_to_check = [
|
||||
"/dev/dri",
|
||||
"/dev/accel",
|
||||
"/dev/apex_0",
|
||||
"/dev/apex_1",
|
||||
"/dev/kfd",
|
||||
"/dev/bus/usb"
|
||||
]
|
||||
|
||||
# Use ruamel.yaml with better formatting preservation
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
# Explicitly set roundtrip mode for comment preservation
|
||||
yaml.typ = 'rt'
|
||||
# Match the original formatting - 4 space indentation
|
||||
yaml.indent = 4
|
||||
# No special block sequence indentation
|
||||
yaml.block_seq_indent = 0
|
||||
# Don't wrap lines
|
||||
yaml.width = None
|
||||
# Preserve unicode
|
||||
yaml.allow_unicode = True
|
||||
|
||||
# Read the docker-compose.yml file
|
||||
with open('docker-compose.yml', 'r') as file:
|
||||
compose_data = yaml.load(file)
|
||||
|
||||
# Get a direct reference to the devices key
|
||||
scrypted_service = compose_data['services']['scrypted']
|
||||
devices = scrypted_service.setdefault('devices', [])
|
||||
|
||||
# Check for devices and add them if they exist
|
||||
for device_path in devices_to_check:
|
||||
if os.path.exists(device_path):
|
||||
device_mapping = f"{device_path}:{device_path}"
|
||||
if device_mapping not in devices:
|
||||
devices.append(device_mapping)
|
||||
|
||||
# Write the modified docker-compose.yml file (preserving comments and formatting)
|
||||
with open('docker-compose.yml', 'w') as file:
|
||||
yaml.dump(compose_data, file)
|
||||
9
install/docker/docker-compose-setup.sh
Normal file
9
install/docker/docker-compose-setup.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# run as privileged so all the devices can be detected and only the necessary ones passed through.
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
-v "$(pwd):/app" \
|
||||
-w /app \
|
||||
python:3.12-slim \
|
||||
sh -c "pip install -q --root-user-action=ignore ruamel.yaml && python docker-compose-setup.py"
|
||||
@@ -45,10 +45,14 @@ services:
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# NVIDIA (Part 1 of 2)
|
||||
# runtime: nvidia
|
||||
# nvidia runtime: nvidia
|
||||
|
||||
# NVIDIA (Part 2 of 2) - Use NVIDIA image, and remove subsequent default image.
|
||||
# image: ghcr.io/koush/scrypted:nvidia
|
||||
# Valid images:
|
||||
# ghcr.io/koush/scrypted
|
||||
# ghcr.io/koush/scrypted:nvidia
|
||||
# ghcr.io/koush/scrypted:intel
|
||||
# ghcr.io/koush/scrypted:lite
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
volumes:
|
||||
@@ -128,6 +132,12 @@ services:
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=scrypted"
|
||||
|
||||
# Use global DNS servers to avoid issues with some local DNS servers.
|
||||
# particularly with npm registry, etc.
|
||||
dns:
|
||||
- ${SCRYPTED_DNS_SERVER_0:-1.1.1.1}
|
||||
- ${SCRYPTED_DNS_SERVER_1:-8.8.8.8}
|
||||
|
||||
# watchtower manages updates for Scrypted.
|
||||
watchtower:
|
||||
environment:
|
||||
@@ -148,3 +158,9 @@ services:
|
||||
- 10444:8080
|
||||
# check for updates once an hour (interval is in seconds)
|
||||
command: --interval 3600 --cleanup --scope scrypted
|
||||
|
||||
# Use global DNS servers to avoid issues with some local DNS servers.
|
||||
# particularly with npm registry, etc.
|
||||
dns:
|
||||
- ${SCRYPTED_DNS_SERVER_0:-1.1.1.1}
|
||||
- ${SCRYPTED_DNS_SERVER_1:-8.8.8.8}
|
||||
|
||||
@@ -21,23 +21,34 @@ else
|
||||
distro="noble"
|
||||
fi
|
||||
|
||||
apt -y update
|
||||
apt -y install rsync gpg
|
||||
# the deb no longer seems to install a key?
|
||||
gpg --keyserver keyserver.ubuntu.com --recv-keys 9386B48A1A693C5C
|
||||
gpg --export --armor 9386B48A1A693C5C | tee /etc/apt/trusted.gpg.d/amdgpu.asc
|
||||
|
||||
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
|
||||
|
||||
FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
|
||||
if [ -z "$FILENAME" ]
|
||||
then
|
||||
echo "AMD graphics package can not be installed. Could not find the package name."
|
||||
exit 1
|
||||
fi
|
||||
# AMD keeps breaking these links. Use hard links.
|
||||
|
||||
# FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
|
||||
# if [ -z "$FILENAME" ]
|
||||
# then
|
||||
# echo "AMD graphics package can not be installed. Could not find the package name."
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
set -e
|
||||
mkdir -p /tmp/amd
|
||||
cd /tmp/amd
|
||||
curl -O -L http://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
|
||||
apt -y update
|
||||
apt -y install rsync
|
||||
# curl -O -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
|
||||
|
||||
FILENAME=amdgpu-install_7.0.1.70001-1_all.deb
|
||||
curl -O -L https://repo.radeon.com/amdgpu-install/7.0.1/ubuntu/$distro/$FILENAME
|
||||
|
||||
dpkg -i $FILENAME
|
||||
apt -y update
|
||||
|
||||
amdgpu-install --usecase=opencl --no-dkms -y --accept-eula
|
||||
cd /tmp
|
||||
rm -rf /tmp/amd
|
||||
|
||||
@@ -72,12 +72,12 @@ apt-get install -y ocl-icd-libopencl1
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-core_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-opencl_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/libigdgmm12_22.5.0_amd64.deb
|
||||
@@ -85,20 +85,17 @@ curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.3087
|
||||
dpkg -i *.deb
|
||||
rm -f *.deb
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.45.31740.9
|
||||
# https://github.com/intel/compute-runtime/releases
|
||||
# note that at time of commit, IGC supports ubuntu 24.04 only possibly due to their builder being on 24.04.
|
||||
IGC_BASE_VERSION=2.5.6
|
||||
IGC_VERSION=2_$IGC_BASE_VERSION+18417_amd64
|
||||
COMPUTE_VERSION=24.52.32224.5
|
||||
ZERO_GPU_VERSION=1.6.32224.5_amd64
|
||||
LIBIGDGMM_VERSION=22.5.5_amd64
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v$IGC_BASE_VERSION/intel-igc-core-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v$IGC_BASE_VERSION/intel-igc-opencl-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu-dbgsym_$ZERO_GPU_VERSION.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu_$ZERO_GPU_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd-dbgsym_"$COMPUTE_VERSION"_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd_"$COMPUTE_VERSION"_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/libigdgmm12_$LIBIGDGMM_VERSION.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.18.5/intel-igc-core-2_2.18.5+19820_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.18.5/intel-igc-opencl-2_2.18.5+19820_amd64.deb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-ocloc-dbgsym_25.35.35096.9-0_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-ocloc_25.35.35096.9-0_amd64.deb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-opencl-icd-dbgsym_25.35.35096.9-0_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-opencl-icd_25.35.35096.9-0_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/libigdgmm12_22.8.1_amd64.deb
|
||||
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/libze-intel-gpu1-dbgsym_25.35.35096.9-0_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/libze-intel-gpu1_25.35.35096.9-0_amd64.deb
|
||||
|
||||
set +e
|
||||
dpkg -i *.deb
|
||||
|
||||
@@ -9,11 +9,17 @@ UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
then
|
||||
# proxmox is compatible with intel's ubuntu builds, check for /etc/pve directory
|
||||
# then determine debian version
|
||||
version=$(cat /etc/debian_version 2>/dev/null)
|
||||
|
||||
# Determine if it's Debian 12 or 13
|
||||
if [[ "$version" == 12* ]]; then
|
||||
UBUNTU_22_04=true
|
||||
elif [[ "$version" == 13* ]]; then
|
||||
UBUNTU_24_04=true
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
@@ -25,8 +31,10 @@ fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
ubuntu_distro=ubuntu2204
|
||||
distro="22.04_amd64"
|
||||
else
|
||||
ubuntu_distro=ubuntu2404
|
||||
distro="24.04_amd64"
|
||||
fi
|
||||
|
||||
@@ -38,22 +46,24 @@ set -e
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# level zero must also be installed
|
||||
LEVEL_ZERO_VERSION=1.19.2
|
||||
LEVEL_ZERO_VERSION=1.24.2
|
||||
# https://github.com/oneapi-src/level-zero
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
|
||||
# npu driver
|
||||
# https://github.com/intel/linux-npu-driver
|
||||
NPU_VERSION=1.13.0
|
||||
NPU_VERSION_DATE=20250131-13074932693
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
NPU_VERSION=1.23.0
|
||||
NPU_VERSION_DATE=20250827-17270089246
|
||||
NPU_TAR_FILENAME=linux-npu-driver-v"$NPU_VERSION"."$NPU_VERSION_DATE"-$ubuntu_distro.tar.gz
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/"$NPU_TAR_FILENAME"
|
||||
tar xzvf "$NPU_TAR_FILENAME"
|
||||
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
if [ ! -z "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-fw-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
rm *fw-npu*
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-level-zero-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
|
||||
18
install/docker/install-intel-oneapi.sh
Normal file
18
install/docker/install-intel-oneapi.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
apt -y update
|
||||
apt -y install gpg
|
||||
|
||||
# download the key to system keyring
|
||||
curl -1sLf https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor --yes --output /usr/share/keyrings/oneapi-archive-keyring.gpg
|
||||
|
||||
# add signed entry to apt sources and configure the APT client to use Intel repository:
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" | tee /etc/apt/sources.list.d/oneAPI.list
|
||||
|
||||
apt -y update
|
||||
apt -y install intel-oneapi-mkl-sycl-blas intel-oneapi-runtime-dnnl intel-oneapi-runtime-compilers
|
||||
else
|
||||
echo "NVIDIA graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,3 +1,5 @@
|
||||
apt -y install lsb-release
|
||||
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
@@ -9,6 +11,16 @@ set -e
|
||||
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
|
||||
# Do not apt install nvidia-open, must use cuda-drivers.
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
then
|
||||
apt -y install pve-headers-$(uname -r)
|
||||
UBUNTU_22_04=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "NVIDIA container toolkit can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
@@ -36,8 +48,16 @@ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dea
|
||||
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
apt -y update
|
||||
# is there a way to get a versioned package automatically?
|
||||
apt -y install cuda-drivers
|
||||
apt -y install nvidia-container-toolkit
|
||||
# cuda-drivers does not work with blackwell for some reason, container toolkit it broken IIRC.
|
||||
apt -y install nvidia-open
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
systemctl restart docker
|
||||
if [ ! -d "/etc/pve" ]
|
||||
then
|
||||
apt -y install nvidia-container-toolkit
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
systemctl restart docker
|
||||
fi
|
||||
|
||||
# need this if running inside lxc...
|
||||
# nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
|
||||
|
||||
@@ -23,7 +23,7 @@ then
|
||||
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/$distro/$(uname -m)/cuda-keyring_1.1-1_all.deb \
|
||||
&& dpkg -i /cuda-keyring.deb \
|
||||
&& apt update -q \
|
||||
&& apt install -y cuda-nvcc-12-6 libcublas-12-6 libcudnn9-cuda-12 cuda-libraries-12-6;
|
||||
&& apt install -y cuda-nvcc-12-9 libcublas-12-9 libcudnn9-cuda-12 cuda-libraries-12-9;
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
|
||||
@@ -13,6 +13,8 @@ then
|
||||
fi
|
||||
|
||||
function readyn() {
|
||||
echo
|
||||
echo
|
||||
if [ ! -z "$SCRYPTED_NONINTERACTIVE" ]
|
||||
then
|
||||
yn="y"
|
||||
@@ -51,6 +53,9 @@ rm -rf $SCRYPTED_HOME/install.json
|
||||
rm -rf $SCRYPTED_HOME/package.json
|
||||
rm -rf $SCRYPTED_HOME/package-lock.json
|
||||
|
||||
# must get this value as grep returns non zero if empty
|
||||
HAS_NVIDIA=$(lspci | grep -i nvidia)
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
|
||||
@@ -93,6 +98,24 @@ else
|
||||
sudo apt -y purge apparmor || true
|
||||
fi
|
||||
|
||||
if [ ! -z "$HAS_NVIDIA" ]
|
||||
then
|
||||
readyn "NVIDIA GPU detected. Use NVIDIA image for GPU acceleration?"
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
readyn "NVIDIA image requires the NVIDIA Drivers and Container Toolkit to be installed. This script can install them for you. Install NVIDIA Drivers and Container Toolkit for GPU acceleration?"
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
curl -fsSL https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-container-toolkit.sh -o install-nvidia-container-toolkit.sh
|
||||
chmod +x install-nvidia-container-toolkit.sh
|
||||
./install-nvidia-container-toolkit.sh
|
||||
rm install-nvidia-container-toolkit.sh
|
||||
fi
|
||||
sed -i 's/'#' nvidia //g' $DOCKER_COMPOSE_YML
|
||||
sed -i 's/ghcr.io\/koush\/scrypted/ghcr.io\/koush\/scrypted:nvidia/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
fi
|
||||
|
||||
readyn "Install avahi-daemon? This is the recommended for reliable HomeKit discovery and pairing."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
@@ -163,4 +186,4 @@ echo
|
||||
echo
|
||||
echo "Optional:"
|
||||
echo "Scrypted NVR Recording storage directory can be configured with an additional script located at:"
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#docker-volume"
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/storage/docker.html"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
################################################################
|
||||
# Begin section generated from template/Dockerfile.full.footer
|
||||
################################################################
|
||||
FROM header as base
|
||||
FROM header AS base
|
||||
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
@@ -32,7 +32,7 @@ RUN python3.9 -m pip install debugpy
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/coral-edgetpu.gpg
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# set default shell to bash
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# install script.
|
||||
################################################################
|
||||
ARG BASE="noble"
|
||||
FROM ubuntu:${BASE} as header
|
||||
FROM ubuntu:${BASE} AS header
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
@@ -42,9 +42,6 @@ RUN brew update
|
||||
|
||||
# in sequoia, brew node is unusable because it is not signed and can't access local network unless run as root.
|
||||
# https://developer.apple.com/forums/thread/766270
|
||||
# RUN_IGNORE brew install node@20
|
||||
# NODE_PATH=$(brew --prefix node@20)
|
||||
# NODE_BIN_PATH=$NODE_PATH/bin
|
||||
RUN_IGNORE curl -L https://nodejs.org/dist/v22.14.0/node-v22.14.0.pkg -o /tmp/node.pkg
|
||||
RUN_IGNORE sudo installer -pkg /tmp/node.pkg -target /
|
||||
NODE_PATH=/usr/local # used to pass var test
|
||||
@@ -88,13 +85,13 @@ RUN mkdir -p ~/Library/LaunchAgents
|
||||
|
||||
if [ ! -d "$NODE_PATH" ]
|
||||
then
|
||||
echo "Unable to determine node@20 path."
|
||||
echo "Unable to determine node path."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$NODE_BIN_PATH" ]
|
||||
then
|
||||
echo "Unable to determine node@20 bin path."
|
||||
echo "Unable to determine node bin path."
|
||||
echo "$NODE_BIN_PATH does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -19,7 +19,7 @@ sc.exe stop scrypted.exe
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
|
||||
# Install node.js
|
||||
choco upgrade -y nodejs-lts --version=20.18.0
|
||||
choco upgrade -y nodejs-lts --version=22.15.0
|
||||
|
||||
# Install VC Redist, which is necessary for portable python
|
||||
choco install -y vcredist140
|
||||
@@ -34,7 +34,7 @@ $SCRYPTED_WINDOWS_PYTHON_VERSION="-3.9"
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
# Workaround Windows Node no longer creating %APPDATA%\npm which causes npx to fail
|
||||
# Fixed in newer versions of NPM but not the one bundled with Node 20
|
||||
# Fixed in newer versions of NPM but not the one bundled with Node 2x
|
||||
# https://github.com/nodejs/node/issues/53538
|
||||
npm i -g npm
|
||||
|
||||
|
||||
@@ -7,10 +7,25 @@ export DEBIAN_FRONTEND=noninteractive
|
||||
yes | dpkg --configure -a
|
||||
apt -y --fix-broken install && apt -y update && apt -y dist-upgrade
|
||||
|
||||
function cleanup() {
|
||||
IS_UP=$(docker compose ps scrypted -a | grep Up)
|
||||
# Only clean up when scrypted is running to safely free space without risking its image deletion
|
||||
if [ -z "$IS_UP" ]; then
|
||||
echo "scrypted is not running, skipping cleanup to preserve its image"
|
||||
return
|
||||
fi
|
||||
echo $(date) > .last_cleanup
|
||||
echo "scrypted is running, proceeding with cleanup to free space"
|
||||
docker container prune -f
|
||||
docker image prune -a -f
|
||||
}
|
||||
|
||||
# force a pull to ensure we have the latest images.
|
||||
# not using --pull always cause that fails everything on network down
|
||||
docker compose pull
|
||||
|
||||
(sleep 60 && cleanup) &
|
||||
|
||||
# do not daemonize, when it exits, systemd will restart it.
|
||||
# force a recreate as .env may have changed.
|
||||
# furthermore force recreate gets the container back into a known state
|
||||
|
||||
@@ -18,7 +18,7 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.137.0
|
||||
SCRYPTED_VERSION=v0.139.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
|
||||
@@ -27,7 +27,8 @@ async function getAuth(options: AuthFetchOptions, url: string | URL, method: str
|
||||
++credential.count;
|
||||
const nc = ('00000000' + credential.count).slice(-8);
|
||||
const cnonce = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
||||
const uri = new URL(url).pathname;
|
||||
const parsedURL = new URL(url);
|
||||
const uri = parsedURL.pathname + parsedURL.search;
|
||||
|
||||
const { DIGEST, buildAuthorizationHeader } = await import('http-auth-utils');
|
||||
|
||||
|
||||
217
packages/client/package-lock.json
generated
217
packages/client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.26",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"engine.io-client": "^6.6.3",
|
||||
@@ -15,13 +15,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@scrypted/types": "^0.5.12"
|
||||
"@scrypted/types": "^0.5.45"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -37,6 +37,27 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -65,9 +86,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -83,11 +104,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.5.12",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.12.tgz",
|
||||
"integrity": "sha512-nTwcMHZyH3nXThL22eNcVw7OjSyL5qoTgUay6K7y43HKz1mBnFEmIUkW8eLdyP4nbpwwA0b60MOPDKZVnssB0Q==",
|
||||
"version": "0.5.45",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.45.tgz",
|
||||
"integrity": "sha512-ysySpWkGUrUpNj0BoTZpyn2HeVCyN0kfsQ2qyUoegdj7O8Z4VWROQa1mSrrPAAftM8zhTHrgYw8RcvMsfh0BTQ==",
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"openai": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
@@ -134,19 +158,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"version": "24.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.1.tgz",
|
||||
"integrity": "sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -154,9 +178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -180,9 +204,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -192,9 +216,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -210,21 +234,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -316,6 +325,27 @@
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
@@ -326,9 +356,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -362,14 +392,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
|
||||
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^4.0.1",
|
||||
"minimatch": "^10.0.0",
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
@@ -400,9 +430,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
|
||||
"integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@@ -415,9 +445,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
|
||||
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
|
||||
"integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -431,12 +461,12 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -460,6 +490,28 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.1.0.tgz",
|
||||
"integrity": "sha512-5sqb1wK67HoVgGlsPwcH2bUbkg66nnoIYKoyV9zi5pZPqh7EWlmSrSDjAh4O5jaIg/0rIlcDKBtWvZBuacmGZg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -603,9 +655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@@ -684,9 +736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -698,9 +750,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -817,27 +869,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.26",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -13,13 +13,13 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/ws": "^8.18.0",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@scrypted/types": "^0.5.12"
|
||||
"@scrypted/types": "^0.5.45"
|
||||
},
|
||||
"dependencies": {
|
||||
"engine.io-client": "^6.6.3",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { MediaObjectCreateOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
|
||||
import { ConnectRPCObjectOptions, MediaObjectCreateOptions, ScryptedStatic } from "@scrypted/types";
|
||||
import * as eio from 'engine.io-client';
|
||||
import { SocketOptions } from 'engine.io-client';
|
||||
import { Deferred } from "../../../common/src/deferred";
|
||||
import { timeoutPromise } from "../../../common/src/promise-utils";
|
||||
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
|
||||
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
|
||||
import type { ClusterObject, ConnectRPCObject } from '../../../server/src/cluster/connect-rpc-object';
|
||||
import type { IOSocket } from '../../../server/src/io';
|
||||
import { MediaObject } from '../../../server/src/plugin/mediaobject';
|
||||
@@ -17,6 +14,9 @@ import { isIPAddress } from "./ip";
|
||||
import { domFetch } from "../../../server/src/fetch";
|
||||
import { httpFetch } from '../../../server/src/fetch/http-fetch';
|
||||
|
||||
export * as rpc from '../../../server/src/rpc';
|
||||
export * as rpc_serializer from '../../../server/src/rpc-serializer';
|
||||
|
||||
let fetcher: typeof httpFetch | typeof domFetch;
|
||||
try {
|
||||
if (process.arch === 'browser' as any)
|
||||
@@ -52,7 +52,13 @@ function once(socket: IOClientSocket, event: 'open' | 'message') {
|
||||
});
|
||||
}
|
||||
|
||||
export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
|
||||
/**
|
||||
* The type of connection used by the Scrypted client.
|
||||
* http-cloud is through Scrypted Cloud
|
||||
* http-direct is a direct connection to the Scrypted server via one of the local network interfaces or public IP addresses.
|
||||
* http is a direct connection with the base url or browser url.
|
||||
*/
|
||||
export type ScryptedClientConnectionType = 'http-cloud' | 'http-direct' | 'http';
|
||||
|
||||
export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
userId?: string;
|
||||
@@ -60,8 +66,6 @@ export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
admin: boolean;
|
||||
disconnect(): void;
|
||||
onClose?: Function;
|
||||
rtcConnectionManagement?: RTCConnectionManagement;
|
||||
browserSignalingSession?: BrowserSignalingSession;
|
||||
address?: string;
|
||||
connectionType: ScryptedClientConnectionType;
|
||||
rpcPeer: RpcPeer;
|
||||
@@ -71,7 +75,6 @@ export interface ScryptedClientStatic extends ScryptedStatic {
|
||||
export interface ScryptedConnectionOptions {
|
||||
direct?: boolean;
|
||||
local?: boolean;
|
||||
webrtc?: boolean;
|
||||
baseUrl?: string;
|
||||
previousLoginResult?: ScryptedClientLoginResult;
|
||||
}
|
||||
@@ -112,19 +115,51 @@ export async function logoutScryptedClient(baseUrl?: string) {
|
||||
return response.body;
|
||||
}
|
||||
|
||||
export function getCurrentBaseUrl() {
|
||||
// an endpoint within scrypted will be served at /endpoint/[org/][id]
|
||||
// find the endpoint prefix and anything prior to that will be the server base url.
|
||||
const url = new URL(window.location.href);
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
let endpointPath = window.location.pathname;
|
||||
const parts = endpointPath.split('/');
|
||||
const index = parts.findIndex(p => p === 'endpoint');
|
||||
if (index === -1) {
|
||||
// console.warn('path not recognized, does not contain the segment "endpoint".')
|
||||
return undefined;
|
||||
function getBaseUrl(href: string) {
|
||||
try {
|
||||
// an endpoint within scrypted will be served at /endpoint/[org/][id]
|
||||
// find the endpoint prefix and anything prior to that will be the server base url.
|
||||
const url = new URL(href);
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
let endpointPath = url.pathname;
|
||||
const parts = endpointPath.split('/');
|
||||
const index = parts.findIndex(p => p === 'endpoint');
|
||||
if (index === -1) {
|
||||
// console.warn('path not recognized, does not contain the segment "endpoint".')
|
||||
return;
|
||||
}
|
||||
return { url, parts, index };
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function importMetaUrlWithoutAssetsPath() {
|
||||
// @ts-ignore
|
||||
const url = new URL(import.meta.url);
|
||||
const parts = url.pathname.split('/');
|
||||
parts.pop();
|
||||
parts.pop();
|
||||
parts.push('public')
|
||||
parts.push('');
|
||||
url.pathname = parts.join('/');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function getCurrentBaseUrlRaw() {
|
||||
const url = getBaseUrl(window.location.href)
|
||||
|| getBaseUrl(document.baseURI)
|
||||
|| getBaseUrl(importMetaUrlWithoutAssetsPath());
|
||||
return url;
|
||||
}
|
||||
|
||||
export function getCurrentBaseUrl() {
|
||||
const s = getCurrentBaseUrlRaw();
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
const { url, parts, index } = s;
|
||||
const keep = parts.slice(0, index);
|
||||
keep.push('');
|
||||
url.pathname = keep.join('/');
|
||||
@@ -385,11 +420,11 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
const eioPath = `endpoint/${pluginId}/engine.io/api`;
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
// https://github.com/socketio/engine.io/issues/690
|
||||
const cacehBust = Math.random().toString(36).substring(3, 10);
|
||||
const cacheBust = Math.random().toString(36).substring(3, 10);
|
||||
const eioOptions: Partial<SocketOptions> = {
|
||||
path: eioEndpoint,
|
||||
query: {
|
||||
cacehBust,
|
||||
cacheBust,
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
@@ -399,10 +434,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
|
||||
const explicitBaseUrl = baseUrl || `${globalThis.location.protocol}//${globalThis.location.host}`;
|
||||
|
||||
// underlying webrtc rpc transport may queue up messages before its ready to be to be handled.
|
||||
// watch for this flush.
|
||||
const flush = new Deferred<void>();
|
||||
|
||||
const addresses: string[] = [];
|
||||
const localAddressDefault = isNotChromeOrIsInstalledApp;
|
||||
|
||||
@@ -426,25 +457,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
}
|
||||
|
||||
const tryAddresses = !!addresses.length;
|
||||
const webrtcLastFailedKey = 'webrtcLastFailed';
|
||||
const canUseWebrtc = !!globalThis.RTCPeerConnection;
|
||||
let tryWebrtc = canUseWebrtc && options.webrtc;
|
||||
// try webrtc by default on scrypted cloud.
|
||||
// but webrtc takes a while to fail, so backoff if it fails to prevent continual slow starts.
|
||||
if (scryptedCloud && canUseWebrtc && globalThis.localStorage && options.webrtc === undefined) {
|
||||
tryWebrtc = true;
|
||||
const webrtcLastFailed = parseFloat(localStorage.getItem(webrtcLastFailedKey));
|
||||
// if webrtc has failed in the past day, dont attempt to use it.
|
||||
const now = Date.now();
|
||||
if (webrtcLastFailed < now && webrtcLastFailed > now - 1 * 24 * 60 * 60 * 1000) {
|
||||
tryWebrtc = false;
|
||||
console.warn('WebRTC API connection recently failed. Skipping.')
|
||||
}
|
||||
}
|
||||
|
||||
console.log({
|
||||
tryLocalAddressess: tryAddresses,
|
||||
tryWebrtc,
|
||||
});
|
||||
|
||||
const localEioOptions: Partial<SocketOptions> = {
|
||||
@@ -484,145 +499,11 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
}
|
||||
}
|
||||
|
||||
if (tryWebrtc) {
|
||||
console.log('trying webrtc');
|
||||
const webrtcEioOptions: Partial<SocketOptions> = {
|
||||
path: '/endpoint/@scrypted/webrtc/engine.io/',
|
||||
query: {
|
||||
cacehBust,
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
transports: options?.transports,
|
||||
};
|
||||
const check = new eio.Socket(explicitBaseUrl, webrtcEioOptions) as IOClientSocket;
|
||||
sockets.push(check);
|
||||
promises.push((async () => {
|
||||
await once(check, 'open');
|
||||
|
||||
const connectionManagementId = `connectionManagement-${Math.random()}`;
|
||||
const updateSessionId = `updateSessionId-${Math.random()}`;
|
||||
check.send(JSON.stringify({
|
||||
pluginId,
|
||||
updateSessionId,
|
||||
connectionManagementId,
|
||||
}));
|
||||
const dcDeferred = new Deferred<RTCDataChannel>();
|
||||
const session = new BrowserSignalingSession();
|
||||
const droppedMessages: any[] = [];
|
||||
session.onPeerConnection = async pc => {
|
||||
pc.ondatachannel = e => {
|
||||
e.channel.onmessage = message => droppedMessages.push(message);
|
||||
e.channel.binaryType = 'arraybuffer';
|
||||
dcDeferred.resolve(e.channel)
|
||||
};
|
||||
}
|
||||
const pcPromise = session.pcDeferred.promise;
|
||||
|
||||
const serializer = createRpcSerializer({
|
||||
sendMessageBuffer: buffer => check.send(buffer),
|
||||
sendMessageFinish: message => check.send(JSON.stringify(message)),
|
||||
});
|
||||
const upgradingPeer = new RpcPeer(clientName || 'webrtc-upgrade', "api", (message, reject, serializationContext) => {
|
||||
try {
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
|
||||
check.on('message', data => {
|
||||
if (data.constructor === Buffer || data.constructor === ArrayBuffer) {
|
||||
serializer.onMessageBuffer(Buffer.from(data as string));
|
||||
}
|
||||
else {
|
||||
serializer.onMessageFinish(JSON.parse(data as string));
|
||||
}
|
||||
});
|
||||
serializer.setupRpcPeer(upgradingPeer);
|
||||
|
||||
// is this an issue?
|
||||
// const readyClose = new Promise<RpcPeer>((resolve, reject) => {
|
||||
// check.on('close', () => reject(new Error('closed')))
|
||||
// })
|
||||
|
||||
upgradingPeer.params['session'] = session;
|
||||
|
||||
const pc = await pcPromise;
|
||||
console.log('peer connection received');
|
||||
|
||||
await waitPeerConnectionIceConnected(pc);
|
||||
console.log('waiting for data channel');
|
||||
|
||||
const dc = await dcDeferred.promise;
|
||||
console.log('datachannel received', Date.now() - start);
|
||||
|
||||
const debouncer = new DataChannelDebouncer(dc, e => {
|
||||
console.error('datachannel send error', e);
|
||||
rpcPeer.kill('datachannel send error');
|
||||
});
|
||||
const dcSerializer = createRpcDuplexSerializer({
|
||||
write: (data) => debouncer.send(data),
|
||||
});
|
||||
|
||||
while (droppedMessages.length) {
|
||||
const message = droppedMessages.shift();
|
||||
dc.dispatchEvent(message);
|
||||
}
|
||||
|
||||
const rpcPeer = new RpcPeer('webrtc-client', "api", (message, reject, serializationContext) => {
|
||||
try {
|
||||
dcSerializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
pc.close();
|
||||
}
|
||||
});
|
||||
dcSerializer.setupRpcPeer(rpcPeer);
|
||||
|
||||
rpcPeer.params['connectionManagementId'] = connectionManagementId;
|
||||
rpcPeer.params['updateSessionId'] = updateSessionId;
|
||||
rpcPeer.params['browserSignalingSession'] = session;
|
||||
|
||||
waitPeerIceConnectionClosed(pc).then(() => check.close());
|
||||
check.on('close', () => {
|
||||
console.log('datachannel upgrade cancelled/closed');
|
||||
pc.close()
|
||||
});
|
||||
|
||||
await new Promise(resolve => {
|
||||
let buffers: Buffer[] = [];
|
||||
dc.onmessage = message => {
|
||||
buffers.push(Buffer.from(message.data));
|
||||
resolve(undefined);
|
||||
|
||||
flush.promise.finally(() => {
|
||||
if (buffers) {
|
||||
for (const buffer of buffers) {
|
||||
dcSerializer.onData(Buffer.from(buffer));
|
||||
}
|
||||
buffers = undefined;
|
||||
}
|
||||
dc.onmessage = message => dcSerializer.onData(Buffer.from(message.data));
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
ready: check,
|
||||
connectionType: 'webrtc',
|
||||
rpcPeer,
|
||||
};
|
||||
})());
|
||||
}
|
||||
|
||||
const p2pPromises = [...promises];
|
||||
|
||||
promises.push((async () => {
|
||||
const waitDuration = tryWebrtc ? 10000 : (tryAddresses ? 1000 : 0);
|
||||
const waitDuration = tryAddresses ? 1000 : 0;
|
||||
console.log('waiting', waitDuration);
|
||||
if (waitDuration) {
|
||||
// give the peer to peers a second, but then try connecting directly.
|
||||
@@ -643,16 +524,13 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
return {
|
||||
ready: check,
|
||||
address: explicitBaseUrl,
|
||||
connectionType: 'http',
|
||||
connectionType: scryptedCloud ? 'http-cloud' : 'http',
|
||||
};
|
||||
})());
|
||||
|
||||
const any = Promise.any(promises);
|
||||
let { ready, connectionType, address, rpcPeer } = await any;
|
||||
|
||||
if (tryWebrtc && connectionType !== 'webrtc')
|
||||
localStorage.setItem(webrtcLastFailedKey, Date.now().toString());
|
||||
|
||||
console.log('connected', connectionType, address)
|
||||
|
||||
socket = ready;
|
||||
@@ -692,7 +570,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
serializer.setupRpcPeer(rpcPeer);
|
||||
}
|
||||
|
||||
setTimeout(() => flush.resolve(undefined), 0);
|
||||
const scrypted = await attachPluginRemote(rpcPeer, undefined);
|
||||
const {
|
||||
serverVersion,
|
||||
@@ -708,20 +585,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
return new MediaObject(mimeType, data, options) as any;
|
||||
}
|
||||
|
||||
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
|
||||
if (updateSessionId && browserSignalingSession) {
|
||||
systemManager.getComponent('plugins').then(async plugins => {
|
||||
const updateSession: (session: RTCSignalingSession) => Promise<void> = await plugins.getHostParam('@scrypted/webrtc', updateSessionId);
|
||||
if (!updateSession)
|
||||
return;
|
||||
await updateSession(browserSignalingSession);
|
||||
console.log('signaling channel upgraded.');
|
||||
socket.removeAllListeners();
|
||||
socket.close();
|
||||
});
|
||||
}
|
||||
|
||||
const [admin, rtcConnectionManagement] = await Promise.all([
|
||||
const [admin] = await Promise.all([
|
||||
(async () => {
|
||||
try {
|
||||
// info is
|
||||
@@ -732,18 +596,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
}
|
||||
return false;
|
||||
})(),
|
||||
(async () => {
|
||||
let rtcConnectionManagement: RTCConnectionManagement;
|
||||
if (connectionManagementId) {
|
||||
try {
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
rtcConnectionManagement = await plugins.getHostParam('@scrypted/webrtc', connectionManagementId);
|
||||
return rtcConnectionManagement;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
|
||||
console.log('api initialized', Date.now() - start);
|
||||
@@ -753,62 +605,137 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
.find(device => device.pluginId === '@scrypted/core' && device.nativeId === `user:${username}`);
|
||||
|
||||
const clusterPeers = new Map<number, Promise<RpcPeer>>();
|
||||
const ensureClusterPeer = (clusterObject: ClusterObject) => {
|
||||
let clusterPeerPromise = clusterPeers.get(clusterObject.port);
|
||||
if (!clusterPeerPromise) {
|
||||
clusterPeerPromise = (async () => {
|
||||
const eioPath = 'engine.io/connectRPCObject';
|
||||
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
|
||||
const clusterPeerOptions = {
|
||||
path: eioEndpoint,
|
||||
query: {
|
||||
cacehBust,
|
||||
clusterObject: JSON.stringify(clusterObject),
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
transports: options?.transports,
|
||||
};
|
||||
const finalizationRegistry = new FinalizationRegistry((clusterPeer: RpcPeer) => {
|
||||
clusterPeer.kill('object finalized');
|
||||
});
|
||||
const ensureClusterPeer = (clusterObject: ClusterObject, connectRPCObjectOptions?: ConnectRPCObjectOptions) => {
|
||||
// If dedicatedTransport is true, don't reuse existing cluster peers
|
||||
if (!connectRPCObjectOptions?.dedicatedTransport) {
|
||||
let clusterPeerPromise = clusterPeers.get(clusterObject.port);
|
||||
if (clusterPeerPromise)
|
||||
return clusterPeerPromise;
|
||||
}
|
||||
|
||||
const clusterPeerSocket = new eio.Socket(explicitBaseUrl, clusterPeerOptions);
|
||||
let peerReady = false;
|
||||
clusterPeerSocket.on('close', () => {
|
||||
clusterPeers.delete(clusterObject.port);
|
||||
if (!peerReady) {
|
||||
throw new Error("peer disconnected before setup completed");
|
||||
const clusterPeerPromise = (async () => {
|
||||
const eioPath = 'engine.io/connectRPCObject';
|
||||
const eioEndpoint = new URL(eioPath, address).pathname;
|
||||
const eioQueryToken = connectionType === 'http' ? undefined : queryToken;
|
||||
const clusterPeerOptions = {
|
||||
path: eioEndpoint,
|
||||
query: {
|
||||
cacheBust,
|
||||
clusterObject: JSON.stringify(clusterObject),
|
||||
...eioQueryToken,
|
||||
},
|
||||
withCredentials: true,
|
||||
extraHeaders,
|
||||
rejectUnauthorized: false,
|
||||
transports: options?.transports,
|
||||
};
|
||||
|
||||
const clusterPeerSocket = new eio.Socket(address, clusterPeerOptions);
|
||||
let peerReady = false;
|
||||
|
||||
// Timeout handling for dedicated transports
|
||||
let receiveTimeout: NodeJS.Timeout | undefined;
|
||||
let sendTimeout: NodeJS.Timeout | undefined;
|
||||
let clusterPeer: RpcPeer | undefined;
|
||||
|
||||
const clearTimers = () => {
|
||||
if (receiveTimeout) {
|
||||
clearTimeout(receiveTimeout);
|
||||
receiveTimeout = undefined;
|
||||
}
|
||||
if (sendTimeout) {
|
||||
clearTimeout(sendTimeout);
|
||||
sendTimeout = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const resetReceiveTimeout = connectRPCObjectOptions?.dedicatedTransport?.receiveTimeout ? () => {
|
||||
if (receiveTimeout) {
|
||||
clearTimeout(receiveTimeout);
|
||||
}
|
||||
receiveTimeout = setTimeout(() => {
|
||||
if (clusterPeer) {
|
||||
clusterPeer.kill('receive timeout');
|
||||
}
|
||||
}, connectRPCObjectOptions.dedicatedTransport.receiveTimeout);
|
||||
} : undefined;
|
||||
|
||||
const resetSendTimeout = connectRPCObjectOptions?.dedicatedTransport?.sendTimeout ? () => {
|
||||
if (sendTimeout) {
|
||||
clearTimeout(sendTimeout);
|
||||
}
|
||||
sendTimeout = setTimeout(() => {
|
||||
if (clusterPeer) {
|
||||
clusterPeer.kill('send timeout');
|
||||
}
|
||||
}, connectRPCObjectOptions.dedicatedTransport.sendTimeout);
|
||||
} : undefined;
|
||||
|
||||
clusterPeerSocket.on('close', () => {
|
||||
clusterPeer?.kill('socket closed');
|
||||
// Only remove from clusterPeers if it's not a dedicated transport
|
||||
if (!connectRPCObjectOptions?.dedicatedTransport) {
|
||||
clusterPeers.delete(clusterObject.port);
|
||||
}
|
||||
if (!peerReady) {
|
||||
throw new Error("peer disconnected before setup completed");
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await once(clusterPeerSocket, 'open');
|
||||
|
||||
const serializer = createRpcDuplexSerializer({
|
||||
write: data => {
|
||||
resetSendTimeout?.();
|
||||
clusterPeerSocket.send(data);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await once(clusterPeerSocket, 'open');
|
||||
clusterPeerSocket.on('message', data => {
|
||||
resetReceiveTimeout?.();
|
||||
serializer.onData(Buffer.from(data));
|
||||
});
|
||||
|
||||
const serializer = createRpcDuplexSerializer({
|
||||
write: data => clusterPeerSocket.send(data),
|
||||
});
|
||||
clusterPeerSocket.on('message', data => serializer.onData(Buffer.from(data)));
|
||||
|
||||
const clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => {
|
||||
try {
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
serializer.setupRpcPeer(clusterPeer);
|
||||
clusterPeer.tags.localPort = sourcePeerId;
|
||||
peerReady = true;
|
||||
return clusterPeer;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('failure ipc connect', e);
|
||||
clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => {
|
||||
try {
|
||||
resetSendTimeout?.();
|
||||
serializer.sendMessage(message, reject, serializationContext);
|
||||
}
|
||||
catch (e) {
|
||||
reject?.(e as Error);
|
||||
}
|
||||
});
|
||||
clusterPeer.killedSafe.finally(() => {
|
||||
clearTimers();
|
||||
clusterPeerSocket.close();
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
});
|
||||
serializer.setupRpcPeer(clusterPeer);
|
||||
clusterPeer.tags.localPort = sourcePeerId;
|
||||
peerReady = true;
|
||||
|
||||
// Initialize timeouts if configured
|
||||
resetReceiveTimeout?.();
|
||||
resetSendTimeout?.();
|
||||
|
||||
return clusterPeer;
|
||||
}
|
||||
catch (e) {
|
||||
clearTimers();
|
||||
console.error('failure ipc connect', e);
|
||||
clusterPeerSocket.close();
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
|
||||
// Only store in clusterPeers if it's not a dedicated transport
|
||||
if (!connectRPCObjectOptions?.dedicatedTransport) {
|
||||
clusterPeers.set(clusterObject.port, clusterPeerPromise);
|
||||
}
|
||||
|
||||
return clusterPeerPromise;
|
||||
};
|
||||
|
||||
@@ -822,7 +749,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectRPCObject = async (value: any) => {
|
||||
const connectRPCObject = async (value: any, options?: ConnectRPCObjectOptions) => {
|
||||
const clusterObject: ClusterObject = value?.__cluster;
|
||||
if (!clusterObject) {
|
||||
return value;
|
||||
@@ -837,13 +764,29 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
}
|
||||
|
||||
try {
|
||||
const clusterPeerPromise = ensureClusterPeer(clusterObject);
|
||||
const clusterPeerPromise = ensureClusterPeer(clusterObject, options);
|
||||
const clusterPeer = await clusterPeerPromise;
|
||||
const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
|
||||
const newValue = await connectRPCObject(clusterObject);
|
||||
if (!newValue)
|
||||
throw new Error('ipc object not found?');
|
||||
return newValue;
|
||||
try {
|
||||
const newValue = await connectRPCObject(clusterObject);
|
||||
if (!newValue)
|
||||
throw new Error('ipc object not found?');
|
||||
|
||||
// If dedicatedTransport is true, register the object for cleanup
|
||||
if (options?.dedicatedTransport) {
|
||||
finalizationRegistry.register(newValue, clusterPeer);
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
catch (e) {
|
||||
// If we have a clusterPeer and this is a dedicated transport, kill the connection
|
||||
// to prevent resource leaks when connectRPCObject fails
|
||||
if (options?.dedicatedTransport) {
|
||||
clusterPeer.kill('connectRPCObject failed');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('failure ipc', e);
|
||||
@@ -868,8 +811,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
rpcPeer.kill('disconnect requested');
|
||||
},
|
||||
pluginHostAPI: undefined,
|
||||
rtcConnectionManagement,
|
||||
browserSignalingSession,
|
||||
rpcPeer,
|
||||
loginResult: {
|
||||
username,
|
||||
|
||||
947
packages/deferred/package-lock.json
generated
947
packages/deferred/package-lock.json
generated
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.5",
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.5",
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.8",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^24.0.10",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -43,19 +43,141 @@
|
||||
"../sdk/types": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rimraf": "dist/cjs/src/bin.js"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -64,38 +186,793 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^11.0.0",
|
||||
"package-json-from-dist": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@isaacs/brace-expansion": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
|
||||
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/balanced-match": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "24.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
|
||||
"integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
|
||||
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@isaacs/brace-expansion": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true
|
||||
},
|
||||
"package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
|
||||
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
|
||||
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^11.0.0",
|
||||
"package-json-from-dist": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"string-width-cjs": {
|
||||
"version": "npm:string-width@4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi-cjs": {
|
||||
"version": "npm:strip-ansi@6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"wrap-ansi-cjs": {
|
||||
"version": "npm:wrap-ansi@7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/deferred",
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
@@ -12,8 +12,8 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"rimraf": "^4.1.1",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^24.0.10",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/rpc/package-lock.json
generated
4
packages/rpc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { AuthFetchCredentialState, authHttpFetch, HttpFetchOptions } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { readLine } from '@scrypted/common/src/read-stream';
|
||||
import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
|
||||
import { MediaStreamConfiguration, Point } from '@scrypted/sdk';
|
||||
import contentType from 'content-type';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { EventEmitter, Readable } from 'stream';
|
||||
import { createRtspMediaStreamOptions, Destroyable, UrlMediaStreamOptions } from '../../rtsp/src/rtsp';
|
||||
import { getDeviceInfo } from './probe';
|
||||
import { MediaStreamConfiguration, MediaStreamOptions, Point } from '@scrypted/sdk';
|
||||
|
||||
export interface AmcrestObjectDetails {
|
||||
Action: string;
|
||||
@@ -81,8 +81,11 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
function findValue(blob: string, prefix: string, key: string) {
|
||||
const lines = blob.split('\n');
|
||||
function getLines(blob: string) {
|
||||
return blob.split(/\r?\n/).filter(line => line);
|
||||
}
|
||||
|
||||
function findValue(lines: string[], prefix: string, key: string) {
|
||||
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
|
||||
if (!value)
|
||||
return;
|
||||
@@ -124,7 +127,7 @@ const amcrestResolutions = {
|
||||
"720P": [1280, 720],
|
||||
"D1": [704, 480],
|
||||
"HD1": [352, 480],
|
||||
"BCIF": [704, 240],
|
||||
"BCIF": [528, 240],
|
||||
"2CIF": [704, 240],
|
||||
"CIF": [352, 240],
|
||||
"QCIF": [176, 120],
|
||||
@@ -133,7 +136,21 @@ const amcrestResolutions = {
|
||||
"QVGA": [320, 240]
|
||||
};
|
||||
|
||||
function fromAmcrestResolution(resolution: string) {
|
||||
const palAmcrestResolutions = {
|
||||
"D1": [704, 576],
|
||||
"HD1": [352, 576],
|
||||
"BCIF": [528, 288],
|
||||
"2CIF": [704, 288],
|
||||
"CIF": [352, 288],
|
||||
"QCIF": [176, 144],
|
||||
};
|
||||
|
||||
function fromAmcrestResolution(resolution: string, videoStandard: string) {
|
||||
if (videoStandard === 'PAL') {
|
||||
const named = palAmcrestResolutions[resolution];
|
||||
if (named)
|
||||
return named;
|
||||
}
|
||||
const named = amcrestResolutions[resolution];
|
||||
if (named)
|
||||
return named;
|
||||
@@ -438,6 +455,12 @@ export class AmcrestCameraClient {
|
||||
|
||||
this.console.log(capsResponse.body);
|
||||
|
||||
const videoStandardResponse = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=getConfig&name=VideoStandard`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(videoStandardResponse.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}]`;
|
||||
@@ -493,17 +516,19 @@ export class AmcrestCameraClient {
|
||||
|
||||
const caps = `caps[${cameraNumber - 1}].${format}[${formatNumber}]`;
|
||||
const singleCaps = `caps.${format}[${formatNumber}]`;
|
||||
const capsLines = getLines(capsResponse.body);
|
||||
const videoStandard = findValue(getLines(videoStandardResponse.body), 'table', 'VideoStandard');
|
||||
|
||||
const findCaps = (key: string) => {
|
||||
const found = findValue(capsResponse.body, caps, key);
|
||||
const found = findValue(capsLines, 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);
|
||||
return findValue(capsLines, singleCaps, key);
|
||||
}
|
||||
|
||||
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
|
||||
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(r => fromAmcrestResolution(r, videoStandard));
|
||||
const bitrates = findCaps('Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
|
||||
const fpsMax = parseInt(findCaps('Video.FPSMax'));
|
||||
const vso: MediaStreamConfiguration = {
|
||||
@@ -533,6 +558,7 @@ export class AmcrestCameraClient {
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log(encodeResponse.body);
|
||||
const encodeLines = getLines(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
@@ -544,27 +570,27 @@ export class AmcrestCameraClient {
|
||||
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'));
|
||||
const videoCodec = fromAmcrestVideoCodec(findValue(encodeLines, encName, 'Video.Compression'));
|
||||
const audioCodec = fromAmcrestAudioCodec(findValue(encodeLines, 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');
|
||||
const width = findValue(encodeLines, encName, 'Video.Width');
|
||||
const height = findValue(encodeLines, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
|
||||
const videoEnable = findValue(encodeLines, 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');
|
||||
const encodeOptions = findValue(encodeLines, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
|
||||
58
plugins/core/package-lock.json
generated
58
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.120",
|
||||
"version": "0.3.135",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.120",
|
||||
"version": "0.3.135",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -77,6 +77,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
@@ -88,28 +89,29 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.100",
|
||||
"version": "0.5.33",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -122,9 +124,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -276,6 +278,7 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
@@ -286,28 +289,29 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.120",
|
||||
"version": "0.3.135",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeviceState, MixinProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { typeToIcon } from "../../../../manage.scrypted.app/src/device-icons";
|
||||
import { typeToIcon } from "../../../../manage.scrypted.app/src/util/device-icons";
|
||||
|
||||
export class LauncherMixin extends ScryptedDeviceBase implements MixinProvider, Readme {
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readFileAsString, tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { writeFileSync } from 'fs';
|
||||
@@ -8,6 +9,7 @@ import yaml from 'yaml';
|
||||
import { getUsableNetworkAddresses } from '../../../server/src/ip';
|
||||
import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
|
||||
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
import { LauncherMixin } from './launcher-mixin';
|
||||
import { MediaCore } from './media-core';
|
||||
import { checkLegacyLxc, checkLxc } from './platform/lxc';
|
||||
@@ -15,7 +17,6 @@ import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from
|
||||
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId, newTerminalService } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
|
||||
const { deviceManager, endpointManager } = sdk;
|
||||
|
||||
@@ -210,6 +211,32 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
||||
// check on workers once an hour.
|
||||
this.updateWorkers();
|
||||
setInterval(() => this.updateWorkers(), 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
async updateWorkers() {
|
||||
const workers = await sdk.clusterManager?.getClusterWorkers();
|
||||
if (!workers)
|
||||
return;
|
||||
for (const [id, worker] of Object.entries(workers)) {
|
||||
const forked = sdk.fork<ReturnType<typeof fork>>({
|
||||
clusterWorkerId: id,
|
||||
runtime: 'node',
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await forked.result;
|
||||
result.checkLxc();
|
||||
}
|
||||
catch (e) {
|
||||
forked.worker.terminate();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
@@ -332,5 +359,15 @@ export async function fork() {
|
||||
tsCompile,
|
||||
newScript,
|
||||
newTerminalService,
|
||||
checkLxc: async () => {
|
||||
try {
|
||||
// console.warn('Checking for LXC installation...');
|
||||
await checkLxc();
|
||||
}
|
||||
finally {
|
||||
await sleep(1000);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ export async function checkLxc() {
|
||||
return;
|
||||
}
|
||||
|
||||
// console.warn('lxc needs updating', sdk.clusterManager.getClusterWorkerId());
|
||||
// console.warn(foundDockerComposeSh);
|
||||
await fs.promises.copyFile(LXC_DOCKER_COMPOSE_SH_PATH, DOCKER_COMPOSE_SH_PATH);
|
||||
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedUser, ScryptedUserAccessControl, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceManifest, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedUser, ScryptedUserAccessControl, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
export const UsersNativeId = 'users';
|
||||
@@ -111,7 +111,7 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
|
||||
const { username, admin } = user;
|
||||
const nativeId = `user:${username}`;
|
||||
const aclId = await sdk.deviceManager.onDeviceDiscovered({
|
||||
providerNativeId: this.nativeId,
|
||||
providerNativeId: UsersNativeId,
|
||||
name: username.toString(),
|
||||
nativeId,
|
||||
interfaces: [
|
||||
@@ -132,7 +132,13 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
|
||||
deviceCreator: 'Scrypted User',
|
||||
};
|
||||
|
||||
this.syncUsers();
|
||||
this.syncUsers()
|
||||
.then(length => {
|
||||
if (!length) {
|
||||
this.console.log('no users found, looping for first user');
|
||||
setInterval(() => this.syncUsers(), 60 * 1000);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
@@ -192,7 +198,7 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
|
||||
async syncUsers() {
|
||||
const usersService = await sdk.systemManager.getComponent('users');
|
||||
const users: DBUser[] = await usersService.getAllUsers();
|
||||
await sdk.deviceManager.onDevicesChanged({
|
||||
const manifest: DeviceManifest = {
|
||||
providerNativeId: this.nativeId,
|
||||
devices: users.map(user => ({
|
||||
name: user.username,
|
||||
@@ -203,6 +209,16 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
|
||||
],
|
||||
type: ScryptedDeviceType.Person,
|
||||
})),
|
||||
})
|
||||
};
|
||||
const nativeIds = new Set(manifest.devices.map(d => d.nativeId));
|
||||
for (const nativeId of sdk.deviceManager.getNativeIds()) {
|
||||
nativeIds.delete(nativeId);
|
||||
}
|
||||
if (nativeIds.size) {
|
||||
// add any missing users.
|
||||
await sdk.deviceManager.onDevicesChanged(manifest);
|
||||
}
|
||||
|
||||
return manifest.devices.length;
|
||||
}
|
||||
}
|
||||
|
||||
58
plugins/coreml/package-lock.json
generated
58
plugins/coreml/package-lock.json
generated
@@ -1,34 +1,42 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.77",
|
||||
"version": "0.1.83",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.77",
|
||||
"version": "0.1.83",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.77",
|
||||
"version": "0.5.22",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -41,11 +49,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -60,23 +66,29 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^24.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"DeviceCreator",
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"ClusterForkInterface",
|
||||
@@ -48,5 +50,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.77"
|
||||
"version": "0.1.83"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ from scrypted_sdk import Setting, SettingValue
|
||||
|
||||
from common import yolo
|
||||
from coreml.face_recognition import CoreMLFaceRecognition
|
||||
from coreml.custom_detection import CoreMLCustomDetection
|
||||
from coreml.clip_embedding import CoreMLClipEmbedding
|
||||
|
||||
try:
|
||||
from coreml.text_recognition import CoreMLTextRecognition
|
||||
@@ -77,6 +79,8 @@ class CoreMLPlugin(
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
self.custom_models = {}
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
if model != "Default":
|
||||
@@ -143,13 +147,14 @@ class CoreMLPlugin(
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
self.clipDevice = None
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
devices = [
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -159,10 +164,10 @@ class CoreMLPlugin(
|
||||
],
|
||||
"name": "CoreML Face Recognition",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if CoreMLTextRecognition:
|
||||
devices.append(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
@@ -174,9 +179,17 @@ class CoreMLPlugin(
|
||||
},
|
||||
)
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged(
|
||||
await scrypted_sdk.deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
"devices": devices,
|
||||
"nativeId": "clipembedding",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
scrypted_sdk.ScryptedInterface.TextEmbedding.value,
|
||||
scrypted_sdk.ScryptedInterface.ImageEmbedding.value,
|
||||
],
|
||||
"name": "CoreML CLIP Embedding",
|
||||
}
|
||||
)
|
||||
except:
|
||||
@@ -186,10 +199,19 @@ class CoreMLPlugin(
|
||||
if nativeId == "facerecognition":
|
||||
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(self, nativeId)
|
||||
return self.faceDevice
|
||||
if nativeId == "textrecognition":
|
||||
elif nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or CoreMLTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
elif nativeId == "clipembedding":
|
||||
self.clipDevice = self.clipDevice or CoreMLClipEmbedding(self, nativeId)
|
||||
return self.clipDevice
|
||||
custom_model = self.custom_models.get(nativeId, None)
|
||||
if custom_model:
|
||||
return custom_model
|
||||
custom_model = CoreMLCustomDetection(self, nativeId)
|
||||
self.custom_models[nativeId] = custom_model
|
||||
await custom_model.reportDevice(nativeId, custom_model.providedName)
|
||||
return custom_model
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
|
||||
85
plugins/coreml/src/coreml/clip_embedding.py
Normal file
85
plugins/coreml/src/coreml/clip_embedding.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import coremltools as ct
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from scrypted_sdk import ObjectsDetected
|
||||
|
||||
from predict.clip import ClipEmbedding
|
||||
|
||||
|
||||
class CoreMLClipEmbedding(ClipEmbedding):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "predict-clip")
|
||||
|
||||
def getFiles(self):
|
||||
return [
|
||||
"text.mlpackage/Manifest.json",
|
||||
"text.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
||||
"text.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
||||
|
||||
"vision.mlpackage/Manifest.json",
|
||||
"vision.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
||||
"vision.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
||||
]
|
||||
|
||||
def loadModel(self, files):
|
||||
# find the xml file in the files list
|
||||
text_manifest = [f for f in files if f.lower().endswith('text.mlpackage/manifest.json')]
|
||||
if not text_manifest:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
text_manifest = text_manifest[0]
|
||||
|
||||
vision_manifest = [f for f in files if f.lower().endswith('vision.mlpackage/manifest.json')]
|
||||
if not vision_manifest:
|
||||
raise ValueError("No XML model file found in the provided files list")
|
||||
vision_manifest = vision_manifest[0]
|
||||
|
||||
|
||||
textModel = ct.models.MLModel(os.path.dirname(text_manifest))
|
||||
visionModel = ct.models.MLModel(os.path.dirname(vision_manifest))
|
||||
|
||||
return textModel, visionModel
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def predict():
|
||||
inputs = self.processor(images=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
_, vision_model = self.model
|
||||
vision_predictions = vision_model.predict({'x': inputs['pixel_values']})
|
||||
image_embeds = vision_predictions['var_877']
|
||||
# this is a hack to utilize the existing image massaging infrastructure
|
||||
embedding = bytearray(image_embeds.astype(np.float32).tobytes())
|
||||
ret: ObjectsDetected = {
|
||||
"detections": [
|
||||
{
|
||||
"embedding": embedding,
|
||||
}
|
||||
],
|
||||
"inputDimensions": src_size
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
self.predictExecutor, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
|
||||
async def getTextEmbedding(self, input):
|
||||
def predict():
|
||||
inputs = self.processor(text=input, return_tensors="np", padding="max_length", truncation=True)
|
||||
text_model, _ = self.model
|
||||
text_predictions = text_model.predict({'input_ids_1': inputs['input_ids'].astype(np.float32), 'attention_mask_1': inputs['attention_mask'].astype(np.float32)})
|
||||
text_embeds = text_predictions['var_1050']
|
||||
return bytearray(text_embeds.astype(np.float32).tobytes())
|
||||
|
||||
ret = await asyncio.get_event_loop().run_in_executor(
|
||||
self.predictExecutor, lambda: predict()
|
||||
)
|
||||
return ret
|
||||
60
plugins/coreml/src/coreml/custom_detection.py
Normal file
60
plugins/coreml/src/coreml/custom_detection.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
|
||||
import coremltools as ct
|
||||
import numpy as np
|
||||
import scrypted_sdk
|
||||
from PIL import Image
|
||||
|
||||
from predict.custom_detect import CustomDetection
|
||||
|
||||
|
||||
class CoreMLCustomDetection(CustomDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.prefer_relu = True
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-custom")
|
||||
|
||||
def loadModel(self, files: list[str]):
|
||||
# find the xml file in the files list
|
||||
manifest_files = [f for f in files if f.lower().endswith('manifest.json')]
|
||||
if not manifest_files:
|
||||
raise ValueError("No Manifest.json file found in the provided files list")
|
||||
manifest_file = manifest_files[0]
|
||||
modelFile = os.path.dirname(manifest_file)
|
||||
|
||||
model = ct.models.MLModel(modelFile)
|
||||
inputName = model.get_spec().description.input[0].name
|
||||
return model, inputName
|
||||
|
||||
async def predictModel(self, input: Image.Image) -> scrypted_sdk.ObjectsDetected:
|
||||
model, inputName = self.model
|
||||
def predict():
|
||||
if self.model_config.get("mean", None) and self.model_config.get("std", None):
|
||||
im = np.array(input)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
|
||||
mean = np.array(self.model_config.get("mean", None), dtype=np.float32)
|
||||
std = np.array(self.model_config.get("std", None), dtype=np.float32)
|
||||
im = (im - mean) / std
|
||||
|
||||
# Convert HWC to CHW
|
||||
im = im.transpose(2, 0, 1) # Channels first
|
||||
im = im.astype(np.float32)
|
||||
im = np.ascontiguousarray(im)
|
||||
im = np.expand_dims(im, axis=0)
|
||||
|
||||
out_dict = model.predict({inputName: im})
|
||||
else:
|
||||
out_dict = model.predict({inputName: input})
|
||||
|
||||
results = list(out_dict.values())[0][0]
|
||||
return results
|
||||
|
||||
results = await asyncio.get_event_loop().run_in_executor(
|
||||
self.detectExecutor, lambda: predict()
|
||||
)
|
||||
return results
|
||||
@@ -25,9 +25,6 @@ def cosine_similarity(vector_a, vector_b):
|
||||
similarity = dot_product / (norm_a * norm_b)
|
||||
return similarity
|
||||
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
|
||||
|
||||
class CoreMLFaceRecognition(FaceRecognizeDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin, nativeId)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
coremltools==8.0
|
||||
Pillow==10.3.0
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
transformers==4.52.4
|
||||
|
||||
2
plugins/diagnostics/.vscode/settings.json
vendored
2
plugins/diagnostics/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "koushik-winvm",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
98
plugins/diagnostics/package-lock.json
generated
98
plugins/diagnostics/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.21",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
"@types/node": "^22.18.8"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -22,32 +22,41 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/node": "^20.19.11",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.69",
|
||||
"version": "0.5.48",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^6.1.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -60,11 +69,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
@@ -427,12 +434,13 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"version": "22.18.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
|
||||
"integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
@@ -549,10 +557,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -710,7 +719,8 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@types/node": "^20.11.0",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"@types/node": "^20.19.11",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
@@ -720,33 +730,39 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^24.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^6.1.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "22.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"version": "22.18.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
|
||||
"integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
@@ -839,9 +855,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.21",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -32,6 +32,6 @@
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
"@types/node": "^22.18.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +331,26 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
timeout: 5000,
|
||||
}).then(r => r.body.trim()));
|
||||
|
||||
await this.validate(this.console, 'System Time Accuracy', async () => {
|
||||
const response = await httpFetch({
|
||||
url: 'https://cloudflare.com',
|
||||
responseType: 'text',
|
||||
timeout: 10000,
|
||||
});
|
||||
const dateHeader = response.headers.get('date');
|
||||
if (!dateHeader) {
|
||||
throw new Error('No date header in response');
|
||||
}
|
||||
|
||||
const serverTime = new Date(dateHeader).getTime(); const localTime = Date.now();
|
||||
const difference = Math.abs(serverTime - localTime);
|
||||
const differenceSeconds = Math.floor(difference / 1000);
|
||||
|
||||
if (differenceSeconds > 5) {
|
||||
throw new Error(`Time drift detected: ${differenceSeconds} seconds difference from accurate time source.`);
|
||||
}
|
||||
});
|
||||
|
||||
await this.validate(this.console, 'IPv6 (wtfismyip.com)', httpFetch({
|
||||
url: 'https://wtfismyip.com/text',
|
||||
family: 6,
|
||||
@@ -338,6 +358,30 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
timeout: 5000,
|
||||
}).then(r => r.body.trim()));
|
||||
|
||||
await this.validate(this.console, 'Scrypted Cloud Services', async () => {
|
||||
const endpoints = [
|
||||
'https://home.scrypted.app',
|
||||
'https://billing.scrypted.app'
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const response = await httpFetch({
|
||||
url: endpoint,
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
throw new Error(`${endpoint} returned status ${response.statusCode}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`${endpoint} is not accessible: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Both endpoints accessible';
|
||||
});
|
||||
|
||||
await this.validate(this.console, 'Scrypted Server Address', async () => {
|
||||
const addresses = await sdk.endpointManager.getLocalAddresses();
|
||||
const hasIPv4 = addresses?.find(address => net.isIPv4(address));
|
||||
|
||||
32
plugins/doorbird/package-lock.json
generated
32
plugins/doorbird/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"dependencies": {
|
||||
"doorbird": "2.6.0"
|
||||
},
|
||||
@@ -24,6 +24,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
@@ -35,29 +36,30 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.108",
|
||||
"version": "0.5.33",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -70,9 +72,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
import { authHttpFetch } from "@scrypted/common/src/http-auth-fetch";
|
||||
import { listenZero } from '@scrypted/common/src/listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { BinarySensor, Camera, DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, FFmpegInput, Intercom, MediaObject, MotionSensor, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from '@scrypted/sdk';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { randomBytes } from 'crypto';
|
||||
import {httpFetch} from '../../../server/src/fetch/http-fetch';
|
||||
import {listenZero} from '@scrypted/common/src/listen-cluster';
|
||||
import {ffmpegLogInitialOutput, safePrintFFmpegArguments} from "@scrypted/common/src/media-helpers";
|
||||
import {readLength, StreamEndError} from "@scrypted/common/src/read-stream";
|
||||
import sdk, {
|
||||
BinarySensor,
|
||||
Camera,
|
||||
DeviceCreator,
|
||||
DeviceCreatorSettings,
|
||||
DeviceInformation,
|
||||
DeviceProvider,
|
||||
FFmpegInput,
|
||||
Intercom,
|
||||
MediaObject,
|
||||
MotionSensor,
|
||||
PictureOptions,
|
||||
ResponseMediaStreamOptions,
|
||||
ScryptedDeviceBase,
|
||||
ScryptedDeviceType,
|
||||
ScryptedInterface,
|
||||
ScryptedMimeTypes,
|
||||
Setting,
|
||||
Settings,
|
||||
VideoCamera
|
||||
} from '@scrypted/sdk';
|
||||
import child_process, {ChildProcess} from 'child_process';
|
||||
import {randomBytes} from 'crypto';
|
||||
import net from 'net';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { ApiMotionEvent, ApiRingEvent, DoorbirdAPI } from "./doorbird-api";
|
||||
import {PassThrough, Readable} from "stream";
|
||||
import {ApiMotionEvent, ApiRingEvent, DoorbirdAPI} from "./doorbird-api";
|
||||
|
||||
const { deviceManager, mediaManager } = sdk;
|
||||
const {deviceManager, mediaManager} = sdk;
|
||||
|
||||
class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCamera, Settings, BinarySensor, MotionSensor {
|
||||
doorbirdApi: DoorbirdAPI | undefined;
|
||||
@@ -22,6 +42,8 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
audioRXClientSocket: net.Socket;
|
||||
pendingPicture: Promise<MediaObject>;
|
||||
|
||||
private static readonly TRANSMIT_AUDIO_CHUNK_SIZE: number = 256;
|
||||
|
||||
constructor(nativeId: string, public provider: DoorbirdCamProvider) {
|
||||
super(nativeId);
|
||||
this.binaryState = false;
|
||||
@@ -89,7 +111,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
public async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
return [{
|
||||
id: 'VGA',
|
||||
picture: { width: 640, height: 480 }
|
||||
picture: {width: 640, height: 480}
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -141,6 +163,22 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
placeholder: 'rtsp://192.168.2.100/my_doorbird_video_stream',
|
||||
value: this.storage.getItem('rtspUrl'),
|
||||
description: 'Use this in case you are already using another RTSP server/proxy (e.g. mediamtx, go2rtc, etc.) to limit the number of streams from the camera.',
|
||||
},
|
||||
{
|
||||
key: 'audioDenoise',
|
||||
type: 'boolean',
|
||||
subgroup: 'Advanced',
|
||||
title: 'Denoise',
|
||||
value: this.storage.getItem('audioDenoise') === 'true',
|
||||
description: 'Denoise both input and output audio streams to reduce background noises.',
|
||||
},
|
||||
{
|
||||
key: 'audioSpeechEnhancement',
|
||||
type: 'boolean',
|
||||
subgroup: 'Advanced',
|
||||
title: 'Speech Enhancement',
|
||||
value: this.storage.getItem('audioSpeechEnhancement') === 'true',
|
||||
description: 'Apply band filtering and dynamic normalization to both audio streams.',
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -159,25 +197,47 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
}
|
||||
|
||||
async startAudioTransmitter(media: MediaObject): Promise<void> {
|
||||
this.console.log('Doorbird: Init audio transmitter...');
|
||||
const ffmpegInput: FFmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput)).toString());
|
||||
|
||||
const ffmpegArgs = ffmpegInput.inputArguments.slice();
|
||||
ffmpegArgs.push(
|
||||
'-vn', '-dn', '-sn',
|
||||
// Do not process video streams (disable video)
|
||||
'-vn',
|
||||
// Do not process data streams (e.g. timed metadata)
|
||||
'-dn',
|
||||
// Do not process subtitle streams
|
||||
'-sn',
|
||||
// Encode audio using PCM µ-law (G.711 codec, 8-bit logarithmic compression)
|
||||
'-acodec', 'pcm_mulaw',
|
||||
'-flags', '+global_header',
|
||||
// Bypass internal I/O buffering (write directly to output)
|
||||
"-avioflags", "direct",
|
||||
// Disable input buffering
|
||||
'-fflags', '+flush_packets+nobuffer',
|
||||
// Force flushing packets after every frame
|
||||
'-flush_packets', '1',
|
||||
// Use global headers (required by some muxers) and enable low-latency flags
|
||||
'-flags', '+global_header+low_delay',
|
||||
// Set number of audio channels to mono
|
||||
'-ac', '1',
|
||||
'-ar', '8k',
|
||||
// Set audio sample rate to 8000 Hz (expected by Doorbird)
|
||||
'-ar', '8000',
|
||||
// Force raw µ-law output format (no container)
|
||||
'-f', 'mulaw',
|
||||
// Do not buffer or delay packets in the muxer
|
||||
'-muxdelay', '0',
|
||||
// --- Audio Filtering ---
|
||||
...(this.getAudioFilter()),
|
||||
// Output to file descriptor 3 (e.g. pipe:3, for inter-process communication)
|
||||
'pipe:3'
|
||||
);
|
||||
|
||||
safePrintFFmpegArguments(console, ffmpegArgs);
|
||||
safePrintFFmpegArguments(this.console, ffmpegArgs);
|
||||
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), ffmpegArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
this.audioTXProcess = cp;
|
||||
ffmpegLogInitialOutput(console, cp);
|
||||
ffmpegLogInitialOutput(this.console, cp);
|
||||
cp.on('exit', () => this.console.log('Doorbird: Audio transmitter ended.'));
|
||||
cp.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cp.stderr.on('data', data => this.console.log(data.toString()));
|
||||
@@ -188,43 +248,60 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
const password: string = this.getPassword();
|
||||
const audioTxUrl: string = `${this.getHttpBaseAddress()}/bha-api/audio-transmit.cgi`;
|
||||
|
||||
this.console.log('Doorbird: Starting audio transmitter...');
|
||||
|
||||
(async () => {
|
||||
this.console.log('Doorbird: audio transmitter started.');
|
||||
|
||||
this.console.log('Doorbird: Audio transmitter started.');
|
||||
const passthrough = new PassThrough();
|
||||
authHttpFetch({
|
||||
method: 'POST',
|
||||
url: audioTxUrl,
|
||||
credential: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'audio/basic',
|
||||
'Content-Length': '9999999'
|
||||
},
|
||||
data: passthrough,
|
||||
});
|
||||
const abortController = new AbortController();
|
||||
let totalBytesWritten: number = 0;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const data = await readLength(socket, 1024);
|
||||
passthrough.push(data);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
finally {
|
||||
this.console.log('Doorbird: audio transmitter finished.');
|
||||
passthrough.end();
|
||||
}
|
||||
// Perform POST request instantly instead of unneeded handling with DIGEST authentication.
|
||||
// Credentials will be thrown into network by all other requests anyway.
|
||||
httpFetch({
|
||||
url: audioTxUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'audio/basic',
|
||||
'Content-Length': '9999999',
|
||||
'Authorization': this.getBasicAuthorization(username, password),
|
||||
},
|
||||
signal: abortController.signal,
|
||||
body: passthrough,
|
||||
responseType: 'readable',
|
||||
})
|
||||
|
||||
this.stopAudioTransmitter();
|
||||
while (true) { // Loop will be broken by StreamEndError.
|
||||
|
||||
// Read the next chunk of audio data from the Doorbird camera.
|
||||
const data = await readLength(socket, DoorbirdCamera.TRANSMIT_AUDIO_CHUNK_SIZE);
|
||||
if (data.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Actually write the data to the passthrough stream.
|
||||
passthrough.push(data);
|
||||
|
||||
// Add the length of the data to the total bytes written.
|
||||
totalBytesWritten += data.length;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof StreamEndError)) {
|
||||
this.console.error('Doorbird: Audio transmitter error', e);
|
||||
}
|
||||
} finally {
|
||||
this.console.log(`Doorbird: Audio transmitter finished. bytesOut=${totalBytesWritten}ms`);
|
||||
passthrough.destroy();
|
||||
abortController.abort();
|
||||
}
|
||||
this.stopIntercom();
|
||||
})();
|
||||
}
|
||||
|
||||
private getBasicAuthorization(username: string, password: string) {
|
||||
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
||||
}
|
||||
|
||||
stopAudioTransmitter() {
|
||||
this.audioTXProcess?.kill('SIGKILL');
|
||||
this.audioTXProcess = undefined;
|
||||
@@ -238,18 +315,53 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
|
||||
const ffmpegPath = await mediaManager.getFFmpegPath();
|
||||
|
||||
const audioFilters = this.getAudioFilter();
|
||||
|
||||
const ffmpegArgs = [
|
||||
// Suppress printing the FFmpeg banner. Keeps logs clean.
|
||||
'-hide_banner',
|
||||
// Disable periodic progress/statistics logging. Reduces noise and CPU usage.
|
||||
'-nostats',
|
||||
|
||||
// --- Low-latency Input Flags ---
|
||||
// Reduce input buffer latency by flushing packets immediately and disabling demuxer buffering.
|
||||
'-fflags', '+flush_packets+nobuffer',
|
||||
// Do not spend time analyzing the stream to determine properties. Crucial for live streams.
|
||||
'-analyzeduration', '0',
|
||||
// Set a very small probe size to speed up initial connection, as we already know the format.
|
||||
'-probesize', '32',
|
||||
// Read input at its native frame rate to ensure real-time processing.
|
||||
'-re',
|
||||
|
||||
// --- Input Format Specification ---
|
||||
// Set the audio sample rate to 8000 Hz, matching the Doorbird's stream.
|
||||
'-ar', '8000',
|
||||
// Set the number of audio channels to 1 (mono).
|
||||
'-ac', '1',
|
||||
// Force the input format to be interpreted as G.711 µ-law.
|
||||
'-f', 'mulaw',
|
||||
// Specify the input URL for the Doorbird's audio stream.
|
||||
'-i', `${audioRxUrl}`,
|
||||
'-acodec', 'copy',
|
||||
|
||||
// --- Audio Filtering ---
|
||||
...(audioFilters),
|
||||
|
||||
// --- Low-latency Output Flags ---
|
||||
// Enable low-delay flags in the encoder, preventing frame buffering for lookahead.
|
||||
'-flags', '+global_header+low_delay',
|
||||
// Bypass FFmpeg's internal I/O buffering, writing directly to the output pipe.
|
||||
'-avioflags', 'direct',
|
||||
// Force flushing packets to the output immediately after encoding.
|
||||
'-flush_packets', '1',
|
||||
// Set the maximum demux-decode delay to zero, preventing buffering in the muxer.
|
||||
'-muxdelay', '0',
|
||||
|
||||
// --- Output Format Specification ---
|
||||
// Re-encode the audio to PCM µ-law after the filter has been applied, or just copy it if no filters are applied.
|
||||
'-acodec', (audioFilters.length > 0 ? 'pcm_mulaw' : 'copy'),
|
||||
// Force the output container format to raw µ-law.
|
||||
'-f', 'mulaw',
|
||||
// Output the processed audio to file descriptor 3 (the pipe).
|
||||
'pipe:3'
|
||||
];
|
||||
|
||||
@@ -262,7 +374,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
|
||||
cp.on('exit', () => {
|
||||
this.console.log('Doorbird: audio receiver ended.')
|
||||
this.audioRXProcess = undefined;
|
||||
this.stopIntercom();
|
||||
});
|
||||
cp.stdout.on('data', data => this.console.log(data.toString()));
|
||||
cp.stderr.on('data', data => this.console.log(data.toString()));
|
||||
@@ -298,24 +410,60 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
|
||||
async getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
|
||||
|
||||
const port = await this.startAudioRXServer();
|
||||
const audioRtspStreamPort = await this.startAudioRXServer();
|
||||
|
||||
const ffmpegInput: FFmpegInput = {
|
||||
url: undefined,
|
||||
inputArguments: [
|
||||
// --- Low-latency Input Flags (for both streams) ---
|
||||
// Suppress printing the FFmpeg banner.
|
||||
'-hide_banner',
|
||||
// Disable periodic progress/statistics logging.
|
||||
'-nostats',
|
||||
// Set the log level to 'error' to suppress verbose informational messages.
|
||||
'-loglevel', 'error',
|
||||
|
||||
// Reduce input buffer latency by flushing packets immediately and disabling demuxer buffering.
|
||||
// '+nobuffer' is particularly important for live streams.
|
||||
'-fflags', '+flush_packets+nobuffer',
|
||||
// Do not spend time analyzing the stream to determine properties. Crucial for live streams.
|
||||
'-analyzeduration', '0',
|
||||
// Set a very small probe size to speed up initial connection, as we know the formats.
|
||||
'-probesize', '32',
|
||||
'-fflags', 'nobuffer',
|
||||
// Request low-delay flags from decoders.
|
||||
'-flags', 'low_delay',
|
||||
|
||||
// --- Video Input (Input 0) ---
|
||||
// Force the input format to be interpreted as RTSP.
|
||||
'-f', 'rtsp',
|
||||
// Use TCP for RTSP transport for better reliability over potentially lossy networks.
|
||||
'-rtsp_transport', 'tcp',
|
||||
// Specify the input URL for the Doorbird's RTSP video stream.
|
||||
'-i', `${this.getRtspAddress()}`,
|
||||
|
||||
// --- Audio Input (Input 1) ---
|
||||
// Force the format of the second input to be interpreted as G.711 µ-law.
|
||||
'-f', 'mulaw',
|
||||
// Set the number of audio channels to 1 (mono) for the audio input.
|
||||
'-ac', '1',
|
||||
// Set the audio sample rate to 8000 Hz for the audio input.
|
||||
'-ar', '8000',
|
||||
// Explicitly define the channel layout as mono.
|
||||
'-channel_layout', 'mono',
|
||||
'-use_wallclock_as_timestamps', 'true',
|
||||
'-i', `tcp://127.0.0.1:${port}?tcp_nodelay=1`,
|
||||
// Use the system's wall clock for timestamps. This helps synchronize the separate audio
|
||||
// and video streams, which do not share a common clock source.
|
||||
'-use_wallclock_as_timestamps', '1',
|
||||
// Specify the second input as the local TCP socket providing the audio stream.
|
||||
// `tcp_nodelay=1` disables Nagle's algorithm, reducing latency for small packets.
|
||||
'-i', `tcp://127.0.0.1:${audioRtspStreamPort}?tcp_nodelay=1`,
|
||||
// --- Output Stream Handling ---
|
||||
// Increase the maximum delay for the muxing queue to 5 seconds (in microseconds).
|
||||
// This prevents the "Delay between the first packet and last packet" error
|
||||
// by allowing more time for packets from different streams to arrive.
|
||||
'-max_delay', '5000000',
|
||||
// Finish encoding when the shortest input stream (the video) ends.
|
||||
// This ensures ffmpeg terminates if the video stream is interrupted by Doorbird.
|
||||
'-shortest',
|
||||
],
|
||||
mediaStreamOptions: options,
|
||||
};
|
||||
@@ -332,12 +480,29 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
|
||||
const ffmpegPath = await mediaManager.getFFmpegPath();
|
||||
const ffmpegArgs = [
|
||||
// Suppress printing the FFmpeg banner.
|
||||
'-hide_banner',
|
||||
// Disable periodic progress/statistics logging.
|
||||
'-nostats',
|
||||
// Read input at its native frame rate to ensure real-time processing.
|
||||
'-re',
|
||||
// Use the lavfi (libavfilter) virtual input device.
|
||||
'-f', 'lavfi',
|
||||
// Specify the input source as a null audio source (silence) with a sample rate of 8000 Hz and mono channel layout.
|
||||
'-i', 'anullsrc=r=8000:cl=mono',
|
||||
|
||||
// --- Low-latency Output Flags ---
|
||||
// Bypass FFmpeg's internal I/O buffering, writing directly to the output pipe.
|
||||
'-avioflags', 'direct',
|
||||
// Force flushing packets to the output immediately after encoding.
|
||||
'-flush_packets', '1',
|
||||
// Set the maximum demux-decode delay to zero, preventing buffering in the muxer.
|
||||
'-muxdelay', '0',
|
||||
|
||||
// --- Output Format Specification ---
|
||||
// Force the output container format to raw µ-law.
|
||||
'-f', 'mulaw',
|
||||
// Output the processed audio to file descriptor 3 (the pipe).
|
||||
'pipe:3'
|
||||
];
|
||||
|
||||
@@ -370,6 +535,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
|
||||
const server = net.createServer(async (clientSocket) => {
|
||||
clearTimeout(serverTimeout);
|
||||
this.console.log(`Doorbird: audio connection from client ${JSON.stringify(clientSocket.address())}`);
|
||||
|
||||
this.audioRXClientSocket = clientSocket;
|
||||
|
||||
@@ -379,13 +545,14 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
this.stopSilenceGenerator();
|
||||
this.audioRXClientSocket = null;
|
||||
});
|
||||
|
||||
});
|
||||
const serverTimeout = setTimeout(() => {
|
||||
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
|
||||
server.close();
|
||||
}, 30000);
|
||||
const port = await listenZero(server, '127.0.0.1');
|
||||
|
||||
this.console.log(`Doorbird: audio server started on port ${port}`);
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -412,8 +579,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
getRtspAddress() {
|
||||
if (this.storage.getItem('rtspUrl') !== undefined) {
|
||||
return this.storage.getItem('rtspUrl');
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return this.getRtspDefaultAddress();
|
||||
}
|
||||
}
|
||||
@@ -437,6 +603,52 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
getPassword() {
|
||||
return this.storage.getItem('password');
|
||||
}
|
||||
|
||||
setAudioDenoise(enabled: boolean) {
|
||||
this.storage.setItem('audioDenoise', enabled.toString());
|
||||
}
|
||||
|
||||
getAudioDenoise(): boolean {
|
||||
return this.storage.getItem('audioDenoise') === 'true';
|
||||
}
|
||||
|
||||
setAudioSpeechEnhancement(enabled: boolean) {
|
||||
this.storage.setItem('audioSpeechEnhancement', enabled.toString());
|
||||
}
|
||||
|
||||
getAudioSpeechEnhancement(): boolean {
|
||||
return this.storage.getItem('audioSpeechEnhancement') === 'true';
|
||||
}
|
||||
|
||||
private getAudioFilter() {
|
||||
const filters: string[] = [];
|
||||
if (this.getAudioDenoise()) {
|
||||
// Apply noise reduction using the 'afftdn' filter.
|
||||
// - 'afftdn=nf=-50' removes background noise below -50 dB (e.g. hiss, hum)
|
||||
// - 'agate=threshold=0.06:attack=20:release=250' gates quiet sounds:
|
||||
// threshold=0.06 → suppresses signals below ~-24 dBFS (breaths, room noise)
|
||||
// attack=20 → gate opens smoothly in 20 ms to preserve speech onset
|
||||
// release=250 → gate closes slowly in 250 ms to avoid cutting word ends
|
||||
filters.push('afftdn=nf=-50', 'agate=threshold=0.06:attack=20:release=250');
|
||||
}
|
||||
if (this.getAudioSpeechEnhancement()) {
|
||||
// Apply high-pass and low-pass filters to remove frequencies outside the human voice range and apply dynamic normalization.
|
||||
// - 'highpass=f=200' → removes low rumbles below 200 Hz (e.g. touch intercom while speaking, low street noise)
|
||||
// - 'lowpass=f=3000' → removes harsh highs above 3 kHz to reduce hiss/sibilance
|
||||
// - 'acompressor=threshold=0.1:ratio=4:attack=20:release=200'
|
||||
// threshold=0.1 → starts compressing above ~-20 dBFS
|
||||
// ratio=4 → reduces dynamic range by a 4:1 ratio
|
||||
// attack=20 → begins compression quickly to catch loud speech
|
||||
// release=200 → smooths out gain after loud parts
|
||||
// - 'volume=4' → boosts output gain 4x after compression
|
||||
filters.push('highpass=f=200', 'lowpass=f=3000', 'acompressor=threshold=0.1:ratio=4:attack=20:release=200', 'volume=4');
|
||||
}
|
||||
|
||||
if (filters.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return ['-af', filters.join(',')];
|
||||
}
|
||||
}
|
||||
|
||||
export class DoorbirdCamProvider extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
||||
@@ -472,8 +684,7 @@ export class DoorbirdCamProvider extends ScryptedDeviceBase implements DevicePro
|
||||
info.mac = deviceInfo.serialNumber;
|
||||
info.manufacturer = 'Bird Home Automation GmbH';
|
||||
info.managementUrl = 'https://webadmin.doorbird.com';
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
this.console.error('Error adding Doorbird camera', e);
|
||||
throw e;
|
||||
}
|
||||
@@ -490,6 +701,8 @@ export class DoorbirdCamProvider extends ScryptedDeviceBase implements DevicePro
|
||||
device.putSetting('password', password);
|
||||
device.setIPAddress(settings.ip.toString());
|
||||
device.setHttpPortOverride(settings.httpPort?.toString());
|
||||
device.setAudioDenoise(settings.audioDenoise === 'true');
|
||||
device.setAudioSpeechEnhancement(settings.audioSpeechEnhancement === 'true');
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ export function syncResponse(device: ScryptedDevice, type: string): homegraph_v1
|
||||
defaultNames: [],
|
||||
nicknames: [],
|
||||
},
|
||||
otherDeviceIds: [
|
||||
otherDeviceIds: (device.type !== ScryptedDeviceType.Camera && device.type !== ScryptedDeviceType.Doorbell) ? [
|
||||
{
|
||||
deviceId: device.id,
|
||||
}
|
||||
],
|
||||
] : undefined,
|
||||
attributes: {},
|
||||
traits: [],
|
||||
type,
|
||||
|
||||
@@ -8,6 +8,18 @@ Most commonly this plugin is used with 2 plugins: Rebroadcast and HomeKit.
|
||||
Device must have built-in motion detection (most Hikvision doorbells have this).
|
||||
If the doorbell do not have motion detection, you will have to use a separate plugin or device to achieve this (e.g., `opencv`, `pam-diff`, or `dummy-switch`) and group it to the doorbell.
|
||||
|
||||
## ⚠️ Important: Version 2.x Breaking Changes
|
||||
|
||||
Version 2 of this plugin is **not compatible** with version 1.x. Before installing or upgrading to version 2:
|
||||
- **Option 1**: Completely remove the old plugin from Scrypted
|
||||
- **Option 2**: Delete all devices that belong to the old plugin
|
||||
|
||||
After removing the old version, you will need to reconfigure all doorbell devices from scratch.
|
||||
|
||||
### Firmware Requirements
|
||||
|
||||
This version **requires firmware v3.7 or higher**. Older firmware versions are not supported.
|
||||
|
||||
## Two Way Audio
|
||||
|
||||
Two Way Audio is supported if the audio codec is set to G.711ulaw on the doorbell, which is usually the default audio codec. This audio codec will also work with HomeKit. Changing the audio codec from G.711ulaw will cause Two Way Audio to fail on the doorbells that were tested.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Tamper Alert Mechanism Interface
|
||||
|
||||
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the doorbell tamper alert, which is integrated into models such as the DS-KV6113.
|
||||
In the settings section, you can see the linked (parent) device, as well as the IP address of the Hikvision Doorbell (phisical device). These fields are not editable, they are for information purposes only.
|
||||
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the doorbell's tamper alert sensor, which is integrated into models such as the DS-KV6113-PE1(C).
|
||||
|
||||
When the doorbell's tamper sensor is triggered, this device will turn **on**. You can manually turn it **off** in the Scrypted web interface. This device is automatically removed when the parent doorbell device is deleted.
|
||||
|
||||
@@ -1,26 +1,45 @@
|
||||
# Hikvision Doorbell
|
||||
|
||||
At the moment, plugin was tested with the **DS-KV6113PE1[C]** model `doorbell` with firmware version: **V2.2.65 build 231213**, in the following modes:
|
||||
**⚠️ Important: Version 2.x Breaking Changes**
|
||||
|
||||
Version 2 of this plugin is **not compatible** with version 1.x. Before installing or upgrading to version 2:
|
||||
- **Option 1**: Completely remove the old plugin from Scrypted
|
||||
- **Option 2**: Delete all devices that belong to the old plugin
|
||||
|
||||
After removing the old version, you will need to reconfigure all doorbell devices from scratch.
|
||||
|
||||
## Introduction
|
||||
|
||||
At the moment, plugin was tested with the **DS-KV6113-PE1(C)** model `doorbell` with firmware version: **V3.7.0 build 250818**, in the following modes:
|
||||
|
||||
- the `doorbell` is connected to the `Hik-Connect` service;
|
||||
- the `doorbell` is connected to a local SIP proxy (asterisk);
|
||||
- the `doorbell` is connected to a fake SIP proxy, which this plugin runs.
|
||||
|
||||
## Settings
|
||||
|
||||
### Support door lock opening
|
||||
|
||||
Most of these doorbells have the ability to control an electromechanical lock. To implement the lock controller software interface in Scrypted, you need to create a separate device with the `Lock` type. Such a device is created automatically if you enable the **Expose Door Lock Controller** checkbox.
|
||||
The doorbell can control electromechanical locks connected to it. To enable lock control in Scrypted, go to the doorbell device settings, navigate to **Advanced Settings**, and select **Locks** in the **Provided devices** option.
|
||||
|
||||
The lock controller is linked to this device (doorbell). Therefore, when the doorbell is deleted, the associated lock controller will also be deleted.
|
||||
This will create dependent lock device(s) with the `Lock` type. The plugin automatically detects how many doors the doorbell supports (typically 1, but some models support multiple doors). If multiple doors are supported, each lock device will be named with its door number (e.g., "Door Lock 1", "Door Lock 2").
|
||||
|
||||
Lock devices are automatically removed when the parent doorbell device is deleted.
|
||||
|
||||
### Support contact sensors
|
||||
|
||||
Door open/close status monitoring is available through contact sensors. To enable this functionality in Scrypted, go to the doorbell device settings, navigate to **Advanced Settings**, and select **Contact Sensors** in the **Provided devices** option.
|
||||
|
||||
This will create dependent contact sensor device(s) with the `BinarySensor` type. The plugin automatically detects how many doors the doorbell supports (typically 1, but some models support multiple doors). If multiple doors are supported, each contact sensor will be named with its door number (e.g., "Contact Sensor 1", "Contact Sensor 2").
|
||||
|
||||
Contact sensor devices are automatically removed when the parent doorbell device is deleted.
|
||||
|
||||
### Support tamper alert
|
||||
|
||||
Most of a doorbells have a tamper alert. To implement the tamper alert software interface in Scrypted, you need to create a separate device with the `Switch` type. Such a device is created automatically if you enable the **Expose Tamper Alert Controller** checkbox. If you leave this checkbox disabled, the tamper signal will be interpreted as a `Motion Detection` event.
|
||||
For security, the doorbell includes a built-in tamper detection sensor. To enable tamper alert monitoring in Scrypted, go to the doorbell device settings, navigate to **Advanced Settings**, and select **Tamper Alert** in the **Provided devices** option. If you don't enable this option, tamper alert signals will be interpreted as `Motion Detection` events.
|
||||
|
||||
If the tamper on the doorbell is triggered, the controller (`Switch`) will **turn on**. You can **turn off** the switch manually in the Scrypted web interface only.
|
||||
This will create a dependent tamper alert device with the `BinarySensor` type. When the doorbell's tamper sensor is triggered, the device will turn **on**. You can manually turn it **off** in the Scrypted web interface.
|
||||
|
||||
The tamper alert controller is linked to this device (doorbell). Therefore, when the doorbell is deleted, the associated tamper alert controller will also be deleted.
|
||||
The tamper alert device is automatically removed when the parent doorbell device is deleted.
|
||||
|
||||
### Setting up a receiving call (the ability to ringing)
|
||||
|
||||
@@ -44,10 +63,17 @@ This mode should be used when you have a separate SIP gateway and all your inter
|
||||
|
||||
#### Emulate SIP Proxy
|
||||
|
||||
This mode should be used when you have a `doorbell` but no **Indoor Station**, and you want to connect this `doorbell` to Scrypted server only.
|
||||
This mode should be used when you have a `doorbell` but no **Indoor Station**, and you want to connect the `doorbell` directly to the Scrypted server.
|
||||
|
||||
In this mode, the plugin creates a fake SIP proxy that listens for a connection on the specified port (or auto-select a port if not specified). The task of this server is to receive a notification about a call and, in the event of an intercom start (two way audio), simulate picking up the handset so that the `doorbell` switches to conversation mode (stops ringing).
|
||||
In this mode, the plugin creates a fake SIP proxy that listens for connections on the specified port (or auto-selects a port if left blank). This server receives call notifications and, when intercom starts (two-way audio), simulates picking up the handset so the `doorbell` switches to conversation mode (stops ringing).
|
||||
|
||||
On the additional tab, configure the desired port, and you can also enable the **Autoinstall Fake SIP Proxy** checkbox, for not to configure `doorbell` manually.
|
||||
**Important**: When you enable this mode, the plugin **automatically configures the doorbell** with the necessary SIP settings. You don't need to configure the doorbell manually.
|
||||
|
||||
In the `doorbell` settings you can configure the connection to the fake SIP proxy manually. You should specify the IP address of the Scrypted server and the port of the fake proxy. The contents of the other fields do not matter, since the SIP proxy authorizes the “*client*” using the known doorbell’s IP address.
|
||||
On the additional settings tab, you can configure:
|
||||
- **Port**: The listening port for the fake SIP proxy (leave blank for automatic selection)
|
||||
- **Room Number**: Virtual room number (1-9999) that represents this fake SIP proxy
|
||||
- **SIP Proxy Phone Number**: Phone number representing the fake SIP proxy (default: 10102)
|
||||
- **Doorbell Phone Number**: Phone number representing the doorbell (default: 10101)
|
||||
- **Button Number**: Call button number for doorbells with multiple buttons (1-99, default: 1)
|
||||
|
||||
The plugin automatically applies these settings to the doorbell device via ISAPI. If the doorbell is temporarily unreachable, the plugin will retry the configuration automatically.
|
||||
|
||||
9
plugins/hikvision-doorbell/fs/ENTRY_SENSOR_README.md
Normal file
9
plugins/hikvision-doorbell/fs/ENTRY_SENSOR_README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Binary Sensor Interface
|
||||
|
||||
This device serves as a companion for the Hikvision Doorbell device. It provides a binary sensor interface for monitoring the door opening state, which is integrated into models such as the DS-KV6113.
|
||||
|
||||
The Binary Sensor monitors the door opening state and reports:
|
||||
- **Closed** (binaryState: false) - Door is closed
|
||||
- **Open** (binaryState: true) - Door is open
|
||||
|
||||
This sensor provides a simple binary state indication that can be used for automation and monitoring purposes.
|
||||
@@ -1,4 +1,3 @@
|
||||
# Lock Opening Mechanism Interface
|
||||
|
||||
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the lock opening mechanism, which is integrated into models such as the DS-KV6113.
|
||||
In the settings section, you can see the linked (parent) device, as well as the IP address of the Hikvision Doorbell (phisical device). These fields are not editable, they are for information purposes only.
|
||||
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the lock opening mechanism, which is integrated into models such as the DS-KV6113.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vityevato/hikvision-doorbell",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.2",
|
||||
"description": "Hikvision Doorbell Plugin for Scrypted",
|
||||
"author": "Roman Sokolov",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -7,6 +7,8 @@ import * as Auth from 'http-auth-client';
|
||||
export interface AuthRequestOptions extends Http.RequestOptions {
|
||||
sessionAuth?: Auth.Basic | Auth.Digest | Auth.Bearer;
|
||||
responseType: HttpFetchResponseType;
|
||||
// Internal: number of digest retries performed for this request
|
||||
digestRetry?: number;
|
||||
}
|
||||
|
||||
export type AuthRequestBody = string | Buffer | Readable;
|
||||
@@ -15,11 +17,13 @@ export class AuthRequst {
|
||||
|
||||
private username: string;
|
||||
private password: string;
|
||||
private console: Console;
|
||||
private auth: Auth.Basic | Auth.Digest | Auth.Bearer;
|
||||
|
||||
constructor(username:string, password: string, console: Console) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.console = console;
|
||||
}
|
||||
|
||||
async request(url: string, options: AuthRequestOptions, body?: AuthRequestBody) {
|
||||
@@ -42,18 +46,29 @@ export class AuthRequst {
|
||||
|
||||
if (resp.statusCode == 401) {
|
||||
|
||||
if (opt.sessionAuth) {
|
||||
// Hikvision quirk: even if we already had a sessionAuth, a fresh
|
||||
// WWW-Authenticate challenge may require rebuilding credentials.
|
||||
// Limit the number of digest rebuilds to avoid infinite loops.
|
||||
const attempt = (opt.digestRetry ?? 0);
|
||||
if (attempt >= 2) {
|
||||
// Give up after a couple of rebuild attempts and surface the 401 response
|
||||
resolve(await this.parseResponse (opt.responseType, resp));
|
||||
return;
|
||||
}
|
||||
|
||||
opt.sessionAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth);
|
||||
const newAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth);
|
||||
// Clear cached auth to avoid stale nonce reuse
|
||||
this.auth = undefined;
|
||||
opt.sessionAuth = newAuth;
|
||||
opt.digestRetry = attempt + 1;
|
||||
const result = await this.request(url, opt, body);
|
||||
resolve(result);
|
||||
}
|
||||
else {
|
||||
// Cache the negotiated session auth only if it was provided for this request.
|
||||
if (opt.sessionAuth) {
|
||||
this.auth = opt.sessionAuth;
|
||||
}
|
||||
resolve(await this.parseResponse(opt.responseType, resp));
|
||||
}
|
||||
});
|
||||
@@ -73,7 +88,6 @@ export class AuthRequst {
|
||||
req.end();
|
||||
}
|
||||
else {
|
||||
|
||||
this.readableBody(req, body).pipe(req);
|
||||
req.flushHeaders();
|
||||
}
|
||||
|
||||
43
plugins/hikvision-doorbell/src/debug-console.ts
Normal file
43
plugins/hikvision-doorbell/src/debug-console.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Console } from 'console';
|
||||
|
||||
/**
|
||||
* Interface for managing debug state
|
||||
*/
|
||||
export interface DebugController {
|
||||
setDebugEnabled(enabled: boolean): void;
|
||||
getDebugEnabled(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates an existing Console object to provide conditional debug output
|
||||
* @param console - The console object to mutate
|
||||
* @returns Controller object for managing debug state
|
||||
*/
|
||||
export function makeDebugConsole(console: Console): DebugController {
|
||||
let debugEnabled = process.env.DEBUG === 'true' ||
|
||||
process.env.NODE_ENV === 'development';
|
||||
|
||||
// Store original debug method
|
||||
const originalDebug = console.debug.bind (console);
|
||||
|
||||
// Replace debug method with conditional version
|
||||
console.debug = (message?: any, ...optionalParams: any[]): void => {
|
||||
if (debugEnabled)
|
||||
{
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString();
|
||||
originalDebug (`[DEBUG ${timestamp}] ${message}`, ...optionalParams);
|
||||
}
|
||||
};
|
||||
|
||||
// Return controller for managing debug state
|
||||
return {
|
||||
setDebugEnabled(enabled: boolean): void {
|
||||
debugEnabled = enabled;
|
||||
},
|
||||
|
||||
getDebugEnabled(): boolean {
|
||||
return debugEnabled;
|
||||
}
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
30
plugins/hikvision-doorbell/src/entry-sensor.ts
Normal file
30
plugins/hikvision-doorbell/src/entry-sensor.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BinarySensor, Readme, ScryptedDeviceBase, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { HikvisionDoorbellAPI } from "./doorbell-api";
|
||||
import type { HikvisionCameraDoorbell } from "./main";
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
export class HikvisionEntrySensor extends ScryptedDeviceBase implements BinarySensor, Readme {
|
||||
|
||||
constructor(public camera: HikvisionCameraDoorbell, nativeId: string, public doorNumber: string = '1')
|
||||
{
|
||||
super (nativeId);
|
||||
this.binaryState = this.binaryState || false;
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string>
|
||||
{
|
||||
const fileName = join (process.cwd(), 'ENTRY_SENSOR_README.md');
|
||||
return fs.readFile (fileName, 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
private getClient(): HikvisionDoorbellAPI {
|
||||
return this.camera.getClient();
|
||||
}
|
||||
|
||||
static deviceInterfaces: string[] = [
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.Readme
|
||||
];
|
||||
}
|
||||
144
plugins/hikvision-doorbell/src/http-stream-switcher.ts
Normal file
144
plugins/hikvision-doorbell/src/http-stream-switcher.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { PassThrough } from 'stream';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* HTTP Stream Switcher
|
||||
* Receives data from single source and writes to single active PassThrough stream
|
||||
* Supports seamless stream switching without stopping the data source
|
||||
*/
|
||||
export interface HttpSession {
|
||||
sessionId: string;
|
||||
stream: PassThrough;
|
||||
putPromise: Promise<any>;
|
||||
}
|
||||
|
||||
export class HttpStreamSwitcher extends EventEmitter
|
||||
{
|
||||
private currentStream?: PassThrough;
|
||||
private currentSession?: HttpSession;
|
||||
private byteCount: number = 0;
|
||||
private streamSwitchCount: number = 0;
|
||||
|
||||
constructor (private console: Console) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to current active stream
|
||||
*/
|
||||
write (data: Buffer): void
|
||||
{
|
||||
if (!this.currentStream) {
|
||||
// No active stream, drop data
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const canWrite = this.currentStream.write (data);
|
||||
this.byteCount += data.length;
|
||||
|
||||
if (!canWrite) {
|
||||
// Stream buffer is full, apply backpressure
|
||||
this.console.warn ('Stream buffer full, applying backpressure');
|
||||
}
|
||||
} catch (error) {
|
||||
this.console.error ('Error writing to stream:', error);
|
||||
this.clearSession();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to new HTTP session
|
||||
* Old session will be ended gracefully
|
||||
*/
|
||||
switchSession (session: HttpSession): void
|
||||
{
|
||||
const oldSession = this.currentSession;
|
||||
|
||||
if (oldSession) {
|
||||
this.console.debug (`Switching HTTP session ${oldSession.sessionId} -> ${session.sessionId} (${this.byteCount} bytes sent)`);
|
||||
|
||||
// End old stream gracefully
|
||||
try {
|
||||
oldSession.stream.end();
|
||||
} catch (e) {
|
||||
// Ignore errors on old stream
|
||||
}
|
||||
|
||||
this.streamSwitchCount++;
|
||||
} else {
|
||||
this.console.debug (`Setting initial HTTP session ${session.sessionId}`);
|
||||
}
|
||||
|
||||
this.currentSession = session;
|
||||
this.currentStream = session.stream;
|
||||
this.byteCount = 0;
|
||||
|
||||
// Setup error handler for new stream
|
||||
session.stream.on ('error', (error) => {
|
||||
this.console.error (`Stream error for session ${session.sessionId}:`, error);
|
||||
if (this.currentSession === session) {
|
||||
this.clearSession();
|
||||
}
|
||||
});
|
||||
|
||||
session.stream.on ('close', () => {
|
||||
this.console.debug (`Stream closed for session ${session.sessionId}`);
|
||||
if (this.currentSession === session) {
|
||||
this.clearSession();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current session without replacement
|
||||
*/
|
||||
private clearSession(): void
|
||||
{
|
||||
this.currentStream = undefined;
|
||||
this.currentSession = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session ID
|
||||
*/
|
||||
getCurrentSessionId(): string | undefined
|
||||
{
|
||||
return this.currentSession?.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given putPromise is current
|
||||
*/
|
||||
isCurrentPutPromise (putPromise: Promise<any>): boolean
|
||||
{
|
||||
return this.currentSession?.putPromise === putPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session
|
||||
*/
|
||||
getCurrentSession(): HttpSession | undefined
|
||||
{
|
||||
return this.currentSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy switcher and cleanup
|
||||
*/
|
||||
destroy(): void
|
||||
{
|
||||
this.console.debug (`Destroying HTTP switcher (sent ${this.byteCount} bytes, ${this.streamSwitchCount} switches)`);
|
||||
|
||||
if (this.currentStream) {
|
||||
try {
|
||||
this.currentStream.end();
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
this.currentStream = undefined;
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,42 @@
|
||||
import sdk, { ScryptedDeviceBase, SettingValue, ScryptedInterface, Setting, Settings, Lock, LockState, Readme } from "@scrypted/sdk";
|
||||
import { Lock, LockState, Readme, ScryptedDeviceBase, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { HikvisionDoorbellAPI } from "./doorbell-api";
|
||||
import { HikvisionDoorbellProvider } from "./main";
|
||||
import type { HikvisionCameraDoorbell } from "./main";
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
export class HikvisionLock extends ScryptedDeviceBase implements Lock, Readme {
|
||||
|
||||
export class HikvisionLock extends ScryptedDeviceBase implements Lock, Settings, Readme {
|
||||
|
||||
// timeout: NodeJS.Timeout;
|
||||
|
||||
private provider: HikvisionDoorbellProvider;
|
||||
|
||||
constructor(nativeId: string, provider: HikvisionDoorbellProvider) {
|
||||
constructor (public camera: HikvisionCameraDoorbell, nativeId: string, public doorNumber: string = '1') {
|
||||
super (nativeId);
|
||||
|
||||
this.lockState = this.lockState || LockState.Unlocked;
|
||||
this.provider = provider;
|
||||
|
||||
// provider.updateLock (nativeId, this.name);
|
||||
// Initialize lock state by attempting to close the lock
|
||||
this.initializeLockState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize lock state by attempting to close the lock.
|
||||
* If close command succeeds, assume the lock is now locked.
|
||||
* If it fails, assume the lock state remains as default.
|
||||
*/
|
||||
private async initializeLockState(): Promise<void>
|
||||
{
|
||||
try {
|
||||
const capabilities = await this.getClient().getDoorControlCapabilities();
|
||||
const command = capabilities.availableCommands.includes ('close') ? 'close' : 'resume';
|
||||
|
||||
// Attempt to close/lock the door
|
||||
await this.getClient().controlDoor (this.doorNumber, command);
|
||||
|
||||
// If successful, set state to Locked
|
||||
this.lockState = LockState.Locked;
|
||||
this.camera.console.info (`Lock ${this.doorNumber} initialized as Locked (close command succeeded)`);
|
||||
|
||||
} catch (error) {
|
||||
// If command fails, keep default state
|
||||
this.camera.console.warn (`Lock ${this.doorNumber} initialization failed: ${error}. Using default state.`);
|
||||
this.lockState = LockState.Unlocked;
|
||||
}
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string>
|
||||
@@ -27,52 +45,24 @@ export class HikvisionLock extends ScryptedDeviceBase implements Lock, Settings,
|
||||
return fs.readFile (fileName, 'utf-8');
|
||||
}
|
||||
|
||||
lock(): Promise<void> {
|
||||
return this.getClient().closeDoor();
|
||||
}
|
||||
unlock(): Promise<void> {
|
||||
return this.getClient().openDoor();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const cameraNativeId = this.storage.getItem (HikvisionDoorbellProvider.CAMERA_NATIVE_ID_KEY);
|
||||
const state = deviceManager.getDeviceState (cameraNativeId);
|
||||
return [
|
||||
{
|
||||
key: 'parentDevice',
|
||||
title: 'Linked Doorbell Device Name',
|
||||
description: 'The name of the associated doorbell plugin device (for information)',
|
||||
value: state.id,
|
||||
readonly: true,
|
||||
type: 'device',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
title: 'IP Address',
|
||||
description: 'IP address of the doorbell device (for information)',
|
||||
value: this.storage.getItem ('ip'),
|
||||
readonly: true,
|
||||
type: 'string',
|
||||
}
|
||||
]
|
||||
}
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
}
|
||||
|
||||
getClient(): HikvisionDoorbellAPI
|
||||
async lock(): Promise<void>
|
||||
{
|
||||
const ip = this.storage.getItem ('ip');
|
||||
const port = this.storage.getItem ('port');
|
||||
const user = this.storage.getItem ('user');
|
||||
const pass = this.storage.getItem ('pass');
|
||||
const capabilities = await this.getClient().getDoorControlCapabilities();
|
||||
const command = capabilities.availableCommands.includes ('close') ? 'close' : 'resume';
|
||||
await this.getClient().controlDoor (this.doorNumber, command);
|
||||
}
|
||||
|
||||
return this.provider.createSharedClient(ip, port, user, pass, this.console, this.storage);
|
||||
async unlock(): Promise<void>
|
||||
{
|
||||
await this.getClient().controlDoor (this.doorNumber, 'open');
|
||||
}
|
||||
|
||||
private getClient(): HikvisionDoorbellAPI {
|
||||
return this.camera.getClient();
|
||||
}
|
||||
|
||||
static deviceInterfaces: string[] = [
|
||||
ScryptedInterface.Lock,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme
|
||||
];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
121
plugins/hikvision-doorbell/src/rtp-stream-switcher.ts
Normal file
121
plugins/hikvision-doorbell/src/rtp-stream-switcher.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import dgram from 'dgram';
|
||||
import { udpSocketType } from './utils';
|
||||
|
||||
/**
|
||||
* RTP Stream Switcher
|
||||
* Receives RTP packets from single source and sends to single active target
|
||||
* Supports seamless target switching without stopping the data source
|
||||
* Supports both IPv4 and IPv6
|
||||
*/
|
||||
export interface RtpTarget {
|
||||
ip: string;
|
||||
port: number;
|
||||
socket: dgram.Socket;
|
||||
}
|
||||
|
||||
export class RtpStreamSwitcher extends EventEmitter
|
||||
{
|
||||
private currentTarget?: RtpTarget;
|
||||
private packetCount: number = 0;
|
||||
private targetSwitchCount: number = 0;
|
||||
|
||||
constructor (private console: Console) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to new RTP target
|
||||
* Old target will be closed gracefully
|
||||
*/
|
||||
switchTarget (ip: string, port: number): void
|
||||
{
|
||||
const oldTarget = this.currentTarget;
|
||||
|
||||
if (oldTarget) {
|
||||
this.console.debug (`Switching RTP target ${oldTarget.ip}:${oldTarget.port} -> ${ip}:${port} (${this.packetCount} packets sent)`);
|
||||
|
||||
// Close old socket gracefully
|
||||
try {
|
||||
oldTarget.socket.close();
|
||||
} catch (e) {
|
||||
// Ignore errors on old socket
|
||||
}
|
||||
|
||||
this.targetSwitchCount++;
|
||||
} else {
|
||||
this.console.debug (`Setting initial RTP target ${ip}:${port}`);
|
||||
}
|
||||
|
||||
const socketType = udpSocketType (ip);
|
||||
const socket = dgram.createSocket (socketType);
|
||||
|
||||
// Setup error handler for new socket
|
||||
socket.on ('error', (err) => {
|
||||
this.console.error (`Socket error for target ${ip}:${port}:`, err);
|
||||
if (this.currentTarget?.socket === socket) {
|
||||
this.clearTarget();
|
||||
}
|
||||
});
|
||||
|
||||
this.currentTarget = { ip, port, socket };
|
||||
this.packetCount = 0;
|
||||
|
||||
this.console.info (`RTP target set: ${ip}:${port} (${socketType})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current target without replacement
|
||||
*/
|
||||
private clearTarget(): void
|
||||
{
|
||||
this.currentTarget = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send RTP packet to current active target
|
||||
*/
|
||||
sendRtp (rtp: Buffer): void
|
||||
{
|
||||
if (!this.currentTarget) {
|
||||
// No active target, drop packet
|
||||
return;
|
||||
}
|
||||
|
||||
this.packetCount++;
|
||||
|
||||
try {
|
||||
this.currentTarget.socket.send (rtp, this.currentTarget.port, this.currentTarget.ip, (err) => {
|
||||
if (err) {
|
||||
this.console.error (`Failed to send RTP packet:`, err);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.console.error (`Error sending RTP packet:`, error);
|
||||
this.clearTarget();
|
||||
}
|
||||
|
||||
if (this.packetCount % 100 === 0) {
|
||||
this.console.debug (`Sent ${this.packetCount} RTP packets to current target`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy switcher and cleanup
|
||||
*/
|
||||
destroy(): void
|
||||
{
|
||||
this.console.debug (`Destroying RTP switcher (sent ${this.packetCount} packets, ${this.targetSwitchCount} switches)`);
|
||||
|
||||
if (this.currentTarget) {
|
||||
try {
|
||||
this.currentTarget.socket.close();
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
this.currentTarget = undefined;
|
||||
}
|
||||
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,27 @@ import { localServiceIpAddress, rString, udpSocketType, unq } from './utils';
|
||||
import { isV4Format } from 'ip';
|
||||
import dgram from 'node:dgram';
|
||||
import { timeoutPromise } from "@scrypted/common/src/promise-utils";
|
||||
import { parseSdp } from '@scrypted/common/src/sdp-utils';
|
||||
|
||||
|
||||
export interface SipAudioTarget {
|
||||
ip: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
enum DialogStatus
|
||||
{
|
||||
Idle,
|
||||
// Incoming call states
|
||||
Ringing,
|
||||
Answer,
|
||||
AnswerAc,
|
||||
Hangup,
|
||||
HangupAc,
|
||||
Bye,
|
||||
ByeOk,
|
||||
// Outgoing call states
|
||||
Inviting,
|
||||
InviteAc,
|
||||
// Connected states (in/out)
|
||||
Connected,
|
||||
// Registration
|
||||
Regitering
|
||||
}
|
||||
|
||||
@@ -30,42 +40,90 @@ const clientRegistrationExpires = 3600; // in seconds
|
||||
|
||||
export interface SipRegistration
|
||||
{
|
||||
user: string;
|
||||
password: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
callId: string;
|
||||
realm?: string;
|
||||
user: string; // username for registration
|
||||
password: string; // password for registration
|
||||
ip: string; // ip address for registration or doorbell ip
|
||||
port: number; // port for registration or doorbell port
|
||||
callId: string; // call id for registration (local phone number)
|
||||
realm?: string; // realm for registration
|
||||
doorbellId: string; // doorbell id for registration (remote phone number)
|
||||
}
|
||||
|
||||
export class SipManager {
|
||||
|
||||
localIp: string;
|
||||
localPort: number;
|
||||
remoteAudioTarget?: SipAudioTarget;
|
||||
audioCodec?: string;
|
||||
|
||||
private onInviteHandler?: () => void;
|
||||
private onStopRingingHandler?: () => void;
|
||||
private onHangupHandler?: () => void;
|
||||
|
||||
private callId: string = '10012';
|
||||
|
||||
constructor(private ip: string, private console: Console, private storage: Storage) {
|
||||
}
|
||||
|
||||
setOnInviteHandler (handler: () => void)
|
||||
{
|
||||
this.onInviteHandler = handler;
|
||||
}
|
||||
|
||||
setOnStopRingingHandler (handler: () => void)
|
||||
{
|
||||
this.onStopRingingHandler = handler;
|
||||
}
|
||||
|
||||
setOnHangupHandler (handler: () => void)
|
||||
{
|
||||
this.onHangupHandler = handler;
|
||||
}
|
||||
|
||||
private parseSdpAudioTarget (sdpContent?: string): SipAudioTarget | undefined
|
||||
{
|
||||
if (!sdpContent) return undefined;
|
||||
|
||||
try {
|
||||
const parsed = parseSdp (sdpContent);
|
||||
|
||||
// Find audio section
|
||||
const audioSection = parsed.msections.find (s => s.type === 'audio');
|
||||
if (!audioSection) {
|
||||
this.console.warn ('No audio section found in SDP');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Extract IP from header (c=IN IP4 ...)
|
||||
const cLine = parsed.header.lines.find (l => l.startsWith ('c='));
|
||||
const ipMatch = cLine?.match (/c=IN IP[46] ([\d.:a-fA-F]+)/);
|
||||
const ip = ipMatch?.[1];
|
||||
|
||||
const port = audioSection.port;
|
||||
|
||||
if (ip && port) {
|
||||
this.console.debug (`Parsed SDP audio target: ${ip}:${port}`);
|
||||
return { ip, port };
|
||||
}
|
||||
} catch (e) {
|
||||
this.console.error (`Failed to parse SDP: ${e}`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async startClient (creds: SipRegistration)
|
||||
{
|
||||
this.clientMode = true;
|
||||
|
||||
this.stop();
|
||||
await this.startServer();
|
||||
|
||||
this.clientCreds = creds;
|
||||
// {
|
||||
// user: '4442',
|
||||
// password: '4443',
|
||||
// ip: '10.210.210.150',
|
||||
// port: 5060,
|
||||
// callId: '4442'
|
||||
// }
|
||||
this.remoteCreds = creds;
|
||||
|
||||
return this.register();
|
||||
}
|
||||
|
||||
async startGateway (port?: number)
|
||||
async startGateway (callId?: string, port?: number)
|
||||
{
|
||||
if (this.clientMode && sip.stop) {
|
||||
await this.unregister();
|
||||
@@ -77,6 +135,9 @@ export class SipManager {
|
||||
if (port) {
|
||||
this.localPort = port;
|
||||
}
|
||||
if (callId) {
|
||||
this.callId = callId;
|
||||
}
|
||||
return this.startServer (!port);
|
||||
}
|
||||
|
||||
@@ -91,7 +152,6 @@ export class SipManager {
|
||||
{
|
||||
const ring = this.state.msg;
|
||||
|
||||
let bye = true;
|
||||
let rs = this.makeRs (ring, 200, 'Ok');
|
||||
|
||||
rs.content = this.fakeSdpContent();
|
||||
@@ -99,11 +159,11 @@ export class SipManager {
|
||||
|
||||
try {
|
||||
await timeoutPromise<void> (waitResponseTimeout, new Promise<void> (resolve => {
|
||||
this.state = {
|
||||
this.setState ({
|
||||
status: DialogStatus.Answer,
|
||||
msg: ring,
|
||||
waitAck: resolve
|
||||
}
|
||||
});
|
||||
sip.send (rs);
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -111,56 +171,262 @@ export class SipManager {
|
||||
}
|
||||
// await Promise.race ([waitAck, awaitTimeout (waitResponseTimeout)]);
|
||||
|
||||
this.state = {
|
||||
status: DialogStatus.AnswerAc,
|
||||
this.setState ({
|
||||
status: DialogStatus.Connected,
|
||||
msg: ring
|
||||
}
|
||||
const byeMsg = this.bye (ring);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
async invite(): Promise<boolean>
|
||||
{
|
||||
if (this.state.status !== DialogStatus.Idle) {
|
||||
this.console.warn ('Cannot send INVITE: dialog not idle');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.remoteCreds) {
|
||||
this.console.error ('Cannot send INVITE: no remote credentials');
|
||||
return false;
|
||||
}
|
||||
|
||||
const creds = this.remoteCreds;
|
||||
const fromUri = sip.parseUri (`sip:${creds.callId}@${this.localIp}:${this.localPort}`);
|
||||
const toUri = sip.parseUri (`sip:${creds.doorbellId}@${creds.ip}:${creds.port}`);
|
||||
|
||||
const inviteMsg = {
|
||||
method: 'INVITE',
|
||||
uri: toUri,
|
||||
headers: {
|
||||
to: { uri: toUri },
|
||||
from: { uri: fromUri, params: { tag: rString() } },
|
||||
'call-id': `${rString()}@${this.localIp}:${this.localPort}`,
|
||||
cseq: { seq: 1, method: 'INVITE' },
|
||||
contact: [{ uri: fromUri }],
|
||||
'content-type': 'application/sdp',
|
||||
},
|
||||
content: this.fakeSdpContent()
|
||||
};
|
||||
|
||||
this.setState ({
|
||||
status: DialogStatus.Inviting,
|
||||
msg: inviteMsg
|
||||
});
|
||||
|
||||
try {
|
||||
// Send INVITE and collect all responses until final (200 or 4xx/5xx/6xx)
|
||||
const response = await timeoutPromise<any> (waitResponseTimeout * 3, new Promise<any> ((resolve, reject) => {
|
||||
sip.send (inviteMsg, (rs) => {
|
||||
if (rs.status >= 100 && rs.status < 200) {
|
||||
// Provisional response (100 Trying, 180 Ringing)
|
||||
this.console.debug (`INVITE: Provisional response ${rs.status}`);
|
||||
// Don't resolve, callback will be called again for final response
|
||||
} else if (rs.status >= 200) {
|
||||
// Final response (200 OK or error)
|
||||
resolve (rs);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
if (response.status === 200)
|
||||
{
|
||||
const doit = new Promise<boolean> (resolve => {
|
||||
|
||||
sip.send (byeMsg, (rs) => {
|
||||
this.console.log (`BYE response:\n${sip.stringify (rs)}`);
|
||||
if (rs.status == 200) {
|
||||
this.state.status = DialogStatus.HangupAc;
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
this.state.status = DialogStatus.Hangup;
|
||||
|
||||
this.console.info ('INVITE: Call accepted (200 OK)');
|
||||
|
||||
// Parse remote SDP
|
||||
this.remoteAudioTarget = this.parseSdpAudioTarget (response.content);
|
||||
|
||||
this.setState ({
|
||||
status: DialogStatus.InviteAc,
|
||||
msg: response
|
||||
});
|
||||
|
||||
var result = await timeoutPromise<boolean> (waitResponseTimeout, doit);
|
||||
} catch (error) {
|
||||
this.console.error (`Wait OK error: ${error}`);
|
||||
}
|
||||
// Send ACK
|
||||
const ackMsg = {
|
||||
method: 'ACK',
|
||||
uri: toUri,
|
||||
headers: {
|
||||
to: response.headers.to,
|
||||
from: inviteMsg.headers.from,
|
||||
'call-id': inviteMsg.headers['call-id'],
|
||||
cseq: { seq: 1, method: 'ACK' },
|
||||
contact: inviteMsg.headers.contact,
|
||||
}
|
||||
};
|
||||
|
||||
// const result = await Promise.race ([waitOk, awaitTimeout(waitResponseTimeout).then (()=> false)])
|
||||
if (!result) {
|
||||
this.console.error (`When BYE, timeut occurred`);
|
||||
}
|
||||
sip.send (ackMsg);
|
||||
|
||||
this.setState ({
|
||||
status: DialogStatus.Connected,
|
||||
msg: response
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (response.status >= 400)
|
||||
{
|
||||
this.console.error (`INVITE failed: ${response.status} ${response.reason}`);
|
||||
this.clearState();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
this.console.error (`INVITE error: ${error}`);
|
||||
this.clearState();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.clearState();
|
||||
return false;
|
||||
}
|
||||
|
||||
async hangup(): Promise<boolean>
|
||||
{
|
||||
if (this.state.status !== DialogStatus.Connected) {
|
||||
this.console.warn ('Cannot send BYE: dialog not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
const byeMsg = this.bye (this.state.msg);
|
||||
|
||||
try
|
||||
{
|
||||
const doit = new Promise<boolean> (resolve => {
|
||||
|
||||
sip.send (byeMsg, (rs) => {
|
||||
this.console.info (`BYE response:\n${sip.stringify (rs)}`);
|
||||
if (rs.status == 200) {
|
||||
this.setState ({ status: DialogStatus.ByeOk, msg: byeMsg });
|
||||
resolve (true);
|
||||
}
|
||||
});
|
||||
this.setState ({ status: DialogStatus.Connected, msg: byeMsg });
|
||||
|
||||
});
|
||||
|
||||
var result = await timeoutPromise<boolean> (waitResponseTimeout, doit);
|
||||
} catch (error) {
|
||||
this.console.error (`Wait OK error: ${error}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// const result = await Promise.race ([waitOk, awaitTimeout(waitResponseTimeout).then (()=> false)])
|
||||
if (!result) {
|
||||
this.console.error (`When BYE, timeout occurred`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.clearState();
|
||||
return true;
|
||||
}
|
||||
|
||||
private state: SipState = { status: DialogStatus.Idle};
|
||||
private clientMode: boolean = false;
|
||||
private authCtx: any = { nc: 1 };
|
||||
private registrationExpires: number = clientRegistrationExpires;
|
||||
private clientCreds: SipRegistration;
|
||||
private remoteCreds: SipRegistration;
|
||||
|
||||
private setState (newState: SipState)
|
||||
{
|
||||
const oldStatus = this.state.status;
|
||||
const newStatus = newState.status;
|
||||
|
||||
this.state = newState;
|
||||
|
||||
// Hook for future actions on state transitions
|
||||
this.onStateChange (oldStatus, newStatus);
|
||||
}
|
||||
|
||||
private onStateChange(oldStatus: DialogStatus, newStatus: DialogStatus)
|
||||
{
|
||||
if (oldStatus === newStatus)
|
||||
return;
|
||||
|
||||
this.console.debug (`State transition: ${DialogStatus[oldStatus]} -> ${DialogStatus[newStatus]}`);
|
||||
|
||||
switch (oldStatus)
|
||||
{
|
||||
case DialogStatus.Ringing:
|
||||
if (this.onStopRingingHandler) {
|
||||
// Call handler asynchronously to avoid blocking SIP message flow
|
||||
setImmediate (() => {
|
||||
try {
|
||||
this.onStopRingingHandler();
|
||||
} catch (e) {
|
||||
this.console.error(`Error in onStopRinging handler: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (newStatus)
|
||||
{
|
||||
|
||||
case DialogStatus.Ringing:
|
||||
if (this.onInviteHandler) {
|
||||
// Call handler asynchronously to avoid blocking SIP message flow
|
||||
setImmediate (() => {
|
||||
try {
|
||||
this.onInviteHandler();
|
||||
} catch (e) {
|
||||
this.console.error(`Error in onInvite handler: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
case DialogStatus.Bye:
|
||||
if (this.onHangupHandler) {
|
||||
// Call handler asynchronously to avoid blocking SIP message flow
|
||||
setImmediate (() => {
|
||||
try {
|
||||
this.onHangupHandler();
|
||||
} catch (e) {
|
||||
this.console.error(`Error in onHangup handler: ${e}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private incomeRegister(rq: any): boolean {
|
||||
|
||||
let rs = sip.makeResponse(rq, 200, 'OK');
|
||||
private incomeRegister (rq: any): boolean
|
||||
{
|
||||
// Parse registration request to extract credentials
|
||||
const fromUri = sip.parseUri (rq.headers.from.uri);
|
||||
const contactUri = rq.headers.contact && rq.headers.contact[0] && sip.parseUri (rq.headers.contact[0].uri);
|
||||
const toUri = sip.parseUri (rq.headers.to.uri);
|
||||
|
||||
const user = fromUri.user || toUri.user; // username for registration
|
||||
const doorbellId = toUri.user || fromUri.user; // remote phone number (doorbell extension)
|
||||
const ip = contactUri?.host || fromUri.host;
|
||||
const port = contactUri?.port || fromUri.port || 5060;
|
||||
|
||||
if (!user || !ip || !doorbellId) {
|
||||
this.console.warn ('REGISTER: Missing user, doorbellId or IP in request');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store registration (only one client supported in gateway mode)
|
||||
this.remoteCreds = {
|
||||
user,
|
||||
password: '', // Password will be handled via digest auth if needed
|
||||
ip,
|
||||
port,
|
||||
callId: this.callId,
|
||||
doorbellId,
|
||||
realm: undefined
|
||||
};
|
||||
|
||||
this.console.debug (`REGISTER: Stored registration for user ${user} from ${ip}:${port}`);
|
||||
|
||||
let rs = sip.makeResponse (rq, 200, 'OK');
|
||||
rs.headers.contact = rq.headers.contact;
|
||||
sip.send(rs);
|
||||
|
||||
rs.headers.expires = rq.headers.expires || clientRegistrationExpires;
|
||||
sip.send (rs);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
private async startServer (findFreePort: boolean = true)
|
||||
@@ -176,10 +442,10 @@ export class SipManager {
|
||||
await sip.start({
|
||||
logger: {
|
||||
send: (message, addrInfo) => {
|
||||
this.console.log(`send to ${addrInfo.address}:\n${sip.stringify(message)}`);
|
||||
this.console.debug (`send to ${addrInfo.address}:\n${sip.stringify(message)}`);
|
||||
},
|
||||
recv: (message, addrInfo) => {
|
||||
this.console.log(`recv to ${addrInfo.address}:\n${sip.stringify(message)}`);
|
||||
this.console.debug (`recv to ${addrInfo.address}:\n${sip.stringify(message)}`);
|
||||
}
|
||||
},
|
||||
address: this.localIp,
|
||||
@@ -246,13 +512,21 @@ export class SipManager {
|
||||
{
|
||||
if (this.state.status === DialogStatus.Idle)
|
||||
{
|
||||
// Parse SDP to extract audio target
|
||||
this.remoteAudioTarget = this.parseSdpAudioTarget (rq.content);
|
||||
|
||||
rq.headers.to = {uri: rq.headers.to.uri, params: { tag: 'govno' }};
|
||||
this.state = {
|
||||
status: DialogStatus.Ringing,
|
||||
msg: rq
|
||||
}
|
||||
|
||||
// Send 180 Ringing FIRST, before changing state
|
||||
let rs = this.makeRs(rq, 180, 'Ringing');
|
||||
sip.send(rs);
|
||||
|
||||
// Then update state (this will trigger onInviteHandler asynchronously)
|
||||
this.setState ({
|
||||
status: DialogStatus.Ringing,
|
||||
msg: rq
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -281,11 +555,12 @@ export class SipManager {
|
||||
|
||||
private incomeBye (rq: any): boolean
|
||||
{
|
||||
if (this.state.status == DialogStatus.AnswerAc ||
|
||||
this.state.status == DialogStatus.Hangup)
|
||||
if (this.state.status == DialogStatus.Connected ||
|
||||
this.state.status == DialogStatus.Bye)
|
||||
{
|
||||
this.clearState();
|
||||
this.setState ({ status: DialogStatus.Bye, msg: rq });
|
||||
sip.send (this.makeRs (rq, 200, 'OK'));
|
||||
this.clearState();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -299,37 +574,27 @@ export class SipManager {
|
||||
return rs;
|
||||
}
|
||||
|
||||
private fakeSdpContent()
|
||||
{
|
||||
const ipv = isV4Format (this.localIp) ? 'IP4' : 'IP6';
|
||||
const ip = `${ipv} ${this.localIp}`;
|
||||
return 'v=0\r\n' +
|
||||
`o=yate 1707679323 1707679323 IN ${ip}\r\n` +
|
||||
's=SIP Call\r\n' +
|
||||
`c=IN ${ip}\r\n` +
|
||||
't=0 0\r\n' +
|
||||
'm=audio 9654 RTP/AVP 0 101\r\n' +
|
||||
'a=rtpmap:0 PCMU/8000\r\n' +
|
||||
'a=rtpmap:101 telephone-event/8000\r\n';
|
||||
}
|
||||
|
||||
private bye (rq: any): any
|
||||
{
|
||||
const toUser = sip.parseUri(rq.headers.to.uri).user;
|
||||
let uri = rq.headers.contact[0] && rq.headers.contact[0].uri;
|
||||
if (uri === undefined) {
|
||||
uri = rq.headers.from.uri;
|
||||
}
|
||||
|
||||
// In SIP dialog, BYE From/To depend on who initiated the call
|
||||
// If we received INVITE (server mode): swap headers
|
||||
// If we sent INVITE (client mode): keep headers as is
|
||||
const isServerMode = rq.method === 'INVITE';
|
||||
|
||||
let msg = {
|
||||
method: 'BYE',
|
||||
uri: uri,
|
||||
headers: {
|
||||
to: rq.headers.from,
|
||||
from: rq.headers.to,
|
||||
to: isServerMode ? rq.headers.from : rq.headers.to,
|
||||
from: isServerMode ? rq.headers.to : rq.headers.from,
|
||||
'call-id': rq.headers['call-id'],
|
||||
cseq: {method: 'BYE', seq: rq.headers.cseq.seq + 1},
|
||||
contact: `sip:${toUser}@${this.localIp}:${this.localPort}`
|
||||
contact: `sip:${this.callId}@${this.localIp}:${this.localPort}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +607,36 @@ export class SipManager {
|
||||
return msg;
|
||||
}
|
||||
|
||||
private fakeSdpContent()
|
||||
{
|
||||
const ipv = isV4Format (this.localIp) ? 'IP4' : 'IP6';
|
||||
const ip = `${ipv} ${this.localIp}`;
|
||||
|
||||
// Determine codec payload type and name
|
||||
let payloadType = '0';
|
||||
let codecName = 'PCMU/8000';
|
||||
|
||||
if (this.audioCodec === 'pcm_alaw' || this.audioCodec === 'alaw') {
|
||||
payloadType = '8';
|
||||
codecName = 'PCMA/8000';
|
||||
} else if (this.audioCodec === 'pcm_mulaw' || this.audioCodec === 'mulaw') {
|
||||
payloadType = '0';
|
||||
codecName = 'PCMU/8000';
|
||||
}
|
||||
|
||||
return 'v=0\r\n' +
|
||||
`o=yate 1707679323 1707679323 IN ${ip}\r\n` +
|
||||
's=SIP Call\r\n' +
|
||||
`c=IN ${ip}\r\n` +
|
||||
't=0 0\r\n' +
|
||||
`m=audio 9654 RTP/AVP ${payloadType} 101\r\n` +
|
||||
`a=rtpmap:${payloadType} ${codecName}\r\n` +
|
||||
'a=rtpmap:101 telephone-event/8000\r\n' +
|
||||
'a=sendonly\r\n' +
|
||||
'm=video 0 RTP/AVP 96\r\n' +
|
||||
'a=inactive\r\n';
|
||||
}
|
||||
|
||||
private async getFreeUdpPort (ip: string, type: dgram.SocketType)
|
||||
{
|
||||
return new Promise<number> (resolve => {
|
||||
@@ -369,7 +664,7 @@ export class SipManager {
|
||||
{
|
||||
if (this.state.status !== DialogStatus.Idle) return false;
|
||||
|
||||
const creds = this.clientCreds;
|
||||
const creds = this.remoteCreds;
|
||||
const hereUri = sip.parseUri (`sip:${creds.callId}@${this.localIp}:${this.localPort}`);
|
||||
|
||||
const initMsg = {
|
||||
@@ -386,10 +681,10 @@ export class SipManager {
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
this.setState ({
|
||||
status: DialogStatus.Regitering,
|
||||
msg: {...initMsg}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.authCtx.realm) {
|
||||
digest.signRequest (this.authCtx, initMsg);
|
||||
@@ -431,8 +726,9 @@ export class SipManager {
|
||||
|
||||
}
|
||||
|
||||
private clearState() {
|
||||
this.state = { status: DialogStatus.Idle };
|
||||
private clearState()
|
||||
{
|
||||
this.setState ({ status: DialogStatus.Idle });
|
||||
}
|
||||
|
||||
/// Simple check that request came from doorbell
|
||||
@@ -444,7 +740,7 @@ export class SipManager {
|
||||
const puri = sip.parseUri (uri);
|
||||
const ip = puri && puri.host;
|
||||
if (ip) {
|
||||
return this.clientCreds.ip === ip || this.ip === ip;
|
||||
return this.remoteCreds.ip === ip || this.ip === ip;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import sdk, { ScryptedDeviceBase, SettingValue, ScryptedInterface, Setting, Settings, Readme, OnOff } from "@scrypted/sdk";
|
||||
import { HikvisionDoorbellProvider } from "./main";
|
||||
import { OnOff, Readme, ScryptedDeviceBase, ScryptedInterface } from "@scrypted/sdk";
|
||||
import type { HikvisionCameraDoorbell } from "./main";
|
||||
import * as fs from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { parseBooleans } from "xml2js/lib/processors";
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export class HikvisionTamperAlert extends ScryptedDeviceBase implements OnOff, Settings, Readme {
|
||||
|
||||
// timeout: NodeJS.Timeout;
|
||||
export class HikvisionTamperAlert extends ScryptedDeviceBase implements OnOff, Readme {
|
||||
on: boolean = false;
|
||||
|
||||
private static ONOFF_KEY: string = "onoff";
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super (nativeId);
|
||||
|
||||
this.on = parseBooleans (this.storage.getItem (HikvisionTamperAlert.ONOFF_KEY));
|
||||
constructor(public camera: HikvisionCameraDoorbell, nativeId: string) {
|
||||
super(nativeId);
|
||||
this.on = parseBooleans(this.storage.getItem(HikvisionTamperAlert.ONOFF_KEY)) || false;
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string>
|
||||
@@ -24,48 +20,19 @@ export class HikvisionTamperAlert extends ScryptedDeviceBase implements OnOff, S
|
||||
return fs.readFile (fileName, 'utf-8');
|
||||
}
|
||||
|
||||
turnOff(): Promise<void>
|
||||
{
|
||||
async turnOff(): Promise<void> {
|
||||
this.on = false;
|
||||
this.storage.setItem(HikvisionTamperAlert.ONOFF_KEY, 'false');
|
||||
return;
|
||||
}
|
||||
turnOn(): Promise<void>
|
||||
{
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
this.on = true;
|
||||
this.storage.setItem(HikvisionTamperAlert.ONOFF_KEY, 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const cameraNativeId = this.storage.getItem (HikvisionDoorbellProvider.CAMERA_NATIVE_ID_KEY);
|
||||
const state = deviceManager.getDeviceState (cameraNativeId);
|
||||
return [
|
||||
{
|
||||
key: 'parentDevice',
|
||||
title: 'Linked Doorbell Device Name',
|
||||
description: 'The name of the associated doorbell plugin device (for information)',
|
||||
value: state.id,
|
||||
readonly: true,
|
||||
type: 'device',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
title: 'IP Address',
|
||||
description: 'IP address of the doorbell device (for information)',
|
||||
value: this.storage.getItem ('ip'),
|
||||
readonly: true,
|
||||
type: 'string',
|
||||
}
|
||||
]
|
||||
}
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
}
|
||||
|
||||
static deviceInterfaces: string[] = [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme
|
||||
];
|
||||
}
|
||||
|
||||
8
plugins/hikvision-doorbell/src/types.d.ts
vendored
Normal file
8
plugins/hikvision-doorbell/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Local type declarations to support Symbol.dispose without affecting other plugins
|
||||
declare global {
|
||||
interface SymbolConstructor {
|
||||
readonly dispose: unique symbol;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -5,7 +5,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.165",
|
||||
"version": "0.0.166",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -522,32 +522,74 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
|
||||
async setSupplementLight(params: { on?: boolean, brightness?: number, mode?: 'auto' | 'manual' }): Promise<void> {
|
||||
const { json } = await this.getSupplementLight();
|
||||
|
||||
if (json.ResponseStatus) {
|
||||
throw new Error("Supplemental light is not supported on this device.");
|
||||
}
|
||||
|
||||
const supp: any = json.SupplementLight;
|
||||
if (!supp) {
|
||||
throw new Error("Supplemental light configuration not available.");
|
||||
}
|
||||
|
||||
if (supp.supplementLightMode && supp.supplementLightMode.opt) {
|
||||
const availableModes = supp.supplementLightMode.opt.split(',').map(s => s.trim());
|
||||
const selectedMode = params.on
|
||||
? (availableModes.find(mode => mode.toLowerCase() !== 'close') || 'close')
|
||||
: 'close';
|
||||
supp.supplementLightMode = [selectedMode];
|
||||
const getCurrentValue = (obj: any) => Array.isArray(obj) ? obj[0] : obj;
|
||||
const setValue = (t: any, k: string, v: string) => {
|
||||
t[k] = Array.isArray(t[k]) ? [v] : v;
|
||||
};
|
||||
|
||||
const setBrightnessForMode = (level: number, mode: string) => {
|
||||
const v = level.toString();
|
||||
const map: Record<string, Array<{ obj: any; key: string }>> = {
|
||||
colorVuWhiteLight: [
|
||||
{ obj: supp, key: 'whiteLightBrightness' },
|
||||
{ obj: supp.colorVuWhiteLightModeCfg, key: 'whiteLightbrightLimit' }
|
||||
],
|
||||
irLight: [
|
||||
{ obj: supp, key: 'irLightBrightness' },
|
||||
{ obj: supp.IrLightModeCfg, key: 'irLightbrightLimit' }
|
||||
],
|
||||
eventIntelligence: [
|
||||
{ obj: supp.EventIntelligenceModeCfg, key: 'whiteLightBrightness' },
|
||||
{ obj: supp.EventIntelligenceModeCfg, key: 'irLightBrightness' }
|
||||
]
|
||||
};
|
||||
(map[mode] || []).forEach(({ obj, key }) => {
|
||||
if (obj && obj[key] !== undefined) setValue(obj, key, v);
|
||||
});
|
||||
};
|
||||
|
||||
const setModeConfigs = (m: 'auto' | 'manual') => {
|
||||
if (getCurrentValue(supp.supplementLightMode) === 'eventIntelligence' && supp.EventIntelligenceModeCfg) {
|
||||
setValue(supp.EventIntelligenceModeCfg, 'brightnessRegulatMode', m);
|
||||
} else if (supp.mixedLightBrightnessRegulatMode !== undefined) {
|
||||
setValue(supp, 'mixedLightBrightnessRegulatMode', m);
|
||||
} else if (supp.isAutoModeBrightnessCfg !== undefined) {
|
||||
setValue(supp, 'isAutoModeBrightnessCfg', m === 'auto' ? 'true' : 'false');
|
||||
}
|
||||
};
|
||||
|
||||
if (params.on !== undefined && supp.supplementLightMode) {
|
||||
const opts = supp.supplementLightMode.opt?.split(',').map((s: string) => s.trim()) || [];
|
||||
this.console.log('[API] Available supplemental light modes:', opts);
|
||||
if (params.on) {
|
||||
const preferred = ['colorVuWhiteLight', 'eventIntelligence', 'irLight'];
|
||||
const sel = preferred.find(m => opts.includes(m));
|
||||
if (!sel) {
|
||||
throw new Error(`Cannot turn on: no supported mode. Available: ${opts.join(', ')}`);
|
||||
}
|
||||
setValue(supp, 'supplementLightMode', sel);
|
||||
} else {
|
||||
setValue(supp, 'supplementLightMode', 'close');
|
||||
}
|
||||
}
|
||||
|
||||
if (params.mode) {
|
||||
supp.mixedLightBrightnessRegulatMode = [params.mode];
|
||||
} else if (params.on !== undefined) {
|
||||
supp.mixedLightBrightnessRegulatMode = [params.on ? "manual" : "auto"];
|
||||
setModeConfigs(params.mode);
|
||||
}
|
||||
|
||||
if (params.brightness !== undefined) {
|
||||
let brightness = Math.max(0, Math.min(100, params.brightness));
|
||||
supp.whiteLightBrightness = [brightness.toString()];
|
||||
const lvl = Math.min(100, Math.max(0, params.brightness));
|
||||
const mode = getCurrentValue(supp.supplementLightMode);
|
||||
if (mode !== 'close') {
|
||||
setBrightnessForMode(lvl, mode);
|
||||
} else {
|
||||
this.console.warn('[API] Brightness change ignored: light is off');
|
||||
}
|
||||
}
|
||||
|
||||
const builder = new xml2js.Builder({
|
||||
@@ -555,7 +597,7 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
renderOpts: { pretty: false },
|
||||
});
|
||||
const newXml = builder.buildObject({ SupplementLight: supp });
|
||||
|
||||
|
||||
await this.request({
|
||||
method: 'PUT',
|
||||
url: `http://${this.ip}/ISAPI/Image/channels/1/supplementLight`,
|
||||
|
||||
361
plugins/homekit/package-lock.json
generated
361
plugins/homekit/package-lock.json
generated
@@ -20,7 +20,7 @@
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/node": "^22.18.0",
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"@types/url-parse": "^1.4.11"
|
||||
}
|
||||
@@ -32,6 +32,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
@@ -49,23 +50,21 @@
|
||||
"examples/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.4.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.6",
|
||||
"jest": "^29.7.0",
|
||||
"knip": "^3.9.0",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^22.13.4",
|
||||
"knip": "^5.44.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"organize-imports-cli": "^0.10.0",
|
||||
"process": "^0.11.10",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typedoc": "0.25.5",
|
||||
"typedoc-plugin-markdown": "3.17.1",
|
||||
"typescript": "5.3.3"
|
||||
"tsx": "^4.19.3",
|
||||
"typedoc": "0.27.7",
|
||||
"typedoc-plugin-markdown": "4.4.2",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"../../external/werift/packages/webrtc": {
|
||||
@@ -123,24 +122,31 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.45",
|
||||
"version": "0.5.38",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -152,11 +158,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^24.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.28.5"
|
||||
}
|
||||
},
|
||||
"../HAP-NodeJS": {
|
||||
@@ -271,12 +275,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.14.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
|
||||
"integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
|
||||
"version": "22.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
|
||||
"integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qrcode-svg": {
|
||||
@@ -340,6 +345,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/check-disk-space": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz",
|
||||
@@ -438,18 +456,30 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
@@ -481,6 +511,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/event-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
|
||||
@@ -538,15 +580,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -555,12 +603,26 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -666,21 +728,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -937,6 +989,15 @@
|
||||
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
|
||||
"integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ=="
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -1210,10 +1271,11 @@
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which-boxed-primitive": {
|
||||
"version": "1.0.2",
|
||||
@@ -1311,20 +1373,18 @@
|
||||
"@koush/werift-src": {
|
||||
"version": "file:../../external/werift",
|
||||
"requires": {
|
||||
"@biomejs/biome": "^1.4.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.6",
|
||||
"jest": "^29.7.0",
|
||||
"knip": "^3.9.0",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^22.13.4",
|
||||
"knip": "^5.44.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"organize-imports-cli": "^0.10.0",
|
||||
"process": "^0.11.10",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typedoc": "0.25.5",
|
||||
"typedoc-plugin-markdown": "3.17.1",
|
||||
"typescript": "5.3.3"
|
||||
"tsx": "^4.19.3",
|
||||
"typedoc": "0.27.7",
|
||||
"typedoc-plugin-markdown": "4.4.2",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.5"
|
||||
}
|
||||
},
|
||||
"@leichtgewicht/ip-codec": {
|
||||
@@ -1336,6 +1396,7 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/types": "^0.5.27",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
@@ -1346,25 +1407,30 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.5",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^24.0.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.10.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"openai": "^5.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.43.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/debug": {
|
||||
@@ -1389,12 +1455,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.14.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
|
||||
"integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
|
||||
"version": "22.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz",
|
||||
"integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"@types/qrcode-svg": {
|
||||
@@ -1440,6 +1506,15 @@
|
||||
"set-function-length": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"check-disk-space": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz",
|
||||
@@ -1506,18 +1581,25 @@
|
||||
"@leichtgewicht/ip-codec": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||
},
|
||||
"es-define-property": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.2.4"
|
||||
}
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
|
||||
},
|
||||
"es-errors": {
|
||||
"version": "1.3.0",
|
||||
@@ -1540,6 +1622,14 @@
|
||||
"stop-iteration-iterator": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"requires": {
|
||||
"es-errors": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"event-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
|
||||
@@ -1588,24 +1678,35 @@
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"requires": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"requires": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||
"requires": {
|
||||
"get-intrinsic": "^1.1.3"
|
||||
}
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
|
||||
},
|
||||
"hap-nodejs": {
|
||||
"version": "1.1.0",
|
||||
@@ -1688,15 +1789,10 @@
|
||||
"es-define-property": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"has-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
|
||||
},
|
||||
"has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
@@ -1857,6 +1953,11 @@
|
||||
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
|
||||
"integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ=="
|
||||
},
|
||||
"math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -2049,9 +2150,9 @@
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true
|
||||
},
|
||||
"which-boxed-primitive": {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/node": "^22.18.0",
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"@types/url-parse": "^1.4.11"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
48
plugins/ncnn/package-lock.json
generated
48
plugins/ncnn/package-lock.json
generated
@@ -1,34 +1,41 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.77",
|
||||
"name": "@scrypted/ncnn",
|
||||
"version": "0.1.88",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.77",
|
||||
"name": "@scrypted/ncnn",
|
||||
"version": "0.1.88",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.77",
|
||||
"version": "0.5.12",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -41,11 +48,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -61,22 +66,27 @@
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,16 +33,22 @@
|
||||
"runtime": "python",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"ScryptedSystemDevice",
|
||||
"DeviceCreator",
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"ClusterForkInterface",
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"@scrypted/ncnn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.77"
|
||||
"version": "0.1.88"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user