Compare commits

..

106 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
ea628a7130 wip: unifi 2024-11-28 08:47:11 -08:00
116 changed files with 10201 additions and 2015 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

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

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

@@ -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,6 +1,6 @@
{
"scrypted.debugHost": "scrypted-amd",
"scrypted.debugHost": "scrypted-nvr",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/ncnn",
"version": "0.1.82",
"version": "0.1.88",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ncnn",
"version": "0.1.82",
"version": "0.1.88",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

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.82"
"version": "0.1.88"
}

View File

@@ -15,6 +15,7 @@ from scrypted_sdk import Setting, SettingValue
import ncnn
from common import yolo
from .custom_detection import NCNNCustomDetection
try:
from nc.face_recognition import NCNNFaceRecognition
except:
@@ -78,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":
@@ -118,9 +121,9 @@ class NCNNPlugin(
self.net = ncnn.Net()
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_fp16_packed = False
# self.net.opt.use_fp16_storage = False
# self.net.opt.use_fp16_arithmetic = False
self.net.load_param(paramFile)
self.net.load_model(binFile)
@@ -151,7 +154,7 @@ class NCNNPlugin(
async def prepareRecognitionModels(self):
try:
devices = [
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -161,10 +164,10 @@ class NCNNPlugin(
],
"name": "NCNN Face Recognition",
},
]
)
if NCNNTextRecognition:
devices.append(
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -175,12 +178,6 @@ class NCNNPlugin(
"name": "NCNN Text Recognition",
},
)
await scrypted_sdk.deviceManager.onDevicesChanged(
{
"devices": devices,
}
)
except:
pass
@@ -191,7 +188,13 @@ class NCNNPlugin(
if nativeId == "textrecognition":
self.textDevice = self.textDevice or NCNNTextRecognition(self, nativeId)
return self.textDevice
raise Exception("unknown device")
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"

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

@@ -42,9 +42,9 @@ class NCNNFaceRecognition(FaceRecognizeDetection):
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.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)

View File

@@ -35,9 +35,9 @@ class NCNNTextRecognition(TextRecognition):
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.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)

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

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

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

@@ -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/tapo",
"version": "0.0.20",
"version": "0.0.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/tapo",
"version": "0.0.20",
"version": "0.0.22",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/tapo",
"version": "0.0.20",
"version": "0.0.22",
"description": "Tapo Camera Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",

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

@@ -1,6 +1,7 @@
{
"name": "@scrypted/unifi-protect",
"version": "0.0.165",
"type": "module",
"version": "0.0.164",
"description": "Unifi Protect Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -22,26 +23,26 @@
"plugin"
],
"scrypted": {
"rollup": true,
"name": "Unifi Protect Plugin",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"Settings"
],
"babel": true,
"pluginDependencies": [
"@scrypted/prebuffer-mixin"
]
},
"devDependencies": {
"@types/node": "^22.13.2",
"@types/ws": "^8.5.14"
"@types/node": "^22.15.29",
"@types/ws": "^8.18.1"
},
"dependencies": {
"@koush/unifi-protect": "file:../../external/unifi-protect",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^1.7.9",
"ws": "^8.18.0"
"unifi-protect": "^4.21.0",
"ws": "^8.18.2"
}
}

View File

@@ -96,16 +96,16 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
});
const camera = this.findCamera() as any;
const camera = this.findCamera();
await this.protect.api.updateCamera(camera, {
await this.protect.api.updateDevice(camera, {
privacyZones,
} as any);
}
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
const camera = this.findCamera();
await this.protect.api.updateDevice(camera, {
ispSettings: {
zoomPosition: Math.abs(command.zoom * 100),
}
@@ -113,8 +113,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
async setStatusLight(on: boolean) {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
const camera = this.findCamera();
await this.protect.api.updateDevice(camera, {
ledSettings: {
isEnabled: on,
}
@@ -170,8 +170,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
const camera = this.findCamera();
const params = new URLSearchParams({ camera: camera.id });
const response = await this.protect.loginFetch(this.protect.api.wsUrl() + "/talkback?" + params.toString());
const endpoint = new URL(this.protect.api.getApiEndpoint("talkback"));
endpoint.searchParams.set('camera', camera.id);
const response = await this.protect.loginFetch(endpoint.toString());
const tb = response.data as Record<string, string>;
// Adjust the URL for our address.
@@ -275,7 +276,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
classes.push('ring');
if (this.interfaces.includes(ScryptedInterface.ObjectDetector))
classes.push(...this.findCamera().featureFlags.smartDetectTypes);
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor)
if (this.findCamera().featureFlags.hasFingerprintSensor)
classes.push('fingerprintIdentified');
return {
classes,
@@ -375,7 +376,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
findCamera() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.cameras.find(camera => camera.id === id);
return this.protect.api.bootstrap.cameras.find(camera => camera.id === id);
}
async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
const camera = this.findCamera();
@@ -391,7 +392,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
const data = Buffer.from(JSON.stringify({
url: u,
container: 'rtsp',
mediaStreamOptions: this.createMediaStreamOptions(rtspChannel, (camera as any).videoCodec),
mediaStreamOptions: this.createMediaStreamOptions(rtspChannel, camera.videoCodec),
} as MediaStreamUrl));
return this.createMediaObject(data, ScryptedMimeTypes.MediaStreamUrl);
}
@@ -425,7 +426,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
const camera = this.findCamera();
const vsos = camera.channels
.map(channel => this.createMediaStreamOptions(channel, (camera as any).videoCodec));
.map(channel => this.createMediaStreamOptions(channel, camera.videoCodec));
return vsos;
}
@@ -441,7 +442,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
const sanitizedBitrate = Math.min(channel.maxBitrate, Math.max(channel.minBitrate, bitrate));
this.console.log(channel.name, 'bitrate change requested', bitrate, 'clamped to', sanitizedBitrate);
channel.bitrate = sanitizedBitrate;
const cameraResult = await this.protect.api.updateCameraChannels(camera);
const cameraResult = await this.protect.api.updateDevice(camera, {
channels: camera.channels,
});
if (!cameraResult) {
throw new Error("setVideoStreamOptions failed")
}
@@ -458,7 +461,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
setMotionDetected(motionDetected: boolean) {
this.motionDetected = motionDetected;
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
if (this.findCamera().featureFlags.hasPackageCamera) {
if (deviceManager.getNativeIds().includes(this.packageCameraNativeId)) {
this.ensurePackageCamera();
this.packageCamera.motionDetected = motionDetected;
@@ -467,7 +470,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
setFingerprintDetected(fingerprintDetected: boolean) {
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
if (this.findCamera().featureFlags.hasFingerprintSensor) {
if (deviceManager.getNativeIds().includes(this.fingerprintSensorNativeId)) {
this.ensureFingerprintSensor();
this.fingerprintSensor.binaryState = fingerprintDetected;
@@ -480,7 +483,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
text: title.substring(0, 30),
type: 'CUSTOM_MESSAGE',
};
this.protect.api.updateCamera(this.findCamera(), {
this.protect.api.updateDevice(this.findCamera(), {
lcdMessage: payload,
})

View File

@@ -14,23 +14,23 @@ export class UnifiLight extends ScryptedDeviceBase implements OnOff, Brightness,
this.console.log(protectLight);
}
async turnOff(): Promise<void> {
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
if (!result)
this.console.error('turnOff failed.');
}
async turnOn(): Promise<void> {
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
if (!result)
this.console.error('turnOn failed.');
}
async setBrightness(brightness: number): Promise<void> {
const ledLevel = Math.round(((brightness as number) / 20) + 1);
this.protect.api.updateLight(this.findLight(), { lightDeviceSettings: { ledLevel } });
this.protect.api.updateDevice(this.findLight(), { lightDeviceSettings: { ledLevel } });
}
findLight() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.lights.find(light => light.id === id);
return this.protect.api.bootstrap.lights.find(light => light.id === id);
}
updateState(light?: Readonly<ProtectLightConfig>) {

View File

@@ -1,9 +1,8 @@
import { Lock, LockState, ScryptedDeviceBase } from "@scrypted/sdk";
import { UnifiProtect } from "./main";
import { ProtectDoorLockConfig } from "./unifi-protect";
export class UnifiLock extends ScryptedDeviceBase implements Lock {
constructor(public protect: UnifiProtect, nativeId: string, protectLock: Readonly<ProtectDoorLockConfig>) {
constructor(public protect: UnifiProtect, nativeId: string, protectLock: any) {
super(nativeId);
this.updateState(protectLock);
@@ -11,23 +10,23 @@ export class UnifiLock extends ScryptedDeviceBase implements Lock {
}
async lock(): Promise<void> {
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/close`, {
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/close`, {
method: 'POST',
});
}
async unlock(): Promise<void> {
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/open`, {
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/open`, {
method: 'POST',
});
}
findLock() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.doorlocks.find(doorlock => doorlock.id === id);
return (this.protect.api.bootstrap.doorlocks as any).find(doorlock => doorlock.id === id);
}
updateState(lock?: Readonly<ProtectDoorLockConfig>) {
updateState(lock?: any) {
lock = lock || this.findLock();
if (!lock)
return;

View File

@@ -2,14 +2,18 @@ import { createInstanceableProviderPlugin, enableInstanceableProviderMode, isIns
import { sleep } from "@scrypted/common/src/sleep";
import sdk, { Device, DeviceProvider, ObjectDetectionResult, ObjectsDetected, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios from "axios";
import axios, {ResponseType} from "axios";
import { UnifiCamera } from "./camera";
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
import { UnifiLight } from "./light";
import { UnifiLock } from "./lock";
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
import { UnifiSensor } from "./sensor";
import { FeatureFlagsShim, LastSeenShim } from "./shim";
import { ProtectApi, ProtectApiUpdates, ProtectNvrUpdatePayloadCameraUpdate, ProtectNvrUpdatePayloadEventAdd } from "./unifi-protect";
import { ProtectApi, ProtectCameraConfigInterface, ProtectEventAddInterface, ProtectEventPacket } from "./unifi-protect";
import https from 'https';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
const { deviceManager } = sdk;
@@ -46,18 +50,18 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
this.updateManagementUrl();
}
handleUpdatePacket(packet: any) {
if (packet.action.action !== "update") {
handleUpdatePacket(packet: ProtectEventPacket) {
if (packet.header.action !== "update") {
return;
}
if (!packet.action.id) {
if (!packet.header.id) {
return;
}
const device = this.api.cameras?.find(c => c.id === packet.action.id)
|| this.api.lights?.find(c => c.id === packet.action.id)
|| this.api.doorlocks?.find(c => c.id === packet.action.id)
|| this.api.sensors?.find(c => c.id === packet.action.id);
const device = this.api.bootstrap.cameras?.find(c => c.id === packet.header.id)
|| this.api.bootstrap.lights?.find(c => c.id === packet.header.id)
|| (this.api.bootstrap.doorlocks as any)?.find(c => c.id === packet.header.id)
|| this.api.bootstrap.sensors?.find(c => c.id === packet.header.id);
if (!device) {
return;
@@ -81,27 +85,48 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return ret;
}
public async loginFetch(url: string, options?: { method?: string, signal?: AbortSignal, responseType?: axios.ResponseType }) {
const api = this.api as any;
if (!(await api.login()))
throw new Error('Login failed.');
const headers: Record<string, string> = {};
for (const [header, value] of api.headers) {
headers[header] = value;
async relogin() {
const ip = this.getSetting('ip');
const username = this.getSetting('username');
const password = this.getSetting('password');
const loginResult = await this.api.login(ip, username, password);
if (!loginResult) {
this.log.a('Login failed. Check credentials.');
return;
}
return axios(url, {
responseType: options?.responseType,
method: options?.method,
headers,
httpsAgent: api.httpsAgent,
signal: options?.signal,
})
if (!await this.api.getBootstrap()) {
this.reconnect('refresh failed')();
return;
}
return loginResult;
}
listener(event: Buffer) {
const updatePacket = ProtectApiUpdates.decodeUpdatePacket(this.console, event);
public async loginFetch(url: string, options?: { method?: string, signal?: AbortSignal, responseType?: ResponseType }, relogin = false) {
try {
const api = this.api as any;
const headers: Record<string, string> = {};
for (const [header, value] of api.headers) {
headers[header] = value;
}
return await axios(url, {
responseType: options?.responseType,
method: options?.method,
headers,
httpsAgent,
signal: options?.signal,
});
}
catch (e) {
if (relogin) {
await this.relogin();
return this.loginFetch(url, options);
}
}
}
listener(updatePacket: ProtectEventPacket) {
if (!updatePacket)
return;
@@ -109,27 +134,27 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
const unifiDevice = this.handleUpdatePacket(updatePacket);
switch (updatePacket.action.modelKey) {
switch (updatePacket.header.modelKey) {
case "sensor":
case "doorlock":
case "light":
case "camera": {
if (!unifiDevice) {
this.console.log('unknown device, sync needed?', updatePacket.action.id);
this.console.log('unknown device, sync needed?', updatePacket.header.id);
return;
}
if (updatePacket.action.action !== "update") {
unifiDevice.console.log('non update', updatePacket.action.action);
if (updatePacket.header.action !== "update") {
unifiDevice.console.log('non update', updatePacket.header.action);
return;
}
unifiDevice.updateState();
if (updatePacket.action.modelKey === "doorlock")
if (updatePacket.header.modelKey === "doorlock")
return;
const payload = updatePacket.payload as any as ProtectNvrUpdatePayloadCameraUpdate & LastSeenShim;
const payload = updatePacket.payload as ProtectCameraConfigInterface;
if (updatePacket.action.modelKey !== "camera")
if (updatePacket.header.modelKey !== "camera")
return;
const unifiCamera = unifiDevice as UnifiCamera;
@@ -142,19 +167,19 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
break;
}
case "event": {
if (updatePacket.action.action !== "add") {
if ((updatePacket?.payload as any)?.end && updatePacket.action.id) {
const payload = updatePacket.payload as ProtectEventAddInterface;
if (updatePacket.header.action !== "add") {
if (payload.end && updatePacket.header.id) {
// unifi reports the event ended but it seems to take a moment before the snapshot
// is actually ready.
setTimeout(() => {
const running = this.runningEvents.get(updatePacket.action.id);
const running = this.runningEvents.get(updatePacket.header.id);
running?.resolve?.(undefined)
}, 2000);
}
return;
}
const payload = updatePacket.payload as ProtectNvrUpdatePayloadEventAdd;
if (!payload.camera)
return;
const nativeId = this.getNativeId({ id: payload.camera }, false);
@@ -166,7 +191,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
const detectionId = payload.id;
const actionId = updatePacket.action.id;
const actionId = updatePacket.header.id;
let resolve: (value: unknown) => void;
const promise = new Promise(r => resolve = r);
@@ -203,7 +228,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
detections = payload.smartDetectTypes.map(type => ({
className: type,
score: payload.score,
label: (payload as any).metadata?.[type]?.name,
label: payload.metadata?.[type]?.name,
}));
}
else {
@@ -249,7 +274,26 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
this.console.log(message, ...parameters);
}
reconnecting = false;
wsTimeout: NodeJS.Timeout;
reconnect(reason: string) {
return async () => {
if (this.reconnecting)
return;
this.reconnecting = true;
this.api?.reset();
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
await sleep(10000);
this.discoverDevices(0);
}
}
async discoverDevices(duration: number) {
this.api?.reset();
this.reconnecting = false;
clearTimeout(this.wsTimeout);
const ip = this.getSetting('ip');
const username = this.getSetting('username');
const password = this.getSetting('password');
@@ -271,10 +315,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return
}
this.api?.eventsWs?.removeAllListeners();
this.api?.eventsWs?.close();
if (!this.api) {
this.api = new ProtectApi(ip, username, password, {
this.api = new ProtectApi({
debug() { },
error: (...args) => {
this.console.error(...args);
@@ -284,48 +326,37 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
});
}
let reconnecting = false;
const reconnect = (reason: string) => {
return async () => {
if (reconnecting)
return;
reconnecting = true;
this.api?.eventsWs?.close();
this.api?.eventsWs?.emit('close');
this.api?.eventsWs?.removeAllListeners();
if (this.api.eventsWs) {
this.console.warn('Event Listener failed to close. Requesting plugin restart.');
deviceManager.requestRestart();
}
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
await sleep(10000);
this.discoverDevices(0);
}
}
try {
if (!await this.api.refreshDevices()) {
reconnect('refresh failed')();
const loginResult = await this.relogin();
if (!loginResult) {
this.log.a('Login failed. Check credentials.');
return;
}
if (!await this.api.getBootstrap()) {
this.reconnect('refresh failed')();
return;
}
let wsTimeout: NodeJS.Timeout;
const resetWsTimeout = () => {
clearTimeout(wsTimeout);
wsTimeout = setTimeout(reconnect('timeout'), 5 * 60 * 1000);
clearTimeout(this.wsTimeout);
this.wsTimeout = setTimeout(() => this.reconnect('timeout'), 5 * 60 * 1000);
};
resetWsTimeout();
this.api.eventsWs?.on('message', (data) => {
this.api.on('message', message => {
resetWsTimeout();
this.listener(data as Buffer);
});
this.api.eventsWs?.on('close', reconnect('close'));
this.api.eventsWs?.on('error', reconnect('error'));
this.listener(message);
})
const devices: Device[] = [];
for (let camera of this.api.cameras || []) {
if (!this.api.bootstrap.cameras.length) {
this.console.warn('no cameras found. is this an admin account? cancelling sync.');
return;
}
for (let camera of this.api.bootstrap.cameras || []) {
if (camera.isAdoptedByOther) {
this.console.log('skipping camera that is adopted by another nvr', camera.id, camera.name);
continue;
@@ -347,7 +378,9 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
if (needUpdate) {
camera = await this.api.updateCameraChannels(camera);
camera = await this.api.updateDevice(camera, {
channels: camera.channels,
});
if (!camera) {
this.log.a('Unable to enable RTSP and IDR interval on camera. Is this an admin account?');
continue;
@@ -392,7 +425,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
if (camera.featureFlags.hasLcdScreen) {
d.interfaces.push(ScryptedInterface.Notifier);
}
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
if (camera.featureFlags.hasPackageCamera) {
d.interfaces.push(ScryptedInterface.DeviceProvider);
}
if (camera.featureFlags.hasLedStatus) {
@@ -405,7 +438,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const sensor of this.api.sensors || []) {
for (const sensor of this.api.bootstrap.sensors || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: sensor.name,
@@ -433,7 +466,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const light of this.api.lights || []) {
for (const light of this.api.bootstrap.lights || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: light.name,
@@ -458,7 +491,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const lock of this.api.doorlocks || []) {
for (const lock of (this.api.bootstrap.doorlocks as any) || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: lock.name,
@@ -480,6 +513,11 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
if (!devices.length) {
this.console.warn('no devices found. is this an admin account? cancelling sync.');
return;
}
await deviceManager.onDevicesChanged({
providerNativeId: this.nativeId,
devices,
@@ -490,12 +528,12 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
// handle package cameras as a sub device
for (const camera of this.api.cameras) {
for (const camera of this.api.bootstrap.cameras) {
const devices: Device[] = [];
const providerNativeId = this.getNativeId(camera, true);
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
if (camera.featureFlags.hasPackageCamera) {
const nativeId = providerNativeId + '-packageCamera';
const d: Device = {
providerNativeId,
@@ -518,7 +556,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
if ((camera.featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
if (camera.featureFlags.hasFingerprintSensor) {
const nativeId = providerNativeId + '-fingerprintSensor';
const d: Device = {
providerNativeId,
@@ -569,25 +607,25 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return this.locks.get(nativeId);
const id = this.findId(nativeId);
const camera = this.api.cameras.find(camera => camera.id === id);
const camera = this.api.bootstrap.cameras.find(camera => camera.id === id);
if (camera) {
const ret = new UnifiCamera(this, nativeId, camera);
this.cameras.set(nativeId, ret);
return ret;
}
const sensor = this.api.sensors.find(sensor => sensor.id === id);
const sensor = this.api.bootstrap.sensors.find(sensor => sensor.id === id);
if (sensor) {
const ret = new UnifiSensor(this, nativeId, sensor);
this.unifiSensors.set(nativeId, ret);
return ret;
}
const light = this.api.lights.find(light => light.id === id);
const light = this.api.bootstrap.lights.find(light => light.id === id);
if (light) {
const ret = new UnifiLight(this, nativeId, light);
this.lights.set(nativeId, ret);
return ret;
}
const lock = this.api.doorlocks?.find(lock => lock.id === id);
const lock = (this.api.bootstrap.doorlocks as any)?.find(lock => lock.id === id);
if (lock) {
const ret = new UnifiLock(this, nativeId, lock);
this.locks.set(nativeId, ret);
@@ -636,11 +674,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
hide: true,
json: true,
defaultValue: {
mac: {},
anonymousDeviceId: {},
id: {},
nativeId: {},
host: {},
},
}
});
@@ -655,10 +690,10 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
const idMaps = this.storageSettings.values.idMaps;
// try to find an existing nativeId given the mac and anonymous device id
const found = (mac && idMaps.mac[mac])
|| (anonymousDeviceId && idMaps.anonymousDeviceId[anonymousDeviceId])
|| (id && idMaps.id[id])
|| (host && idMaps.host[host])
const found = (mac && idMaps.mac?.[mac])
|| (anonymousDeviceId && idMaps.anonymousDeviceId?.[anonymousDeviceId])
|| (id && idMaps.id?.[id])
|| (host && idMaps.host?.[host])
;
// use the found id if one exists (device got provisioned a new id), otherwise use the id provided by the device.
@@ -667,25 +702,42 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
if (!update)
return nativeId;
// Remove any existing mappings to this nativeId
const cleanDict = (dict: Record<string, string>) => {
if (!dict) return;
const entries = Object.entries(dict);
for (const [key, value] of entries) {
if (value === nativeId) {
delete dict[key];
}
}
}
// Clean existing mappings before adding new ones
idMaps.mac ||= {};
idMaps.anonymousDeviceId ||= {};
idMaps.host ||= {};
idMaps.id ||= {};
idMaps.nativeId ||= {};
cleanDict(idMaps.mac);
cleanDict(idMaps.anonymousDeviceId);
cleanDict(idMaps.host);
cleanDict(idMaps.id);
// map the mac, host, and anonymous device id to the native id.
if (mac) {
idMaps.mac ||= {};
idMaps.mac[mac] = nativeId;
}
if (anonymousDeviceId) {
idMaps.anonymousDeviceId ||= {};
idMaps.anonymousDeviceId[anonymousDeviceId] = nativeId;
}
if (host) {
idMaps.host ||= {};
idMaps.host[host] = nativeId;
}
// map the id and native id to each other.
idMaps.id ||= {};
idMaps.id[id] = nativeId;
idMaps.nativeId ||= {};
idMaps.nativeId[nativeId] = id;
this.storageSettings.values.idMaps = idMaps;

View File

@@ -15,7 +15,7 @@ export class UnifiSensor extends ScryptedDeviceBase implements Thermometer, Humi
findSensor() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.sensors.find(sensor => sensor.id === id);
return this.protect.api.bootstrap.sensors.find(sensor => sensor.id === id);
}
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {

View File

@@ -1,13 +1,8 @@
export interface FeatureFlagsShim {
hasPackageCamera: boolean;
hasFingerprintSensor: boolean;
}
export interface LastSeenShim {
lastSeen: number;
}
export interface PrivacyZone {
id: number;
name: string;

View File

@@ -1,3 +1,3 @@
// export * from '@koush/unifi-protect'
export * from '@koush/unifi-protect/src/index'
export * from 'unifi-protect'
// export * from '../../../external/unifi-protect/src/index'

View File

@@ -1,13 +1,15 @@
{
"compilerOptions": {
"module": "Node16",
"module": "es2022",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",
"moduleResolution": "bundler",
"esModuleInterop": true,
"sourceMap": true
},
"include": [
"src/**/*"
"src/**/*",
"../../common/src/**/*",
"../../server/src/**/*"
]
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/webrtc",
"version": "0.2.77",
"version": "0.2.79",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/webrtc",
"version": "0.2.77",
"version": "0.2.79",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/webrtc",
"version": "0.2.78",
"version": "0.2.79",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -69,7 +69,7 @@ export async function createTrackForwarder(options: {
requestDestination = 'remote';
}
const hasH265Support = !!videoTransceiver.codecs.find(codec => codec.mimeType === 'video/H265');
const hasH265Support = !!videoTransceiver?.codecs.find(codec => codec.mimeType === 'video/H265');
const mo = await requestMediaStream({
video: {
@@ -141,7 +141,7 @@ export async function createTrackForwarder(options: {
});
const findAndSetCodec = (transceiver: RTCRtpTransceiver, mimeType: string) => {
const found = transceiver.codecs.find(codec => codec.mimeType === mimeType);
const found = transceiver?.codecs.find(codec => codec.mimeType === mimeType);
if (found)
transceiver.sender.codec = found;
return found;

View File

@@ -46,6 +46,8 @@ if (fs.existsSync(path.resolve(cwd, 'src/main.py'))) {
}
const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json').toString()));
const interfaceDescriptors = packageJson.scrypted?.interfaceDescriptors;
delete packageJson.scrypted?.interfaceDescriptors;
const optionalDependencies = Object.keys(packageJson.optionalDependencies || {});
@@ -219,6 +221,7 @@ function finishZip() {
const sdkVersion = require(path.join(__dirname, '../package.json')).version;
zip.addFile('sdk.json', Buffer.from(JSON.stringify({
version: sdkVersion,
interfaceDescriptors,
})));
if (packageJson.type === 'module') {

1943
sdk/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.5.12",
"version": "0.5.33",
"description": "",
"main": "dist/src/index.js",
"exports": {
@@ -28,31 +28,32 @@
"author": "",
"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"
},
"devDependencies": {
"@types/node": "^22.10.1",
"@types/node": "^24.0.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.11"
"typedoc": "^0.28.5"
},
"types": "dist/src/index.d.ts"
}

1
sdk/polyfill/level.js Normal file
View File

@@ -0,0 +1 @@
const level = __non_webpack_require__('level'); module.exports = level;

View File

@@ -32,6 +32,19 @@ const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json').to
const external = Object.keys(packageJson.optionalDependencies || {});
const tsconfig = JSON.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json').toString()));
external.push(
'@scrypted/node-pty',
'node-forge',
'sharp',
'level',
'source-map-support/register',
'adm-zip',
"memfs",
"realfs",
"fakefs",
"mdns",
"typescript",
);
const defaultMainNodeJs = 'main.nodejs.js';
const entries = [];

View File

@@ -1,4 +1,5 @@
export * from '../types/gen/index';
import fs from 'fs';
import type { DeviceManager, DeviceState, EndpointManager, EventListenerRegister, Logger, MediaManager, MediaObject, ScryptedInterface, ScryptedNativeId, ScryptedStatic, SystemManager, WritableDeviceState } from '../types/gen/index';
import { DeviceBase, ScryptedInterfaceDescriptors, ScryptedInterfaceProperty, TYPES_VERSION } from '../types/gen/index';
import { createRequire } from 'module';
@@ -266,7 +267,23 @@ try {
}
try {
(sdk.systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, ScryptedInterfaceDescriptors)?.catch(() => { });
let descriptors = {
...ScryptedInterfaceDescriptors,
};
try {
const sdkJson = JSON.parse(fs.readFileSync('../sdk.json').toString());
const customDescriptors = sdkJson.interfaceDescriptors;
if (customDescriptors) {
descriptors = {
...descriptors,
...customDescriptors,
};
}
}
catch (e) {
console.warn('failed to load custom interface descriptors', e);
}
(sdk.systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, descriptors)?.catch(() => { });
}
catch (e) {
}

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