Compare commits

..

133 Commits

Author SHA1 Message Date
Koushik Dutta
b784399afa server: verup 2025-07-25 11:04:16 -07:00
Koushik Dutta
0f16568edb tapo: fix broken plugin on windows 2025-07-22 11:11:16 -07:00
Koushik Dutta
7ecee115a6 sdk: remove object tracker 2025-07-20 10:14:16 -07:00
Koushik Dutta
34eb2be551 sdk: use mcp for tool call 2025-07-16 10:18:13 -07:00
Koushik Dutta
27ff0c8c80 Merge branch 'main' of github.com:koush/scrypted 2025-07-16 08:45:06 -07:00
Koushik Dutta
51c5df6802 core: build fix 2025-07-16 08:45:01 -07:00
Koushik Dutta
328bd78771 docker: fix grep error 2025-07-15 11:47:02 -07:00
Koushik Dutta
3d2ae6384f sdk: add support for custom interface descriptors 2025-07-13 13:32:53 -07:00
Koushik Dutta
e1ba16f708 openvino: use explicit shape for CRAFT 2025-07-13 13:10:12 -07:00
Koushik Dutta
6f47e39bf3 sdk: add level to externals, support rollup externals 2025-07-13 08:01:22 -07:00
Koushik Dutta
e38c3c975f server: dead code 2025-07-13 07:57:18 -07:00
Koushik Dutta
9c75b074b5 sdk: chat completion capabilties 2025-07-11 21:30:23 -07:00
Koushik Dutta
299d926eae install: add nvidia to install script 2025-07-10 19:53:13 -07:00
Koushik Dutta
22d0ce4f82 install: add nvidia to install script 2025-07-10 19:45:27 -07:00
Koushik Dutta
53c2b7cb58 postbeta 2025-07-10 08:52:43 -07:00
Koushik Dutta
86548f6fa4 server: add plugin node_volumes to path 2025-07-10 08:52:31 -07:00
Koushik Dutta
0e1e641f8f intel: fix oneapi path 2025-07-07 13:57:59 -07:00
Koushik Dutta
58e0a748c4 intel: fix oneapi path 2025-07-07 12:49:01 -07:00
Koushik Dutta
b4a58df53a intel: fix oneapi path 2025-07-07 10:40:33 -07:00
Koushik Dutta
b83b7ff559 intel: fix missing gpg 2025-07-07 10:37:15 -07:00
Koushik Dutta
de2173567e onnx: bump deps 2025-07-07 09:39:34 -07:00
Koushik Dutta
9c931b21dc ncnn: update 2025-07-07 09:18:57 -07:00
Koushik Dutta
5291afad6a install: update nvidia 2025-07-07 08:29:13 -07:00
Koushik Dutta
e1ac1ace87 install: update intel libs 2025-07-07 08:25:42 -07:00
Koushik Dutta
1f6f1a82aa Merge branch 'main' of github.com:koush/scrypted 2025-07-07 08:06:04 -07:00
Koushik Dutta
70af66a875 router: add cron 2025-07-07 08:05:55 -07:00
Koushik Dutta
b7bab5b2e2 vscode-typescript 2025-07-06 13:45:24 -07:00
Koushik Dutta
5d5686a9e7 common: util functions 2025-07-06 11:26:13 -07:00
Koushik Dutta
1eb5012e9b sdk: alternate streamChatCompletion signature 2025-07-05 09:28:02 -07:00
Koushik Dutta
3574e72e4f sdk: publish 2025-07-05 07:30:04 -07:00
Koushik Dutta
b7ff4dfd5e sdk: alternate streamChatCompletion signature 2025-07-05 07:29:32 -07:00
Koushik Dutta
e0ed953963 sdk: publish 2025-07-05 07:18:27 -07:00
Koushik Dutta
930690a4ba sdk: alternate streamChatCompletion signature 2025-07-05 07:16:33 -07:00
Koushik Dutta
1aa4d45caa sdk: update 2025-07-03 23:45:36 -07:00
Koushik Dutta
28fb2b0853 packages/deferred: publish 2025-07-03 23:02:48 -07:00
Koushik Dutta
4fae4fba3b sdk: update 2025-07-03 20:05:26 -07:00
Vitor Furlanetti
b72c8f59eb server: Fallback pip to latin (#1841) 2025-07-02 21:34:29 -07:00
Koushik Dutta
369ad59324 amcrest/http: fix http authentication when it includes query parameters 2025-07-02 09:05:24 -07:00
Koushik Dutta
51ac5a1042 core: fix first run missing users 2025-06-24 10:16:03 -07:00
Koushik Dutta
200c107e97 reolink: fix vs caching 2025-06-18 14:01:16 -07:00
Koushik Dutta
35139abe30 openvino: note int8 2025-06-18 09:39:47 -07:00
Koushik Dutta
dc7f305687 predict: publish clip 2025-06-17 20:40:45 -07:00
Koushik Dutta
2a479dd38a onnx: clip 2025-06-17 10:55:21 -07:00
Koushik Dutta
d32f9bb07a coreml: clip 2025-06-17 10:33:38 -07:00
Koushik Dutta
a33bed0b44 openvino: clip threads 2025-06-17 10:25:34 -07:00
Koushik Dutta
f9847f6f72 predict: wip clip 2025-06-17 10:14:11 -07:00
Koushik Dutta
add53d07f3 core: publish ui 2025-06-17 09:39:54 -07:00
Koushik Dutta
db21159299 sdk: fix broken package lock 2025-06-17 09:36:03 -07:00
Koushik Dutta
6fa7f06852 postbeta 2025-06-17 09:22:19 -07:00
Koushik Dutta
58387e5046 postbeta 2025-06-17 09:15:00 -07:00
Koushik Dutta
1589908698 sdk: fix python Buffer mapping 2025-06-17 09:11:25 -07:00
Koushik Dutta
d0183c29a8 sdk: add support for text embeddings 2025-06-17 09:07:35 -07:00
Koushik Dutta
99dcdd12cf postbeta 2025-06-16 08:41:56 -07:00
Koushik Dutta
b1861e4630 server: update deps 2025-06-16 08:41:47 -07:00
Koushik Dutta
193bfce979 core: publish 2025-06-14 19:56:03 -07:00
Koushik Dutta
5b7cc826a6 sdk/client: fix build issues 2025-06-14 19:54:05 -07:00
Koushik Dutta
8484d75e82 core: publish 2025-06-14 18:57:43 -07:00
Koushik Dutta
e8fef925bb ring: fix startup crash due to server changes 2025-06-14 16:01:11 -07:00
Koushik Dutta
fa200e1bbf sdk: update 2025-06-14 15:35:27 -07:00
Koushik Dutta
df0991b882 Merge branch 'main' of github.com:koush/scrypted 2025-06-14 13:30:03 -07:00
Koushik Dutta
93ff686000 sdk: add openai api for types 2025-06-14 13:29:58 -07:00
gtfrog
6ae9a5618d amcrest: fix NaN resolution values due to newline/cr, and add support for PAL named resolutions (#1833) 2025-06-14 10:35:43 -07:00
Koushik Dutta
c882b9a04e sdk: publish 2025-06-13 11:17:02 -07:00
Koushik Dutta
af4269be49 docker: include killall 2025-06-13 10:39:29 -07:00
Koushik Dutta
61ad99a3f6 docker: update flavors 2025-06-12 22:28:35 -07:00
Koushik Dutta
d71bbf1824 docker: better tags 2025-06-12 22:17:26 -07:00
Koushik Dutta
74674dab00 docker: lint 2025-06-12 21:35:58 -07:00
Koushik Dutta
247f860a23 intel: fix curl/gpg interaction maybe 2025-06-12 21:21:57 -07:00
Koushik Dutta
a801fe1f4e intel: fix curl/gpg interaction maybe 2025-06-12 21:18:28 -07:00
Koushik Dutta
6744851256 intel: add logging 2025-06-12 21:13:13 -07:00
Koushik Dutta
10569731aa intel: fix curl usage 2025-06-12 21:08:29 -07:00
Koushik Dutta
4965b1f99a intel: bump npu 2025-06-12 20:44:32 -07:00
Koushik Dutta
510250c60b intel: bump npu 2025-06-12 20:44:12 -07:00
Koushik Dutta
8e33775b0e docker: fix builds 2025-06-12 20:34:51 -07:00
Koushik Dutta
1077bd1f56 docker: add intel builds 2025-06-12 20:23:04 -07:00
Koushik Dutta
a485d8ae69 install: prep intel llm deps 2025-06-12 20:20:15 -07:00
Koushik Dutta
17f42762e7 install: prep intel llm deps 2025-06-12 20:08:08 -07:00
Koushik Dutta
49943a5408 postbeta 2025-06-09 12:09:39 -07:00
Koushik Dutta
585c638220 server: keepalive needs an explicit non-default duration. 2025-06-09 12:09:26 -07:00
Koushik Dutta
6767892c63 unifi-protect: fix login failures 2025-06-04 08:30:56 -07:00
Koushik Dutta
289555c03e unifi-protect: update api 2025-06-03 20:52:51 -07:00
Koushik Dutta
a563e17c56 core: publish ui 2025-05-31 09:02:33 -07:00
Koushik Dutta
54c317b217 detect: fix custom classifier filtering 2025-05-29 07:56:35 -07:00
Koushik Dutta
0df9c31480 core: publish ui 2025-05-28 12:05:47 -07:00
Koushik Dutta
19c8436256 Merge branch 'main' of github.com:koush/scrypted 2025-05-28 11:59:05 -07:00
Koushik Dutta
b73526674a detect: add custom classifier filtering 2025-05-28 11:59:00 -07:00
LV Nilesh
fd863f4ba3 Update Dockerfile.full (#1818) 2025-05-26 19:41:59 -07:00
LV Nilesh
634b65c216 Update Dockerfile.lite (#1817) 2025-05-26 19:41:51 -07:00
Brett Jia
548086403b server: bind single address if cluster address is 127.0.0.1 (#1820) 2025-05-26 19:41:17 -07:00
LV Nilesh
867432cd82 docker: Update Dockerfile to noble (#1813) 2025-05-24 21:25:54 -07:00
Koushik Dutta
b3cc914772 Merge branch 'main' of github.com:koush/scrypted 2025-05-23 10:01:39 -07:00
Koushik Dutta
b297a4d3d6 webrtc: fix possible crash if no video stream is negotiated 2025-05-23 10:01:32 -07:00
Mehmet Bayram
8144588bcf hikvision: improve supplemental light mode handling (#1812) 2025-05-22 20:13:37 -07:00
Koushik Dutta
f3265f5fb6 detect: cluster fixes 2025-05-21 12:33:04 -07:00
Koushik Dutta
f686812f01 core: update ui 2025-05-21 12:32:52 -07:00
Koushik Dutta
552787e06b detect: custom model support 2025-05-20 22:01:26 -07:00
Koushik Dutta
3c4de5af39 core: publish ui update 2025-05-20 21:12:49 -07:00
Koushik Dutta
e08df29373 ncnn: fp16 math 2025-05-20 10:34:42 -07:00
Koushik Dutta
1efb624681 proxmox: bump to 139 2025-05-13 08:04:48 -07:00
apocaliss92
09afc6c96c reolink: Fix events stopping for NVRs (#1804)
Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2025-05-09 20:38:17 -07:00
Koushik Dutta
666d2903e4 install: remove intel debug symbols 2025-05-08 18:31:42 -07:00
Koushik Dutta
24eb60bce1 install: bump ha 2025-05-08 13:21:32 -07:00
Koushik Dutta
d1951687be postbeta 2025-05-08 07:06:10 -07:00
Koushik Dutta
3c3c2c1610 core: patch lxc updater 2025-05-07 16:20:54 -07:00
Koushik Dutta
0f9106c639 postrelease 2025-05-07 10:18:33 -07:00
Koushik Dutta
ab00ade016 install: bump node 2025-05-07 09:32:35 -07:00
Koushik Dutta
6cfc3db05c server: fix package lock 2025-04-29 11:43:02 -07:00
Koushik Dutta
95aa58ce38 postbeta 2025-04-28 20:30:02 -07:00
Koushik Dutta
0d88b4746b ncnn: publish face/text 2025-04-28 12:54:15 -07:00
Koushik Dutta
8c4beeb3a0 Merge branch 'main' of github.com:koush/scrypted 2025-04-28 12:09:32 -07:00
Koushik Dutta
4846cfaddf ncnn: face recognition support 2025-04-28 12:09:26 -07:00
Koushik Dutta
4e14f7fd6f common: rtsp server basic auth fix 2025-04-28 12:09:13 -07:00
Roman Sokolov
266be72606 Fixed an issue for some devices. They send screen width as not even value. (#1797) 2025-04-27 14:04:00 -07:00
Koushik Dutta
6a1970c075 ncnn: update model list 2025-04-27 10:15:54 -07:00
Koushik Dutta
0575d98424 ncnn: publish 2025-04-26 21:31:06 -07:00
Koushik Dutta
cdf42fc1a2 rebroadcast: fix url escaping for basic auth 2025-04-24 19:23:23 -07:00
Koushik Dutta
fc1fabc49e common/webrtc: expand h265 keyframe types 2025-04-22 22:20:24 -07:00
Koushik Dutta
4e08daecb2 Merge branch 'main' of github.com:koush/scrypted 2025-04-21 09:02:30 -07:00
Koushik Dutta
58b27805ba common: fix sdp default rtpmap props 2025-04-21 09:02:25 -07:00
Koushik Dutta
b37c6bbd06 postbeta 2025-04-19 12:07:22 -07:00
Koushik Dutta
8eca02d819 server: move cluster fork timeout to prior to fork 2025-04-19 12:07:07 -07:00
Koushik Dutta
0efdb34114 postbeta 2025-04-19 10:53:40 -07:00
Koushik Dutta
1a25100de2 server: replace mime with mime-type which isnt esmodule 2025-04-19 10:53:30 -07:00
Koushik Dutta
51e0a8836d videoanalysis: fix occupancy sensor picking 2025-04-19 08:11:43 -07:00
Koushik Dutta
562d0839b7 videoanalysis: fix smart sensor picking 2025-04-19 08:10:37 -07:00
Koushik Dutta
e3df6accea videoanalysis: make sure duplciate nvr vs camera detections dont cause ui weirdness 2025-04-18 12:43:26 -07:00
Koushik Dutta
03d159a89c server: remove debug code 2025-04-18 11:51:00 -07:00
Koushik Dutta
4ead4726a9 postbeta 2025-04-18 11:49:45 -07:00
Koushik Dutta
b06ef623b3 server: fix potential socket leak if cluster server is down 2025-04-18 11:49:36 -07:00
Koushik Dutta
8edb157e2a snapshot: fix crop and scale 2025-04-17 16:03:03 -07:00
Koushik Dutta
155a1ceb38 rpc: publish 2025-04-15 15:10:30 -07:00
Koushik Dutta
1cb6212fc6 webrtc: implement default clocks for assigned payload types 2025-04-15 07:53:28 -07:00
Koushik Dutta
ea628a7130 wip: unifi 2024-11-28 08:47:11 -08:00
142 changed files with 10732 additions and 3349 deletions

View File

@@ -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"]
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

View File

@@ -20,10 +20,11 @@ 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-full", ".s6", "noble-full", "full"],
["noble-lite", "", "noble-lite", "lite"],
["noble-lite", ".router", "noble-router", "router"],
]
steps:
- name: Check out the repo
@@ -94,19 +95,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

View File

@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
@@ -21,28 +22,29 @@
},
"../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.3",
"version": "0.5.29",
"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,
@@ -3393,6 +3404,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,

View File

@@ -12,6 +12,7 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},

5
common/src/devices.ts Normal file
View 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));
}

8
common/src/json.ts Normal file
View File

@@ -0,0 +1,8 @@
export function safeParseJson(value: string) {
try {
return JSON.parse(value);
}
catch (e) {
}
}

View File

@@ -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 = '';

View File

@@ -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,
}
}

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "v0.130.1-noble-full"
version: "v0.139.0-noble-full"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"

View File

@@ -1,4 +1,4 @@
ARG BASE="20-jammy-full"
ARG BASE="noble-full"
FROM ghcr.io/koush/scrypted-common:${BASE}
WORKDIR /

View File

@@ -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

View 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

View File

@@ -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"

View File

@@ -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

View File

@@ -1,8 +1,10 @@
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

View File

@@ -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 /

View File

@@ -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:

View File

@@ -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.12.5/intel-igc-core-2_2.12.5+19302_amd64.deb
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.12.5/intel-igc-opencl-2_2.12.5+19302_amd64.deb
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-ocloc-dbgsym_25.22.33944.8-0_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-ocloc_25.22.33944.8-0_amd64.deb
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-opencl-icd-dbgsym_25.22.33944.8-0_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/intel-opencl-icd_25.22.33944.8-0_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/libigdgmm12_22.7.0_amd64.deb
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/libze-intel-gpu1-dbgsym_25.22.33944.8-0_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.22.33944.8/libze-intel-gpu1_25.22.33944.8-0_amd64.deb
set +e
dpkg -i *.deb

View File

@@ -38,15 +38,15 @@ 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.22.4
# 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
NPU_VERSION=1.19.0
NPU_VERSION_DATE=20250707-16111289554
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
# firmware can only be installed on host. will cause problems inside container.
if [ -n "$INTEL_FW_NPU" ]

View 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

View File

@@ -36,7 +36,8 @@ 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
# cuda-drivers does not work with blackwell for some reason, container toolkit it broken IIRC.
apt -y install nvidia-open
apt -y install nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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');

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/client",
"version": "1.3.13",
"version": "1.3.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.13",
"version": "1.3.17",
"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.23"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -83,11 +83,59 @@
}
},
"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.23",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.23.tgz",
"integrity": "sha512-is/UJHgS3lvEuXyb+C/OPeIP5CKp+M6SQt1l/WFJr1Oj+KYYHGU8Ztlh/qOmAWgONhg286N4/cLNzTtAAh4YnA==",
"license": "ISC",
"peer": true
"peer": true,
"dependencies": {
"openai": "^5.3.0"
}
},
"node_modules/@scrypted/types/node_modules/openai": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.3.0.tgz",
"integrity": "sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/@scrypted/types/node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"optional": true,
"peer": true,
"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/@socket.io/component-emitter": {
"version": "3.1.2",
@@ -134,19 +182,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.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": "~6.20.0"
"undici-types": "~7.8.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": {
@@ -684,9 +732,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.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": {
@@ -698,9 +746,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.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"dev": true,
"license": "MIT"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.3.13",
"version": "1.3.17",
"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.23"
},
"dependencies": {
"engine.io-client": "^6.6.3",

View File

@@ -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"
}
}
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/rpc",
"version": "0.0.7",
"version": "0.0.8",
"description": "",
"main": "dist/index.js",
"scripts": {

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.165",
"version": "0.0.166",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -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;

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.3.120",
"version": "0.3.129",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.3.120",
"version": "0.3.129",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.3.120",
"version": "0.3.129",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -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> {

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}

View File

@@ -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';
@@ -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;
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}

View File

@@ -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"

View 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

View 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

View File

@@ -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)

View File

@@ -1,3 +1,5 @@
coremltools==8.0
Pillow==10.3.0
opencv-python-headless==4.10.0.84
transformers==4.52.4

View File

@@ -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`,

View File

@@ -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"
}
}

View File

@@ -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"
}

View File

@@ -3,9 +3,7 @@ from __future__ import annotations
import ast
import asyncio
import concurrent.futures
import os
import re
import threading
import traceback
from typing import Any, List, Tuple
@@ -17,12 +15,13 @@ from scrypted_sdk import Setting, SettingValue
import ncnn
from common import yolo
from .custom_detection import NCNNCustomDetection
try:
from ncnn.face_recognition import NCNNFaceRecognition
from nc.face_recognition import NCNNFaceRecognition
except:
NCNNFaceRecognition = None
try:
from ncnn.text_recognition import NCNNTextRecognition
from nc.text_recognition import NCNNTextRecognition
except:
NCNNTextRecognition = None
from predict import Prediction, PredictPlugin
@@ -33,18 +32,14 @@ prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "NCNN-Prepare")
availableModels = [
"Default",
"scrypted_yolov10m_320",
"scrypted_yolov10n_320",
"scrypted_yolo_nas_s_320",
"scrypted_yolov9e_320",
"scrypted_yolov9c_relu_320",
"scrypted_yolov9m_relu_320",
"scrypted_yolov9s_relu_320",
"scrypted_yolov9t_relu_320",
"scrypted_yolov9c_320",
"scrypted_yolov9m_320",
"scrypted_yolov9s_320",
"scrypted_yolov9t_320",
"scrypted_yolov6n_320",
"scrypted_yolov6s_320",
"scrypted_yolov8n_320",
"ssdlite_mobilenet_v2",
"yolov4-tiny",
]
@@ -84,6 +79,8 @@ class NCNNPlugin(
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":
@@ -106,7 +103,7 @@ class NCNNPlugin(
}
files = [
f"{model}/best_converted.ncnn.bin",
f"{model}//best_converted.ncnn.param",
f"{model}/best_converted.ncnn.param",
]
for f in files:
@@ -123,14 +120,10 @@ class NCNNPlugin(
self.net = ncnn.Net()
# self.net.opt.use_vulkan_compute = True
# self.net.opt.use_winograd_convolution = False
# self.net.opt.use_sgemm_convolution = False
self.net.opt.use_vulkan_compute = True
# self.net.opt.use_fp16_packed = False
# self.net.opt.use_fp16_storage = False
# self.net.opt.use_fp16_arithmetic = False
# self.net.opt.use_int8_storage = False
# self.net.opt.use_int8_arithmetic = False
self.net.load_param(paramFile)
self.net.load_model(binFile)
@@ -153,55 +146,55 @@ class NCNNPlugin(
# self.loop = asyncio.get_event_loop()
# self.minThreshold = 0.2
# self.faceDevice = None
# self.textDevice = None
self.faceDevice = None
self.textDevice = None
# if not self.forked:
# asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
if not self.forked:
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
# async def prepareRecognitionModels(self):
# try:
# devices = [
# {
# "nativeId": "facerecognition",
# "type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
# "interfaces": [
# scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
# scrypted_sdk.ScryptedInterface.ObjectDetection.value,
# ],
# "name": "NCNN Face Recognition",
# },
# ]
async def prepareRecognitionModels(self):
try:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "NCNN Face Recognition",
},
)
# if NCNNTextRecognition:
# devices.append(
# {
# "nativeId": "textrecognition",
# "type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
# "interfaces": [
# scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
# scrypted_sdk.ScryptedInterface.ObjectDetection.value,
# ],
# "name": "NCNN Text Recognition",
# },
# )
if NCNNTextRecognition:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "NCNN Text Recognition",
},
)
except:
pass
# await scrypted_sdk.deviceManager.onDevicesChanged(
# {
# "devices": devices,
# }
# )
# except:
# pass
# async def getDevice(self, nativeId: str) -> Any:
# if nativeId == "facerecognition":
# self.faceDevice = self.faceDevice or NCNNFaceRecognition(self, nativeId)
# return self.faceDevice
# if nativeId == "textrecognition":
# self.textDevice = self.textDevice or NCNNTextRecognition(self, nativeId)
# return self.textDevice
# raise Exception("unknown device")
async def getDevice(self, nativeId: str) -> Any:
if nativeId == "facerecognition":
self.faceDevice = self.faceDevice or NCNNFaceRecognition(self, nativeId)
return self.faceDevice
if nativeId == "textrecognition":
self.textDevice = self.textDevice or NCNNTextRecognition(self, nativeId)
return self.textDevice
custom_model = self.custom_models.get(nativeId, None)
if custom_model:
return custom_model
custom_model = NCNNCustomDetection(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"
@@ -239,6 +232,8 @@ class NCNNPlugin(
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
im = im.astype(np.float32) / 255.0
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
im = im.reshape((1, 3, 320, 320)).squeeze(0)
im = np.ascontiguousarray(im) # contiguous
return im

View File

@@ -0,0 +1 @@
../../../openvino/src/ov/async_infer.py

View File

@@ -0,0 +1,82 @@
from __future__ import annotations
import asyncio
import numpy as np
from PIL import Image
import os
import ncnn
from predict.custom_detect import CustomDetection
from scrypted_sdk import ObjectsDetected
import concurrent.futures
class NCNNCustomDetection(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")
self.prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "prepare-custom")
def loadModel(self, files: list[str]):
# find the xml file in the files list
bin_files = [f for f in files if f.lower().endswith('.bin')]
if not bin_files:
raise ValueError("No bkin file found in the provided files list")
bin_file = bin_files[0]
param_files = [f for f in files if f.lower().endswith('.param')]
if not param_files:
raise ValueError("No param file found in the provided files list")
param_file = param_files[0]
net = ncnn.Net()
net.opt.use_vulkan_compute = True
# net.opt.use_fp16_packed = False
# net.opt.use_fp16_storage = False
# net.opt.use_fp16_arithmetic = False
net.load_param(param_file)
net.load_model(bin_file)
input_name = net.input_names()[0]
return net, input_name
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
def prepare():
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
im = im.astype(np.float32) / 255.0
if self.model_config.get("mean", None) and self.model_config.get("std", None):
mean = np.array(self.model_config["mean"])
std = np.array(self.model_config["std"])
mean = mean.reshape(1, -1, 1, 1)
std = std.reshape(1, -1, 1, 1)
im = (im - mean) / std
im = im.astype(np.float32)
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
im = im.squeeze(0)
im = np.ascontiguousarray(im) # contiguous
return im
def predict(input_tensor):
net, input_name = self.model
input_ncnn = ncnn.Mat(input_tensor)
ex = net.create_extractor()
ex.input(input_name, input_ncnn)
output_ncnn = ncnn.Mat()
ex.extract("out0", output_ncnn)
output_tensors = np.array(output_ncnn)
return output_tensors
input_tensor = await asyncio.get_event_loop().run_in_executor(
self.prepareExecutor, lambda: prepare()
)
return await asyncio.get_event_loop().run_in_executor(
self.detectExecutor, lambda: predict(input_tensor)
)

View File

@@ -0,0 +1,111 @@
from __future__ import annotations
import asyncio
import numpy as np
from PIL import Image
import ncnn
from nc import async_infer
from predict.face_recognize import FaceRecognizeDetection
faceDetectPrepare, faceDetectPredict = async_infer.create_executors("FaceDetect")
faceRecognizePrepare, faceRecognizePredict = async_infer.create_executors(
"FaceRecognize"
)
class NCNNFaceRecognition(FaceRecognizeDetection):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
self.prefer_relu = True
def downloadModel(self, model: str):
scrypted_yolov9 = "scrypted_yolov9" in model
ncnnmodel = "best_converted" if scrypted_yolov9 else model
model_version = "v1"
files = [
f"{model}/{ncnnmodel}.ncnn.bin",
f"{model}/{ncnnmodel}.ncnn.param",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/ncnn-models/raw/main/{f}",
f"{model_version}/{f}",
)
if ".bin" in p:
binFile = p
if ".param" in p:
paramFile = p
net = ncnn.Net()
net.opt.use_vulkan_compute = True
# net.opt.use_fp16_packed = False
# net.opt.use_fp16_storage = False
# net.opt.use_fp16_arithmetic = False
net.load_param(paramFile)
net.load_model(binFile)
input_name = net.input_names()[0]
return [net, input_name]
async def predictDetectModel(self, input: Image.Image):
def prepare():
im = np.array(input)
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
im = im.astype(np.float32) / 255.0
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
im = im.reshape((1, 3, 320, 320)).squeeze(0)
im = np.ascontiguousarray(im) # contiguous
return im
def predict(input_tensor):
net, input_name = self.detectModel
input_ncnn = ncnn.Mat(input_tensor)
ex = net.create_extractor()
ex.input(input_name, input_ncnn)
output_ncnn = ncnn.Mat()
ex.extract("out0", output_ncnn)
output_tensors = np.array(output_ncnn)
return output_tensors
input_tensor = await asyncio.get_event_loop().run_in_executor(
faceDetectPrepare, lambda: prepare()
)
return await asyncio.get_event_loop().run_in_executor(
faceDetectPredict, lambda: predict(input_tensor)
)
async def predictFaceModel(self, input: np.ndarray):
def prepare():
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
im = input.squeeze(0)
im = np.ascontiguousarray(im) # contiguous
return im
def predict(input_tensor):
net, input_name = self.faceModel
input_ncnn = ncnn.Mat(input_tensor)
ex = net.create_extractor()
ex.input(input_name, input_ncnn)
output_ncnn = ncnn.Mat()
ex.extract("out0", output_ncnn)
output_tensors = np.array(output_ncnn)
return output_tensors
input_tensor = await asyncio.get_event_loop().run_in_executor(
faceDetectPrepare, lambda: prepare()
)
return await asyncio.get_event_loop().run_in_executor(
faceDetectPredict, lambda: predict(input_tensor)
)

View File

@@ -0,0 +1,105 @@
from __future__ import annotations
import asyncio
import numpy as np
import ncnn
from nc import async_infer
from predict.text_recognize import TextRecognition
textDetectPrepare, textDetectPredict = async_infer.create_executors("TextDetect")
textRecognizePrepare, textRecognizePredict = async_infer.create_executors(
"TextRecognize"
)
class NCNNTextRecognition(TextRecognition):
def downloadModel(self, model: str):
model_version = "v1"
files = [
f"{model}/{model}.ncnn.bin",
f"{model}/{model}.ncnn.param",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/ncnn-models/raw/main/{f}",
f"{model_version}/{f}",
)
if ".bin" in p:
binFile = p
if ".param" in p:
paramFile = p
net = ncnn.Net()
net.opt.use_vulkan_compute = True
# net.opt.use_fp16_packed = False
# net.opt.use_fp16_storage = False
# net.opt.use_fp16_arithmetic = False
net.load_param(paramFile)
net.load_model(binFile)
input_name = net.input_names()[0]
return [net, input_name]
async def predictDetectModel(self, input: np.ndarray):
def prepare():
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
im = input.squeeze(0)
im = np.ascontiguousarray(im) # contiguous
return im
def predict(input_tensor):
net, input_name = self.detectModel
input_ncnn = ncnn.Mat(input_tensor)
ex = net.create_extractor()
ex.input(input_name, input_ncnn)
output_ncnn = ncnn.Mat()
ex.extract("out0", output_ncnn)
output_tensors = np.array(output_ncnn)
output_tensors = output_tensors.transpose((1, 2, 0))
# readd a batch dimension
output_tensors = np.expand_dims(output_tensors, axis=0)
return output_tensors
input_tensor = await asyncio.get_event_loop().run_in_executor(
textDetectPrepare, lambda: prepare()
)
return await asyncio.get_event_loop().run_in_executor(
textDetectPredict, lambda: predict(input_tensor)
)
async def predictTextModel(self, input: np.ndarray):
def prepare():
# no batch? https://github.com/Tencent/ncnn/issues/5990#issuecomment-2832927105
im = input.squeeze(0)
im = np.ascontiguousarray(im) # contiguous
return im
def predict(input_tensor):
net, input_name = self.textModel
input_ncnn = ncnn.Mat(input_tensor)
ex = net.create_extractor()
ex.input(input_name, input_ncnn)
output_ncnn = ncnn.Mat()
ex.extract("out0", output_ncnn)
output_tensors = np.array(output_ncnn)
# readd a batch dimension
output_tensors = np.expand_dims(output_tensors, axis=0)
return output_tensors
input_tensor = await asyncio.get_event_loop().run_in_executor(
textRecognizePrepare, lambda: prepare()
)
return await asyncio.get_event_loop().run_in_executor(
textRecognizePredict, lambda: predict(input_tensor)
)

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/objectdetector",
"version": "0.1.68",
"version": "0.1.72",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/objectdetector",
"version": "0.1.68",
"version": "0.1.72",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.1.68",
"version": "0.1.72",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -138,7 +138,6 @@ export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements Vid
try {
reader();
const flush = async () => { };
while (!finished) {
frameDeferred = new Deferred();
@@ -151,9 +150,7 @@ export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements Vid
yield {
__json_copy_serialize_children: true,
timestamp: 0,
queued: 0,
image,
flush,
};
}
finally {

View File

@@ -9,7 +9,8 @@ import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonIntersectsBoundingBox } from './polygon';
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
import { getAllDevices, safeParseJson } from './util';
import { safeParseJson } from '../../../common/src/json';
import { getAllDevices } from '../../../common/src/devices';
import { FFmpegAudioDetectionMixinProvider } from './ffmpeg-audiosensor';
@@ -94,7 +95,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
onGet: async () => {
const choices = [
'Default',
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
...getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
return {
hide: this.model?.decoder,
@@ -690,7 +691,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (frameGenerator === 'Default')
frameGenerator = this.plugin.storageSettings.values.defaultDecoder || 'Default';
const pipelines = getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
const pipelines = getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
const webassembly = sdk.systemManager.getDeviceById('@scrypted/nvr', 'decoder') || undefined;
const gstreamer = sdk.systemManager.getDeviceById('@scrypted/python-codecs', 'gstreamer') || undefined;
const libav = sdk.systemManager.getDeviceById('@scrypted/python-codecs', 'libav') || undefined;
@@ -1026,7 +1027,7 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
onGet: async () => {
const choices = [
'Default',
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
...getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
return {
choices,

View File

@@ -85,7 +85,8 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
this.storageSettings.settings.detections.onGet = async () => {
const objectDetector: ObjectDetector = this.storageSettings.values.objectDetector;
const choices = (await objectDetector?.getObjectTypes?.())?.classes || [];
const classes = (await objectDetector?.getObjectTypes?.())?.classes || [];
const choices = [...new Set(classes)];
return {
hide: !objectDetector,
choices,

View File

@@ -104,7 +104,8 @@ export class SmartOccupancySensor extends ScryptedDeviceBase implements Settings
this.storageSettings.settings.detections.onGet = async () => {
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
const choices = (await objectDetection?.getDetectionModel())?.classes || [];
const classes = (await objectDetection?.getDetectionModel())?.classes || []
const choices = [...new Set(classes)];
return {
hide: !objectDetection,
choices,

View File

@@ -1,13 +0,0 @@
import sdk from '@scrypted/sdk';
export function safeParseJson(value: string) {
try {
return JSON.parse(value);
}
catch (e) {
}
}
export function getAllDevices() {
return Object.keys(sdk.systemManager.getSystemState()).map(id => sdk.systemManager.getDeviceById(id));
}

View File

@@ -1,20 +1,4 @@
{
// docker installation
"scrypted.debugHost": "scrypted-nvr",
"scrypted.serverRoot": "/server",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// local checkout
// "scrypted.debugHost": "127.0.0.1",
// "scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "koushik-winvm",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}
"scrypted.debugHost": "koushik-ubuntu24",
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/onnx",
"version": "0.1.120",
"version": "0.1.127",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/onnx",
"version": "0.1.120",
"version": "0.1.127",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -33,6 +33,8 @@
"runtime": "python",
"type": "API",
"interfaces": [
"ScryptedSystemDevice",
"DeviceCreator",
"DeviceProvider",
"Settings",
"ClusterForkInterface",
@@ -48,5 +50,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.120"
"version": "0.1.127"
}

View File

@@ -17,10 +17,12 @@ from PIL import Image
from scrypted_sdk.other import SettingValue
from scrypted_sdk.types import Setting
from .custom_detection import ONNXCustomDetection
import common.yolo as yolo
from predict import PredictPlugin
from .face_recognition import ONNXFaceRecognition
from .clip_embedding import ONNXClipEmbedding
try:
from .text_recognition import ONNXTextRecognition
@@ -58,6 +60,8 @@ class ONNXPlugin(
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":
@@ -162,13 +166,14 @@ class ONNXPlugin(
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,
@@ -178,10 +183,10 @@ class ONNXPlugin(
],
"name": "ONNX Face Recognition",
},
]
)
if ONNXTextRecognition:
devices.append(
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -193,9 +198,17 @@ class ONNXPlugin(
},
)
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": "ONNX CLIP Embedding",
}
)
except:
@@ -208,7 +221,16 @@ class ONNXPlugin(
elif nativeId == "textrecognition":
self.textDevice = self.textDevice or ONNXTextRecognition(self, nativeId)
return self.textDevice
raise Exception("unknown device")
elif nativeId == "clipembedding":
self.clipDevice = self.clipDevice or ONNXClipEmbedding(self, nativeId)
return self.clipDevice
custom_model = self.custom_models.get(nativeId, None)
if custom_model:
return custom_model
custom_model = ONNXCustomDetection(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"

View File

@@ -0,0 +1,122 @@
from __future__ import annotations
import asyncio
from typing import Any
import numpy as np
import onnxruntime
from PIL import Image
import threading
from predict.clip import ClipEmbedding
from scrypted_sdk import ObjectsDetected
import concurrent.futures
import sys
import platform
class ONNXClipEmbedding(ClipEmbedding):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
def getFiles(self):
return [
"text.onnx",
"vision.onnx",
]
def loadModel(self, files):
# find the xml file in the files list
text_onnx = [f for f in files if f.lower().endswith('text.onnx')]
if not text_onnx:
raise ValueError("No onnx model file found in the provided files list")
text_onnx = text_onnx[0]
vision_onnx = [f for f in files if f.lower().endswith('vision.onnx')]
if not vision_onnx:
raise ValueError("No onnx model file found in the provided files list")
vision_onnx = vision_onnx[0]
compiled_models_array = []
compiled_models = {}
deviceIds = self.plugin.deviceIds
for deviceId in deviceIds:
sess_options = onnxruntime.SessionOptions()
providers: list[str] = []
if sys.platform == "darwin":
providers.append("CoreMLExecutionProvider")
if "linux" in sys.platform and platform.machine() == "x86_64":
deviceId = int(deviceId)
providers.append(("CUDAExecutionProvider", {"device_id": deviceId}))
providers.append("CPUExecutionProvider")
text_model = onnxruntime.InferenceSession(
text_onnx, sess_options=sess_options, providers=providers
)
vision_model = onnxruntime.InferenceSession(
vision_onnx, sess_options=sess_options, providers=providers
)
compiled_models_array.append((text_model, vision_model))
def executor_initializer():
thread_name = threading.current_thread().name
interpreter = compiled_models_array.pop()
compiled_models[thread_name] = interpreter
print("Runtime initialized on thread {}".format(thread_name))
executor = concurrent.futures.ThreadPoolExecutor(
initializer=executor_initializer,
max_workers=len(compiled_models_array),
thread_name_prefix="custom",
)
return compiled_models, executor
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
compiled_models, executor = self.model
def predict():
inputs = self.processor(images=input, return_tensors="np", padding="max_length", truncation=True)
compiled_model = compiled_models[threading.current_thread().name]
_, vision_session = compiled_model
vision_predictions = vision_session.run(None, {vision_session.get_inputs()[0].name: inputs.data['pixel_values']})
image_embeds = vision_predictions[0]
# 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
objs = await asyncio.get_event_loop().run_in_executor(
executor, predict
)
return objs
async def getTextEmbedding(self, input):
compiled_models, executor = self.model
def predict():
inputs = self.processor(text=input, return_tensors="np", padding="max_length", truncation=True)
compiled_model = compiled_models[threading.current_thread().name]
text_session, _ = compiled_model
text_inputs = {
text_session.get_inputs()[0].name: inputs['input_ids'],
text_session.get_inputs()[1].name: inputs['attention_mask']
}
text_predictions = text_session.run(None, text_inputs)
text_embeds = text_predictions[0]
return bytearray(text_embeds.astype(np.float32).tobytes())
objs = await asyncio.get_event_loop().run_in_executor(
executor, predict
)
return objs

View File

@@ -0,0 +1,127 @@
from __future__ import annotations
import asyncio
import numpy as np
from PIL import Image
import onnxruntime
import sys
import threading
import platform
from predict.custom_detect import CustomDetection
from scrypted_sdk import ObjectsDetected
import concurrent.futures
class ONNXCustomDetection(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
onnx_files = [f for f in files if f.lower().endswith('.onnx')]
if not onnx_files:
raise ValueError("No Manifest.json file found in the provided files list")
onnx_file = onnx_files[0]
compiled_models_array = []
compiled_models = {}
deviceIds = self.plugin.deviceIds
for deviceId in deviceIds:
sess_options = onnxruntime.SessionOptions()
providers: list[str] = []
if sys.platform == "darwin":
providers.append("CoreMLExecutionProvider")
if "linux" in sys.platform and platform.machine() == "x86_64":
deviceId = int(deviceId)
providers.append(("CUDAExecutionProvider", {"device_id": deviceId}))
providers.append("CPUExecutionProvider")
compiled_model = onnxruntime.InferenceSession(
onnx_file, sess_options=sess_options, providers=providers
)
compiled_models_array.append(compiled_model)
input = compiled_model.get_inputs()[0]
input_name = input.name
def executor_initializer():
thread_name = threading.current_thread().name
interpreter = compiled_models_array.pop()
compiled_models[thread_name] = interpreter
print("Runtime initialized on thread {}".format(thread_name))
executor = concurrent.futures.ThreadPoolExecutor(
initializer=executor_initializer,
max_workers=len(compiled_models_array),
thread_name_prefix="custom",
)
prepareExecutor = concurrent.futures.ThreadPoolExecutor(
max_workers=len(compiled_models_array),
thread_name_prefix="custom-prepare",
)
return compiled_models, input_name, prepareExecutor, executor
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
compiled_models, input_name, prepareExecutor, executor = self.model
def predict():
if self.model_config.get("mean", None) and self.model_config.get("std", None):
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
im = im.astype(np.float32) / 255.0
mean = np.array(self.model_config["mean"])
std = np.array(self.model_config["std"])
mean = mean.reshape(1, -1, 1, 1)
std = std.reshape(1, -1, 1, 1)
im = (im - mean) / std
im = np.ascontiguousarray(im.astype(np.float32)) # contiguous
out_dict = model.predict({inputName: im})
else:
out_dict = self.model.predict({self.inputName: input})
results = list(out_dict.values())[0][0]
return results
def prepare():
im = np.array(input)
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
im = im.astype(np.float32) / 255.0
if self.model_config.get("mean", None) and self.model_config.get("std", None):
mean = np.array(self.model_config["mean"])
std = np.array(self.model_config["std"])
mean = mean.reshape(1, -1, 1, 1)
std = std.reshape(1, -1, 1, 1)
im = (im - mean) / std
im = im.astype(np.float32)
im = np.ascontiguousarray(im)
return im
def predict(input_tensor):
compiled_model = compiled_models[threading.current_thread().name]
output_tensors = compiled_model.run(None, {input_name: input_tensor})
return output_tensors
input_tensor = await asyncio.get_event_loop().run_in_executor(
prepareExecutor, lambda: prepare()
)
objs = await asyncio.get_event_loop().run_in_executor(
executor, lambda: predict(input_tensor)
)
return objs[0][0]

View File

@@ -1,7 +1,7 @@
# uncomment to require cuda 12, but most stuff is still targetting cuda 11.
# however, stuff targetted for cuda 11 can still run on cuda 12.
# --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
onnxruntime-gpu==1.19.2; 'darwin' not in sys_platform and platform_machine != 'aarch64'
onnxruntime-gpu==1.22.0; 'darwin' not in sys_platform and platform_machine != 'aarch64'
# cpu and coreml execution provider
onnxruntime; 'darwin' in sys_platform or platform_machine == 'aarch64'
# nightly?
@@ -9,3 +9,5 @@ onnxruntime; 'darwin' in sys_platform or platform_machine == 'aarch64'
Pillow==10.3.0
opencv-python-headless==4.10.0.84
transformers==4.52.4

View File

@@ -1,36 +1,43 @@
{
"name": "@scrypted/openvino",
"version": "0.1.177",
"version": "0.1.185",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/openvino",
"version": "0.1.177",
"version": "0.1.185",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.29",
"version": "0.5.20",
"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",
@@ -42,11 +49,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"
}
},
"../sdk": {
@@ -61,25 +66,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"
}
}
}

View File

@@ -33,6 +33,8 @@
"runtime": "python",
"type": "API",
"interfaces": [
"ScryptedSystemDevice",
"DeviceCreator",
"DeviceProvider",
"Settings",
"ClusterForkInterface",
@@ -48,5 +50,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.177"
"version": "0.1.185"
}

View File

@@ -0,0 +1,25 @@
from urllib.parse import urlparse, urlunparse
def replace_last_path_component(url, new_path):
# Parse the original URL
parsed_url = urlparse(url)
# Split the path into components
path_components = parsed_url.path.split('/')
# Remove the last component
if len(path_components) > 1:
path_components.pop()
else:
raise ValueError("URL path has no components to replace")
# Join the path components back together
new_path = '/'.join(path_components) + '/' + new_path
# Create a new parsed URL with the updated path
new_parsed_url = parsed_url._replace(path=new_path)
# Reconstruct the URL
new_url = urlunparse(new_parsed_url)
return new_url

View File

@@ -18,6 +18,7 @@ import common.yolo as yolo
from predict import Prediction, PredictPlugin
from predict.rectangle import Rectangle
from .custom_detection import OpenVINOCustomDetection
from .face_recognition import OpenVINOFaceRecognition
try:
@@ -25,6 +26,8 @@ try:
except:
OpenVINOTextRecognition = None
from .clip_embedding import OpenVINOClipEmbedding
predictExecutor = concurrent.futures.ThreadPoolExecutor(
thread_name_prefix="OpenVINO-Predict"
)
@@ -104,6 +107,8 @@ class OpenVINOPlugin(
def __init__(self, nativeId: str | None = None, forked: bool = False):
super().__init__(nativeId=nativeId, forked=forked)
self.custom_models = {}
self.core = ov.Core()
dump_device_properties(self.core)
available_devices = self.core.available_devices
@@ -297,6 +302,7 @@ class OpenVINOPlugin(
self.faceDevice = None
self.textDevice = None
self.clipDevice = None
if not self.forked:
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
@@ -395,7 +401,7 @@ class OpenVINOPlugin(
async def prepareRecognitionModels(self):
try:
devices = [
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -405,10 +411,10 @@ class OpenVINOPlugin(
],
"name": "OpenVINO Face Recognition",
},
]
)
if OpenVINOTextRecognition:
devices.append(
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -417,12 +423,20 @@ class OpenVINOPlugin(
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "OpenVINO Text Recognition",
},
}
)
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": "OpenVINO CLIP Embedding",
}
)
except:
@@ -435,4 +449,13 @@ class OpenVINOPlugin(
elif nativeId == "textrecognition":
self.textDevice = self.textDevice or OpenVINOTextRecognition(self, nativeId)
return self.textDevice
raise Exception("unknown device")
elif nativeId == "clipembedding":
self.clipDevice = self.clipDevice or OpenVINOClipEmbedding(self, nativeId)
return self.clipDevice
custom_model = self.custom_models.get(nativeId, None)
if custom_model:
return custom_model
custom_model = OpenVINOCustomDetection(self, nativeId)
self.custom_models[nativeId] = custom_model
await custom_model.reportDevice(nativeId, custom_model.providedName)
return custom_model

View File

@@ -2,6 +2,6 @@ import concurrent.futures
def create_executors(name: str):
prepare = concurrent.futures.ThreadPoolExecutor(1, f"OpenVINO-{name}Prepare")
predict = concurrent.futures.ThreadPoolExecutor(1, f"OpenVINO-{name}Predict")
prepare = concurrent.futures.ThreadPoolExecutor(1, f"{name}Prepare")
predict = concurrent.futures.ThreadPoolExecutor(1, f"{name}Predict")
return prepare, predict

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
import asyncio
from typing import Any
import numpy as np
import openvino.runtime as ov
from PIL import Image
from ov import async_infer
from predict.clip import ClipEmbedding
from scrypted_sdk import ObjectsDetected
clipPrepare, clipPredict = async_infer.create_executors("ClipPredict")
# _int8 is available but seems slower in addition to the accuracy loss.
model_suffix = ""
text_xml_name = f"text{model_suffix}.xml"
vision_xml_name = f"vision{model_suffix}.xml"
class OpenVINOClipEmbedding(ClipEmbedding):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
def getFiles(self):
return [
f"openvino/{text_xml_name}",
f"openvino/text{model_suffix}.bin",
f"openvino/{vision_xml_name}",
f"openvino/vision{model_suffix}.bin"
]
def loadModel(self, files):
# find the xml file in the files list
text_xml = [f for f in files if f.lower().endswith(text_xml_name)]
if not text_xml:
raise ValueError("No XML model file found in the provided files list")
text_xml = text_xml[0]
vision_xml = [f for f in files if f.lower().endswith(vision_xml_name)]
if not vision_xml:
raise ValueError("No XML model file found in the provided files list")
vision_xml = vision_xml[0]
textModel = self.plugin.core.compile_model(text_xml, self.plugin.mode)
model = self.plugin.core.read_model(vision_xml)
# for some reason this is exporting as dynamic axes and causing npu to crash
model.reshape([1, 3, 224, 224])
visionModel = self.plugin.core.compile_model(model, self.plugin.mode)
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(inputs.data['pixel_values'])
image_embeds = vision_predictions[0]
# 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(
clipPredict, 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((inputs.data['input_ids'], inputs.data['attention_mask']))
text_embeds = text_predictions[0]
return bytearray(text_embeds.astype(np.float32).tobytes())
ret = await asyncio.get_event_loop().run_in_executor(
clipPredict, lambda: predict()
)
return ret

View File

@@ -0,0 +1,56 @@
from __future__ import annotations
import asyncio
import numpy as np
import openvino.runtime as ov
from PIL import Image
from ov import async_infer
from predict.custom_detect import CustomDetection
from scrypted_sdk import ObjectsDetected
customDetectPrepare, customDetectPredict = async_infer.create_executors("CustomDetect")
class OpenVINOCustomDetection(CustomDetection):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
self.prefer_relu = True
def loadModel(self, files: list[str]):
# find the xml file in the files list
xml_files = [f for f in files if f.lower().endswith('.xml')]
if not xml_files:
raise ValueError("No XML model file found in the provided files list")
xmlFile = xml_files[0]
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
def predict():
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
im = im.astype(np.float32) / 255.0
if self.model_config.get("mean", None) and self.model_config.get("std", None):
mean = np.array(self.model_config["mean"]).astype(np.float32)
std = np.array(self.model_config["std"]).astype(np.float32)
mean = mean.reshape(1, -1, 1, 1)
std = std.reshape(1, -1, 1, 1)
im = (im - mean) / std
im = im.astype(np.float32)
im = np.ascontiguousarray(im)
infer_request = self.model.create_infer_request()
tensor = ov.Tensor(array=im)
infer_request.set_input_tensor(tensor)
output_tensors = infer_request.infer()
ret = output_tensors[0][0]
return ret
ret = await asyncio.get_event_loop().run_in_executor(
customDetectPredict, lambda: predict()
)
return ret

View File

@@ -32,7 +32,9 @@ class OpenVINOTextRecognition(TextRecognition):
model.reshape([1, 1, 64, 384])
return self.plugin.core.compile_model(model, self.plugin.mode)
else:
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
model = self.plugin.core.read_model(xmlFile)
model.reshape([1, 3, 640, 640])
return self.plugin.core.compile_model(model, self.plugin.mode)
async def predictDetectModel(self, input: np.ndarray):
def predict():

View File

@@ -1,5 +1,8 @@
from __future__ import annotations
import json
import random
import re
import asyncio
import math
import os
@@ -45,7 +48,7 @@ class Prediction:
self.embedding = embedding
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface, scrypted_sdk.ScryptedSystemDevice, scrypted_sdk.DeviceCreator, scrypted_sdk.DeviceProvider):
labels: dict
def __init__(
@@ -56,6 +59,10 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
):
super().__init__(nativeId=nativeId)
self.systemDevice = {
"deviceCreator": "Model",
}
self.plugin = plugin
# self.clusterIndex = 0
@@ -356,6 +363,10 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
ret = await result.getTextRecognition()
elif self.nativeId == "facerecognition":
ret = await result.getFaceRecognition()
elif self.nativeId == "clipembedding":
ret = await result.getClipEmbedding()
else:
ret = await result.getCustomDetection(self.nativeId)
return ret
async def startCluster(self):
@@ -394,6 +405,79 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
asyncio.ensure_future(startClusterWorker(), loop=self.loop)
async def getCreateDeviceSettings(self):
ret: list[Setting] = []
ret.append({
"key": "name",
"title": "Model Name",
"description": "The name or description of this model. E.g., Bird Classifier."
})
ret.append({
"key": "url",
"title": "Model URL",
"description": "The URL of the model. This should be a Github repo or url path to the model's config.json."
})
ret.append({
"key": "info",
"type": "html",
"title": "Sample Model",
"value": "<a href='https://github.com/scryptedapp/bird-classifier'>A reference bird classification model.</a>"
})
return ret
async def createDevice(self, settings):
name = settings.get('name', None)
if not name:
raise Exception("Model name not provided")
model_url: str = settings.get('url', None)
if not model_url:
raise Exception("Model URL not provided")
if not model_url.endswith('config.json'):
plugin_suffix = self.pluginId.split('/')[1]
match = re.match(r'https://github\.com/([^/]+)/([^/]+)', model_url)
if not match:
raise ValueError("Invalid GitHub repository URL.")
org, repo = match.groups()
model_url = f"https://raw.githubusercontent.com/{org}/{repo}/refs/heads/main/models/{plugin_suffix}/config.json"
response = urllib.request.urlopen(model_url)
if response.getcode() < 200 or response.getcode() >= 300:
raise Exception(f"non-2xx response code")
data = response.read()
config = json.loads(data)
nativeId = ''.join(random.choices('0123456789abcdef', k=8))
id = await self.reportDevice(nativeId, name)
from .custom_detect import CustomDetection
device: CustomDetection = await self.getDevice(nativeId)
device.storage.setItem("config_url", model_url)
device.storage.setItem("config", json.dumps(config))
device.init_model()
return id
async def reportDevice(self, nativeId: str, name: str):
return await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": nativeId,
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
scrypted_sdk.ScryptedInterface.Settings.value,
"CustomObjectDetection",
],
"name": name,
},
)
class Fork:
def __init__(self, PluginType: Any):
if PluginType:
@@ -409,3 +493,9 @@ class Fork:
async def getFaceRecognition(self):
return await self.plugin.getDevice("facerecognition")
async def getClipEmbedding(self):
return await self.plugin.getDevice("clipembedding")
async def getCustomDetection(self, nativeId: str):
return await self.plugin.getDevice(nativeId)

View File

@@ -0,0 +1,63 @@
from __future__ import annotations
import asyncio
import base64
import os
from typing import Tuple
import scrypted_sdk
from transformers import CLIPProcessor
from predict import PredictPlugin
class ClipEmbedding(PredictPlugin, scrypted_sdk.TextEmbedding, scrypted_sdk.ImageEmbedding):
def __init__(self, plugin: PredictPlugin, nativeId: str):
super().__init__(nativeId=nativeId, plugin=plugin)
self.inputwidth = 224
self.inputheight = 224
self.labels = {}
self.loop = asyncio.get_event_loop()
self.minThreshold = 0.5
self.model = self.initModel()
self.processor = CLIPProcessor.from_pretrained(
"openai/clip-vit-base-patch32",
cache_dir=os.path.join(os.environ["SCRYPTED_PLUGIN_VOLUME"], "files", "hf"),
)
def getFiles(self):
pass
def initModel(self):
local_files: list[str] = []
for file in self.getFiles():
remote_file = "https://huggingface.co/koushd/clip/resolve/main/" + file
localFile = self.downloadFile(remote_file, f"{self.id}/{file}")
local_files.append(localFile)
return self.loadModel(local_files)
def loadModel(self, files: list[str]):
pass
async def getImageEmbedding(self, input):
detections = await super().detectObjects(input, None)
return detections["detections"][0]["embedding"]
async def detectObjects(self, mediaObject, session = None):
ret = await super().detectObjects(mediaObject, session)
embedding = ret["detections"][0]['embedding']
ret["detections"][0]['embedding'] = base64.b64encode(embedding).decode("utf-8")
return ret
# width, height, channels
def get_input_details(self) -> Tuple[int, int, int]:
return (self.inputwidth, self.inputheight, 3)
def get_input_size(self) -> Tuple[float, float]:
return (self.inputwidth, self.inputheight)
def get_input_format(self) -> str:
return "rgb"

View File

@@ -0,0 +1,135 @@
from __future__ import annotations
import asyncio
from typing import Any, List, Tuple
import json
import numpy as np
from PIL import Image
from scrypted_sdk import (ObjectDetectionResult, ObjectDetectionSession,
ObjectsDetected)
import scrypted_sdk
from common import yolo
from predict import PredictPlugin
from common import softmax
from common.path_tools import replace_last_path_component
def safe_parse_json(value: str):
try:
return json.loads(value)
except Exception:
return None
class CustomDetection(PredictPlugin, scrypted_sdk.Settings):
def __init__(self, plugin: PredictPlugin, nativeId: str):
super().__init__(nativeId=nativeId, plugin=plugin)
if not hasattr(self, "prefer_relu"):
self.prefer_relu = False
self.inputheight = 320
self.inputwidth = 320
self.labels = {}
self.loop = asyncio.get_event_loop()
self.minThreshold = 0.5
self.init_model()
# self.detectModel = self.downloadModel("scrypted_yolov9t_relu_face_320" if self.prefer_relu else "scrypted_yolov9t_face_320")
# self.faceModel = self.downloadModel("inception_resnet_v1")
def init_model(self):
config_url = self.storage.getItem('config_url')
if not config_url:
return
config_str = self.storage.getItem('config')
if not config_str:
return
config = json.loads(config_str)
self.model_config = config
for key in self.model_config['labels']:
self.labels[int(key)] = self.model_config['labels'][key]
self.inputwidth = config["input_shape"][2]
self.inputheight = config["input_shape"][3]
files: list[str] = config["files"]
local_files: list[str] = []
for file in files:
remote_file = replace_last_path_component(config_url, file)
localFile = self.downloadFile(remote_file, f"{self.id}/{file}")
local_files.append(localFile)
self.model = self.loadModel(local_files)
def loadModel(self, files: list[str]):
pass
# width, height, channels
def get_input_details(self) -> Tuple[int, int, int]:
return (self.inputwidth, self.inputheight, 3)
def get_input_size(self) -> Tuple[float, float]:
return (self.inputwidth, self.inputheight)
def get_input_format(self) -> str:
return "rgb"
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
results = await self.predictModel(input)
if self.model_config["model"] == "yolov9":
objs = yolo.parse_yolov9(results)
ret = self.create_detection_result(objs, src_size, cvss)
return ret
elif self.model_config["model"] == "resnet":
exclude_classes = safe_parse_json(self.storage.getItem('excludeClasses')) or []
while len(exclude_classes):
excluded_class = exclude_classes.pop()
for idx, class_name in self.labels.items():
if class_name == excluded_class:
results[idx] = 0
sm = softmax.softmax(results)
# get anything over the threshold, sort by score, top 3
min_indexes = np.where(sm > self.minThreshold)[0]
min_indexes = min_indexes[np.argsort(sm[min_indexes])[::-1]]
min_indexes = min_indexes[:3]
detection_result: ObjectsDetected = {}
detections: List[ObjectDetectionResult] = []
detection_result["detections"] = detections
detection_result["inputDimensions"] = src_size
for idx in min_indexes:
label = self.labels[int(idx)]
score = float(sm[int(idx)])
detections.append(
{
"className": label,
"score": score,
}
)
return detection_result
else:
raise ValueError("Unknown model type")
async def predictModel(self, input: Image.Image) -> ObjectsDetected:
pass
async def getSettings(self):
return [
{
'key': 'excludeClasses',
'title': 'Exclude Classes',
'description': 'Classes to exclude from detection.',
'multiple': True,
'choices': list(self.labels.values()),
'value': safe_parse_json(self.storage.getItem('excludeClasses')),
}
]
async def putSetting(self, key: str, value: str):
if value:
self.storage.setItem(key, json.dumps(value))
else:
self.storage.removeItem(key)
await scrypted_sdk.deviceManager.onDeviceEvent(self.nativeId, scrypted_sdk.ScryptedInterface.Settings.value, None)

View File

@@ -7,3 +7,5 @@
openvino==2024.5.0
Pillow==10.3.0
opencv-python-headless==4.10.0.84
transformers==4.52.4

View File

@@ -1,24 +1,22 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.58",
"version": "0.10.59",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.58",
"version": "0.10.59",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/libav": "^1.0.199",
"@scrypted/sdk": "file:../../sdk",
"h264-sps-parser": "^0.2.1",
"semver": "^7.3.7"
},
"devDependencies": {
"@types/node": "^22.13.14",
"@types/semver": "^7.3.12",
"prebuild-install": "npm:@scrypted/prebuild-install@^7.1.9"
"@types/semver": "^7.3.12"
}
},
"../../common": {
@@ -80,34 +78,10 @@
"../sdk": {
"extraneous": true
},
"node_modules/@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"license": "ISC",
"dependencies": {
"minipass": "^7.0.4"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
"link": true
},
"node_modules/@scrypted/libav": {
"version": "1.0.199",
"resolved": "https://registry.npmjs.org/@scrypted/libav/-/libav-1.0.199.tgz",
"integrity": "sha512-sIrTRwa5CSZgnbCzPEB/laBNJ6RoZ3BKkVrgMCX/GE7oYCBcxIYtgFq942DVckXA6+A6NfXBR1lLt8a52SPpyQ==",
"hasInstallScript": true,
"dependencies": {
"detect-libc": "^2.0.3",
"follow-redirects": "^1.15.9",
"node-addon-api": "^8.3.1",
"tar": "^7.4.3"
}
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
@@ -128,369 +102,11 @@
"integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==",
"dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true,
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"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==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true,
"license": "MIT"
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true,
"license": "MIT"
},
"node_modules/h264-sps-parser": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/h264-sps-parser/-/h264-sps-parser-0.2.1.tgz",
"integrity": "sha512-1OCliBfrgCe2fu6eRRqN0xvhp9tFH/UVa1zdVmX/WcQAZrEB6xm/2RJ13F4h2OtQUMJqWv31fZakmTgrvbyIDA=="
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true,
"license": "ISC"
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"license": "MIT",
"dependencies": {
"minipass": "^7.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
"license": "MIT",
"bin": {
"mkdirp": "dist/cjs/src/bin.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true,
"license": "MIT"
},
"node_modules/napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
"dev": true,
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz",
"integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/prebuild-install": {
"name": "@scrypted/prebuild-install",
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/@scrypted/prebuild-install/-/prebuild-install-7.1.9.tgz",
"integrity": "sha512-lKKX7TMlwToQIH2UX8p0JwFbp0Y2dwedEqgkpcTmRvaA9DOfhau7m40i4+Qo7F1ISMHo/NK1XntURrFlSm4ibg==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"follow-redirects": "^1.15.6",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -503,133 +119,15 @@
"node": ">=10"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"license": "ISC",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true,
"license": "ISC"
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"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==",
"dev": true,
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
}
},
"dependencies": {
"@isaacs/fs-minipass": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
"requires": {
"minipass": "^7.0.4"
}
},
"@scrypted/common": {
"version": "file:../../common",
"requires": {
@@ -641,17 +139,6 @@
"typescript": "^5.5.3"
}
},
"@scrypted/libav": {
"version": "1.0.199",
"resolved": "https://registry.npmjs.org/@scrypted/libav/-/libav-1.0.199.tgz",
"integrity": "sha512-sIrTRwa5CSZgnbCzPEB/laBNJ6RoZ3BKkVrgMCX/GE7oYCBcxIYtgFq942DVckXA6+A6NfXBR1lLt8a52SPpyQ==",
"requires": {
"detect-libc": "^2.0.3",
"follow-redirects": "^1.15.9",
"node-addon-api": "^8.3.1",
"tar": "^7.4.3"
}
},
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
@@ -695,319 +182,21 @@
"integrity": "sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==",
"dev": true
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"dev": true
},
"detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"requires": {
"once": "^1.4.0"
}
},
"expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"dev": true
},
"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=="
},
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"dev": true
},
"h264-sps-parser": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/h264-sps-parser/-/h264-sps-parser-0.2.1.tgz",
"integrity": "sha512-1OCliBfrgCe2fu6eRRqN0xvhp9tFH/UVa1zdVmX/WcQAZrEB6xm/2RJ13F4h2OtQUMJqWv31fZakmTgrvbyIDA=="
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true
},
"minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
},
"minizlib": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
"requires": {
"minipass": "^7.1.2"
}
},
"mkdirp": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="
},
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"napi-build-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
"dev": true
},
"node-abi": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"dev": true,
"requires": {
"semver": "^7.3.5"
}
},
"node-addon-api": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz",
"integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"prebuild-install": {
"version": "npm:@scrypted/prebuild-install@7.1.9",
"resolved": "https://registry.npmjs.org/@scrypted/prebuild-install/-/prebuild-install-7.1.9.tgz",
"integrity": "sha512-lKKX7TMlwToQIH2UX8p0JwFbp0Y2dwedEqgkpcTmRvaA9DOfhau7m40i4+Qo7F1ISMHo/NK1XntURrFlSm4ibg==",
"dev": true,
"requires": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"follow-redirects": "^1.15.6",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
}
},
"pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dev": true,
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
}
},
"readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true,
"requires": {
"safe-buffer": "~5.2.0"
}
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"dev": true
},
"tar": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"requires": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
"minipass": "^7.1.2",
"minizlib": "^3.0.1",
"mkdirp": "^3.0.1",
"yallist": "^5.0.0"
}
},
"tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
},
"dependencies": {
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
}
}
},
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
},
"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==",
"dev": true
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.58",
"version": "0.10.59",
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
"author": "Scrypted",
"license": "Apache-2.0",
@@ -38,14 +38,12 @@
},
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/libav": "^1.0.199",
"@scrypted/sdk": "file:../../sdk",
"h264-sps-parser": "^0.2.1",
"semver": "^7.3.7"
},
"devDependencies": {
"@types/node": "^22.13.14",
"@types/semver": "^7.3.12",
"prebuild-install": "npm:@scrypted/prebuild-install@^7.1.9"
"@types/semver": "^7.3.12"
}
}

View File

@@ -1,206 +0,0 @@
import { Deferred } from "@scrypted/common/src/deferred";
import { parseSdp } from "@scrypted/common/src/sdp-utils";
import { sleep } from "@scrypted/common/src/sleep";
import { StreamChunk } from "@scrypted/common/src/stream-parser";
import { AVFormatContext, createAVFormatContext } from '@scrypted/libav';
import { ResponseMediaStreamOptions } from "@scrypted/sdk";
import { once } from 'events';
import net from 'net';
import { EventEmitter } from "stream";
import tls from 'tls';
import { RTSP_FRAME_MAGIC } from "../../../common/src/rtsp-server";
import { ParserSession, setupActivityTimer } from "./ffmpeg-session";
import { installLibavAddon } from "./libav-setup";
import { negotiateMediaStream } from "./rfc4571";
let installPromise: Promise<void>;
export async function startLibavSession(console: Console, url: string, mediaStreamOptions: ResponseMediaStreamOptions, options: {
useUdp: boolean,
audioSoftMuted: boolean,
activityTimeout: number,
}): Promise<ParserSession<"rtsp">> {
installPromise ||= installLibavAddon();
await installPromise;
const formatContext = createAVFormatContext();
try {
return await startLibavSessionWrapped(formatContext, console, url, mediaStreamOptions, options);
}
catch (e) {
await formatContext.close();
throw e;
}
}
export async function startLibavSessionWrapped(formatContext: AVFormatContext, console: Console, url: string, mediaStreamOptions: ResponseMediaStreamOptions, options: {
useUdp: boolean,
audioSoftMuted: boolean,
activityTimeout: number,
}): Promise<ParserSession<"rtsp">> {
const events = new EventEmitter();
let tlsProxy: net.Server;
try {
if (url.startsWith('rtsps:') || url.startsWith('https:')) {
let { hostname, port } = new URL(url);
if (!port) {
if (url.startsWith('rtsps:'))
port = '322';
else
port = '443';
}
const portNumber = parseInt(port);
if (!portNumber)
throw new Error('invalid port number');
tlsProxy = net.createServer(async socket => {
try {
const tlsSocket = tls.connect({
host: hostname,
port: portNumber,
rejectUnauthorized: false,
});
await once(tlsSocket, 'secureConnect');
socket.pipe(tlsSocket).pipe(socket);
}
catch (e) {
console.error('tls proxy error', e);
socket.destroy();
}
});
tlsProxy.listen(0, '127.0.0.1');
await once(tlsProxy, 'listening');
const localPort = (tlsProxy.address() as net.AddressInfo).port;
// rewrite the url to use the local port
const u = new URL(url);
u.protocol = u.protocol.replace('s:', ':');
u.hostname = '127.0.0.1';
u.port = localPort.toString();
url = u.toString();
}
await formatContext.open(url, {
rtsp_transport: options.useUdp ? 'udp' : 'tcp',
});
}
catch (e) {
tlsProxy?.close();
throw e;
}
let sdp = formatContext.createSDP();
const parsedSdp = parseSdp(sdp);
// sdp may contain multiple audio/video sections. take only the first video section.
sdp = [...parsedSdp.header.lines, ...parsedSdp.msections.map(msection => msection.lines).flat()].join('\r\n');
const killDeferred = new Deferred<void>();
const startDeferred = new Deferred<void>();
killDeferred.promise.catch(e => {
events.emit('killed');
events.emit('error', e);
tlsProxy?.close();
});
const kill = (e?: Error) => {
killDeferred.reject(e || new Error('killed'));
startDeferred.reject(e || new Error('killed'));
}
const { resetActivityTimer } = setupActivityTimer('rtsp', kill, events, options?.activityTimeout);
(async () => {
const pipelines: {
streamIndex: number,
writeFormatContext: AVFormatContext,
}[] = [];
try {
await startDeferred.promise;
formatContext.streams.forEach(stream => {
if (options.audioSoftMuted && stream.type === 'audio')
return;
if (stream.type !== 'video' && stream.type !== 'audio')
return;
const { codec } = stream;
const rtp = createAVFormatContext();
rtp.create('rtp', rtp => {
const prefix = Buffer.alloc(4);
prefix.writeUInt8(RTSP_FRAME_MAGIC, 0);
prefix.writeUInt8(stream.index, 1);
prefix.writeUInt16BE(rtp.length, 2);
const chunk: StreamChunk = {
chunks: [prefix, rtp],
type: codec === 'hevc' ? 'h265' : codec,
};
events.emit('rtsp', chunk);
});
rtp.newStream({
formatContext,
streamIndex: stream.index,
});
pipelines.push({
streamIndex: stream.index,
writeFormatContext: rtp,
});
});
while (!killDeferred.finished) {
using result = await formatContext.receiveFrame(pipelines);
if (result)
resetActivityTimer();
if (killDeferred.finished)
break;
}
}
catch (e) {
kill(e);
}
finally {
kill(new Error('rtsp read loop exited'));
await sleep(1000);
await Promise.allSettled(pipelines.map(pipeline => pipeline.writeFormatContext.close()));
await sleep(1000);
await formatContext.close();
}
})();
return {
start: () => {
startDeferred.resolve();
},
sdp: Promise.resolve(sdp),
get isActive() { return !killDeferred.finished },
kill(error?: Error) {
kill(error);
},
killed: killDeferred.promise,
resetActivityTimer,
negotiateMediaStream: (requestMediaStream, inputVideoCodec, inputAudioCodec) => {
return negotiateMediaStream(sdp, mediaStreamOptions, inputVideoCodec, inputAudioCodec, requestMediaStream);
},
emit(container: 'rtsp', chunk: StreamChunk) {
events.emit(container, chunk);
return this;
},
on(event: string, cb: any) {
events.on(event, cb);
return this;
},
once(event: any, cb: any) {
events.once(event, cb);
return this;
},
removeListener(event, cb) {
events.removeListener(event, cb);
return this;
}
}
}

View File

@@ -1,19 +0,0 @@
import * as libav from '@scrypted/libav';
import path from 'path';
function getAddonInstallPath() {
if (process.versions.electron)
process.env.npm_config_runtime = 'electron';
const binaryUrl = libav.getBinaryUrl();
const u = new URL(binaryUrl);
const withoutExtension = path.basename(u.pathname).replace(/\.tar.gz$/, '');
return path.join(process.env.SCRYPTED_PLUGIN_VOLUME, libav.version, withoutExtension);
}
export async function installLibavAddon(installOnly = false) {
const nr = installOnly
? null
// @ts-expect-error
: __non_webpack_require__;
await libav.install(getAddonInstallPath(), nr);
}

View File

@@ -14,9 +14,8 @@ import { parse as h264SpsParse } from "h264-sps-parser";
import net, { AddressInfo } from 'net';
import path from 'path';
import { Duplex } from 'stream';
import { ParserOptions, ParserSession, startParserSession } from './ffmpeg-session';
import { ParserOptions, ParserSession, startParserSession } from './ffmpeg-rebroadcast';
import { FileRtspServer } from './file-rtsp-server';
import { startLibavSession } from './libav-parser';
import { getUrlLocalAdresses } from './local-addresses';
import { REBROADCAST_MIXIN_INTERFACE_TOKEN } from './rebroadcast-mixin-token';
import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
@@ -33,8 +32,6 @@ const SCRYPTED_PARSER_TCP = 'Scrypted (TCP)';
const SCRYPTED_PARSER_UDP = 'Scrypted (UDP)';
const FFMPEG_PARSER_TCP = 'FFmpeg (TCP)';
const FFMPEG_PARSER_UDP = 'FFmpeg (UDP)';
const LIBAV_PARSER_TCP = 'Scrypted libav (TCP)';
const LIBAV_PARSER_UDP = 'Scrypted libav (UDP)';
const STRING_DEFAULT = 'Default';
interface PrebufferStreamChunk extends StreamChunk {
@@ -62,6 +59,7 @@ class PrebufferSession {
rtspPrebuffer: PrebufferStreamChunk[] = []
parsers: { [container: string]: StreamParser };
sdp: Promise<string>;
usingScryptedParser = false;
usingScryptedUdpParser = false;
mixinDevice: VideoCamera;
@@ -238,8 +236,6 @@ class PrebufferSession {
rtspParser = localStorage.getItem('defaultRtspParser');
}
switch (rtspParser) {
case LIBAV_PARSER_TCP:
case LIBAV_PARSER_UDP:
case FFMPEG_PARSER_TCP:
case FFMPEG_PARSER_UDP:
case SCRYPTED_PARSER_TCP:
@@ -396,8 +392,6 @@ class PrebufferSession {
SCRYPTED_PARSER_UDP,
FFMPEG_PARSER_TCP,
FFMPEG_PARSER_UDP,
LIBAV_PARSER_TCP,
LIBAV_PARSER_UDP,
],
}
);
@@ -548,11 +542,12 @@ class PrebufferSession {
// before launching the parser session, clear out the last detected codec.
// an erroneous cached codec could cause ffmpeg to fail to start.
this.storage.removeItem(this.lastDetectedAudioCodecKey);
let usingScryptedParser = false;
this.usingScryptedParser = false;
const h264Oddities = this.getLastH264Oddities();
if (isRfc4571) {
usingScryptedParser = true;
this.usingScryptedParser = true;
this.console.log('bypassing ffmpeg: using scrypted rfc4571 parser')
const json = await mediaManager.convertMediaObjectToJSON<any>(mo, 'x-scrypted/x-rfc4571');
let { url, sdp, mediaStreamOptions } = json;
@@ -571,22 +566,21 @@ class PrebufferSession {
sessionMso = ffmpegInput.mediaStreamOptions || this.advertisedMediaStreamOptions;
let { parser, isDefault } = this.getParser(sessionMso);
usingScryptedParser = parser === SCRYPTED_PARSER_TCP || parser === SCRYPTED_PARSER_UDP;
const usingLibavParser = parser === LIBAV_PARSER_TCP || parser === LIBAV_PARSER_UDP;
this.usingScryptedParser = parser === SCRYPTED_PARSER_TCP || parser === SCRYPTED_PARSER_UDP;
this.usingScryptedUdpParser = parser === SCRYPTED_PARSER_UDP;
// prefer ffmpeg if this is a prebuffered stream.
if (isDefault
&& usingScryptedParser
&& this.usingScryptedParser
&& h264Oddities
&& !this.stopInactive
&& sessionMso.tool !== 'scrypted') {
this.console.warn('H264 oddities were detected in prebuffered video stream, the Default Scrypted RTSP Parser will not be used. Falling back to FFmpeg. This can be overriden by setting the RTSP Parser to Scrypted.');
usingScryptedParser = false;
this.usingScryptedParser = false;
parser = FFMPEG_PARSER_TCP;
}
if (usingScryptedParser) {
if (this.usingScryptedParser) {
const rtspParser = createRtspParser();
rbo.parsers.rtsp = rtspParser;
@@ -596,16 +590,6 @@ class PrebufferSession {
rtspRequestTimeout: 10000,
});
}
else if (usingLibavParser) {
const rtspParser = createRtspParser();
rbo.parsers.rtsp = rtspParser;
session = await startLibavSession(this.console, ffmpegInput.url, ffmpegInput.mediaStreamOptions, {
useUdp: parser === LIBAV_PARSER_UDP,
audioSoftMuted,
activityTimeout: 10000,
});
}
else {
let acodec: string[];
@@ -662,7 +646,7 @@ class PrebufferSession {
console.error('rebroadcast error', e)
});
if (usingScryptedParser && !isRfc4571) {
if (this.usingScryptedParser && !isRfc4571) {
// watch the stream for 10 seconds to see if an weird nalu is encountered.
// if one is found and using scrypted parser as default, will need to restart rebroadcast to prevent
// downstream issues.
@@ -1022,6 +1006,10 @@ class PrebufferSession {
const codecInfo = await this.parseCodecs(true);
const mediaStreamOptions: ResponseMediaStreamOptions = session.negotiateMediaStream(options, codecInfo.inputVideoCodec, codecInfo.inputAudioCodec);
let sdp = await this.sdp;
if (!mediaStreamOptions.video?.h264Info && this.usingScryptedParser) {
mediaStreamOptions.video ||= {};
mediaStreamOptions.video.h264Info = this.getLastH264Probe();
}
if (this.mixin.streamSettings.storageSettings.values.noAudio)
mediaStreamOptions.audio = null;
@@ -1635,8 +1623,6 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
SCRYPTED_PARSER_UDP,
FFMPEG_PARSER_TCP,
FFMPEG_PARSER_UDP,
LIBAV_PARSER_TCP,
LIBAV_PARSER_UDP,
],
onPut: () => {
this.log.a('Rebroadcast Plugin will restart momentarily.');
@@ -1769,9 +1755,7 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
this.setHasEnabledMixin(mixinDeviceState.id);
const { id } = mixinDeviceState;
const forked = sdk.fork<RebroadcastPluginFork>({
runtime: 'node',
});
const forked = sdk.fork<RebroadcastPluginFork>();
const { worker } = forked;
const result = await forked.result;
const mixin = await result.newPrebufferMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState);

View File

@@ -7,7 +7,7 @@ import { MediaStreamOptions, ResponseMediaStreamOptions } from "@scrypted/sdk";
import { parse as spsParse } from "h264-sps-parser";
import net from 'net';
import { EventEmitter, Readable } from "stream";
import { ParserSession, setupActivityTimer } from "./ffmpeg-session";
import { ParserSession, setupActivityTimer } from "./ffmpeg-rebroadcast";
import { getSpsResolution } from "./sps-resolution";
export function negotiateMediaStream(sdp: string, mediaStreamOptions: MediaStreamOptions, inputVideoCodec: string, inputAudioCodec: string, requestMediaStream: MediaStreamOptions) {

View File

@@ -5,7 +5,7 @@ import { StreamChunk } from "@scrypted/common/src/stream-parser";
import { ResponseMediaStreamOptions } from "@scrypted/sdk";
import dgram from 'dgram';
import { EventEmitter } from "stream";
import { ParserSession, setupActivityTimer } from "./ffmpeg-session";
import { ParserSession, setupActivityTimer } from "./ffmpeg-rebroadcast";
import { negotiateMediaStream } from "./rfc4571";
export type RtspChannelCodecMapping = { [key: number]: string };

View File

@@ -7,7 +7,7 @@ import { OnvifCameraAPI, OnvifEvent, connectCameraAPI } from './onvif-api';
import { listenEvents } from './onvif-events';
import { OnvifIntercom } from './onvif-intercom';
import { DevInfo } from './probe';
import { AIState, Enc, ReolinkCameraClient } from './reolink-api';
import { AIState, Enc, isDeviceNvr, ReolinkCameraClient } from './reolink-api';
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
sirenTimeout: NodeJS.Timeout;
@@ -103,7 +103,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
clientWithToken: ReolinkCameraClient;
onvifClient: OnvifCameraAPI;
onvifIntercom = new OnvifIntercom(this);
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
motionTimeout: NodeJS.Timeout;
siren: ReolinkCameraSiren;
floodlight: ReolinkCameraFloodlight;
@@ -571,6 +570,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
async listenEvents() {
let killed = false;
const client = this.getClient();
const deviceInfo = await client.getDeviceInfo();
// reolink ai might not trigger motion if objects are detected, weird.
const startAI = async (ret: Destroyable, triggerMotion: () => void) => {
@@ -620,7 +620,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
}
}
catch (e) {
if (!hasSucceeded)
if (!hasSucceeded && !isDeviceNvr(deviceInfo))
return;
ret.emit('error', e);
}
@@ -769,15 +769,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
}
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
this.videoStreamOptions ||= this.getConstructedVideoStreamOptionsInternal().catch(e => {
this.constructedVideoStreamOptions = undefined;
throw e;
});
return this.videoStreamOptions;
}
async getConstructedVideoStreamOptionsInternal(): Promise<UrlMediaStreamOptions[]> {
let deviceInfo: DevInfo;
try {
const client = this.getClient();

View File

@@ -75,6 +75,8 @@ export interface PtzPreset {
name: string;
}
export const isDeviceNvr = (deviceInfo: DevInfo) => ['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType);
export class ReolinkCameraClient {
credential: AuthFetchCredentialState;
parameters: Record<string, string>;
@@ -320,7 +322,7 @@ export class ReolinkCameraClient {
const deviceInfo: DevInfo = await response.body?.[0]?.value?.DevInfo;
// Will need to check if it's valid for NVR and NVR_WIFI
if (!['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType)) {
if (!isDeviceNvr(deviceInfo)) {
return deviceInfo;
}

View File

@@ -10,7 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"**/plugin-console.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@
"dependencies": {
"@koush/ring-client-api": "file:../../external/ring-client-api",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/sdk": "^0.3.61",
"@types/node": "^18.15.11",
"axios": "^1.3.5",
"rxjs": "^7.8.0"
@@ -45,5 +45,5 @@
"got": "11.8.6",
"socket.io-client": "^2.5.0"
},
"version": "0.0.144"
"version": "0.0.145"
}

View File

@@ -365,9 +365,13 @@ export abstract class RtspSmartCamera extends RtspCamera {
return this.constructedVideoStreamOptions;
}
putSettingBase(key: string, value: SettingValue): Promise<void> {
this.constructedVideoStreamOptions = undefined;
return super.putSettingBase(key, value);
async putSettingBase(key: string, value: SettingValue): Promise<void> {
try {
return await super.putSettingBase(key, value);
}
finally {
this.constructedVideoStreamOptions = undefined;
}
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/snapshot",
"version": "0.2.58",
"version": "0.2.59",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/snapshot",
"version": "0.2.58",
"version": "0.2.59",
"dependencies": {
"@types/node": "^22.10.2",
"sharp": "^0.33.5",

Some files were not shown because too many files have changed in this diff Show More