Compare commits

..

10 Commits
main ... libav

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
266 changed files with 9471 additions and 29457 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", "nvidia-legacy", "intel", "amd"]
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,13 +20,10 @@ jobs:
strategy:
matrix:
BASE: [
["noble-nvidia", ".s6", "noble-nvidia", "nvidia"],
["noble-nvidia-legacy", ".s6", "noble-nvidia-legacy", "nvidia-legacy"],
["noble-intel", ".s6", "noble-intel", "intel"],
["noble-amd", ".s6", "noble-amd", "amd"],
["noble-full", ".s6", "noble-full", "full"],
["noble-lite", "", "noble-lite", "lite"],
["noble-lite", ".router", "noble-router", "router"],
["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
@@ -97,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,41 +10,39 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
"devDependencies": {
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}
},
"../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.39",
"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,
@@ -3340,13 +3329,11 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz",
"integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==",
"version": "20.11.0",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~5.26.4"
}
},
"node_modules/acorn": {
@@ -3406,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,
@@ -3481,9 +3447,7 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "5.26.5",
"dev": true,
"license": "MIT"
},

View File

@@ -12,12 +12,11 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
"devDependencies": {
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}

View File

@@ -9,16 +9,6 @@ export function createAsyncQueue<T>() {
const waiting: Deferred<T>[] = [];
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
const wait = async (index: number) => {
const q = queued[index];
if (!q)
return;
if (!q.dequeued) {
q.dequeued = new Deferred<void>();
}
return q.dequeued.promise;
}
const dequeue = async () => {
if (queued.length) {
const { item, dequeued: enqueue } = queued.shift()!;
@@ -76,7 +66,7 @@ export function createAsyncQueue<T>() {
dequeued?.reject(new Error('abort'));
};
dequeued?.promise.catch(() => { }).finally(() => signal.removeEventListener('abort', h));
dequeued?.promise.catch(() => {}).finally(() => signal.removeEventListener('abort', h));
signal.addEventListener('abort', h);
return true;
@@ -164,14 +154,13 @@ export function createAsyncQueue<T>() {
dequeue,
get queue() {
return queue();
},
wait,
}
}
}
export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
const q = createAsyncQueue<T>();
(async () => {
(async() => {
try {
for await (const i of generator) {
await q.enqueue(i);

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

@@ -2,7 +2,6 @@ import type * as monacoEditor from 'monaco-editor';
export interface StandardLibs {
'@types/node/globals.d.ts': string,
'@types/node/module.d.ts': string,
'@types/node/buffer.d.ts': string,
'@types/node/process.d.ts': string,
'@types/node/events.d.ts': string,

View File

@@ -116,7 +116,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
const standardlibs: StandardLibs = {
"@types/node/globals.d.ts": readFileAsString('@types/node/globals.d.ts'),
"@types/node/module.d.ts": readFileAsString('@types/node/module.d.ts'),
"@types/node/buffer.d.ts": readFileAsString('@types/node/buffer.d.ts'),
"@types/node/process.d.ts": readFileAsString('@types/node/process.d.ts'),
"@types/node/events.d.ts": readFileAsString('@types/node/events.d.ts'),

View File

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

View File

@@ -110,9 +110,7 @@ export class BrowserSignalingSession implements RTCSignalingSession {
await this.microphone.replaceTrack(mic.getTracks()[0]);
}
if (this.microphone?.track) {
this.microphone.track.enabled = enabled;
}
this.microphone.track.enabled = enabled;
}
close() {

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

@@ -2,6 +2,7 @@ import { Socket as DatagramSocket } from "dgram";
import { once } from "events";
import { Duplex } from "stream";
import { FFMPEG_FRAGMENTED_MP4_OUTPUT_ARGS, MP4Atom, parseFragmentedMP4 } from "./ffmpeg-mp4-parser-session";
import { readLength } from "./read-stream";
export interface StreamParser {
container: string;
@@ -24,11 +25,59 @@ export interface StreamParserOptions {
export interface StreamChunk {
startStream?: Buffer;
chunks: Buffer[];
type: string;
type?: string;
width?: number;
height?: number;
}
// function checkTsPacket(pkt: Buffer) {
// const pid = ((pkt[1] & 0x1F) << 8) | pkt[2];
// if (pid == 256) {
// // found video stream
// if ((pkt[3] & 0x20) && (pkt[4] > 0)) {
// // have AF
// if (pkt[5] & 0x40) {
// // found keyframe
// console.log('keyframe');
// }
// }
// }
// }
function createLengthParser(length: number, verify?: (concat: Buffer) => void) {
async function* parse(socket: Duplex): AsyncGenerator<StreamChunk> {
let pending: Buffer[] = [];
let pendingSize = 0;
while (true) {
const data: Buffer = socket.read();
if (!data) {
await once(socket, 'readable');
continue;
}
pending.push(data);
pendingSize += data.length;
if (pendingSize < length)
continue;
const concat = Buffer.concat(pending);
verify?.(concat);
const remaining = concat.length % length;
const left = concat.slice(0, concat.length - remaining);
const right = concat.slice(concat.length - remaining);
pending = [right];
pendingSize = right.length;
yield {
chunks: [left],
};
}
}
return parse;
}
export function createDgramParser() {
async function* parse(socket: DatagramSocket, width: number, height: number, type: string) {
while (true) {
@@ -42,6 +91,65 @@ export function createDgramParser() {
return parse;
}
export function createMpegTsParser(options?: StreamParserOptions): StreamParser {
return {
container: 'mpegts',
outputArguments: [
...(options?.vcodec || []),
...(options?.acodec || []),
'-f', 'mpegts',
],
parse: createLengthParser(188, concat => {
if (concat[0] != 0x47) {
throw new Error('Invalid sync byte in mpeg-ts packet. Terminating stream.')
}
}),
findSyncFrame(streamChunks): StreamChunk[] {
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
const streamChunk = streamChunks[prebufferIndex];
for (let chunkIndex = 0; chunkIndex < streamChunk.chunks.length; chunkIndex++) {
const chunk = streamChunk.chunks[chunkIndex];
let offset = 0;
while (offset + 188 < chunk.length) {
const pkt = chunk.subarray(offset, offset + 188);
const pid = ((pkt[1] & 0x1F) << 8) | pkt[2];
if (pid == 256) {
// found video stream
if ((pkt[3] & 0x20) && (pkt[4] > 0)) {
// have AF
if (pkt[5] & 0x40) {
// we found the sync frame, but also need to send the pat and pmt
// which might be at the start of this chunk before the keyframe.
// yolo!
return streamChunks.slice(prebufferIndex);
// const chunks = streamChunk.chunks.slice(chunkIndex + 1);
// const take = chunk.subarray(offset);
// chunks.unshift(take);
// const remainingChunks = streamChunks.slice(prebufferIndex + 1);
// const ret = Object.assign({}, streamChunk);
// ret.chunks = chunks;
// return [
// ret,
// ...remainingChunks
// ];
}
}
}
offset += 188;
}
}
}
return findSyncFrame(streamChunks);
}
}
}
export async function* parseMp4StreamChunks(parser: AsyncGenerator<MP4Atom>) {
let ftyp: MP4Atom;
let moov: MP4Atom;
@@ -105,3 +213,54 @@ export const PIXEL_FORMAT_RGB24: RawVideoPixelFormat = {
name: 'rgb24',
computeLength: (width, height) => width * height * 3,
}
export function createRawVideoParser(options: RawVideoParserOptions): StreamParser {
const pixelFormat = options?.pixelFormat || PIXEL_FORMAT_YUV420P;
let filter: string;
const { size, everyNFrames } = options;
if (size) {
filter = `scale=${size.width}:${size.height}`;
}
if (everyNFrames && everyNFrames > 1) {
if (filter)
filter += ',';
else
filter = '';
filter = filter + `select=not(mod(n\\,${everyNFrames}))`
}
const inputArguments: string[] = [];
if (options.size)
inputArguments.push('-s', `${options.size.width}x${options.size.height}`);
inputArguments.push('-pix_fmt', pixelFormat.name);
return {
inputArguments,
container: 'rawvideo',
outputArguments: [
'-s', `${options.size.width}x${options.size.height}`,
'-an',
'-vcodec', 'rawvideo',
'-pix_fmt', pixelFormat.name,
'-f', 'rawvideo',
],
async *parse(socket: Duplex, width: number, height: number): AsyncGenerator<StreamChunk> {
width = size?.width || width;
height = size?.height || height
if (!width || !height)
throw new Error("error parsing rawvideo, unknown width and height");
const toRead = pixelFormat.computeLength(width, height);
while (true) {
const buffer = await readLength(socket, toRead);
yield {
chunks: [buffer],
width,
height,
}
}
},
findSyncFrame,
}
}

View File

@@ -1,98 +0,0 @@
export abstract class AsyncUsingHolderBase<T> {
constructor(private _value: T) {
}
get value(): T {
return this._value;
}
async [Symbol.asyncDispose]() {
await this.release();
}
abstract asyncDispose(value: T): Promise<void>;
detach() {
const value = this._value;
this._value = undefined;
return value;
}
async replace(value: T) {
this.release();
this._value = value;
}
async release() {
const released = this.detach();
if (released)
await this.asyncDispose(released);
}
}
export abstract class UsingHolderBase<T> {
constructor(private _value: T) {
}
get value(): T {
return this._value;
}
[Symbol.dispose]() {
this.release();
}
abstract dispose(value: T): void;
detach() {
const value = this._value;
this._value = undefined;
return value;
}
replace(value: T) {
this.release();
this._value = value;
}
release() {
const released = this.detach();
if (released)
this.dispose(released);
}
}
export class UsingHolder<T extends Disposable> extends UsingHolderBase<T> {
dispose(value: T) {
value?.[Symbol.dispose]();
}
transferClosure<V>(closure: (value: UsingHolder<T>) => Promise<V>) {
return (async () => {
using attached = new UsingHolder(this.detach());
return await closure(attached);
})();
}
}
export class AsyncUsingHolder<T extends AsyncDisposable> extends AsyncUsingHolderBase<T> {
async asyncDispose(value: T) {
value?.[Symbol.asyncDispose]();
}
transferClosure<V>(closure: (value: AsyncUsingHolder<T>) => Promise<V>) {
return (async () => {
await using attached = new AsyncUsingHolder(this.detach());
return await closure(attached);
})();
}
}
export class DisposableHolder<T> extends UsingHolderBase<T> {
constructor(value: T, private _dispose: (value: T) => void) {
super(value);
}
dispose(value: T) {
this._dispose(value);
}
}

View File

@@ -52,12 +52,7 @@ export function createZygote<T>(options?: ForkOptions): Zygote<T> {
}
const gen = next();
return () => {
const ret = gen.next();
if (ret.done || !ret.value)
throw new Error('Zygote exhausted');
return ret.value;
};
return () => gen.next().value as PluginFork<T>;
}

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "Node16",
"module": "commonjs",
"moduleResolution": "Node16",
"target": "esnext",
"noImplicitAny": true,

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "v0.143.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

@@ -1,4 +1,4 @@
ARG BASE="noble-full"
ARG BASE="16-jammy"
FROM ghcr.io/koush/scrypted-common:${BASE}
WORKDIR /
@@ -8,4 +8,4 @@ WORKDIR /scrypted/server
RUN npm install
RUN npm run build
CMD ["npm", "run", "serve-no-build"]
CMD npm run serve-no-build

View File

@@ -1,7 +0,0 @@
ARG BASE="ghcr.io/koush/scrypted-common:noble-amd"
FROM $BASE
ENV SCRYPTED_DOCKER_FLAVOR="amd"
# amd opencl
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash

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
@@ -35,6 +35,19 @@ RUN apt-get -y install \
python3-setuptools \
python3-wheel
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN echo "Installing gstreamer."
# python-codecs pygobject dependencies
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
RUN apt-get -y install \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
gstreamer1.0-vaapi
# python3 gstreamer bindings
RUN echo "Installing gstreamer bindings."
RUN apt-get -y install \
python3-gst-1.0
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
@@ -48,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
@@ -56,9 +69,12 @@ RUN apt -y install libvulkan1
# intel opencl for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
# intel NPU
# NPU driver will SIGILL on openvino prior to 2024.5.0
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# amd opencl
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
@@ -76,7 +92,7 @@ RUN python3.9 -m pip install debugpy
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
RUN curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/coral-edgetpu.gpg
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
# set default shell to bash

View File

@@ -1,16 +0,0 @@
ARG BASE="ghcr.io/koush/scrypted-common:noble-intel"
FROM $BASE
ENV SCRYPTED_DOCKER_FLAVOR="intel"
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-oneapi.sh | bash
ENV LD_LIBRARY_PATH=/opt/intel/oneapi/tcm/latest/lib
ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/intel/oneapi/umf/latest/lib
# gcc4.8 does not have a latest link however, it does seem to point to a relative lib path
ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/intel/oneapi/tbb/latest/env/../lib/intel64/gcc4.8
ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/intel/oneapi/tbb/latest/lib
ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/intel/oneapi/mkl/latest/lib
ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/intel/oneapi/compiler/latest/opt/compiler/lib
ENV LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/opt/intel/oneapi/compiler/latest/lib

View File

@@ -1,7 +1,5 @@
ARG BASE="noble-lite"
FROM ubuntu:${BASE} AS header
ENV SCRYPTED_DOCKER_FLAVOR="lite"
ARG BASE="jammy"
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:noble-nvidia"
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,11 +0,0 @@
ARG BASE="ghcr.io/koush/scrypted-common:noble-nvidia-legacy"
FROM $BASE
ENV SCRYPTED_DOCKER_FLAVOR="nvidia"
ENV NVIDIA_DRIVER_CAPABILITIES=all
ENV NVIDIA_VISIBLE_DEVICES=all
# nvidia cudnn/libcublas etc.
# for some reason this is not provided by the nvidia container toolkit
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-nvidia-graphics-legacy.sh | bash

View File

@@ -1,23 +1,13 @@
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
# go + caddy
RUN GO_VERSION=1.25.1 && ARCH=$(dpkg --print-architecture) && \
if [ "$ARCH" = "amd64" ]; then GOARCH="amd64"; \
elif [ "$ARCH" = "arm64" ]; then GOARCH="arm64"; \
elif [ "$ARCH" = "armhf" ]; then GOARCH="armv6l"; \
else echo "Unsupported architecture: $ARCH" && exit 1; fi && \
curl -LO "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" && \
tar -C /usr/local -xzf "go${GO_VERSION}.linux-${GOARCH}.tar.gz" && \
rm "go${GO_VERSION}.linux-${GOARCH}.tar.gz"
ENV PATH=$PATH:/usr/local/go/bin
RUN apt -y install golang-go
RUN apt install -y debian-keyring debian-archive-keyring apt-transport-https
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-xcaddy-archive-keyring.gpg
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/xcaddy/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-xcaddy.list

View File

@@ -1,4 +1,4 @@
ARG BASE="noble-full"
ARG BASE="20-jammy-full"
FROM ghcr.io/koush/scrypted-common:${BASE}
# avahi advertiser support
@@ -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

@@ -1,3 +1,3 @@
./docker-build.sh
docker build -t ghcr.io/koush/scrypted:nvidia -f Dockerfile.nvidia .
docker build -t ghcr.io/koush/scrypted:20-jammy-full.nvidia -f Dockerfile.nvidia .

View File

@@ -4,7 +4,7 @@ set -x
NODE_VERSION=22
SCRYPTED_INSTALL_VERSION=beta
IMAGE_BASE=noble
IMAGE_BASE=jammy
FLAVOR=full
BASE=$NODE_VERSION-$IMAGE_BASE-$FLAVOR
echo $BASE

View File

@@ -1,45 +0,0 @@
import os
from ruamel.yaml import YAML
# Define the devices to check for
devices_to_check = [
"/dev/dri",
"/dev/accel",
"/dev/apex_0",
"/dev/apex_1",
"/dev/kfd",
"/dev/bus/usb"
]
# Use ruamel.yaml with better formatting preservation
yaml = YAML()
yaml.preserve_quotes = True
# Explicitly set roundtrip mode for comment preservation
yaml.typ = 'rt'
# Match the original formatting - 4 space indentation
yaml.indent = 4
# No special block sequence indentation
yaml.block_seq_indent = 0
# Don't wrap lines
yaml.width = None
# Preserve unicode
yaml.allow_unicode = True
# Read the docker-compose.yml file
with open('docker-compose.yml', 'r') as file:
compose_data = yaml.load(file)
# Get a direct reference to the devices key
scrypted_service = compose_data['services']['scrypted']
devices = scrypted_service.setdefault('devices', [])
# Check for devices and add them if they exist
for device_path in devices_to_check:
if os.path.exists(device_path):
device_mapping = f"{device_path}:{device_path}"
if device_mapping not in devices:
devices.append(device_mapping)
# Write the modified docker-compose.yml file (preserving comments and formatting)
with open('docker-compose.yml', 'w') as file:
yaml.dump(compose_data, file)

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env bash
# run as privileged so all the devices can be detected and only the necessary ones passed through.
docker run --rm \
--privileged \
-v "$(pwd):/app" \
-w /app \
python:3.12-slim \
sh -c "pip install -q --root-user-action=ignore ruamel.yaml && python docker-compose-setup.py"

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:
@@ -132,12 +128,6 @@ services:
labels:
- "com.centurylinklabs.watchtower.scope=scrypted"
# Use global DNS servers to avoid issues with some local DNS servers.
# particularly with npm registry, etc.
dns:
- ${SCRYPTED_DNS_SERVER_0:-1.1.1.1}
- ${SCRYPTED_DNS_SERVER_1:-8.8.8.8}
# watchtower manages updates for Scrypted.
watchtower:
environment:
@@ -145,7 +135,7 @@ services:
- WATCHTOWER_HTTP_API_UPDATE=true
- WATCHTOWER_SCOPE=scrypted
- WATCHTOWER_HTTP_API_PERIODIC_POLLS=${WATCHTOWER_HTTP_API_PERIODIC_POLLS:-true}
image: nickfedor/watchtower
image: containrrr/watchtower
container_name: scrypted-watchtower
restart: unless-stopped
volumes:
@@ -158,11 +148,3 @@ services:
- 10444:8080
# check for updates once an hour (interval is in seconds)
command: --interval 3600 --cleanup --scope scrypted
# Use global DNS servers to avoid issues with some local DNS servers.
# particularly with npm registry, etc.
dns:
- ${SCRYPTED_DNS_SERVER_0:-1.1.1.1}
- ${SCRYPTED_DNS_SERVER_1:-8.8.8.8}
# LXC usage only
# lxc profiles: ["disabled"]

View File

@@ -21,34 +21,23 @@ else
distro="noble"
fi
apt -y update
apt -y install rsync gpg
# the deb no longer seems to install a key?
gpg --keyserver keyserver.ubuntu.com --recv-keys 9386B48A1A693C5C
gpg --export --armor 9386B48A1A693C5C | tee /etc/apt/trusted.gpg.d/amdgpu.asc
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
# AMD keeps breaking these links. Use hard links.
# FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
# if [ -z "$FILENAME" ]
# then
# echo "AMD graphics package can not be installed. Could not find the package name."
# exit 1
# fi
FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
if [ -z "$FILENAME" ]
then
echo "AMD graphics package can not be installed. Could not find the package name."
exit 1
fi
set -e
mkdir -p /tmp/amd
cd /tmp/amd
# curl -O -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
FILENAME=amdgpu-install_7.0.1.70001-1_all.deb
curl -O -L https://repo.radeon.com/amdgpu-install/7.0.1/ubuntu/$distro/$FILENAME
curl -O -L http://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
apt -y update
apt -y install rsync
dpkg -i $FILENAME
apt -y update
amdgpu-install --usecase=opencl --no-dkms -y --accept-eula
cd /tmp
rm -rf /tmp/amd

View File

@@ -69,50 +69,42 @@ apt-get install -y ocl-icd-libopencl1
# install 24.35.30872.22 for legacy support. Then install latest.
# https://github.com/intel/compute-runtime/issues/770#issuecomment-2515166915
# original legacy packages
# # 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-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-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
# new legacy packages
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.36
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-level-zero-gpu-legacy1-dbgsym_1.5.30872.36_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-level-zero-gpu-legacy1_1.5.30872.36_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1-dbgsym_24.35.30872.36_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/libigdgmm12_22.5.0_amd64.deb
# 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-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-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
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.18.5/intel-igc-core-2_2.18.5+19820_amd64.deb
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.18.5/intel-igc-opencl-2_2.18.5+19820_amd64.deb
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-ocloc-dbgsym_25.35.35096.9-0_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-ocloc_25.35.35096.9-0_amd64.deb
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-opencl-icd-dbgsym_25.35.35096.9-0_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/intel-opencl-icd_25.35.35096.9-0_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/libigdgmm12_22.8.1_amd64.deb
# curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/libze-intel-gpu1-dbgsym_25.35.35096.9-0_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/25.35.35096.9/libze-intel-gpu1_25.35.35096.9-0_amd64.deb
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
set -e
# the legacy + latest process says this may be necessary but it does not seem to be in a clean environment.
apt-get -y install --fix-broken
apt-get install --fix-broken
cd /tmp && rm -rf /tmp/gpu

View File

@@ -9,17 +9,11 @@ UBUNTU_24_04=$(lsb_release -r | grep "24.04")
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
# proxmox is compatible with intel's ubuntu builds, check for /etc/pve directory
# then determine debian version
version=$(cat /etc/debian_version 2>/dev/null)
# Determine if it's Debian 12 or 13
if [[ "$version" == 12* ]]; then
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
if [ -d "/etc/pve" ]
then
UBUNTU_22_04=true
elif [[ "$version" == 13* ]]; then
UBUNTU_24_04=true
fi
fi
# needs either ubuntu 22.0.4 or 24.04
@@ -31,10 +25,8 @@ fi
if [ -n "$UBUNTU_22_04" ]
then
ubuntu_distro=ubuntu2204
distro="22.04_amd64"
else
ubuntu_distro=ubuntu2404
distro="24.04_amd64"
fi
@@ -46,24 +38,22 @@ set -e
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
# level zero must also be installed
LEVEL_ZERO_VERSION=1.24.2
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.23.0
NPU_VERSION_DATE=20250827-17270089246
NPU_TAR_FILENAME=linux-npu-driver-v"$NPU_VERSION"."$NPU_VERSION_DATE"-$ubuntu_distro.tar.gz
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/"$NPU_TAR_FILENAME"
tar xzvf "$NPU_TAR_FILENAME"
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 [ ! -z "$INTEL_FW_NPU" ]
if [ -n "$INTEL_FW_NPU" ]
then
rm *fw-npu*
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-fw-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-level-zero-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
apt -y update
apt -y install libtbb12

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

@@ -1,5 +1,3 @@
apt -y install lsb-release
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
@@ -11,16 +9,6 @@ set -e
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
# Do not apt install nvidia-open, must use cuda-drivers.
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
if [ -d "/etc/pve" ]
then
apt -y install pve-headers-$(uname -r)
UBUNTU_22_04=true
fi
fi
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
echo "NVIDIA container toolkit can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
@@ -48,16 +36,8 @@ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dea
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt -y update
# is there a way to get a versioned package automatically?
# 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
if [ ! -d "/etc/pve" ]
then
apt -y install nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker
fi
# need this if running inside lxc...
# nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
nvidia-ctk runtime configure --runtime=docker
systemctl restart docker

View File

@@ -1,54 +0,0 @@
if [ "$(uname -m)" = "x86_64" ]
then
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
# needs either ubuntu 22.0.4 or 24.04
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
echo "NVIDIA graphics package can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
exit 1
fi
if [ -n "$UBUNTU_22_04" ]
then
distro="ubuntu2204"
else
distro="ubuntu2404"
fi
echo "Installing NVIDIA graphics packages."
apt update -q \
&& apt install -y wget \
&& wget -qO /cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/$distro/$(uname -m)/cuda-keyring_1.1-1_all.deb \
&& dpkg -i /cuda-keyring.deb \
&& apt update -q \
&& apt install -y cuda-nvcc-12-6 libcublas-12-6 libcudnn9-cuda-12=9.10.2.21-1 cuda-libraries-12-6;
if [ "$?" != "0" ]
then
echo "Error: NVIDIA graphics packages failed to install."
exit 1
fi
# Update: the libnvidia-opencl.so.1 file is not present in the container image, it is
# mounted via the nvidia container runtime. This is why the following check is commented out.
# this file is present but for some reason the icd file is not created by nvidia runtime.
# if [ ! -f "/usr/lib/x86_64-linux-gnu/libnvidia-opencl.so.1" ]
# then
# echo "Error: NVIDIA OpenCL library not found."
# exit 1
# fi
# the container runtime doesn't mount this file for some reason. seems to be a bug.
# https://github.com/NVIDIA/nvidia-container-toolkit/issues/682
# but the contents are simply the .so file, which is a symlink the nvidia runtime
# will mount in.
mkdir -p /etc/OpenCL/vendors/
echo "libnvidia-opencl.so.1" > /etc/OpenCL/vendors/nvidia.icd
else
echo "NVIDIA graphics will not be installed on this architecture."
fi
exit 0

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
@@ -186,4 +163,4 @@ echo
echo
echo "Optional:"
echo "Scrypted NVR Recording storage directory can be configured with an additional script located at:"
echo "https://docs.scrypted.app/scrypted-nvr/storage/docker.html"
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#docker-volume"

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
@@ -9,9 +9,12 @@ RUN apt -y install libvulkan1
# intel opencl for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
# intel NPU
# NPU driver will SIGILL on openvino prior to 2024.5.0
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# amd opencl
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
@@ -29,7 +32,7 @@ RUN python3.9 -m pip install debugpy
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
RUN curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/coral-edgetpu.gpg
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
# set default shell to bash

View File

@@ -4,7 +4,7 @@
# install script.
################################################################
ARG BASE="noble"
FROM ubuntu:${BASE} AS header
FROM ubuntu:${BASE} as header
ENV DEBIAN_FRONTEND=noninteractive
@@ -32,6 +32,19 @@ RUN apt-get -y install \
python3-setuptools \
python3-wheel
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN echo "Installing gstreamer."
# python-codecs pygobject dependencies
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
RUN apt-get -y install \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
gstreamer1.0-vaapi
# python3 gstreamer bindings
RUN echo "Installing gstreamer bindings."
RUN apt-get -y install \
python3-gst-1.0
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED

View File

@@ -42,11 +42,17 @@ 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
NODE_BIN_PATH=/usr/local/bin
# gstreamer plugins
RUN_IGNORE brew install gstreamer
ARCH=$(arch)
if [ "$ARCH" = "arm64" ]
then
@@ -82,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.21.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

@@ -4,37 +4,15 @@ cd /root/.scrypted
# always immediately upgrade everything in case there's a broken update.
# this will also be preferable for troubleshooting via lxc reboot.
export DEBIAN_FRONTEND=noninteractive
# auto updates may break the system?
# watchtower stopped working after a docker update, so disabling for now.
# yes | dpkg --configure -a
# apt -y --fix-broken install && apt -y update && apt -y dist-upgrade
function cleanup() {
IS_UP=$(docker compose ps scrypted -a | grep Up)
# Only clean up when scrypted is running to safely free space without risking its image deletion
if [ -z "$IS_UP" ]; then
echo "scrypted is not running, skipping cleanup to preserve its image"
return
fi
echo $(date) > .last_cleanup
echo "scrypted is running, proceeding with cleanup to free space"
docker container prune -f
docker image prune -a -f
}
yes | dpkg --configure -a
apt -y --fix-broken install && apt -y update && apt -y dist-upgrade
# force a pull to ensure we have the latest images.
# not using --pull always cause that fails everything on network down
docker compose pull
(sleep 60 && cleanup) &
# do not daemonize, when it exits, systemd will restart it.
# force a recreate as .env may have changed.
# furthermore force recreate gets the container back into a known state
# which is preferable in case the user has made manual changes and then restarts.
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate
# abort on container exit is problematic if watchtower is the one that aborts.
# this is also redundant now that watchtower is disabled.
# WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit

View File

@@ -18,10 +18,7 @@ function readyn() {
}
cd /tmp
if [ -z "$SCRYPTED_VERSION" ]
then
SCRYPTED_VERSION=v0.143.0
fi
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.26",
"version": "1.3.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.26",
"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.52"
"@scrypted/types": "^0.5.12"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -37,27 +37,6 @@
"node": ">=12"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -86,9 +65,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
},
@@ -104,14 +83,11 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.5.52",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.5.52.tgz",
"integrity": "sha512-c1ra1ENnoC8MqVHf7QQcXIU+5BvQnhU4x5oqx4b20LtoB0/TTXthYFFvEDBvLenBivUr8Bb6dWrji7TZXVax1g==",
"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": "^6.1.0"
}
"peer": true
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
@@ -158,19 +134,19 @@
}
},
"node_modules/@types/node": {
"version": "24.5.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.1.tgz",
"integrity": "sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q==",
"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.12.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": {
@@ -178,9 +154,9 @@
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -204,9 +180,9 @@
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -216,9 +192,9 @@
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -234,6 +210,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -325,27 +316,6 @@
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
@@ -356,9 +326,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"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",
@@ -392,14 +362,14 @@
}
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"foreground-child": "^3.1.0",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -430,9 +400,9 @@
"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==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz",
"integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
@@ -445,9 +415,9 @@
}
},
"node_modules/lru-cache": {
"version": "11.2.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
"integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
"license": "ISC",
"engines": {
"node": "20 || >=22"
@@ -461,12 +431,12 @@
"license": "ISC"
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "20 || >=22"
@@ -490,28 +460,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/openai": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.1.0.tgz",
"integrity": "sha512-5sqb1wK67HoVgGlsPwcH2bUbkg66nnoIYKoyV9zi5pZPqh7EWlmSrSDjAh4O5jaIg/0rIlcDKBtWvZBuacmGZg==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -655,9 +603,9 @@
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
@@ -736,9 +684,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"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": {
@@ -750,9 +698,9 @@
}
},
"node_modules/undici-types": {
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
"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"
},
@@ -869,6 +817,27 @@
"node": ">=8"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",

View File

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

View File

@@ -1,19 +1,21 @@
import { ConnectRPCObjectOptions, ForkOptions, ForkWorker, MediaObjectCreateOptions, PluginFork, ScryptedInterface, ScryptedInterfaceProperty, ScryptedStatic } from "@scrypted/types";
import { MediaObjectCreateOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
import { Deferred } from "../../../common/src/deferred";
import { timeoutPromise } from "../../../common/src/promise-utils";
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
import type { ClusterObject, ConnectRPCObject } from '../../../server/src/cluster/connect-rpc-object';
import { domFetch } from "../../../server/src/fetch";
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import type { IOSocket } from '../../../server/src/io';
import { MediaObject } from '../../../server/src/plugin/mediaobject';
import { PluginAPIProxy, PluginRemote } from "../../../server/src/plugin/plugin-api";
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
import { RpcPeer } from '../../../server/src/rpc';
import { createRpcDuplexSerializer, createRpcSerializer } from '../../../server/src/rpc-serializer';
import packageJson from '../package.json';
import { isIPAddress } from "./ip";
export * as rpc from '../../../server/src/rpc';
export * as rpc_serializer from '../../../server/src/rpc-serializer';
import { domFetch } from "../../../server/src/fetch";
import { httpFetch } from '../../../server/src/fetch/http-fetch';
let fetcher: typeof httpFetch | typeof domFetch;
try {
@@ -31,15 +33,6 @@ const sourcePeerId = RpcPeer.generateId();
type IOClientSocket = eio.Socket & IOSocket;
interface InternalFork extends Pick<ScryptedClientStatic, 'loginResult' | 'username' | 'address' | 'connectionType'> {
extraHeaders: {
[header: string]: string,
};
transports?: string[] | undefined;
clientName?: string;
admin: boolean;
};
function once(socket: IOClientSocket, event: 'open' | 'message') {
return new Promise<any[]>((resolve, reject) => {
const err = (e: any) => {
@@ -59,13 +52,7 @@ function once(socket: IOClientSocket, event: 'open' | 'message') {
});
}
/**
* The type of connection used by the Scrypted client.
* http-cloud is through Scrypted Cloud
* http-direct is a direct connection to the Scrypted server via one of the local network interfaces or public IP addresses.
* http is a direct connection with the base url or browser url.
*/
export type ScryptedClientConnectionType = 'http-cloud' | 'http-direct' | 'http';
export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
export interface ScryptedClientStatic extends ScryptedStatic {
userId?: string;
@@ -73,16 +60,18 @@ export interface ScryptedClientStatic extends ScryptedStatic {
admin: boolean;
disconnect(): void;
onClose?: Function;
rtcConnectionManagement?: RTCConnectionManagement;
browserSignalingSession?: BrowserSignalingSession;
address?: string;
connectionType: ScryptedClientConnectionType;
rpcPeer: RpcPeer;
loginResult: ScryptedClientLoginResult;
fork<T>(options: ForkOptions & { worker: Worker }): PluginFork<T>;
}
export interface ScryptedConnectionOptions {
direct?: boolean;
local?: boolean;
webrtc?: boolean;
baseUrl?: string;
previousLoginResult?: ScryptedClientLoginResult;
}
@@ -123,59 +112,19 @@ export async function logoutScryptedClient(baseUrl?: string) {
return response.body;
}
function getBaseUrl(href: string) {
try {
// an endpoint within scrypted will be served at /endpoint/[org/][id]
// find the endpoint prefix and anything prior to that will be the server base url.
const url = new URL(href);
url.search = '';
url.hash = '';
let endpointPath = url.pathname;
const parts = endpointPath.split('/');
const index = parts.findIndex(p => p === 'endpoint');
if (index === -1) {
// console.warn('path not recognized, does not contain the segment "endpoint".')
return;
}
return { url, parts, index };
}
catch (e) {
}
}
function importMetaUrlWithoutAssetsPath() {
// @ts-ignore
const url = new URL(import.meta.url);
const parts = url.pathname.split('/');
parts.pop();
parts.pop();
parts.push('public')
parts.push('');
url.pathname = parts.join('/');
return url.toString();
}
export function getCurrentBaseUrlRaw() {
const url = getBaseUrl(window.location.href)
|| getBaseUrl(document.baseURI)
|| getBaseUrl(importMetaUrlWithoutAssetsPath());
if (!url) {
try {
return getBaseUrl(process.env.SCRYPTED_ENDPOINT_PATH);
}
catch (e) {
}
}
return url;
}
export function getCurrentBaseUrl() {
const s = getCurrentBaseUrlRaw();
if (!s) {
return;
// an endpoint within scrypted will be served at /endpoint/[org/][id]
// find the endpoint prefix and anything prior to that will be the server base url.
const url = new URL(window.location.href);
url.search = '';
url.hash = '';
let endpointPath = window.location.pathname;
const parts = endpointPath.split('/');
const index = parts.findIndex(p => p === 'endpoint');
if (index === -1) {
// console.warn('path not recognized, does not contain the segment "endpoint".')
return undefined;
}
const { url, parts, index } = s;
const keep = parts.slice(0, index);
keep.push('');
url.pathname = keep.join('/');
@@ -436,10 +385,11 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
const eioPath = `endpoint/${pluginId}/engine.io/api`;
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
// https://github.com/socketio/engine.io/issues/690
const eioOptions: eio.SocketOptions = {
const cacehBust = Math.random().toString(36).substring(3, 10);
const eioOptions: Partial<SocketOptions> = {
path: eioEndpoint,
query: {
cacheBust: Math.random().toString(36).substring(3, 10),
cacehBust,
},
withCredentials: true,
extraHeaders,
@@ -449,6 +399,10 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
const explicitBaseUrl = baseUrl || `${globalThis.location.protocol}//${globalThis.location.host}`;
// underlying webrtc rpc transport may queue up messages before its ready to be to be handled.
// watch for this flush.
const flush = new Deferred<void>();
const addresses: string[] = [];
const localAddressDefault = isNotChromeOrIsInstalledApp;
@@ -472,12 +426,28 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
}
const tryAddresses = !!addresses.length;
const webrtcLastFailedKey = 'webrtcLastFailed';
const canUseWebrtc = !!globalThis.RTCPeerConnection;
let tryWebrtc = canUseWebrtc && options.webrtc;
// try webrtc by default on scrypted cloud.
// but webrtc takes a while to fail, so backoff if it fails to prevent continual slow starts.
if (scryptedCloud && canUseWebrtc && globalThis.localStorage && options.webrtc === undefined) {
tryWebrtc = true;
const webrtcLastFailed = parseFloat(localStorage.getItem(webrtcLastFailedKey));
// if webrtc has failed in the past day, dont attempt to use it.
const now = Date.now();
if (webrtcLastFailed < now && webrtcLastFailed > now - 1 * 24 * 60 * 60 * 1000) {
tryWebrtc = false;
console.warn('WebRTC API connection recently failed. Skipping.')
}
}
console.log({
tryLocalAddressess: tryAddresses,
tryWebrtc,
});
const localEioOptions: eio.SocketOptions = {
const localEioOptions: Partial<SocketOptions> = {
...eioOptions,
extraHeaders: {
...eioOptions.extraHeaders,
@@ -514,11 +484,145 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
}
}
if (tryWebrtc) {
console.log('trying webrtc');
const webrtcEioOptions: Partial<SocketOptions> = {
path: '/endpoint/@scrypted/webrtc/engine.io/',
query: {
cacehBust,
},
withCredentials: true,
extraHeaders,
rejectUnauthorized: false,
transports: options?.transports,
};
const check = new eio.Socket(explicitBaseUrl, webrtcEioOptions) as IOClientSocket;
sockets.push(check);
promises.push((async () => {
await once(check, 'open');
const connectionManagementId = `connectionManagement-${Math.random()}`;
const updateSessionId = `updateSessionId-${Math.random()}`;
check.send(JSON.stringify({
pluginId,
updateSessionId,
connectionManagementId,
}));
const dcDeferred = new Deferred<RTCDataChannel>();
const session = new BrowserSignalingSession();
const droppedMessages: any[] = [];
session.onPeerConnection = async pc => {
pc.ondatachannel = e => {
e.channel.onmessage = message => droppedMessages.push(message);
e.channel.binaryType = 'arraybuffer';
dcDeferred.resolve(e.channel)
};
}
const pcPromise = session.pcDeferred.promise;
const serializer = createRpcSerializer({
sendMessageBuffer: buffer => check.send(buffer),
sendMessageFinish: message => check.send(JSON.stringify(message)),
});
const upgradingPeer = new RpcPeer(clientName || 'webrtc-upgrade', "api", (message, reject, serializationContext) => {
try {
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e as Error);
}
});
check.on('message', data => {
if (data.constructor === Buffer || data.constructor === ArrayBuffer) {
serializer.onMessageBuffer(Buffer.from(data as string));
}
else {
serializer.onMessageFinish(JSON.parse(data as string));
}
});
serializer.setupRpcPeer(upgradingPeer);
// is this an issue?
// const readyClose = new Promise<RpcPeer>((resolve, reject) => {
// check.on('close', () => reject(new Error('closed')))
// })
upgradingPeer.params['session'] = session;
const pc = await pcPromise;
console.log('peer connection received');
await waitPeerConnectionIceConnected(pc);
console.log('waiting for data channel');
const dc = await dcDeferred.promise;
console.log('datachannel received', Date.now() - start);
const debouncer = new DataChannelDebouncer(dc, e => {
console.error('datachannel send error', e);
rpcPeer.kill('datachannel send error');
});
const dcSerializer = createRpcDuplexSerializer({
write: (data) => debouncer.send(data),
});
while (droppedMessages.length) {
const message = droppedMessages.shift();
dc.dispatchEvent(message);
}
const rpcPeer = new RpcPeer('webrtc-client', "api", (message, reject, serializationContext) => {
try {
dcSerializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e as Error);
pc.close();
}
});
dcSerializer.setupRpcPeer(rpcPeer);
rpcPeer.params['connectionManagementId'] = connectionManagementId;
rpcPeer.params['updateSessionId'] = updateSessionId;
rpcPeer.params['browserSignalingSession'] = session;
waitPeerIceConnectionClosed(pc).then(() => check.close());
check.on('close', () => {
console.log('datachannel upgrade cancelled/closed');
pc.close()
});
await new Promise(resolve => {
let buffers: Buffer[] = [];
dc.onmessage = message => {
buffers.push(Buffer.from(message.data));
resolve(undefined);
flush.promise.finally(() => {
if (buffers) {
for (const buffer of buffers) {
dcSerializer.onData(Buffer.from(buffer));
}
buffers = undefined;
}
dc.onmessage = message => dcSerializer.onData(Buffer.from(message.data));
});
};
});
return {
ready: check,
connectionType: 'webrtc',
rpcPeer,
};
})());
}
const p2pPromises = [...promises];
promises.push((async () => {
const waitDuration = tryAddresses ? 1000 : 0;
const waitDuration = tryWebrtc ? 10000 : (tryAddresses ? 1000 : 0);
console.log('waiting', waitDuration);
if (waitDuration) {
// give the peer to peers a second, but then try connecting directly.
@@ -539,13 +643,16 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
return {
ready: check,
address: explicitBaseUrl,
connectionType: scryptedCloud ? 'http-cloud' : 'http',
connectionType: 'http',
};
})());
const any = Promise.any(promises);
let { ready, connectionType, address, rpcPeer } = await any;
if (tryWebrtc && connectionType !== 'webrtc')
localStorage.setItem(webrtcLastFailedKey, Date.now().toString());
console.log('connected', connectionType, address)
socket = ready;
@@ -585,6 +692,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
serializer.setupRpcPeer(rpcPeer);
}
setTimeout(() => flush.resolve(undefined), 0);
const scrypted = await attachPluginRemote(rpcPeer, undefined);
const {
serverVersion,
@@ -593,8 +701,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
endpointManager,
mediaManager,
clusterManager,
pluginHostAPI,
pluginRemoteAPI,
} = scrypted;
console.log('api attached', Date.now() - start);
@@ -602,7 +708,20 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
return new MediaObject(mimeType, data, options) as any;
}
const [admin] = await Promise.all([
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
if (updateSessionId && browserSignalingSession) {
systemManager.getComponent('plugins').then(async plugins => {
const updateSession: (session: RTCSignalingSession) => Promise<void> = await plugins.getHostParam('@scrypted/webrtc', updateSessionId);
if (!updateSession)
return;
await updateSession(browserSignalingSession);
console.log('signaling channel upgraded.');
socket.removeAllListeners();
socket.close();
});
}
const [admin, rtcConnectionManagement] = await Promise.all([
(async () => {
try {
// info is
@@ -613,6 +732,18 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
}
return false;
})(),
(async () => {
let rtcConnectionManagement: RTCConnectionManagement;
if (connectionManagementId) {
try {
const plugins = await systemManager.getComponent('plugins');
rtcConnectionManagement = await plugins.getHostParam('@scrypted/webrtc', connectionManagementId);
return rtcConnectionManagement;
}
catch (e) {
}
}
})(),
]);
console.log('api initialized', Date.now() - start);
@@ -621,131 +752,110 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
.map(id => systemManager.getDeviceById(id))
.find(device => device.pluginId === '@scrypted/core' && device.nativeId === `user:${username}`);
const connectRPCObject = clusterSetup(address, connectionType, queryToken, extraHeaders, options?.transports, sourcePeerId, clientName);
const clusterPeers = new Map<number, Promise<RpcPeer>>();
const ensureClusterPeer = (clusterObject: ClusterObject) => {
let clusterPeerPromise = clusterPeers.get(clusterObject.port);
if (!clusterPeerPromise) {
clusterPeerPromise = (async () => {
const eioPath = 'engine.io/connectRPCObject';
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
const clusterPeerOptions = {
path: eioEndpoint,
query: {
cacehBust,
clusterObject: JSON.stringify(clusterObject),
},
withCredentials: true,
extraHeaders,
rejectUnauthorized: false,
transports: options?.transports,
};
const loginResult: ScryptedClientLoginResult = {
username,
token,
directAddress,
localAddresses,
externalAddresses,
scryptedCloud,
queryToken,
authorization,
cloudAddress,
hostname,
serverId,
const clusterPeerSocket = new eio.Socket(explicitBaseUrl, clusterPeerOptions);
let peerReady = false;
clusterPeerSocket.on('close', () => {
clusterPeers.delete(clusterObject.port);
if (!peerReady) {
throw new Error("peer disconnected before setup completed");
}
});
try {
await once(clusterPeerSocket, 'open');
const serializer = createRpcDuplexSerializer({
write: data => clusterPeerSocket.send(data),
});
clusterPeerSocket.on('message', data => serializer.onData(Buffer.from(data)));
const clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => {
try {
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e as Error);
}
});
serializer.setupRpcPeer(clusterPeer);
clusterPeer.tags.localPort = sourcePeerId;
peerReady = true;
return clusterPeer;
}
catch (e) {
console.error('failure ipc connect', e);
clusterPeerSocket.close();
throw e;
}
})();
clusterPeers.set(clusterObject.port, clusterPeerPromise);
}
return clusterPeerPromise;
};
type ForkType = ScryptedClientStatic['fork'];
const fork: ForkType = (forkOptions) => {
const { worker } = forkOptions;
const resolveObject = async (proxyId: string, sourcePeerPort: number) => {
const sourcePeer = await clusterPeers.get(sourcePeerPort);
if (sourcePeer?.remoteWeakProxies) {
return Object.values(sourcePeer.remoteWeakProxies).find(
v => v.deref()?.__cluster?.proxyId == proxyId
)?.deref();
}
return null;
}
const serializer = createRpcSerializer({
sendMessageBuffer: buffer => worker.postMessage(buffer),
sendMessageFinish: message => worker.postMessage(JSON.stringify(message)),
});
const connectRPCObject = async (value: any) => {
const clusterObject: ClusterObject = value?.__cluster;
if (!clusterObject) {
return value;
}
const threadPeer = new RpcPeer("main-client", 'thread', (message, reject, serializationContext) => {
try {
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e as Error);
}
});
const { port, proxyId } = clusterObject;
rpcPeer.killed.finally(() => threadPeer.kill('main rpc peer killed'));
// check if object is already connected
const resolved = await resolveObject(proxyId, port);
if (resolved) {
return resolved;
}
worker.addEventListener('message', async event => {
if (event.data instanceof Uint8Array) {
serializer.onMessageBuffer(Buffer.from(event.data));
}
else {
serializer.onMessageFinish(JSON.parse(event.data));
}
});
serializer.setupRpcPeer(threadPeer);
// there is no worker close event?
const forkApi = new PluginAPIProxy(pluginHostAPI, mediaManager);
threadPeer.killed.finally(() => {
forkApi.removeListeners();
worker.terminate();
});
const internalFork: InternalFork = {
loginResult,
username,
address,
connectionType,
extraHeaders,
transports: options?.transports,
clientName,
admin,
};
threadPeer.params['client'] = internalFork;
const result = (async () => {
const getRemote = await threadPeer.getParam('getRemote');
const remote = await getRemote(forkApi, pluginId, {
serverVersion
}) as PluginRemote;
await remote.setSystemState(systemManager.getSystemState());
forkApi.listen((id, eventDetails, eventData) => {
// ScryptedDevice events will be handled specially and repropagated by the remote.
if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice) {
if (eventDetails.property === ScryptedInterfaceProperty.id) {
// a change on the id property means device was deleted
remote.updateDeviceState(eventData, undefined);
}
else {
// a change on anything else is a descriptor update
remote.updateDeviceState(id, systemManager.getSystemState()[id]);
}
return;
}
if (eventDetails.property && !eventDetails.mixinId) {
remote.notify(id, eventDetails, systemManager.getSystemState()[id]?.[eventDetails.property]).catch(() => { });
}
else {
remote.notify(id, eventDetails, eventData).catch(() => { });
}
});
const fork = await threadPeer.getParam('fork');
return fork;
})();
result.catch(() => {
threadPeer.kill('fork setup failed');
worker.terminate();
});
return {
[Symbol.dispose]() {
worker.terminate();
threadPeer.kill('disposed');
},
result,
worker: {
terminate() {
worker.terminate();
},
nativeWorker: worker,
} as any as ForkWorker,
};
};
try {
const clusterPeerPromise = ensureClusterPeer(clusterObject);
const clusterPeer = await clusterPeerPromise;
const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
const newValue = await connectRPCObject(clusterObject);
if (!newValue)
throw new Error('ipc object not found?');
return newValue;
}
catch (e) {
console.error('failure ipc', e);
return value;
}
}
const ret: ScryptedClientStatic = {
userId: userDevice?.id,
serverVersion,
username,
pluginRemoteAPI,
pluginRemoteAPI: undefined,
address,
connectionType,
admin,
@@ -757,11 +867,25 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
disconnect() {
rpcPeer.kill('disconnect requested');
},
pluginHostAPI,
pluginHostAPI: undefined,
rtcConnectionManagement,
browserSignalingSession,
rpcPeer,
loginResult,
loginResult: {
username,
token,
directAddress,
localAddresses,
externalAddresses,
scryptedCloud,
queryToken,
authorization,
cloudAddress,
hostname,
serverId,
},
connectRPCObject,
fork,
fork: undefined,
connect: undefined,
}
@@ -781,308 +905,3 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
throw e;
}
}
function clusterSetup(address: string, connectionType: ScryptedClientConnectionType, queryToken: any, extraHeaders: { [header: string]: string }, transports: string[] | undefined, sourcePeerId: string, clientName?: string) {
const clusterPeers = new Map<number, Promise<RpcPeer>>();
const finalizationRegistry = new FinalizationRegistry((clusterPeer: RpcPeer) => {
clusterPeer.kill('object finalized');
});
const ensureClusterPeer = (clusterObject: ClusterObject, connectRPCObjectOptions?: ConnectRPCObjectOptions) => {
// If dedicatedTransport is true, don't reuse existing cluster peers
if (!connectRPCObjectOptions?.dedicatedTransport) {
let clusterPeerPromise = clusterPeers.get(clusterObject.port);
if (clusterPeerPromise)
return clusterPeerPromise;
}
const clusterPeerPromise = (async () => {
const eioPath = 'engine.io/connectRPCObject';
const eioEndpoint = new URL(eioPath, address).pathname;
const eioQueryToken = connectionType === 'http' ? undefined : queryToken;
const clusterPeerOptions: eio.SocketOptions = {
path: eioEndpoint,
query: {
cacheBust: Math.random().toString(36).substring(3, 10),
clusterObject: JSON.stringify(clusterObject),
...eioQueryToken,
},
withCredentials: true,
extraHeaders,
rejectUnauthorized: false,
transports,
};
const clusterPeerSocket = new eio.Socket(address, clusterPeerOptions);
let peerReady = false;
// Timeout handling for dedicated transports
let receiveTimeout: NodeJS.Timeout | undefined;
let sendTimeout: NodeJS.Timeout | undefined;
let clusterPeer: RpcPeer | undefined;
const clearTimers = () => {
if (receiveTimeout) {
clearTimeout(receiveTimeout);
receiveTimeout = undefined;
}
if (sendTimeout) {
clearTimeout(sendTimeout);
sendTimeout = undefined;
}
};
const resetReceiveTimeout = connectRPCObjectOptions?.dedicatedTransport?.receiveTimeout ? () => {
if (receiveTimeout) {
clearTimeout(receiveTimeout);
}
receiveTimeout = setTimeout(() => {
if (clusterPeer) {
clusterPeer.kill('receive timeout');
}
}, connectRPCObjectOptions.dedicatedTransport.receiveTimeout);
} : undefined;
const resetSendTimeout = connectRPCObjectOptions?.dedicatedTransport?.sendTimeout ? () => {
if (sendTimeout) {
clearTimeout(sendTimeout);
}
sendTimeout = setTimeout(() => {
if (clusterPeer) {
clusterPeer.kill('send timeout');
}
}, connectRPCObjectOptions.dedicatedTransport.sendTimeout);
} : undefined;
clusterPeerSocket.on('close', () => {
clusterPeer?.kill('socket closed');
// Only remove from clusterPeers if it's not a dedicated transport
if (!connectRPCObjectOptions?.dedicatedTransport) {
clusterPeers.delete(clusterObject.port);
}
if (!peerReady) {
throw new Error("peer disconnected before setup completed");
}
});
try {
await once(clusterPeerSocket, 'open');
const serializer = createRpcDuplexSerializer({
write: data => {
resetSendTimeout?.();
clusterPeerSocket.send(data);
},
});
clusterPeerSocket.on('message', data => {
resetReceiveTimeout?.();
serializer.onData(Buffer.from(data));
});
clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => {
try {
resetSendTimeout?.();
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e as Error);
}
});
clusterPeer.killedSafe.finally(() => {
clearTimers();
clusterPeerSocket.close();
});
serializer.setupRpcPeer(clusterPeer);
clusterPeer.tags.localPort = sourcePeerId;
peerReady = true;
// Initialize timeouts if configured
resetReceiveTimeout?.();
resetSendTimeout?.();
return clusterPeer;
}
catch (e) {
clearTimers();
console.error('failure ipc connect', e);
clusterPeerSocket.close();
throw e;
}
})();
// Only store in clusterPeers if it's not a dedicated transport
if (!connectRPCObjectOptions?.dedicatedTransport) {
clusterPeers.set(clusterObject.port, clusterPeerPromise);
}
return clusterPeerPromise;
};
const resolveObject = async (proxyId: string, sourcePeerPort: number) => {
const sourcePeer = await clusterPeers.get(sourcePeerPort);
if (sourcePeer?.remoteWeakProxies) {
return Object.values(sourcePeer.remoteWeakProxies).find(
v => v.deref()?.__cluster?.proxyId == proxyId
)?.deref();
}
return null;
}
const connectRPCObject = async (value: any, options?: ConnectRPCObjectOptions) => {
const clusterObject: ClusterObject = value?.__cluster;
if (!clusterObject) {
return value;
}
const { port, proxyId } = clusterObject;
// check if object is already connected
const resolved = await resolveObject(proxyId, port);
if (resolved) {
return resolved;
}
try {
const clusterPeerPromise = ensureClusterPeer(clusterObject, options);
const clusterPeer = await clusterPeerPromise;
const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
try {
const newValue = await connectRPCObject(clusterObject);
if (!newValue)
throw new Error('ipc object not found?');
// If dedicatedTransport is true, register the object for cleanup
if (options?.dedicatedTransport) {
finalizationRegistry.register(newValue, clusterPeer);
}
return newValue;
}
catch (e) {
// If we have a clusterPeer and this is a dedicated transport, kill the connection
// to prevent resource leaks when connectRPCObject fails
if (options?.dedicatedTransport) {
clusterPeer.kill('connectRPCObject failed');
}
throw e;
}
}
catch (e) {
console.error('failure ipc', e);
return value;
}
}
return connectRPCObject;
}
export async function connectScryptedClientFork(forkMain: (client: ScryptedClientStatic) => Promise<any>) {
const start = Date.now();
try {
const serializer = createRpcSerializer({
sendMessageBuffer: buffer => self.postMessage(buffer),
sendMessageFinish: message => self.postMessage(JSON.stringify(message)),
});
const rpcPeer = new RpcPeer('thread', "main-client", (message, reject, serializationContext) => {
try {
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e as Error);
}
});
self.addEventListener('message', event => {
if (event.data instanceof Uint8Array) {
serializer.onMessageBuffer(Buffer.from(event.data));
}
else {
serializer.onMessageFinish(JSON.parse(event.data));
}
});
serializer.setupRpcPeer(rpcPeer);
const scrypted = await attachPluginRemote(rpcPeer, undefined);
const {
serverVersion,
systemManager,
deviceManager,
endpointManager,
mediaManager,
clusterManager,
pluginHostAPI,
pluginRemoteAPI,
} = scrypted;
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async<T extends MediaObjectCreateOptions>(data: any, mimeType: string, options: T) => {
return new MediaObject(mimeType, data, options) as any;
}
console.log('api initialized', Date.now() - start);
const {
loginResult,
username,
address,
connectionType,
extraHeaders,
transports,
clientName,
admin,
} = await rpcPeer.getParam('client') as InternalFork;
const { queryToken } = loginResult;
const userDevice = Object.keys(systemManager.getSystemState())
.map(id => systemManager.getDeviceById(id))
.find(device => device.pluginId === '@scrypted/core' && device.nativeId === `user:${username}`);
const connectRPCObject = clusterSetup(address, connectionType, queryToken, extraHeaders, transports, sourcePeerId, clientName);
type ForkType = ScryptedClientStatic['fork'];
const fork: ForkType = (forkOptions) => {
throw new Error('not implemented');
};
const ret: ScryptedClientStatic = {
userId: userDevice?.id,
serverVersion,
username,
pluginRemoteAPI,
address,
connectionType,
admin,
systemManager,
clusterManager,
deviceManager,
endpointManager,
mediaManager,
disconnect() {
rpcPeer.kill('disconnect requested');
},
pluginHostAPI,
rpcPeer,
loginResult,
connectRPCObject,
fork,
connect: undefined,
}
rpcPeer.killed.finally(() => {
self.close();
ret.onClose?.();
});
const forked = await forkMain(ret);
rpcPeer.params['fork'] = forked;
}
catch (e) {
self.close();
throw e;
}
}

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.168",
"version": "0.0.165",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.168",
"version": "0.0.165",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -16,7 +16,7 @@
},
"devDependencies": {
"@types/content-type": "^1.1.8",
"@types/node": "^22.19.3",
"@types/node": "^20.11.30",
"@types/xml2js": "^0.4.14"
}
},
@@ -26,42 +26,39 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
"devDependencies": {
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.55",
"version": "0.3.114",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.27.1",
"@rollup/plugin-commonjs": "^28.0.9",
"@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-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.3.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.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": "^6.1.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"rollup": "^4.52.5",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.4",
"ts-loader": "^9.5.1",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"webpack": "^5.99.9",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -74,9 +71,9 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^24.9.2",
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.28.14"
"typedoc": "^0.26.11"
}
},
"node_modules/@scrypted/common": {
@@ -94,13 +91,12 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~5.26.4"
}
},
"node_modules/@types/xml2js": {
@@ -128,11 +124,10 @@
"license": "ISC"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/xml2js": {
"version": "0.6.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.168",
"version": "0.0.165",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@types/content-type": "^1.1.8",
"@types/node": "^22.19.3",
"@types/node": "^20.11.30",
"@types/xml2js": "^0.4.14"
}
}

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;
@@ -268,8 +251,8 @@ export class AmcrestCameraClient {
continue;
if (ignore === boundaryEnd)
continue;
// dahua bugs out and sends this (handle both HTTP/1.0 and HTTP/1.1).
if (ignore === 'HTTP/1.1 200 OK' || ignore === 'HTTP/1.0 200 OK') {
// dahua bugs out and sends this.
if (ignore === 'HTTP/1.1 200 OK') {
const message = await readAmcrestMessage(stream);
this.console.log('ignoring dahua http message', message);
message.unshift('');
@@ -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/cloud",
"version": "0.2.51",
"version": "0.2.49",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.2.51",
"version": "0.2.49",
"dependencies": {
"@eneris/push-receiver": "^4.3.0",
"@scrypted/common": "file:../../common",

View File

@@ -52,5 +52,5 @@
"@types/node": "^22.10.1",
"ts-node": "^10.9.2"
},
"version": "0.2.51"
"version": "0.2.49"
}

View File

@@ -240,10 +240,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
upnpClient = upnp.createClient();
upnpStatus = 'Starting';
randomBytes = crypto.randomBytes(16).toString('base64');
healthCheckToken = crypto.randomBytes(16).toString('hex');
reverseConnections = new Set<Duplex>();
cloudflaredLoginController?: AbortController;
healthCheckInterval?: NodeJS.Timeout;
get portForwardingDisabled() {
return this.storageSettings.values.forwardingMode === 'Disabled' || this.storageSettings.values.forwardingMode === 'Default';
@@ -854,12 +852,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
res.end();
}
else if (url.pathname === '/_punch/cloudflared_callback') {
res.writeHead(200);
res.write(this.healthCheckToken);
res.end();
return;
}
else if (url.pathname === '/web/') {
const validDomain = this.getSSLHostname();
if (validDomain) {
@@ -1130,9 +1122,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
maxDelay: 300000,
});
// Start health check after cloudflared is successfully started
this.startHealthCheck();
await once(this.cloudflared.child, 'exit').catch(() => { });
// the successfully started cloudflared process may exit at some point, loop and allow it to restart.
this.console.error('cloudflared exited');
@@ -1142,8 +1131,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.console.error('cloudflared error', e);
}
finally {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = undefined;
this.cloudflared = undefined;
this.cloudflareTunnel = undefined;
this.updateExternalAddresses();
@@ -1151,59 +1138,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
async startHealthCheck() {
// Clear any existing health check interval
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
}
// Local failure counter - only accessible within this method
let failureCount = 0;
const maxFailuresBeforeRestart = 3;
const alertTitle = 'Cloudflared health check failed 3 times consecutively. Restarting cloudflared process.';
const check = async () => {
// Only perform health check if cloudflare is enabled and we have a tunnel URL
if (!this.storageSettings.values.cloudflareEnabled || !this.cloudflareTunnel) {
return;
}
try {
const healthCheckUrl = `${this.cloudflareTunnel}/_punch/cloudflared_callback`;
this.console.log(`Performing health check: ${healthCheckUrl}`);
const response = await httpFetch({
url: healthCheckUrl,
responseType: 'text',
timeout: 30000, // 30 second timeout
});
this.log.clearAlert(alertTitle);
if (response.body !== this.healthCheckToken) {
throw new Error(`Health check failed: Expected token ${this.healthCheckToken}, got ${response.body}`);
}
failureCount = 0;
this.console.log('Cloudflared health check passed');
} catch (error) {
failureCount++;
this.console.error(`Cloudflared health check failed (${failureCount}/${maxFailuresBeforeRestart}):`, error);
if (failureCount >= maxFailuresBeforeRestart) {
this.console.warn('3 consecutive health check failures detected. Restarting cloudflared process.');
this.log.a(alertTitle);
this.cloudflared?.child?.kill();
failureCount = 0;
}
}
};
// Start a new health check interval (every 2 minutes)
this.healthCheckInterval = setInterval(check, 2 * 60 * 1000); // Run every 2 minutes
}
get serverIdentifier() {
const serverIdentifier = `${this.storageSettings.values.registrationSecret}@${this.storageSettings.values.serverId}`;
return serverIdentifier;

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.3.146",
"version": "0.3.120",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.3.146",
"version": "0.3.120",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -77,7 +77,6 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
@@ -89,29 +88,28 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.33",
"version": "0.3.100",
"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": {
@@ -124,9 +122,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"
}
},
"node_modules/@scrypted/common": {
@@ -278,7 +276,6 @@
"version": "file:../../common",
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"monaco-editor": "^0.50.0",
@@ -289,29 +286,28 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@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",
"@types/node": "^24.0.1",
"@types/node": "^22.10.1",
"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",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"typedoc": "^0.26.11",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.3.146",
"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,14 +8,14 @@ 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, checkLxcVersionUpdateNeeded } from './platform/lxc';
import { checkLegacyLxc, checkLxc } from './platform/lxc';
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
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;
@@ -64,10 +63,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
'Default',
'latest',
'beta',
'intel',
'amd',
'nvidia',
`v${sdk.serverVersion}-noble-full`,
`v${sdk.serverVersion}-jammy-full`,
],
combobox: true,
onPut: (ov, nv) => {
@@ -214,69 +210,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
},
);
})();
// check on workers immediately and once an hour.
this.updateWorkers();
setInterval(() => this.updateWorkers(), 60 * 1000 * 60);
// check on worker images once an hour.
// checking immediately is problematic as a failed update may cause a restart loop on startup.
// images are also pruned 1 minute after startup, so avoid that.
setInterval(() => this.updateWorkerImages(), 60 * 1000 * 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 updateWorkerImages() {
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;
if (!await result.checkLxcVersionUpdateNeeded()) {
return;
}
// restart the worker to pick up the new image.
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
const serviceControl = await clusterFork.getServiceControl(worker.id);
await serviceControl.restart().catch(() => { });
}
catch (e) {
}
finally {
await sleep(1000);
forked.worker.terminate();
}
})();
}
}
async getSettings(): Promise<Setting[]> {
@@ -382,6 +315,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
const dockerCompose = yaml.parseDocument(readFileAsString('/root/.scrypted/docker-compose.yml'));
// @ts-ignore
dockerCompose.contents.get('services').get('scrypted').set('image', `ghcr.io/koush/scrypted${releaseChannel}`);
yaml.stringify(dockerCompose);
writeFileSync('/root/.scrypted/docker-compose.yml', yaml.stringify(dockerCompose));
this.setPullImage();
@@ -398,16 +332,5 @@ export async function fork() {
tsCompile,
newScript,
newTerminalService,
checkLxcVersionUpdateNeeded,
checkLxc: async () => {
try {
// console.warn('Checking for LXC installation...');
await checkLxc();
}
finally {
await sleep(1000);
process.exit(0);
}
}
}
}

View File

@@ -1,9 +1,5 @@
import { Deferred } from '@scrypted/common/src/deferred';
import { readFileAsString } from '@scrypted/common/src/eval/scrypted-eval';
import fs from 'fs';
import sdk from '@scrypted/sdk';
import fs, { writeFileSync } from 'fs';
import http from 'http';
import yaml from 'yaml';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC = 'lxc';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER = 'lxc-docker';
@@ -22,119 +18,6 @@ export async function checkLxc() {
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER)
return;
await checkLxcCompose();
await checkLxcScript();
}
async function dockerRequest(options: http.RequestOptions, body?: string) {
const deferred = new Deferred<string>();
const req = http.request({
socketPath: '/var/run/docker.sock',
method: options.method,
path: options.path,
headers: {
'Host': 'localhost',
...options.headers
}
});
req.on('response', (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
deferred.resolve(data);
});
});
req.on('error', (err) => {
deferred.reject(err);
});
if (body) {
req.write(body);
}
req.end();
return deferred.promise;
}
async function dockerPullScryptedTag(tag: string) {
return dockerRequest({
method: 'POST',
path: `/v1.41/images/create?fromImage=ghcr.io%2Fkoush%2Fscrypted&tag=${tag}`,
});
}
async function dockerImageLsScryptedTag(tag: string) {
// List all images and find the specific one
const data = await dockerRequest({
method: 'GET',
path: '/v1.41/images/json'
});
const images = JSON.parse(data);
// Filter for your specific image
const targetImage = images.find(image => {
return image.RepoTags && image.RepoTags.some(t =>
t === `ghcr.io/koush/scrypted:${tag}`
);
});
if (!targetImage) {
throw new Error('Image not found');
}
return targetImage.Id;
}
async function dockerGetScryptedContainerImageId() {
// List running containers filtered by name
const data = await dockerRequest({
method: 'GET',
path: '/v1.41/containers/json?filters={"name":["scrypted"],"status":["running"]}'
});
const containers = JSON.parse(data);
if (!containers.length)
throw new Error('No running container named "scrypted" found');
const container = containers[0];
return container.ImageID;
}
export async function checkLxcVersionUpdateNeeded() {
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER)
return;
const dockerCompose = yaml.parseDocument(readFileAsString('/root/.scrypted/docker-compose.yml'));
// @ts-ignore
const image: string = dockerCompose.contents.get('services').get('scrypted').get('image');
const label = image.split(':')[1] || 'latest';
await dockerPullScryptedTag(label);
const imageId = await dockerImageLsScryptedTag(label);
const containerImageId = await dockerGetScryptedContainerImageId();
console.warn('LXC Scrypted latest image ID:', imageId);
console.warn('LXC Scrypted running image ID:', containerImageId);
return containerImageId !== imageId;
}
async function checkLxcCompose() {
// the lxc-docker used watchtower for automatic updates but watchtower started crashing in the lxc environment
// after a docker update.
// watchtower was removed from the lxc as a result.
// however existing installations may still have watchtower in their docker-compose.yml and need it removed.
const dockerCompose = yaml.parseDocument(readFileAsString('/root/.scrypted/docker-compose.yml'));
// @ts-ignore
const watchtower = dockerCompose.contents.get('services').get('watchtower');
if (watchtower.get('profiles'))
return;
watchtower.set('profiles', ['disabled']);
writeFileSync('/root/.scrypted/docker-compose.yml', yaml.stringify(dockerCompose));
}
async function checkLxcScript() {
const foundDockerComposeSh = await fs.promises.readFile(DOCKER_COMPOSE_SH_PATH, 'utf8');
const dockerComposeSh = await fs.promises.readFile(LXC_DOCKER_COMPOSE_SH_PATH, 'utf8');
@@ -147,8 +30,6 @@ async function checkLxcScript() {
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,7 +1,7 @@
import { createAsyncQueue } from '@scrypted/common/src/async-queue';
import sdk, { ClusterForkInterface, ClusterForkInterfaceOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTY, TTYSettings } from "@scrypted/sdk";
import { ChildProcess, spawn as childSpawn } from "child_process";
import sdk, { ClusterForkInterface, ClusterForkInterfaceOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTYSettings } from "@scrypted/sdk";
import type { IPty, spawn as ptySpawn } from 'node-pty';
import { createAsyncQueue } from '@scrypted/common/src/async-queue'
import { ChildProcess, spawn as childSpawn } from "child_process";
import path from 'path';
export const TerminalServiceNativeId = 'terminalservice';
@@ -19,24 +19,12 @@ function toSpawnPathEnv(paths: string[]): string {
class InteractiveTerminal {
cp: IPty
constructor(cmd: string[], paths: string[], spawn: typeof ptySpawn, cwd?: string) {
constructor(cmd: string[], paths: string[], spawn: typeof ptySpawn) {
const spawnPath = toSpawnPathEnv(paths);
if (cmd?.length) {
this.cp = spawn(cmd[0], cmd.slice(1), {
env: {
...process.env,
PATH: spawnPath,
},
cwd,
});
this.cp = spawn(cmd[0], cmd.slice(1), { env: { ...process.env, PATH: spawnPath } });
} else {
this.cp = spawn(process.env.SHELL as string, [], {
env: {
...process.env,
PATH: spawnPath,
},
cwd,
});
this.cp = spawn(process.env.SHELL as string, [], { env: { ...process.env, PATH: spawnPath } });
}
}
@@ -123,7 +111,7 @@ class NoninteractiveTerminal {
}
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer>, ClusterForkInterface, TTY {
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer>, ClusterForkInterface {
private forks: { [clusterWorkerId: string]: TerminalService } = {};
private forkClients: 0;
@@ -198,7 +186,7 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
async connectStream(input: AsyncGenerator<Buffer | string, void>, options?: any): Promise<AsyncGenerator<Buffer, void>> {
let cp: InteractiveTerminal | NoninteractiveTerminal = null;
const queue = createAsyncQueue<Buffer>();
const extraPaths = [...options?.env?.PATH?.split(path.delimiter) || [], ...await this.getExtraPaths()];
const extraPaths = await this.getExtraPaths();
if (this.isFork) {
this.forkClients++;
@@ -271,7 +259,7 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
let spawn: typeof ptySpawn;
try {
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
cp = new InteractiveTerminal(cmd, extraPaths, spawn, options?.cwd);
cp = new InteractiveTerminal(cmd, extraPaths, spawn);
}
catch (e) {
this.console.error('Error starting pty', e);

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';
@@ -111,7 +111,7 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
const { username, admin } = user;
const nativeId = `user:${username}`;
const aclId = await sdk.deviceManager.onDeviceDiscovered({
providerNativeId: UsersNativeId,
providerNativeId: this.nativeId,
name: username.toString(),
nativeId,
interfaces: [
@@ -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.89",
"version": "0.1.77",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.89",
"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.89"
"version": "0.1.77"
}

View File

@@ -14,9 +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
from coreml.segment import CoreMLSegmentation
try:
from coreml.text_recognition import CoreMLTextRecognition
@@ -29,11 +26,18 @@ predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "CoreML-Predict")
availableModels = [
"Default",
"scrypted_yolov9t_relu_test",
"scrypted_yolov9c_relu",
"scrypted_yolov9m_relu",
"scrypted_yolov9s_relu",
"scrypted_yolov9t_relu",
"scrypted_yolov10m_320",
"scrypted_yolov10n_320",
"scrypted_yolo_nas_s_320",
"scrypted_yolov9e_320",
"scrypted_yolov9c_320",
"scrypted_yolov9s_320",
"scrypted_yolov9t_320",
"scrypted_yolov6n_320",
"scrypted_yolov6s_320",
"scrypted_yolov8n_320",
"ssdlite_mobilenet_v2",
"yolov4-tiny",
]
@@ -73,24 +77,58 @@ class CoreMLPlugin(
def __init__(self, nativeId: str | None = None, forked: bool = False):
super().__init__(nativeId=nativeId, forked=forked)
# this used to work but a bug in macos is causing recompilation of the coreml models every time it restarts
# and the cache is not reused and also not cleared until the whole system reboots.
self.periodic_restart = False
self.custom_models = {}
model = self.storage.getItem("model") or "Default"
if model == "Default" or model not in availableModels:
if model != "Default":
self.storage.setItem("model", "Default")
model = "scrypted_yolov9c_relu"
model = "scrypted_yolov9c_320"
self.yolo = "yolo" in model
self.scrypted_yolov10n = "scrypted_yolov10" in model
self.scrypted_yolo_nas = "scrypted_yolo_nas" in model
self.scrypted_yolo = "scrypted_yolo" in model
self.scrypted_model = "scrypted" in model
model_version = "v8"
mlmodel = "model" if self.scrypted_yolo else model
self.modelName = model
print(f"model: {model}")
model_path = self.downloadHuggingFaceModelLocalFallback(model)
modelFile = os.path.join(model_path, f"{model}.mlpackage")
print(model_path, modelFile)
if not self.yolo:
# todo convert these to mlpackage
modelFile = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{model}/{mlmodel}.mlmodel",
f"{model}.mlmodel",
)
else:
if self.scrypted_yolo:
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{f}",
f"{model_version}/{f}",
)
modelFile = os.path.dirname(p)
else:
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/FeatureDescriptions.json",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/Metadata.json",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{f}",
f"{model_version}/{f}",
)
modelFile = os.path.dirname(p)
self.model = ct.models.MLModel(modelFile)
self.modelspec = self.model.get_spec()
@@ -105,15 +143,13 @@ class CoreMLPlugin(
self.faceDevice = None
self.textDevice = None
self.clipDevice = None
self.segmentDevice = 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,
@@ -123,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,
@@ -138,29 +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",
}
)
await scrypted_sdk.deviceManager.onDeviceDiscovered(
{
"nativeId": "segment",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "CoreML Segmentation",
"devices": devices,
}
)
except:
@@ -170,22 +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
elif nativeId == "segment":
self.segmentDevice = self.segmentDevice or CoreMLSegmentation(self, nativeId)
return self.segmentDevice
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"
@@ -218,8 +222,94 @@ class CoreMLPlugin(
return out_dicts
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
out_dict = await self.queue_batch({self.input_name: input})
results = list(out_dict.values())[0][0]
objs = yolo.parse_yolov9(results)
objs = []
# run in executor if this is the plugin loop
if self.yolo:
out_dict = await self.queue_batch({self.input_name: input})
if self.scrypted_yolov10n:
results = list(out_dict.values())[0][0]
objs = yolo.parse_yolov10(results)
ret = self.create_detection_result(objs, src_size, cvss)
return ret
if self.scrypted_yolo_nas:
predictions = list(out_dict.values())
objs = yolo.parse_yolo_nas(predictions)
ret = self.create_detection_result(objs, src_size, cvss)
return ret
if self.scrypted_yolo:
results = list(out_dict.values())[0][0]
objs = yolo.parse_yolov9(results)
ret = self.create_detection_result(objs, src_size, cvss)
return ret
out_blob = out_dict["Identity"]
objects = yolo.parse_yolo_region(
out_blob,
(input.width, input.height),
(81, 82, 135, 169, 344, 319),
# (23,27, 37,58, 81,82),
False,
)
for r in objects:
obj = Prediction(
r["classId"],
r["confidence"],
Rectangle(
r["xmin"],
r["ymin"],
r["xmax"],
r["ymax"],
),
)
objs.append(obj)
# what about output[1]?
# 26 26
# objects = yolo.parse_yolo_region(out_blob, (input.width, input.height), (23,27, 37,58, 81,82))
ret = self.create_detection_result(objs, src_size, cvss)
return ret
out_dict = await asyncio.get_event_loop().run_in_executor(
predictExecutor,
lambda: self.model.predict(
{"image": input, "confidenceThreshold": self.minThreshold}
),
)
coordinatesList = out_dict["coordinates"]
for index, confidenceList in enumerate(out_dict["confidence"]):
values = confidenceList
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
maxConfidence = confidenceList[maxConfidenceIndex]
if maxConfidence < self.minThreshold:
continue
coordinates = coordinatesList[index]
def torelative(value: float):
return value * self.inputheight
x = torelative(coordinates[0])
y = torelative(coordinates[1])
w = torelative(coordinates[2])
h = torelative(coordinates[3])
w2 = w / 2
h2 = h / 2
l = x - w2
t = y - h2
obj = Prediction(
maxConfidenceIndex, maxConfidence, Rectangle(l, t, l + w, t + h)
)
objs.append(obj)
ret = self.create_detection_result(objs, src_size, cvss)
return ret

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

@@ -6,7 +6,6 @@ import os
import asyncio
import coremltools as ct
import numpy as np
# import Quartz
# from Foundation import NSData, NSMakeSize
@@ -27,6 +26,8 @@ def cosine_similarity(vector_a, vector_b):
return similarity
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
class CoreMLFaceRecognition(FaceRecognizeDetection):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin, nativeId)
@@ -34,12 +35,26 @@ class CoreMLFaceRecognition(FaceRecognizeDetection):
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-face")
def downloadModel(self, model: str):
model_path = self.downloadHuggingFaceModelLocalFallback(model)
modelFile = os.path.join(model_path, f"{model}.mlpackage")
model_version = "v7"
mlmodel = "model"
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{f}",
f"{model_version}/{f}",
)
modelFile = os.path.dirname(p)
model = ct.models.MLModel(modelFile)
inputName = model.get_spec().description.input[0].name
return model, inputName
async def predictDetectModel(self, input: Image.Image):
def predict():
model, inputName = self.detectModel
@@ -58,12 +73,11 @@ class CoreMLFaceRecognition(FaceRecognizeDetection):
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.recogExecutor, lambda: predict()
)
return results
# def predictVision(self, input: Image.Image) -> asyncio.Future[list[Prediction]]:
# buffer = input.tobytes()
# myData = NSData.alloc().initWithBytes_length_(buffer, len(buffer))

View File

@@ -1,48 +0,0 @@
from __future__ import annotations
import asyncio
import os
import traceback
import numpy as np
import coremltools as ct
from common import async_infer
from common import yolov9_seg
from predict.segment import Segmentation
prepareExecutor, predictExecutor = async_infer.create_executors("Segment")
class CoreMLSegmentation(Segmentation):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
def loadModel(self, name):
model_path = self.plugin.downloadHuggingFaceModelLocalFallback(name)
modelFile = os.path.join(model_path, f"{name}.mlpackage")
model = ct.models.MLModel(modelFile)
return model
async def detect_once(self, input, settings, src_size, cvss):
def predict():
input_name = self.model.get_spec().description.input[0].name
out_dict = self.model.predict({input_name: input})
outputs = list(out_dict.values())
pred = outputs[0]
proto = outputs[1]
pred = yolov9_seg.non_max_suppression(pred, nm=32)
return self.process_segmentation_output(pred, proto)
try:
objs = await asyncio.get_event_loop().run_in_executor(
predictExecutor, lambda: predict()
)
except:
traceback.print_exc()
raise
ret = self.create_detection_result(objs, src_size, cvss)
return ret

View File

@@ -20,8 +20,22 @@ class CoreMLTextRecognition(TextRecognition):
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-text")
def downloadModel(self, model: str):
model_path = self.downloadHuggingFaceModelLocalFallback(model)
modelFile = os.path.join(model_path, f"{model}.mlpackage")
model_version = "v8"
mlmodel = "model"
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{f}",
f"{model_version}/{f}",
)
modelFile = os.path.dirname(p)
model = ct.models.MLModel(modelFile)
inputName = model.get_spec().description.input[0].name
return model, inputName

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

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "scrypted-nvr",
"scrypted.debugHost": "koushik-winvm",
}

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/diagnostics",
"version": "0.0.29",
"version": "0.0.19",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/diagnostics",
"version": "0.0.29",
"version": "0.0.19",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"sharp": "^0.33.5"
},
"devDependencies": {
"@types/node": "^22.18.8"
"@types/node": "^22.5.4"
}
},
"../../common": {
@@ -22,41 +22,32 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
"devDependencies": {
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.48",
"version": "0.3.69",
"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": "^6.1.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": {
@@ -69,9 +60,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"
}
},
"node_modules/@emnapi/runtime": {
@@ -434,13 +427,12 @@
"link": true
},
"node_modules/@types/node": {
"version": "22.18.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
"integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
"version": "22.5.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~6.19.2"
}
},
"node_modules/color": {
@@ -557,11 +549,10 @@
"optional": true
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
}
},
"dependencies": {
@@ -719,8 +710,7 @@
"version": "file:../../common",
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2",
@@ -730,39 +720,33 @@
"@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": "^6.1.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"
}
},
"@types/node": {
"version": "22.18.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
"integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
"version": "22.5.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
"dev": true,
"requires": {
"undici-types": "~6.21.0"
"undici-types": "~6.19.2"
}
},
"color": {
@@ -855,9 +839,9 @@
"optional": true
},
"undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/diagnostics",
"version": "0.0.29",
"version": "0.0.19",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -32,6 +32,6 @@
"sharp": "^0.33.5"
},
"devDependencies": {
"@types/node": "^22.18.8"
"@types/node": "^22.5.4"
}
}

View File

@@ -1,37 +1,16 @@
import { Deferred } from '@scrypted/common/src/deferred';
import { safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
import sdk, { Camera, FFmpegInput, Image, MediaObject, MediaStreamDestination, MotionSensor, Notifier, ObjectDetection, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, TextEmbedding, ImageEmbedding } from '@scrypted/sdk';
import sdk, { Camera, FFmpegInput, Image, MediaObject, MediaStreamDestination, MotionSensor, Notifier, ObjectDetection, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import child_process from 'child_process';
import dns from 'dns';
import { once } from 'events';
import fs from 'fs';
import net from 'net';
import os from 'os';
import sharp from 'sharp';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
function cosineSimilarityPrenormalized(e1: Buffer, e2: Buffer) {
const embedding1 = new Float32Array(e1.buffer, e1.byteOffset, e1.length / Float32Array.BYTES_PER_ELEMENT);
const embedding2 = new Float32Array(e2.buffer, e2.byteOffset, e2.length / Float32Array.BYTES_PER_ELEMENT);
let dotProduct = 0;
for (let i = 0; i < embedding1.length; i++) {
dotProduct += embedding1[i] * embedding2[i];
}
return dotProduct;
}
class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
storageSettings = new StorageSettings(this, {
validateSystem: {
console: true,
group: 'System',
title: 'Validate System',
description: 'Validate the system configuration.',
type: 'button',
onPut: () => this.validateSystem(),
},
testDevice: {
group: 'Device',
title: 'Validation Device',
@@ -50,6 +29,14 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
this.validateDevice();
},
},
validateSystem: {
console: true,
group: 'System',
title: 'Validate System',
description: 'Validate the system configuration.',
type: 'button',
onPut: () => this.validateSystem(),
},
});
loggedMotion = new Map<string, number>();
@@ -100,80 +87,6 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
}
}
async validateNVR() {
const console = this.console;
const nvrPlugin = sdk.systemManager.getDeviceById('@scrypted/nvr');
if (!nvrPlugin) {
await this.validate(console, 'NVR Plugin Check', async () => {
throw new Error('NVR plugin not installed.');
});
return;
}
// Consolidated loop for detection plugins
const detectionPlugins = [
'@scrypted/onnx',
'@scrypted/openvino',
'@scrypted/coreml',
'@scrypted/ncnn',
'@scrypted/tensorflow-lite'
];
for (const pluginId of detectionPlugins) {
const plugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>(pluginId);
if (!plugin) {
continue;
}
// Detect objects test
await this.validate(console, `${pluginId}`, async () => {
const settings = await plugin.getSettings();
const executionDevice = settings.find(s => s.key === 'execution_device');
if (executionDevice?.value?.toString().includes('CPU')) {
this.warnStep(console, 'Using CPU execution. GPU recommended for better performance.');
}
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
const detected = await plugin.detectObjects(zidane);
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .8);
if (!personFound) {
throw new Error('Person not detected in test image.');
}
});
const clip = sdk.systemManager.getDeviceById<TextEmbedding & ImageEmbedding>(pluginId, 'clipembedding');
// tflite and ncnn doesnt have it
if (!clip) {
continue;
}
// CLIP implementation test
await this.validate(console, `${pluginId} CLIP`, async () => {
// Test CLIP functionality
const testText = 'test';
const textEmbedding = await clip.getTextEmbedding(testText);
if (!textEmbedding || textEmbedding.length === 0) {
throw new Error('Failed to get text embedding.');
}
const testImage = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
const imageEmbedding = await clip.getImageEmbedding(testImage);
if (!imageEmbedding || imageEmbedding.length === 0) {
throw new Error('Failed to get image embedding.');
}
// Test similarity calculation
const similarity = cosineSimilarityPrenormalized(imageEmbedding, textEmbedding);
if (typeof similarity !== 'number') {
throw new Error('Failed to calculate similarity.');
}
});
}
}
async validateDevice() {
const device = this.storageSettings.values.testDevice as ScryptedDevice & any;
const console = sdk.deviceManager.getMixinConsole(device.id);
@@ -314,28 +227,19 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
const validated = new Set<string | undefined>();
const validateMediaStream = async (stepName: string, destination: MediaStreamDestination) => {
let streamId: string | undefined;
await this.validate(console, stepName + ' (Metadata)', async () => {
const vsos = await device.getVideoStreamOptions();
streamId = vsos.find(vso => vso.destinations?.includes(destination))?.id;
});
if (!streamId) {
await this.validate(console, stepName, async () => "Skipped (Not Configured)");
return;
}
const vsos = await device.getVideoStreamOptions();
const streamId = vsos.find(vso => vso.destinations?.includes(destination))?.id;
if (validated.has(streamId)) {
await this.validate(console, stepName, async () => "Skipped (Duplicate)");
return;
}
validated.add(streamId);
await this.validate(console, stepName + ' (Codec)', async () => {
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await getVideoStream(destination), ScryptedMimeTypes.FFmpegInput);
if (ffmpegInput.mediaStreamOptions?.video?.codec !== 'h264')
this.warnStep(console, `Stream ${stepName} is using codec ${ffmpegInput.mediaStreamOptions?.video?.codec}. h264 is recommended.`);
});
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await getVideoStream(destination), ScryptedMimeTypes.FFmpegInput);
if (ffmpegInput.mediaStreamOptions?.video?.codec !== 'h264')
this.warnStep(console, `Stream ${stepName} is using codec ${ffmpegInput.mediaStreamOptions?.video?.codec}. h264 is recommended.`);
await validateMedia(stepName, getVideoStream(destination));
const start = Date.now();
@@ -427,26 +331,6 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
timeout: 5000,
}).then(r => r.body.trim()));
await this.validate(this.console, 'System Time Accuracy', async () => {
const response = await httpFetch({
url: 'https://cloudflare.com',
responseType: 'text',
timeout: 10000,
});
const dateHeader = response.headers.get('date');
if (!dateHeader) {
throw new Error('No date header in response');
}
const serverTime = new Date(dateHeader).getTime(); const localTime = Date.now();
const difference = Math.abs(serverTime - localTime);
const differenceSeconds = Math.floor(difference / 1000);
if (differenceSeconds > 5) {
throw new Error(`Time drift detected: ${differenceSeconds} seconds difference from accurate time source.`);
}
});
await this.validate(this.console, 'IPv6 (wtfismyip.com)', httpFetch({
url: 'https://wtfismyip.com/text',
family: 6,
@@ -454,30 +338,6 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
timeout: 5000,
}).then(r => r.body.trim()));
await this.validate(this.console, 'Scrypted Cloud Services', async () => {
const endpoints = [
'https://home.scrypted.app',
'https://billing.scrypted.app'
];
for (const endpoint of endpoints) {
try {
const response = await httpFetch({
url: endpoint,
timeout: 5000,
});
if (response.statusCode >= 400) {
throw new Error(`${endpoint} returned status ${response.statusCode}`);
}
} catch (error) {
throw new Error(`${endpoint} is not accessible: ${(error as Error).message}`);
}
}
return 'Both endpoints accessible';
});
await this.validate(this.console, 'Scrypted Server Address', async () => {
const addresses = await sdk.endpointManager.getLocalAddresses();
const hasIPv4 = addresses?.find(address => net.isIPv4(address));
@@ -552,47 +412,18 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
throw new Error('Invalid response received from short lived URL.');
});
if (cloudPlugin) {
await this.validate(this.console, 'Cloud IPv4 Address', async () => {
const externalAddress = await sdk.endpointManager.getCloudEndpoint();
if (!externalAddress)
throw new Error('Scrypted Cloud endpoint not found.');
const url = new URL(externalAddress);
const { hostname } = url;
if (net.isIP(hostname))
return;
const addresses = await dns.promises.lookup(hostname, { all: true });
const hasIPv4 = addresses.find(address => address.family === 4);
if (!hasIPv4)
this.warnStep(this.console, 'No IPv4 address found for Scrypted Cloud endpoint.');
else
return hasIPv4.address;
});
await this.validate(this.console, 'Cloud IPv6 Address', async () => {
const externalAddress = await sdk.endpointManager.getCloudEndpoint();
if (!externalAddress)
throw new Error('Scrypted Cloud endpoint not found.');
const url = new URL(externalAddress);
const { hostname } = url;
if (net.isIP(hostname))
return;
const addresses = await dns.promises.lookup(hostname, { all: true });
const hasIPv6 = addresses.find(address => address.family === 6);
if (!hasIPv6)
this.warnStep(this.console, 'No IPv6 address found for Scrypted Cloud endpoint.');
else
return hasIPv6.address;
});
}
if ((hasCUDA || process.platform === 'win32') && onnxPlugin) {
await this.validate(this.console, 'ONNX Plugin', async () => {
const settings = await onnxPlugin.getSettings();
const executionDevice = settings.find(s => s.key === 'execution_device');
if (executionDevice?.value?.toString().includes('CPU'))
this.warnStep(this.console, 'GPU device unvailable or not passed through to container.');
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
const detected = await onnxPlugin.detectObjects(zidane);
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
if (!personFound)
throw new Error('Person not detected in test image.');
});
}
@@ -602,34 +433,15 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
const availbleDevices = settings.find(s => s.key === 'available_devices');
if (!availbleDevices?.value?.toString().includes('GPU'))
this.warnStep(this.console, 'GPU device unvailable or not passed through to container.');
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
const detected = await openvinoPlugin.detectObjects(zidane);
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
if (!personFound)
throw new Error('Person not detected in test image.');
});
}
await this.validateNVR();
await this.validate(this.console, 'External Resource Access', async () => {
const urls = [
'https://huggingface.co/koushd/clip/resolve/main/requirements.txt',
'https://raw.githubusercontent.com/koush/openvino-models/refs/heads/main/scrypted_labels.txt',
'https://registry.npmjs.org/@scrypted/server'
];
for (const url of urls) {
try {
const response = await httpFetch({
url,
timeout: 5000,
});
if (response.statusCode >= 400) {
throw new Error(`${url} returned status ${response.statusCode}`);
}
} catch (error) {
throw new Error(`${url} is not accessible: ${(error as Error).message}`);
}
}
});
if (nvrPlugin) {
await this.validate(this.console, "GPU Decode", async () => {
const ffmpegPath = await sdk.mediaManager.getFFmpegPath();
@@ -714,7 +526,7 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
if (image.width !== 320)
throw new Error('Unexpected image with from GPU transform.')
const detected = await openvinoPlugin.detectObjects(zidane);
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .8);
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
if (!personFound)
throw new Error('Person not detected in test image.');
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.6",
"version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/doorbird",
"version": "0.0.6",
"version": "0.0.4",
"dependencies": {
"doorbird": "2.6.0"
},
@@ -24,7 +24,6 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
@@ -36,30 +35,29 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.33",
"version": "0.3.108",
"dev": true,
"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": {
@@ -72,9 +70,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"
}
},
"node_modules/@scrypted/common": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.6",
"version": "0.0.4",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -1,35 +1,15 @@
import {httpFetch} from '../../../server/src/fetch/http-fetch';
import {listenZero} from '@scrypted/common/src/listen-cluster';
import {ffmpegLogInitialOutput, safePrintFFmpegArguments} from "@scrypted/common/src/media-helpers";
import {readLength, StreamEndError} from "@scrypted/common/src/read-stream";
import sdk, {
BinarySensor,
Camera,
DeviceCreator,
DeviceCreatorSettings,
DeviceInformation,
DeviceProvider,
FFmpegInput,
Intercom,
MediaObject,
MotionSensor,
PictureOptions,
ResponseMediaStreamOptions,
ScryptedDeviceBase,
ScryptedDeviceType,
ScryptedInterface,
ScryptedMimeTypes,
Setting,
Settings,
VideoCamera
} from '@scrypted/sdk';
import child_process, {ChildProcess} from 'child_process';
import {randomBytes} from 'crypto';
import { authHttpFetch } from "@scrypted/common/src/http-auth-fetch";
import { listenZero } from '@scrypted/common/src/listen-cluster';
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
import { readLength } from "@scrypted/common/src/read-stream";
import sdk, { BinarySensor, Camera, DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, FFmpegInput, Intercom, MediaObject, MotionSensor, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from '@scrypted/sdk';
import child_process, { ChildProcess } from 'child_process';
import { randomBytes } from 'crypto';
import net from 'net';
import {PassThrough, Readable} from "stream";
import {ApiMotionEvent, ApiRingEvent, DoorbirdAPI} from "./doorbird-api";
import { PassThrough, Readable } from "stream";
import { ApiMotionEvent, ApiRingEvent, DoorbirdAPI } from "./doorbird-api";
const {deviceManager, mediaManager} = sdk;
const { deviceManager, mediaManager } = sdk;
class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCamera, Settings, BinarySensor, MotionSensor {
doorbirdApi: DoorbirdAPI | undefined;
@@ -42,8 +22,6 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
audioRXClientSocket: net.Socket;
pendingPicture: Promise<MediaObject>;
private static readonly TRANSMIT_AUDIO_CHUNK_SIZE: number = 256;
constructor(nativeId: string, public provider: DoorbirdCamProvider) {
super(nativeId);
this.binaryState = false;
@@ -111,7 +89,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
public async getPictureOptions(): Promise<PictureOptions[]> {
return [{
id: 'VGA',
picture: {width: 640, height: 480}
picture: { width: 640, height: 480 }
}];
}
@@ -163,22 +141,6 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
placeholder: 'rtsp://192.168.2.100/my_doorbird_video_stream',
value: this.storage.getItem('rtspUrl'),
description: 'Use this in case you are already using another RTSP server/proxy (e.g. mediamtx, go2rtc, etc.) to limit the number of streams from the camera.',
},
{
key: 'audioDenoise',
type: 'boolean',
subgroup: 'Advanced',
title: 'Denoise',
value: this.storage.getItem('audioDenoise') === 'true',
description: 'Denoise both input and output audio streams to reduce background noises.',
},
{
key: 'audioSpeechEnhancement',
type: 'boolean',
subgroup: 'Advanced',
title: 'Speech Enhancement',
value: this.storage.getItem('audioSpeechEnhancement') === 'true',
description: 'Apply band filtering and dynamic normalization to both audio streams.',
}
];
}
@@ -197,47 +159,25 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
}
async startAudioTransmitter(media: MediaObject): Promise<void> {
this.console.log('Doorbird: Init audio transmitter...');
const ffmpegInput: FFmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput)).toString());
const ffmpegArgs = ffmpegInput.inputArguments.slice();
ffmpegArgs.push(
// Do not process video streams (disable video)
'-vn',
// Do not process data streams (e.g. timed metadata)
'-dn',
// Do not process subtitle streams
'-sn',
// Encode audio using PCM µ-law (G.711 codec, 8-bit logarithmic compression)
'-vn', '-dn', '-sn',
'-acodec', 'pcm_mulaw',
// Bypass internal I/O buffering (write directly to output)
"-avioflags", "direct",
// Disable input buffering
'-fflags', '+flush_packets+nobuffer',
// Force flushing packets after every frame
'-flush_packets', '1',
// Use global headers (required by some muxers) and enable low-latency flags
'-flags', '+global_header+low_delay',
// Set number of audio channels to mono
'-flags', '+global_header',
'-ac', '1',
// Set audio sample rate to 8000 Hz (expected by Doorbird)
'-ar', '8000',
// Force raw µ-law output format (no container)
'-ar', '8k',
'-f', 'mulaw',
// Do not buffer or delay packets in the muxer
'-muxdelay', '0',
// --- Audio Filtering ---
...(this.getAudioFilter()),
// Output to file descriptor 3 (e.g. pipe:3, for inter-process communication)
'pipe:3'
);
safePrintFFmpegArguments(this.console, ffmpegArgs);
safePrintFFmpegArguments(console, ffmpegArgs);
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), ffmpegArgs, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
this.audioTXProcess = cp;
ffmpegLogInitialOutput(this.console, cp);
ffmpegLogInitialOutput(console, cp);
cp.on('exit', () => this.console.log('Doorbird: Audio transmitter ended.'));
cp.stdout.on('data', data => this.console.log(data.toString()));
cp.stderr.on('data', data => this.console.log(data.toString()));
@@ -248,58 +188,41 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
const password: string = this.getPassword();
const audioTxUrl: string = `${this.getHttpBaseAddress()}/bha-api/audio-transmit.cgi`;
(async () => {
this.console.log('Doorbird: Starting audio transmitter...');
(async () => {
this.console.log('Doorbird: audio transmitter started.');
this.console.log('Doorbird: Audio transmitter started.');
const passthrough = new PassThrough();
const abortController = new AbortController();
let totalBytesWritten: number = 0;
authHttpFetch({
method: 'POST',
url: audioTxUrl,
credential: {
username,
password,
},
headers: {
'Content-Type': 'audio/basic',
'Content-Length': '9999999'
},
data: passthrough,
});
try {
// Perform POST request instantly instead of unneeded handling with DIGEST authentication.
// Credentials will be thrown into network by all other requests anyway.
httpFetch({
url: audioTxUrl,
method: 'POST',
headers: {
'Content-Type': 'audio/basic',
'Content-Length': '9999999',
'Authorization': this.getBasicAuthorization(username, password),
},
signal: abortController.signal,
body: passthrough,
responseType: 'readable',
})
while (true) { // Loop will be broken by StreamEndError.
// Read the next chunk of audio data from the Doorbird camera.
const data = await readLength(socket, DoorbirdCamera.TRANSMIT_AUDIO_CHUNK_SIZE);
if (data.length === 0) {
break;
}
// Actually write the data to the passthrough stream.
while (true) {
const data = await readLength(socket, 1024);
passthrough.push(data);
// Add the length of the data to the total bytes written.
totalBytesWritten += data.length;
}
} catch (e) {
if (!(e instanceof StreamEndError)) {
this.console.error('Doorbird: Audio transmitter error', e);
}
} finally {
this.console.log(`Doorbird: Audio transmitter finished. bytesOut=${totalBytesWritten}ms`);
passthrough.destroy();
abortController.abort();
}
this.stopIntercom();
})();
}
catch (e) {
}
finally {
this.console.log('Doorbird: audio transmitter finished.');
passthrough.end();
}
private getBasicAuthorization(username: string, password: string) {
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
this.stopAudioTransmitter();
})();
}
stopAudioTransmitter() {
@@ -315,53 +238,18 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
const ffmpegPath = await mediaManager.getFFmpegPath();
const audioFilters = this.getAudioFilter();
const ffmpegArgs = [
// Suppress printing the FFmpeg banner. Keeps logs clean.
'-hide_banner',
// Disable periodic progress/statistics logging. Reduces noise and CPU usage.
'-nostats',
// --- Low-latency Input Flags ---
// Reduce input buffer latency by flushing packets immediately and disabling demuxer buffering.
'-fflags', '+flush_packets+nobuffer',
// Do not spend time analyzing the stream to determine properties. Crucial for live streams.
'-analyzeduration', '0',
// Set a very small probe size to speed up initial connection, as we already know the format.
'-probesize', '32',
// Read input at its native frame rate to ensure real-time processing.
'-re',
// --- Input Format Specification ---
// Set the audio sample rate to 8000 Hz, matching the Doorbird's stream.
'-ar', '8000',
// Set the number of audio channels to 1 (mono).
'-ac', '1',
// Force the input format to be interpreted as G.711 µ-law.
'-f', 'mulaw',
// Specify the input URL for the Doorbird's audio stream.
'-i', `${audioRxUrl}`,
// --- Audio Filtering ---
...(audioFilters),
// --- Low-latency Output Flags ---
// Enable low-delay flags in the encoder, preventing frame buffering for lookahead.
'-flags', '+global_header+low_delay',
// Bypass FFmpeg's internal I/O buffering, writing directly to the output pipe.
'-avioflags', 'direct',
// Force flushing packets to the output immediately after encoding.
'-flush_packets', '1',
// Set the maximum demux-decode delay to zero, preventing buffering in the muxer.
'-muxdelay', '0',
// --- Output Format Specification ---
// Re-encode the audio to PCM µ-law after the filter has been applied, or just copy it if no filters are applied.
'-acodec', (audioFilters.length > 0 ? 'pcm_mulaw' : 'copy'),
// Force the output container format to raw µ-law.
'-acodec', 'copy',
'-f', 'mulaw',
// Output the processed audio to file descriptor 3 (the pipe).
'pipe:3'
];
@@ -374,7 +262,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
cp.on('exit', () => {
this.console.log('Doorbird: audio receiver ended.')
this.stopIntercom();
this.audioRXProcess = undefined;
});
cp.stdout.on('data', data => this.console.log(data.toString()));
cp.stderr.on('data', data => this.console.log(data.toString()));
@@ -410,60 +298,24 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
async getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
const audioRtspStreamPort = await this.startAudioRXServer();
const port = await this.startAudioRXServer();
const ffmpegInput: FFmpegInput = {
url: undefined,
inputArguments: [
// --- Low-latency Input Flags (for both streams) ---
// Suppress printing the FFmpeg banner.
'-hide_banner',
// Disable periodic progress/statistics logging.
'-nostats',
// Set the log level to 'error' to suppress verbose informational messages.
'-loglevel', 'error',
// Reduce input buffer latency by flushing packets immediately and disabling demuxer buffering.
// '+nobuffer' is particularly important for live streams.
'-fflags', '+flush_packets+nobuffer',
// Do not spend time analyzing the stream to determine properties. Crucial for live streams.
'-analyzeduration', '0',
// Set a very small probe size to speed up initial connection, as we know the formats.
'-probesize', '32',
// Request low-delay flags from decoders.
'-fflags', 'nobuffer',
'-flags', 'low_delay',
// --- Video Input (Input 0) ---
// Force the input format to be interpreted as RTSP.
'-f', 'rtsp',
// Use TCP for RTSP transport for better reliability over potentially lossy networks.
'-rtsp_transport', 'tcp',
// Specify the input URL for the Doorbird's RTSP video stream.
'-i', `${this.getRtspAddress()}`,
// --- Audio Input (Input 1) ---
// Force the format of the second input to be interpreted as G.711 µ-law.
'-f', 'mulaw',
// Set the number of audio channels to 1 (mono) for the audio input.
'-ac', '1',
// Set the audio sample rate to 8000 Hz for the audio input.
'-ar', '8000',
// Explicitly define the channel layout as mono.
'-channel_layout', 'mono',
// Use the system's wall clock for timestamps. This helps synchronize the separate audio
// and video streams, which do not share a common clock source.
'-use_wallclock_as_timestamps', '1',
// Specify the second input as the local TCP socket providing the audio stream.
// `tcp_nodelay=1` disables Nagle's algorithm, reducing latency for small packets.
'-i', `tcp://127.0.0.1:${audioRtspStreamPort}?tcp_nodelay=1`,
// --- Output Stream Handling ---
// Increase the maximum delay for the muxing queue to 5 seconds (in microseconds).
// This prevents the "Delay between the first packet and last packet" error
// by allowing more time for packets from different streams to arrive.
'-max_delay', '5000000',
// Finish encoding when the shortest input stream (the video) ends.
// This ensures ffmpeg terminates if the video stream is interrupted by Doorbird.
'-shortest',
'-use_wallclock_as_timestamps', 'true',
'-i', `tcp://127.0.0.1:${port}?tcp_nodelay=1`,
],
mediaStreamOptions: options,
};
@@ -480,29 +332,12 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
const ffmpegPath = await mediaManager.getFFmpegPath();
const ffmpegArgs = [
// Suppress printing the FFmpeg banner.
'-hide_banner',
// Disable periodic progress/statistics logging.
'-nostats',
// Read input at its native frame rate to ensure real-time processing.
'-re',
// Use the lavfi (libavfilter) virtual input device.
'-f', 'lavfi',
// Specify the input source as a null audio source (silence) with a sample rate of 8000 Hz and mono channel layout.
'-i', 'anullsrc=r=8000:cl=mono',
// --- Low-latency Output Flags ---
// Bypass FFmpeg's internal I/O buffering, writing directly to the output pipe.
'-avioflags', 'direct',
// Force flushing packets to the output immediately after encoding.
'-flush_packets', '1',
// Set the maximum demux-decode delay to zero, preventing buffering in the muxer.
'-muxdelay', '0',
// --- Output Format Specification ---
// Force the output container format to raw µ-law.
'-f', 'mulaw',
// Output the processed audio to file descriptor 3 (the pipe).
'pipe:3'
];
@@ -535,7 +370,6 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
const server = net.createServer(async (clientSocket) => {
clearTimeout(serverTimeout);
this.console.log(`Doorbird: audio connection from client ${JSON.stringify(clientSocket.address())}`);
this.audioRXClientSocket = clientSocket;
@@ -545,14 +379,13 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
this.stopSilenceGenerator();
this.audioRXClientSocket = null;
});
});
const serverTimeout = setTimeout(() => {
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
server.close();
}, 30000);
const port = await listenZero(server, '127.0.0.1');
this.console.log(`Doorbird: audio server started on port ${port}`);
return port;
}
@@ -579,7 +412,8 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
getRtspAddress() {
if (this.storage.getItem('rtspUrl') !== undefined) {
return this.storage.getItem('rtspUrl');
} else {
}
else {
return this.getRtspDefaultAddress();
}
}
@@ -603,52 +437,6 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
getPassword() {
return this.storage.getItem('password');
}
setAudioDenoise(enabled: boolean) {
this.storage.setItem('audioDenoise', enabled.toString());
}
getAudioDenoise(): boolean {
return this.storage.getItem('audioDenoise') === 'true';
}
setAudioSpeechEnhancement(enabled: boolean) {
this.storage.setItem('audioSpeechEnhancement', enabled.toString());
}
getAudioSpeechEnhancement(): boolean {
return this.storage.getItem('audioSpeechEnhancement') === 'true';
}
private getAudioFilter() {
const filters: string[] = [];
if (this.getAudioDenoise()) {
// Apply noise reduction using the 'afftdn' filter.
// - 'afftdn=nf=-50' removes background noise below -50 dB (e.g. hiss, hum)
// - 'agate=threshold=0.06:attack=20:release=250' gates quiet sounds:
// threshold=0.06 → suppresses signals below ~-24 dBFS (breaths, room noise)
// attack=20 → gate opens smoothly in 20 ms to preserve speech onset
// release=250 → gate closes slowly in 250 ms to avoid cutting word ends
filters.push('afftdn=nf=-50', 'agate=threshold=0.06:attack=20:release=250');
}
if (this.getAudioSpeechEnhancement()) {
// Apply high-pass and low-pass filters to remove frequencies outside the human voice range and apply dynamic normalization.
// - 'highpass=f=200' → removes low rumbles below 200 Hz (e.g. touch intercom while speaking, low street noise)
// - 'lowpass=f=3000' → removes harsh highs above 3 kHz to reduce hiss/sibilance
// - 'acompressor=threshold=0.1:ratio=4:attack=20:release=200'
// threshold=0.1 → starts compressing above ~-20 dBFS
// ratio=4 → reduces dynamic range by a 4:1 ratio
// attack=20 → begins compression quickly to catch loud speech
// release=200 → smooths out gain after loud parts
// - 'volume=4' → boosts output gain 4x after compression
filters.push('highpass=f=200', 'lowpass=f=3000', 'acompressor=threshold=0.1:ratio=4:attack=20:release=200', 'volume=4');
}
if (filters.length === 0) {
return [];
}
return ['-af', filters.join(',')];
}
}
export class DoorbirdCamProvider extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
@@ -684,7 +472,8 @@ export class DoorbirdCamProvider extends ScryptedDeviceBase implements DevicePro
info.mac = deviceInfo.serialNumber;
info.manufacturer = 'Bird Home Automation GmbH';
info.managementUrl = 'https://webadmin.doorbird.com';
} catch (e) {
}
catch (e) {
this.console.error('Error adding Doorbird camera', e);
throw e;
}
@@ -701,8 +490,6 @@ export class DoorbirdCamProvider extends ScryptedDeviceBase implements DevicePro
device.putSetting('password', password);
device.setIPAddress(settings.ip.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
device.setAudioDenoise(settings.audioDenoise === 'true');
device.setAudioSpeechEnhancement(settings.audioSpeechEnhancement === 'true');
return nativeId;
}

View File

@@ -32,11 +32,11 @@ export function syncResponse(device: ScryptedDevice, type: string): homegraph_v1
defaultNames: [],
nicknames: [],
},
otherDeviceIds: (device.type !== ScryptedDeviceType.Camera && device.type !== ScryptedDeviceType.Doorbell) ? [
otherDeviceIds: [
{
deviceId: device.id,
}
] : undefined,
],
attributes: {},
traits: [],
type,

View File

@@ -8,18 +8,6 @@ Most commonly this plugin is used with 2 plugins: Rebroadcast and HomeKit.
Device must have built-in motion detection (most Hikvision doorbells have this).
If the doorbell do not have motion detection, you will have to use a separate plugin or device to achieve this (e.g., `opencv`, `pam-diff`, or `dummy-switch`) and group it to the doorbell.
## ⚠️ Important: Version 2.x Breaking Changes
Version 2 of this plugin is **not compatible** with version 1.x. Before installing or upgrading to version 2:
- **Option 1**: Completely remove the old plugin from Scrypted
- **Option 2**: Delete all devices that belong to the old plugin
After removing the old version, you will need to reconfigure all doorbell devices from scratch.
### Firmware Requirements
This version **requires firmware v3.7 or higher**. Older firmware versions are not supported.
## Two Way Audio
Two Way Audio is supported if the audio codec is set to G.711ulaw on the doorbell, which is usually the default audio codec. This audio codec will also work with HomeKit. Changing the audio codec from G.711ulaw will cause Two Way Audio to fail on the doorbells that were tested.

View File

@@ -1,5 +1,4 @@
# Tamper Alert Mechanism Interface
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the doorbell's tamper alert sensor, which is integrated into models such as the DS-KV6113-PE1(C).
When the doorbell's tamper sensor is triggered, this device will turn **on**. You can manually turn it **off** in the Scrypted web interface. This device is automatically removed when the parent doorbell device is deleted.
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the doorbell tamper alert, which is integrated into models such as the DS-KV6113.
In the settings section, you can see the linked (parent) device, as well as the IP address of the Hikvision Doorbell (phisical device). These fields are not editable, they are for information purposes only.

View File

@@ -1,45 +1,26 @@
# Hikvision Doorbell
**⚠️ Important: Version 2.x Breaking Changes**
Version 2 of this plugin is **not compatible** with version 1.x. Before installing or upgrading to version 2:
- **Option 1**: Completely remove the old plugin from Scrypted
- **Option 2**: Delete all devices that belong to the old plugin
After removing the old version, you will need to reconfigure all doorbell devices from scratch.
## Introduction
At the moment, plugin was tested with the **DS-KV6113-PE1(C)** model `doorbell` with firmware version: **V3.7.0 build 250818**, in the following modes:
At the moment, plugin was tested with the **DS-KV6113PE1[C]** model `doorbell` with firmware version: **V2.2.65 build 231213**, in the following modes:
- the `doorbell` is connected to the `Hik-Connect` service;
- the `doorbell` is connected to a local SIP proxy (asterisk);
- the `doorbell` is connected to a fake SIP proxy, which this plugin runs.
## Settings
### Support door lock opening
The doorbell can control electromechanical locks connected to it. To enable lock control in Scrypted, go to the doorbell device settings, navigate to **Advanced Settings**, and select **Locks** in the **Provided devices** option.
Most of these doorbells have the ability to control an electromechanical lock. To implement the lock controller software interface in Scrypted, you need to create a separate device with the `Lock` type. Such a device is created automatically if you enable the **Expose Door Lock Controller** checkbox.
This will create dependent lock device(s) with the `Lock` type. The plugin automatically detects how many doors the doorbell supports (typically 1, but some models support multiple doors). If multiple doors are supported, each lock device will be named with its door number (e.g., "Door Lock 1", "Door Lock 2").
Lock devices are automatically removed when the parent doorbell device is deleted.
### Support contact sensors
Door open/close status monitoring is available through contact sensors. To enable this functionality in Scrypted, go to the doorbell device settings, navigate to **Advanced Settings**, and select **Contact Sensors** in the **Provided devices** option.
This will create dependent contact sensor device(s) with the `BinarySensor` type. The plugin automatically detects how many doors the doorbell supports (typically 1, but some models support multiple doors). If multiple doors are supported, each contact sensor will be named with its door number (e.g., "Contact Sensor 1", "Contact Sensor 2").
Contact sensor devices are automatically removed when the parent doorbell device is deleted.
The lock controller is linked to this device (doorbell). Therefore, when the doorbell is deleted, the associated lock controller will also be deleted.
### Support tamper alert
For security, the doorbell includes a built-in tamper detection sensor. To enable tamper alert monitoring in Scrypted, go to the doorbell device settings, navigate to **Advanced Settings**, and select **Tamper Alert** in the **Provided devices** option. If you don't enable this option, tamper alert signals will be interpreted as `Motion Detection` events.
Most of a doorbells have a tamper alert. To implement the tamper alert software interface in Scrypted, you need to create a separate device with the `Switch` type. Such a device is created automatically if you enable the **Expose Tamper Alert Controller** checkbox. If you leave this checkbox disabled, the tamper signal will be interpreted as a `Motion Detection` event.
This will create a dependent tamper alert device with the `BinarySensor` type. When the doorbell's tamper sensor is triggered, the device will turn **on**. You can manually turn it **off** in the Scrypted web interface.
If the tamper on the doorbell is triggered, the controller (`Switch`) will **turn on**. You can **turn off** the switch manually in the Scrypted web interface only.
The tamper alert device is automatically removed when the parent doorbell device is deleted.
The tamper alert controller is linked to this device (doorbell). Therefore, when the doorbell is deleted, the associated tamper alert controller will also be deleted.
### Setting up a receiving call (the ability to ringing)
@@ -63,17 +44,10 @@ This mode should be used when you have a separate SIP gateway and all your inter
#### Emulate SIP Proxy
This mode should be used when you have a `doorbell` but no **Indoor Station**, and you want to connect the `doorbell` directly to the Scrypted server.
This mode should be used when you have a `doorbell` but no **Indoor Station**, and you want to connect this `doorbell` to Scrypted server only.
In this mode, the plugin creates a fake SIP proxy that listens for connections on the specified port (or auto-selects a port if left blank). This server receives call notifications and, when intercom starts (two-way audio), simulates picking up the handset so the `doorbell` switches to conversation mode (stops ringing).
In this mode, the plugin creates a fake SIP proxy that listens for a connection on the specified port (or auto-select a port if not specified). The task of this server is to receive a notification about a call and, in the event of an intercom start (two way audio), simulate picking up the handset so that the `doorbell` switches to conversation mode (stops ringing).
**Important**: When you enable this mode, the plugin **automatically configures the doorbell** with the necessary SIP settings. You don't need to configure the doorbell manually.
On the additional tab, configure the desired port, and you can also enable the **Autoinstall Fake SIP Proxy** checkbox, for not to configure `doorbell` manually.
On the additional settings tab, you can configure:
- **Port**: The listening port for the fake SIP proxy (leave blank for automatic selection)
- **Room Number**: Virtual room number (1-9999) that represents this fake SIP proxy
- **SIP Proxy Phone Number**: Phone number representing the fake SIP proxy (default: 10102)
- **Doorbell Phone Number**: Phone number representing the doorbell (default: 10101)
- **Button Number**: Call button number for doorbells with multiple buttons (1-99, default: 1)
The plugin automatically applies these settings to the doorbell device via ISAPI. If the doorbell is temporarily unreachable, the plugin will retry the configuration automatically.
In the `doorbell` settings you can configure the connection to the fake SIP proxy manually. You should specify the IP address of the Scrypted server and the port of the fake proxy. The contents of the other fields do not matter, since the SIP proxy authorizes the “*client*” using the known doorbells IP address.

View File

@@ -1,9 +0,0 @@
# Binary Sensor Interface
This device serves as a companion for the Hikvision Doorbell device. It provides a binary sensor interface for monitoring the door opening state, which is integrated into models such as the DS-KV6113.
The Binary Sensor monitors the door opening state and reports:
- **Closed** (binaryState: false) - Door is closed
- **Open** (binaryState: true) - Door is open
This sensor provides a simple binary state indication that can be used for automation and monitoring purposes.

View File

@@ -1,3 +1,4 @@
# Lock Opening Mechanism Interface
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the lock opening mechanism, which is integrated into models such as the DS-KV6113.
This device serves as a companion for the Hikvision Doorbell device. It provides an interface for interacting with the lock opening mechanism, which is integrated into models such as the DS-KV6113.
In the settings section, you can see the linked (parent) device, as well as the IP address of the Hikvision Doorbell (phisical device). These fields are not editable, they are for information purposes only.

View File

@@ -1,12 +1,12 @@
{
"name": "@vityevato/hikvision-doorbell",
"version": "2.0.0d",
"version": "1.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@vityevato/hikvision-doorbell",
"version": "2.0.0d",
"version": "1.0.1",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -30,41 +30,39 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
},
"devDependencies": {
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.5.48",
"version": "0.3.118",
"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": "^6.1.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": {
@@ -77,62 +75,60 @@
"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"
}
},
"../../server": {
"name": "@scrypted/server",
"version": "0.142.9",
"version": "0.138.1",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@scrypted/ffmpeg-static": "^6.1.0-build3",
"@scrypted/node-pty": "^1.0.25",
"@scrypted/types": "^0.5.43",
"@scrypted/node-pty": "^1.0.22",
"@scrypted/types": "^0.3.108",
"adm-zip": "^0.5.16",
"body-parser": "^2.2.0",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"dotenv": "^16.5.0",
"engine.io": "^6.6.4",
"express": "^5.1.0",
"dotenv": "^16.4.5",
"engine.io": "^6.6.2",
"express": "^4.21.1",
"follow-redirects": "^1.15.9",
"http-auth": "^4.2.1",
"level": "^10.0.0",
"http-auth": "^4.2.0",
"level": "^8.0.1",
"lodash": "^4.17.21",
"mime-types": "^3.0.1",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^11.2.0",
"py": "npm:@bjia56/portable-python@^0.1.141",
"semver": "^7.7.2",
"sharp": "^0.34.2",
"node-gyp": "^10.2.0",
"py": "npm:@bjia56/portable-python@^0.1.112",
"semver": "^7.6.3",
"sharp": "^0.33.5",
"source-map-support": "^0.5.21",
"tar": "^7.4.3",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript": "^5.5.4",
"whatwg-mimetype": "^4.0.0",
"ws": "^8.18.2"
"ws": "^8.18.0"
},
"bin": {
"scrypted-serve": "bin/scrypted-serve"
},
"devDependencies": {
"@types/adm-zip": "^0.5.7",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.0",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/lodash": "^4.17.17",
"@types/mime-types": "^3.0.1",
"@types/node": "^24.0.3",
"@types/lodash": "^4.17.13",
"@types/node": "^22.10.1",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/semver": "^7.7.0",
"@types/semver": "^7.5.8",
"@types/source-map-support": "^0.5.10",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.18.1",
"@types/ws": "^8.5.13",
"rimraf": "^6.0.1"
}
},
@@ -253,8 +249,7 @@
"version": "file:../../common",
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/types": "^0.5.27",
"@types/node": "^20.19.11",
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2",
@@ -264,29 +259,28 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@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",
"@types/node": "^24.0.1",
"@types/node": "^22.10.1",
"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": "^6.1.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",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"typedoc": "^0.26.11",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
}
},
@@ -294,46 +288,44 @@
"version": "file:../../server",
"requires": {
"@scrypted/ffmpeg-static": "^6.1.0-build3",
"@scrypted/node-pty": "^1.0.25",
"@scrypted/types": "^0.5.43",
"@scrypted/node-pty": "^1.0.22",
"@scrypted/types": "^0.3.108",
"@types/adm-zip": "^0.5.7",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.0",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/lodash": "^4.17.17",
"@types/mime-types": "^3.0.1",
"@types/node": "^24.0.3",
"@types/lodash": "^4.17.13",
"@types/node": "^22.10.1",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/semver": "^7.7.0",
"@types/semver": "^7.5.8",
"@types/source-map-support": "^0.5.10",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.18.1",
"@types/ws": "^8.5.13",
"adm-zip": "^0.5.16",
"body-parser": "^2.2.0",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
"dotenv": "^16.5.0",
"engine.io": "^6.6.4",
"express": "^5.1.0",
"dotenv": "^16.4.5",
"engine.io": "^6.6.2",
"express": "^4.21.1",
"follow-redirects": "^1.15.9",
"http-auth": "^4.2.1",
"level": "^10.0.0",
"http-auth": "^4.2.0",
"level": "^8.0.1",
"lodash": "^4.17.21",
"mime-types": "^3.0.1",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^11.2.0",
"py": "npm:@bjia56/portable-python@^0.1.141",
"node-gyp": "^10.2.0",
"py": "npm:@bjia56/portable-python@^0.1.112",
"rimraf": "^6.0.1",
"semver": "^7.7.2",
"sharp": "^0.34.2",
"semver": "^7.6.3",
"sharp": "^0.33.5",
"source-map-support": "^0.5.21",
"tar": "^7.4.3",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript": "^5.5.4",
"whatwg-mimetype": "^4.0.0",
"ws": "^8.18.2"
"ws": "^8.18.0"
}
},
"@types/ip": {

View File

@@ -1,6 +1,6 @@
{
"name": "@vityevato/hikvision-doorbell",
"version": "2.0.8",
"version": "1.0.1",
"description": "Hikvision Doorbell Plugin for Scrypted",
"author": "Roman Sokolov",
"license": "Apache",

View File

@@ -7,8 +7,6 @@ import * as Auth from 'http-auth-client';
export interface AuthRequestOptions extends Http.RequestOptions {
sessionAuth?: Auth.Basic | Auth.Digest | Auth.Bearer;
responseType: HttpFetchResponseType;
// Internal: number of digest retries performed for this request
digestRetry?: number;
}
export type AuthRequestBody = string | Buffer | Readable;
@@ -17,13 +15,11 @@ export class AuthRequst {
private username: string;
private password: string;
private console: Console;
private auth: Auth.Basic | Auth.Digest | Auth.Bearer;
constructor(username:string, password: string, console: Console) {
this.username = username;
this.password = password;
this.console = console;
}
async request(url: string, options: AuthRequestOptions, body?: AuthRequestBody) {
@@ -42,44 +38,23 @@ export class AuthRequst {
const req = Http.request(url, opt)
// Apply timeout if specified (Node.js http.request doesn't use timeout from options)
if (opt.timeout) {
req.setTimeout (opt.timeout, () => {
req.destroy (new Error (`Request timeout after ${opt.timeout}ms`));
});
}
req.once('response', async (resp) => {
try {
if (resp.statusCode == 401) {
// Hikvision quirk: even if we already had a sessionAuth, a fresh
// WWW-Authenticate challenge may require rebuilding credentials.
// Limit the number of digest rebuilds to avoid infinite loops.
const attempt = (opt.digestRetry ?? 0);
if (attempt >= 2) {
// Give up after a couple of rebuild attempts and surface the 401 response
resolve(await this.parseResponse (opt.responseType, resp));
return;
}
if (resp.statusCode == 401) {
const newAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth);
// Clear cached auth to avoid stale nonce reuse
this.auth = undefined;
opt.sessionAuth = newAuth;
opt.digestRetry = attempt + 1;
const result = await this.request(url, opt, body);
resolve(result);
if (opt.sessionAuth) {
resolve(await this.parseResponse (opt.responseType, resp));
return;
}
else {
// Cache the negotiated session auth only if it was provided for this request.
if (opt.sessionAuth) {
this.auth = opt.sessionAuth;
}
resolve(await this.parseResponse(opt.responseType, resp));
}
} catch (error) {
reject(error);
opt.sessionAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth);
this.auth = undefined;
const result = await this.request(url, opt, body);
resolve(result);
}
else {
this.auth = opt.sessionAuth;
resolve(await this.parseResponse(opt.responseType, resp));
}
});
@@ -98,6 +73,7 @@ export class AuthRequst {
req.end();
}
else {
this.readableBody(req, body).pipe(req);
req.flushHeaders();
}
@@ -179,10 +155,6 @@ export class AuthRequst {
readable.once('end', () => {
resolve(result);
});
readable.once('error', (error) => {
reject(error);
});
});
}
@@ -198,10 +170,6 @@ export class AuthRequst {
readable.once('end', () => {
resolve(result);
});
readable.once('error', (error) => {
reject(error);
});
});
}

View File

@@ -1,43 +0,0 @@
import { Console } from 'console';
/**
* Interface for managing debug state
*/
export interface DebugController {
setDebugEnabled(enabled: boolean): void;
getDebugEnabled(): boolean;
}
/**
* Mutates an existing Console object to provide conditional debug output
* @param console - The console object to mutate
* @returns Controller object for managing debug state
*/
export function makeDebugConsole(console: Console): DebugController {
let debugEnabled = process.env.DEBUG === 'true' ||
process.env.NODE_ENV === 'development';
// Store original debug method
const originalDebug = console.debug.bind (console);
// Replace debug method with conditional version
console.debug = (message?: any, ...optionalParams: any[]): void => {
if (debugEnabled)
{
const now = new Date();
const timestamp = now.toISOString();
originalDebug (`[DEBUG ${timestamp}] ${message}`, ...optionalParams);
}
};
// Return controller for managing debug state
return {
setDebugEnabled(enabled: boolean): void {
debugEnabled = enabled;
},
getDebugEnabled(): boolean {
return debugEnabled;
}
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
import { BinarySensor, Readme, ScryptedDeviceBase, ScryptedInterface } from "@scrypted/sdk";
import { HikvisionDoorbellAPI } from "./doorbell-api";
import type { HikvisionCameraDoorbell } from "./main";
import * as fs from 'fs/promises';
import { join } from 'path';
export class HikvisionEntrySensor extends ScryptedDeviceBase implements BinarySensor, Readme {
constructor(public camera: HikvisionCameraDoorbell, nativeId: string, public doorNumber: string = '1')
{
super (nativeId);
this.binaryState = this.binaryState || false;
}
async getReadmeMarkdown(): Promise<string>
{
const fileName = join (process.cwd(), 'ENTRY_SENSOR_README.md');
return fs.readFile (fileName, 'utf-8');
}
private getClient(): HikvisionDoorbellAPI {
return this.camera.getClient();
}
static deviceInterfaces: string[] = [
ScryptedInterface.BinarySensor,
ScryptedInterface.Readme
];
}

View File

@@ -1,144 +0,0 @@
import { PassThrough } from 'stream';
import { EventEmitter } from 'events';
/**
* HTTP Stream Switcher
* Receives data from single source and writes to single active PassThrough stream
* Supports seamless stream switching without stopping the data source
*/
export interface HttpSession {
sessionId: string;
stream: PassThrough;
putPromise: Promise<any>;
}
export class HttpStreamSwitcher extends EventEmitter
{
private currentStream?: PassThrough;
private currentSession?: HttpSession;
private byteCount: number = 0;
private streamSwitchCount: number = 0;
constructor (private console: Console) {
super();
}
/**
* Write data to current active stream
*/
write (data: Buffer): void
{
if (!this.currentStream) {
// No active stream, drop data
return;
}
try {
const canWrite = this.currentStream.write (data);
this.byteCount += data.length;
if (!canWrite) {
// Stream buffer is full, apply backpressure
this.console.warn ('Stream buffer full, applying backpressure');
}
} catch (error) {
this.console.error ('Error writing to stream:', error);
this.clearSession();
}
}
/**
* Switch to new HTTP session
* Old session will be ended gracefully
*/
switchSession (session: HttpSession): void
{
const oldSession = this.currentSession;
if (oldSession) {
this.console.debug (`Switching HTTP session ${oldSession.sessionId} -> ${session.sessionId} (${this.byteCount} bytes sent)`);
// End old stream gracefully
try {
oldSession.stream.end();
} catch (e) {
// Ignore errors on old stream
}
this.streamSwitchCount++;
} else {
this.console.debug (`Setting initial HTTP session ${session.sessionId}`);
}
this.currentSession = session;
this.currentStream = session.stream;
this.byteCount = 0;
// Setup error handler for new stream
session.stream.on ('error', (error) => {
this.console.error (`Stream error for session ${session.sessionId}:`, error);
if (this.currentSession === session) {
this.clearSession();
}
});
session.stream.on ('close', () => {
this.console.debug (`Stream closed for session ${session.sessionId}`);
if (this.currentSession === session) {
this.clearSession();
}
});
}
/**
* Clear current session without replacement
*/
private clearSession(): void
{
this.currentStream = undefined;
this.currentSession = undefined;
}
/**
* Get current session ID
*/
getCurrentSessionId(): string | undefined
{
return this.currentSession?.sessionId;
}
/**
* Check if given putPromise is current
*/
isCurrentPutPromise (putPromise: Promise<any>): boolean
{
return this.currentSession?.putPromise === putPromise;
}
/**
* Get current session
*/
getCurrentSession(): HttpSession | undefined
{
return this.currentSession;
}
/**
* Destroy switcher and cleanup
*/
destroy(): void
{
this.console.debug (`Destroying HTTP switcher (sent ${this.byteCount} bytes, ${this.streamSwitchCount} switches)`);
if (this.currentStream) {
try {
this.currentStream.end();
} catch (e) {
// Ignore
}
this.currentStream = undefined;
}
this.removeAllListeners();
}
}

View File

@@ -1,42 +1,24 @@
import { Lock, LockState, Readme, ScryptedDeviceBase, ScryptedInterface } from "@scrypted/sdk";
import sdk, { ScryptedDeviceBase, SettingValue, ScryptedInterface, Setting, Settings, Lock, LockState, Readme } from "@scrypted/sdk";
import { HikvisionDoorbellAPI } from "./doorbell-api";
import type { HikvisionCameraDoorbell } from "./main";
import { HikvisionDoorbellProvider } from "./main";
import * as fs from 'fs/promises';
import { join } from 'path';
export class HikvisionLock extends ScryptedDeviceBase implements Lock, Readme {
const { deviceManager } = sdk;
constructor (public camera: HikvisionCameraDoorbell, nativeId: string, public doorNumber: string = '1') {
export class HikvisionLock extends ScryptedDeviceBase implements Lock, Settings, Readme {
// timeout: NodeJS.Timeout;
private provider: HikvisionDoorbellProvider;
constructor(nativeId: string, provider: HikvisionDoorbellProvider) {
super (nativeId);
this.lockState = this.lockState || LockState.Unlocked;
// Initialize lock state by attempting to close the lock
this.initializeLockState();
}
/**
* Initialize lock state by attempting to close the lock.
* If close command succeeds, assume the lock is now locked.
* If it fails, assume the lock state remains as default.
*/
private async initializeLockState(): Promise<void>
{
try {
const capabilities = await this.getClient().getDoorControlCapabilities();
const command = capabilities.availableCommands.includes ('close') ? 'close' : 'resume';
// Attempt to close/lock the door
await this.getClient().controlDoor (this.doorNumber, command);
// If successful, set state to Locked
this.lockState = LockState.Locked;
this.camera.console.info (`Lock ${this.doorNumber} initialized as Locked (close command succeeded)`);
} catch (error) {
// If command fails, keep default state
this.camera.console.warn (`Lock ${this.doorNumber} initialization failed: ${error}. Using default state.`);
this.lockState = LockState.Unlocked;
}
this.lockState = this.lockState || LockState.Unlocked;
this.provider = provider;
// provider.updateLock (nativeId, this.name);
}
async getReadmeMarkdown(): Promise<string>
@@ -45,24 +27,52 @@ export class HikvisionLock extends ScryptedDeviceBase implements Lock, Readme {
return fs.readFile (fileName, 'utf-8');
}
async lock(): Promise<void>
{
const capabilities = await this.getClient().getDoorControlCapabilities();
const command = capabilities.availableCommands.includes ('close') ? 'close' : 'resume';
await this.getClient().controlDoor (this.doorNumber, command);
lock(): Promise<void> {
return this.getClient().closeDoor();
}
unlock(): Promise<void> {
return this.getClient().openDoor();
}
async unlock(): Promise<void>
{
await this.getClient().controlDoor (this.doorNumber, 'open');
async getSettings(): Promise<Setting[]> {
const cameraNativeId = this.storage.getItem (HikvisionDoorbellProvider.CAMERA_NATIVE_ID_KEY);
const state = deviceManager.getDeviceState (cameraNativeId);
return [
{
key: 'parentDevice',
title: 'Linked Doorbell Device Name',
description: 'The name of the associated doorbell plugin device (for information)',
value: state.id,
readonly: true,
type: 'device',
},
{
key: 'ip',
title: 'IP Address',
description: 'IP address of the doorbell device (for information)',
value: this.storage.getItem ('ip'),
readonly: true,
type: 'string',
}
]
}
async putSetting(key: string, value: SettingValue): Promise<void> {
this.storage.setItem(key, value.toString());
}
private getClient(): HikvisionDoorbellAPI {
return this.camera.getClient();
getClient(): HikvisionDoorbellAPI
{
const ip = this.storage.getItem ('ip');
const port = this.storage.getItem ('port');
const user = this.storage.getItem ('user');
const pass = this.storage.getItem ('pass');
return this.provider.createSharedClient(ip, port, user, pass, this.console, this.storage);
}
static deviceInterfaces: string[] = [
ScryptedInterface.Lock,
ScryptedInterface.Settings,
ScryptedInterface.Readme
];
}

File diff suppressed because it is too large Load Diff

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