Compare commits

..

10 Commits

Author SHA1 Message Date
Koushik Dutta
dd3e7fe238 rebroadcast: improve cleanups 2025-04-11 21:38:43 -07:00
Koushik Dutta
b02c17e185 rebroadcast: beta 2025-04-11 16:23:44 -07:00
Koushik Dutta
6dee9b04df rebroadcast: update libav 2025-04-11 16:17:48 -07:00
Koushik Dutta
378fb41908 rebroadcast: update libav with new pipeline 2025-04-11 14:46:22 -07:00
Koushik Dutta
48f7208d55 Merge branch 'main' into libav 2025-04-11 14:22:23 -07:00
Koushik Dutta
71a93805ef Merge branch 'main' into libav 2025-04-09 09:13:53 -07:00
Koushik Dutta
4880c18c48 Merge remote-tracking branch 'origin/libav' into libav 2025-04-08 08:32:22 -07:00
Koushik Dutta
198d4808cb Merge branch 'main' into libav 2025-04-08 08:32:04 -07:00
Koushik Dutta
ef8f4ba9b8 Merge branch 'main' into libav 2025-04-07 15:27:02 -07:00
Koushik Dutta
d87d9bb751 rebroadcast: wip libav 2025-04-01 11:09:57 -07:00
142 changed files with 3345 additions and 10728 deletions

View File

@@ -77,14 +77,13 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-vendor:
name: Push Vendor Docker image to Docker Hub
build-nvidia:
name: Push NVIDIA 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
@@ -139,11 +138,11 @@ jobs:
build-args: |
BASE=ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-full
context: install/docker/
file: install/docker/Dockerfile.${{ matrix.VENDOR }}
file: install/docker/Dockerfile.nvidia
platforms: linux/amd64,linux/arm64
push: true
tags: |
koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.VENDOR }}
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-${{ matrix.VENDOR }}
koush/scrypted-common:${{ matrix.BASE }}-nvidia
ghcr.io/koush/scrypted-common:${{ matrix.BASE }}-nvidia
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -20,11 +20,10 @@ jobs:
strategy:
matrix:
BASE: [
["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"],
["noble-nvidia", ".s6", "noble-nvidia"],
["noble-full", ".s6", "noble-full"],
["noble-lite", "", "noble-lite"],
# ["noble-lite", ".router", "noble-router"],
]
steps:
- name: Check out the repo
@@ -95,25 +94,19 @@ 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{0}-{1}', github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION, matrix.BASE[2]) }}
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[2], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
${{ matrix.BASE[2] == 'noble-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ 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]) }}
${{ 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' || '' }}
${{ 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' && 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]) || '' }}
${{ 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' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -10,7 +10,6 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
@@ -22,29 +21,28 @@
},
"../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.29",
"version": "0.5.3",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.27.1",
"@rollup/plugin-commonjs": "^28.0.5",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.10.0",
"babel-loader": "^10.0.0",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"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",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.2",
"ts-loader": "^9.5.1",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -57,9 +55,9 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^24.0.1",
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.28.5"
"typedoc": "^0.26.11"
}
},
"../sdk/node_modules/@ampproject/remapping": {
@@ -3310,15 +3308,6 @@
"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,
@@ -3404,27 +3393,6 @@
"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,7 +12,6 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},

View File

@@ -1,5 +0,0 @@
import type { SystemManager } from '@scrypted/types';
export function getAllDevices<T>(systemManager: SystemManager) {
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById<T>(id));
}

View File

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

View File

@@ -93,12 +93,8 @@ export const H265_NAL_TYPE_AGG = 48;
export const H265_NAL_TYPE_VPS = 32;
export const H265_NAL_TYPE_SPS = 33;
export const H265_NAL_TYPE_PPS = 34;
export const H265_NAL_TYPE_BLA_W_LP = 16;
export const H265_NAL_TYPE_BLA_W_RADL = 17;
export const H265_NAL_TYPE_BLA_N_LP = 18;
export const H265_NAL_TYPE_IDR_W_RADL = 19;
export const H265_NAL_TYPE_IDR_N_LP = 20;
export const H265_NAL_TYPE_CRA_NUT = 21;
export const H265_NAL_TYPE_IDR_N = 19;
export const H265_NAL_TYPE_IDR_W = 20;
export const H265_NAL_TYPE_FU = 49;
export const H265_NAL_TYPE_SEI_PREFIX = 39;
export const H265_NAL_TYPE_SEI_SUFFIX = 40;
@@ -256,26 +252,6 @@ export function getNaluTypesInH265Nalu(nalu: Buffer, fuaRequireStart = false, fu
return ret;
}
export function isH265KeyFrameRelatedInSet(naluTypes: Set<number>, allowCodecInfo = true) {
if (naluTypes.has(H265_NAL_TYPE_IDR_N_LP)
|| naluTypes.has(H265_NAL_TYPE_IDR_W_RADL)
|| naluTypes.has(H265_NAL_TYPE_CRA_NUT)
|| naluTypes.has(H265_NAL_TYPE_BLA_N_LP)
|| naluTypes.has(H265_NAL_TYPE_BLA_W_LP)
|| naluTypes.has(H265_NAL_TYPE_BLA_W_RADL)) {
return true;
}
if (allowCodecInfo) {
if (naluTypes.has(H265_NAL_TYPE_VPS)
|| naluTypes.has(H265_NAL_TYPE_SPS)
|| naluTypes.has(H265_NAL_TYPE_PPS))
return true;
}
return false;
}
export function createRtspParser(options?: StreamParserOptions): RtspStreamParser {
let resolve: any;
@@ -307,7 +283,12 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
else if (streamChunk.type === 'h265') {
const naluTypes = getStartedH265NaluTypes(streamChunk);
if (isH265KeyFrameRelatedInSet(naluTypes)) {
if (naluTypes.has(H265_NAL_TYPE_VPS)
|| naluTypes.has(H265_NAL_TYPE_SPS)
|| naluTypes.has(H265_NAL_TYPE_PPS)
|| naluTypes.has(H265_NAL_TYPE_IDR_N)
|| naluTypes.has(H265_NAL_TYPE_IDR_W)
) {
return streamChunks.slice(prebufferIndex);
}
}
@@ -689,12 +670,9 @@ export class RtspClient extends RtspBase {
// @ts-ignore
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
const authedUrl = new URL(this.url);
const username = decodeURIComponent(authedUrl.username);
const password = decodeURIComponent(authedUrl.password);
if (this.wwwAuthenticate.includes('Basic')) {
const hash = BASIC.computeHash({ username, password });
const parsedUrl = new URL(this.url);
const hash = BASIC.computeHash({ username: parsedUrl.username, password: parsedUrl.password });
return `Basic ${hash}`;
}
@@ -714,6 +692,10 @@ export class RtspClient extends RtspBase {
REQUIRED_WWW_AUTHENTICATE_KEYS,
) as DigestWWWAuthenticateData;
const authedUrl = new URL(this.url);
const username = decodeURIComponent(authedUrl.username);
const password = decodeURIComponent(authedUrl.password);
const strippedUrl = new URL(url.toString());
strippedUrl.username = '';
strippedUrl.password = '';

View File

@@ -175,8 +175,6 @@ export type RTPMap = ReturnType<typeof parseRtpMap>;
export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string) {
const mlineType = mline.type;
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)(\/([\d]+))?/);
let channels = parseInt(match?.[5]) || undefined;
let payloadType = parseInt(match?.[1]);
rtpmap = rtpmap?.toLowerCase();
@@ -224,20 +222,14 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
if (mline.payloadTypes?.includes(0)) {
codec = 'pcm_mulaw';
ffmpegEncoder = 'pcm_mulaw';
payloadType = 0;
channels = 1;
}
else if (mline.payloadTypes?.includes(8)) {
codec = 'pcm_alaw';
ffmpegEncoder = 'pcm_alaw';
payloadType = 8;
channels = 1;
}
else if (mline.payloadTypes?.includes(14)) {
codec = 'mp3';
ffmpegEncoder = 'mp3';
payloadType = 14;
channels = 2;
}
else {
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
@@ -247,29 +239,17 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
// https://en.wikipedia.org/wiki/RTP_payload_formats
codec = 'pcm_alaw';
ffmpegEncoder = 'pcm_alaw';
payloadType = 8;
channels = 1;
}
}
// assigned payload types do not need to provide a clock, there is a default.
let clock = parseInt(match?.[3]);
if (!clock) {
clock = undefined;
if (codec === 'pcm_mulaw' || codec === 'pcm_alaw')
clock = 8000;
else if (codec === 'pcm_s16be')
clock = 16000;
}
return {
line: rtpmap,
codec,
ffmpegEncoder,
rawCodec: match?.[2],
clock,
channels,
payloadType,
clock: parseInt(match?.[3]),
channels: parseInt(match?.[5]) || undefined,
payloadType: parseInt(match?.[1]),
}
}

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "v0.139.0-noble-full"
version: "v0.130.1-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="noble-full"
ARG BASE="20-jammy-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

@@ -1,9 +0,0 @@
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,7 +1,5 @@
ARG BASE="jammy"
FROM ubuntu:${BASE} AS header
ENV SCRYPTED_DOCKER_FLAVOR="lite"
FROM ubuntu:${BASE} as header
ENV DEBIAN_FRONTEND=noninteractive
@@ -28,3 +26,5 @@ 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,8 +1,6 @@
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,10 +1,8 @@
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 cron
RUN apt -y update && apt -y install nano net-tools dnsutils dnsmasq vlan bridge-utils netplan.io nftables isc-dhcp-client
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,9 +8,6 @@ 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,14 +45,10 @@ services:
# - SCRYPTED_DOCKER_AVAHI=true
# NVIDIA (Part 1 of 2)
# nvidia runtime: nvidia
# runtime: nvidia
# NVIDIA (Part 2 of 2) - Use NVIDIA image, and remove subsequent default image.
# 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:nvidia
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,17 +85,20 @@ 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
# https://github.com/intel/compute-runtime/releases/tag/24.45.31740.9
# note that at time of commit, IGC supports ubuntu 24.04 only possibly due to their builder being on 24.04.
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
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
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.22.4
LEVEL_ZERO_VERSION=1.19.2
# https://github.com/oneapi-src/level-zero
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb
# npu driver
# https://github.com/intel/linux-npu-driver
NPU_VERSION=1.19.0
NPU_VERSION_DATE=20250707-16111289554
NPU_VERSION=1.13.0
NPU_VERSION_DATE=20250131-13074932693
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
# firmware can only be installed on host. will cause problems inside container.
if [ -n "$INTEL_FW_NPU" ]

View File

@@ -1,18 +0,0 @@
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,8 +36,7 @@ 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?
# cuda-drivers does not work with blackwell for some reason, container toolkit it broken IIRC.
apt -y install nvidia-open
apt -y install cuda-drivers
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-9 libcublas-12-9 libcudnn9-cuda-12 cuda-libraries-12-9;
&& apt install -y cuda-nvcc-12-6 libcublas-12-6 libcudnn9-cuda-12 cuda-libraries-12-6;
if [ "$?" != "0" ]
then

View File

@@ -13,8 +13,6 @@ then
fi
function readyn() {
echo
echo
if [ ! -z "$SCRYPTED_NONINTERACTIVE" ]
then
yn="y"
@@ -53,9 +51,6 @@ 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
@@ -98,24 +93,6 @@ else
sudo apt -y purge apparmor || true
fi
if [ ! -z "$HAS_NVIDIA" ]
then
readyn "NVIDIA GPU detected. Use NVIDIA image for GPU acceleration?"
if [ "$yn" == "y" ]
then
readyn "NVIDIA image requires the NVIDIA Drivers and Container Toolkit to be installed. This script can install them for you. Install NVIDIA Drivers and Container Toolkit for GPU acceleration?"
if [ "$yn" == "y" ]
then
curl -fsSL https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-container-toolkit.sh -o install-nvidia-container-toolkit.sh
chmod +x install-nvidia-container-toolkit.sh
./install-nvidia-container-toolkit.sh
rm install-nvidia-container-toolkit.sh
fi
sed -i 's/'#' nvidia //g' $DOCKER_COMPOSE_YML
sed -i 's/ghcr.io\/koush\/scrypted/ghcr.io\/koush\/scrypted:nvidia/g' $DOCKER_COMPOSE_YML
fi
fi
readyn "Install avahi-daemon? This is the recommended for reliable HomeKit discovery and pairing."
if [ "$yn" == "y" ]
then

View File

@@ -1,7 +1,7 @@
################################################################
# Begin section generated from template/Dockerfile.full.footer
################################################################
FROM header AS base
FROM header as base
# vulkan
RUN apt -y install libvulkan1

View File

@@ -42,6 +42,9 @@ RUN brew update
# in sequoia, brew node is unusable because it is not signed and can't access local network unless run as root.
# https://developer.apple.com/forums/thread/766270
# RUN_IGNORE brew install node@20
# NODE_PATH=$(brew --prefix node@20)
# NODE_BIN_PATH=$NODE_PATH/bin
RUN_IGNORE curl -L https://nodejs.org/dist/v22.14.0/node-v22.14.0.pkg -o /tmp/node.pkg
RUN_IGNORE sudo installer -pkg /tmp/node.pkg -target /
NODE_PATH=/usr/local # used to pass var test
@@ -85,13 +88,13 @@ RUN mkdir -p ~/Library/LaunchAgents
if [ ! -d "$NODE_PATH" ]
then
echo "Unable to determine node path."
echo "Unable to determine node@20 path."
exit 1
fi
if [ ! -d "$NODE_BIN_PATH" ]
then
echo "Unable to determine node bin path."
echo "Unable to determine node@20 bin path."
echo "$NODE_BIN_PATH does not exist."
exit 1
fi

View File

@@ -19,7 +19,7 @@ sc.exe stop scrypted.exe
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
# Install node.js
choco upgrade -y nodejs-lts --version=22.15.0
choco upgrade -y nodejs-lts --version=20.18.0
# Install VC Redist, which is necessary for portable python
choco install -y vcredist140
@@ -34,7 +34,7 @@ $SCRYPTED_WINDOWS_PYTHON_VERSION="-3.9"
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# Workaround Windows Node no longer creating %APPDATA%\npm which causes npx to fail
# Fixed in newer versions of NPM but not the one bundled with Node 2x
# Fixed in newer versions of NPM but not the one bundled with Node 20
# https://github.com/nodejs/node/issues/53538
npm i -g npm

View File

@@ -18,7 +18,7 @@ function readyn() {
}
cd /tmp
SCRYPTED_VERSION=v0.139.0
SCRYPTED_VERSION=v0.137.0
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
if [ -z "$VMID" ]
then

View File

@@ -27,8 +27,7 @@ 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 parsedURL = new URL(url);
const uri = parsedURL.pathname + parsedURL.search;
const uri = new URL(url).pathname;
const { DIGEST, buildAuthorizationHeader } = await import('http-auth-utils');

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/client",
"version": "1.3.17",
"version": "1.3.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.17",
"version": "1.3.13",
"license": "ISC",
"dependencies": {
"engine.io-client": "^6.6.3",
@@ -15,13 +15,13 @@
},
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^24.0.10",
"@types/ws": "^8.18.1",
"@types/node": "^22.13.10",
"@types/ws": "^8.18.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
"typescript": "^5.8.2"
},
"peerDependencies": {
"@scrypted/types": "^0.5.23"
"@scrypted/types": "^0.5.12"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -83,59 +83,11 @@
}
},
"node_modules/@scrypted/types": {
"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==",
"version": "0.5.12",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.12.tgz",
"integrity": "sha512-nTwcMHZyH3nXThL22eNcVw7OjSyL5qoTgUay6K7y43HKz1mBnFEmIUkW8eLdyP4nbpwwA0b60MOPDKZVnssB0Q==",
"license": "ISC",
"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
}
}
"peer": true
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
@@ -182,19 +134,19 @@
}
},
"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==",
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
"undici-types": "~6.20.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -732,9 +684,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"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==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -746,9 +698,9 @@
}
},
"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==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.3.17",
"version": "1.3.13",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -13,13 +13,13 @@
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^24.0.10",
"@types/ws": "^8.18.1",
"@types/node": "^22.13.10",
"@types/ws": "^8.18.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
"typescript": "^5.8.2"
},
"peerDependencies": {
"@scrypted/types": "^0.5.23"
"@scrypted/types": "^0.5.12"
},
"dependencies": {
"engine.io-client": "^6.6.3",

View File

@@ -1,17 +1,17 @@
{
"name": "@scrypted/deferred",
"version": "0.0.8",
"name": "@scrypted/rpc",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/deferred",
"version": "0.0.8",
"name": "@scrypted/rpc",
"version": "0.0.5",
"license": "ISC",
"devDependencies": {
"@types/node": "^24.0.10",
"rimraf": "^6.0.1",
"typescript": "^5.8.3"
"@types/node": "^18.11.18",
"rimraf": "^4.1.1",
"typescript": "^4.7.4"
}
},
"../../common": {
@@ -43,325 +43,20 @@
"../sdk/types": {
"extraneous": true
},
"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,
"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"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"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"
}
"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": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^11.0.0",
"package-json-from-dist": "^1.0.0"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
"rimraf": "dist/cjs/src/bin.js"
},
"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"
},
@@ -369,610 +64,38 @@
"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==",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"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"
"node": ">=4.2.0"
}
}
},
"dependencies": {
"@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==",
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"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": "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==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
"dev": true
},
"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==",
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"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.8",
"version": "0.0.5",
"description": "",
"main": "dist/index.js",
"scripts": {
@@ -12,8 +12,8 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^24.0.10",
"rimraf": "^6.0.1",
"typescript": "^5.8.3"
"@types/node": "^18.11.18",
"rimraf": "^4.1.1",
"typescript": "^4.7.4"
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/rpc",
"version": "0.0.8",
"version": "0.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/rpc",
"version": "0.0.8",
"version": "0.0.7",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.11.18",

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.166",
"version": "0.0.165",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.166",
"version": "0.0.165",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

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

View File

@@ -1,12 +1,12 @@
import { AuthFetchCredentialState, authHttpFetch, HttpFetchOptions } from '@scrypted/common/src/http-auth-fetch';
import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } 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,11 +81,8 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
}
}
function getLines(blob: string) {
return blob.split(/\r?\n/).filter(line => line);
}
function findValue(lines: string[], prefix: string, key: string) {
function findValue(blob: string, prefix: string, key: string) {
const lines = blob.split('\n');
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
if (!value)
return;
@@ -127,7 +124,7 @@ const amcrestResolutions = {
"720P": [1280, 720],
"D1": [704, 480],
"HD1": [352, 480],
"BCIF": [528, 240],
"BCIF": [704, 240],
"2CIF": [704, 240],
"CIF": [352, 240],
"QCIF": [176, 120],
@@ -136,21 +133,7 @@ const amcrestResolutions = {
"QVGA": [320, 240]
};
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;
}
function fromAmcrestResolution(resolution: string) {
const named = amcrestResolutions[resolution];
if (named)
return named;
@@ -455,12 +438,6 @@ 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}]`;
@@ -516,19 +493,17 @@ 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(capsLines, caps, key);
const found = findValue(capsResponse.body, caps, key);
if (found)
return found;
// ad410 doesnt return a camera number if accessed directly
if (cameraNumber - 1 === 0)
return findValue(capsLines, singleCaps, key);
return findValue(capsResponse.body, singleCaps, key);
}
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(r => fromAmcrestResolution(r, videoStandard));
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
const bitrates = findCaps('Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
const fpsMax = parseInt(findCaps('Video.FPSMax'));
const vso: MediaStreamConfiguration = {
@@ -558,7 +533,6 @@ 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];
@@ -570,27 +544,27 @@ export class AmcrestCameraClient {
encName = `table.Encode[${cameraNumber - 1}].ExtraFormat[${i - 1}]`;
}
const videoCodec = fromAmcrestVideoCodec(findValue(encodeLines, encName, 'Video.Compression'));
const audioCodec = fromAmcrestAudioCodec(findValue(encodeLines, encName, 'Audio.Compression'));
const videoCodec = fromAmcrestVideoCodec(findValue(encodeResponse.body, encName, 'Video.Compression'));
const audioCodec = fromAmcrestAudioCodec(findValue(encodeResponse.body, encName, 'Audio.Compression'));
if (vso.audio)
vso.audio.codec = audioCodec;
vso.video.codec = videoCodec;
const width = findValue(encodeLines, encName, 'Video.Width');
const height = findValue(encodeLines, encName, 'Video.Height');
const width = findValue(encodeResponse.body, encName, 'Video.Width');
const height = findValue(encodeResponse.body, encName, 'Video.Height');
if (width && height) {
vso.video.width = parseInt(width);
vso.video.height = parseInt(height);
}
const videoEnable = findValue(encodeLines, encName, 'VideoEnable');
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
if (videoEnable?.trim() === 'false') {
this.console.warn('Video stream is disabled and should likely be enabled:', encName);
continue;
}
const encodeOptions = findValue(encodeLines, encName, 'Video.BitRate');
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
if (!encodeOptions)
continue;

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.3.129",
"version": "0.3.120",
"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/util/device-icons";
import { typeToIcon } from "../../../../manage.scrypted.app/src/device-icons";
export class LauncherMixin extends ScryptedDeviceBase implements MixinProvider, Readme {
async getReadmeMarkdown(): Promise<string> {

View File

@@ -1,5 +1,4 @@
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';
@@ -9,7 +8,6 @@ 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';
@@ -17,6 +15,7 @@ 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;
@@ -211,32 +210,6 @@ 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[]> {
@@ -359,15 +332,5 @@ 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,8 +30,6 @@ 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, DeviceManifest, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedUser, ScryptedUserAccessControl, Setting, Settings, SettingValue } from "@scrypted/sdk";
import sdk, { DeviceCreator, DeviceCreatorSettings, 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,13 +132,7 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
deviceCreator: 'Scrypted User',
};
this.syncUsers()
.then(length => {
if (!length) {
this.console.log('no users found, looping for first user');
setInterval(() => this.syncUsers(), 60 * 1000);
}
})
this.syncUsers();
}
async getDevice(nativeId: string): Promise<any> {
@@ -198,7 +192,7 @@ export class UsersCore extends ScryptedDeviceBase implements Readme, DeviceProvi
async syncUsers() {
const usersService = await sdk.systemManager.getComponent('users');
const users: DBUser[] = await usersService.getAllUsers();
const manifest: DeviceManifest = {
await sdk.deviceManager.onDevicesChanged({
providerNativeId: this.nativeId,
devices: users.map(user => ({
name: user.username,
@@ -209,16 +203,6 @@ 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,42 +1,34 @@
{
"name": "@scrypted/coreml",
"version": "0.1.83",
"version": "0.1.77",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.83",
"version": "0.1.77",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.22",
"version": "0.3.77",
"dev": true,
"license": "ISC",
"dependencies": {
"@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",
"@babel/preset-typescript": "^7.26.0",
"adm-zip": "^0.5.16",
"axios": "^1.10.0",
"babel-loader": "^10.0.0",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"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.2",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -49,9 +41,11 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^24.0.1",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"ts-node": "^10.9.2",
"typedoc": "^0.28.5"
"typedoc": "^0.26.10"
}
},
"../sdk": {
@@ -66,29 +60,23 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@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",
"@babel/preset-typescript": "^7.26.0",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"adm-zip": "^0.5.16",
"axios": "^1.10.0",
"babel-loader": "^10.0.0",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"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",
"stringify-object": "^3.3.0",
"tmp": "^0.2.3",
"ts-loader": "^9.5.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"typedoc": "^0.26.10",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
}
}

View File

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

View File

@@ -14,8 +14,6 @@ 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
@@ -79,8 +77,6 @@ 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":
@@ -147,14 +143,13 @@ 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:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
devices = [
{
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -164,10 +159,10 @@ class CoreMLPlugin(
],
"name": "CoreML Face Recognition",
},
)
]
if CoreMLTextRecognition:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
devices.append(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -179,17 +174,9 @@ class CoreMLPlugin(
},
)
await scrypted_sdk.deviceManager.onDeviceDiscovered(
await scrypted_sdk.deviceManager.onDevicesChanged(
{
"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",
"devices": devices,
}
)
except:
@@ -199,19 +186,10 @@ class CoreMLPlugin(
if nativeId == "facerecognition":
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(self, nativeId)
return self.faceDevice
elif nativeId == "textrecognition":
if nativeId == "textrecognition":
self.textDevice = self.textDevice or CoreMLTextRecognition(self, nativeId)
return self.textDevice
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
raise Exception("unknown device")
async def getSettings(self) -> list[Setting]:
model = self.storage.getItem("model") or "Default"

View File

@@ -1,85 +0,0 @@
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

@@ -1,60 +0,0 @@
from __future__ import annotations
import asyncio
import concurrent.futures
import os
import coremltools as ct
import numpy as np
import scrypted_sdk
from PIL import Image
from predict.custom_detect import CustomDetection
class CoreMLCustomDetection(CustomDetection):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
self.prefer_relu = True
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-custom")
def loadModel(self, files: list[str]):
# find the xml file in the files list
manifest_files = [f for f in files if f.lower().endswith('manifest.json')]
if not manifest_files:
raise ValueError("No Manifest.json file found in the provided files list")
manifest_file = manifest_files[0]
modelFile = os.path.dirname(manifest_file)
model = ct.models.MLModel(modelFile)
inputName = model.get_spec().description.input[0].name
return model, inputName
async def predictModel(self, input: Image.Image) -> scrypted_sdk.ObjectsDetected:
model, inputName = self.model
def predict():
if self.model_config.get("mean", None) and self.model_config.get("std", None):
im = np.array(input)
im = im.astype(np.float32) / 255.0
mean = np.array(self.model_config.get("mean", None), dtype=np.float32)
std = np.array(self.model_config.get("std", None), dtype=np.float32)
im = (im - mean) / std
# Convert HWC to CHW
im = im.transpose(2, 0, 1) # Channels first
im = im.astype(np.float32)
im = np.ascontiguousarray(im)
im = np.expand_dims(im, axis=0)
out_dict = model.predict({inputName: im})
else:
out_dict = model.predict({inputName: input})
results = list(out_dict.values())[0][0]
return results
results = await asyncio.get_event_loop().run_in_executor(
self.detectExecutor, lambda: predict()
)
return results

View File

@@ -25,6 +25,9 @@ def cosine_similarity(vector_a, vector_b):
similarity = dot_product / (norm_a * norm_b)
return similarity
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
class CoreMLFaceRecognition(FaceRecognizeDetection):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin, nativeId)

View File

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

View File

@@ -522,74 +522,32 @@ 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.");
}
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 (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];
}
if (params.mode) {
setModeConfigs(params.mode);
supp.mixedLightBrightnessRegulatMode = [params.mode];
} else if (params.on !== undefined) {
supp.mixedLightBrightnessRegulatMode = [params.on ? "manual" : "auto"];
}
if (params.brightness !== undefined) {
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');
}
let brightness = Math.max(0, Math.min(100, params.brightness));
supp.whiteLightBrightness = [brightness.toString()];
}
const builder = new xml2js.Builder({
@@ -597,7 +555,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,41 +1,34 @@
{
"name": "@scrypted/ncnn",
"version": "0.1.88",
"name": "@scrypted/coreml",
"version": "0.1.77",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ncnn",
"version": "0.1.88",
"name": "@scrypted/coreml",
"version": "0.1.77",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.12",
"version": "0.3.77",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -48,9 +41,11 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^22.10.1",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.11"
"typedoc": "^0.26.10"
}
},
"../sdk": {
@@ -66,27 +61,22 @@
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"@types/node": "^22.10.1",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"stringify-object": "^3.3.0",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typedoc": "^0.26.11",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"typedoc": "^0.26.10",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
}
}

View File

@@ -33,22 +33,16 @@
"runtime": "python",
"type": "API",
"interfaces": [
"ScryptedSystemDevice",
"DeviceCreator",
"Settings",
"DeviceProvider",
"ClusterForkInterface",
"ObjectDetection",
"ObjectDetectionPreview"
],
"labels": {
"require": [
"@scrypted/ncnn"
]
}
]
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.88"
"version": "0.1.77"
}

View File

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

View File

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

View File

@@ -1,82 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,7 @@ 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 { safeParseJson } from '../../../common/src/json';
import { getAllDevices } from '../../../common/src/devices';
import { getAllDevices, safeParseJson } from './util';
import { FFmpegAudioDetectionMixinProvider } from './ffmpeg-audiosensor';
@@ -95,7 +94,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
onGet: async () => {
const choices = [
'Default',
...getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
return {
hide: this.model?.decoder,
@@ -691,7 +690,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (frameGenerator === 'Default')
frameGenerator = this.plugin.storageSettings.values.defaultDecoder || 'Default';
const pipelines = getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
const pipelines = getAllDevices().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;
@@ -1027,7 +1026,7 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
onGet: async () => {
const choices = [
'Default',
...getAllDevices(sdk.systemManager).filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
return {
choices,

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
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,4 +1,20 @@
{
"scrypted.debugHost": "koushik-ubuntu24",
}
// 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"
]
}

View File

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

View File

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

View File

@@ -17,12 +17,10 @@ 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
@@ -60,8 +58,6 @@ 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":
@@ -166,14 +162,13 @@ 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:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
devices = [
{
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -183,10 +178,10 @@ class ONNXPlugin(
],
"name": "ONNX Face Recognition",
},
)
]
if ONNXTextRecognition:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
devices.append(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -198,17 +193,9 @@ class ONNXPlugin(
},
)
await scrypted_sdk.deviceManager.onDeviceDiscovered(
await scrypted_sdk.deviceManager.onDevicesChanged(
{
"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",
"devices": devices,
}
)
except:
@@ -221,16 +208,7 @@ class ONNXPlugin(
elif nativeId == "textrecognition":
self.textDevice = self.textDevice or ONNXTextRecognition(self, nativeId)
return self.textDevice
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
raise Exception("unknown device")
async def getSettings(self) -> list[Setting]:
model = self.storage.getItem("model") or "Default"

View File

@@ -1,122 +0,0 @@
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

@@ -1,127 +0,0 @@
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.22.0; 'darwin' not in sys_platform and platform_machine != 'aarch64'
onnxruntime-gpu==1.19.2; '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,5 +9,3 @@ 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,43 +1,36 @@
{
"name": "@scrypted/openvino",
"version": "0.1.185",
"version": "0.1.177",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/openvino",
"version": "0.1.185",
"version": "0.1.177",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.20",
"version": "0.3.29",
"dev": true,
"license": "ISC",
"dependencies": {
"@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",
"@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",
"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.2",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"webpack-bundle-analyzer": "^4.10.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"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
@@ -49,9 +42,11 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^24.0.1",
"ts-node": "^10.9.2",
"typedoc": "^0.28.5"
"@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"
}
},
"../sdk": {
@@ -66,30 +61,25 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@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",
"@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",
"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.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"
"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"
}
}
}

View File

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

View File

@@ -1,25 +0,0 @@
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,7 +18,6 @@ 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:
@@ -26,8 +25,6 @@ try:
except:
OpenVINOTextRecognition = None
from .clip_embedding import OpenVINOClipEmbedding
predictExecutor = concurrent.futures.ThreadPoolExecutor(
thread_name_prefix="OpenVINO-Predict"
)
@@ -107,8 +104,6 @@ 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
@@ -302,7 +297,6 @@ class OpenVINOPlugin(
self.faceDevice = None
self.textDevice = None
self.clipDevice = None
if not self.forked:
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
@@ -401,7 +395,7 @@ class OpenVINOPlugin(
async def prepareRecognitionModels(self):
try:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
devices = [
{
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -411,10 +405,10 @@ class OpenVINOPlugin(
],
"name": "OpenVINO Face Recognition",
},
)
]
if OpenVINOTextRecognition:
await scrypted_sdk.deviceManager.onDeviceDiscovered(
devices.append(
{
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
@@ -423,20 +417,12 @@ class OpenVINOPlugin(
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "OpenVINO Text Recognition",
}
},
)
await scrypted_sdk.deviceManager.onDeviceDiscovered(
await scrypted_sdk.deviceManager.onDevicesChanged(
{
"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",
"devices": devices,
}
)
except:
@@ -449,13 +435,4 @@ class OpenVINOPlugin(
elif nativeId == "textrecognition":
self.textDevice = self.textDevice or OpenVINOTextRecognition(self, nativeId)
return self.textDevice
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
raise Exception("unknown device")

View File

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

View File

@@ -1,87 +0,0 @@
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

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

View File

@@ -1,8 +1,5 @@
from __future__ import annotations
import json
import random
import re
import asyncio
import math
import os
@@ -48,7 +45,7 @@ class Prediction:
self.embedding = embedding
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface, scrypted_sdk.ScryptedSystemDevice, scrypted_sdk.DeviceCreator, scrypted_sdk.DeviceProvider):
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
labels: dict
def __init__(
@@ -59,10 +56,6 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface, scrypted_sd
):
super().__init__(nativeId=nativeId)
self.systemDevice = {
"deviceCreator": "Model",
}
self.plugin = plugin
# self.clusterIndex = 0
@@ -363,10 +356,6 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface, scrypted_sd
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):
@@ -405,79 +394,6 @@ class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface, scrypted_sd
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:
@@ -493,9 +409,3 @@ 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

@@ -1,63 +0,0 @@
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

@@ -1,135 +0,0 @@
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,5 +7,3 @@
openvino==2024.5.0
Pillow==10.3.0
opencv-python-headless==4.10.0.84
transformers==4.52.4

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import { OnvifCameraAPI, OnvifEvent, connectCameraAPI } from './onvif-api';
import { listenEvents } from './onvif-events';
import { OnvifIntercom } from './onvif-intercom';
import { DevInfo } from './probe';
import { AIState, Enc, isDeviceNvr, ReolinkCameraClient } from './reolink-api';
import { AIState, Enc, ReolinkCameraClient } from './reolink-api';
class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
sirenTimeout: NodeJS.Timeout;
@@ -103,6 +103,7 @@ 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;
@@ -570,7 +571,6 @@ 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 && !isDeviceNvr(deviceInfo))
if (!hasSucceeded)
return;
ret.emit('error', e);
}
@@ -769,6 +769,15 @@ 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,8 +75,6 @@ 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>;
@@ -322,7 +320,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 (!isDeviceNvr(deviceInfo)) {
if (!['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType)) {
return deviceInfo;
}

View File

@@ -10,7 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-console.*",
"**/plugin-remote-worker.*",
"<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": "^0.3.61",
"@scrypted/sdk": "file:../../sdk",
"@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.145"
"version": "0.0.144"
}

View File

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

View File

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

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