Compare commits

..

71 Commits

Author SHA1 Message Date
Koushik Dutta
adcdd18497 server: add cluster worker address 2025-02-05 11:51:07 -08:00
Koushik Dutta
a95b77fe26 sdk: add cluster worker address 2025-02-05 11:49:26 -08:00
Koushik Dutta
3ff75f0fde postbeta 2025-02-05 08:01:46 -08:00
Koushik Dutta
eecd38d271 postrelease 2025-02-05 08:01:39 -08:00
Koushik Dutta
7128af20af postbeta 2025-02-04 19:23:00 -08:00
Koushik Dutta
c651c2164b server: fixup cluster worker hook 2025-02-04 19:22:49 -08:00
Koushik Dutta
6caafd73f5 postbeta 2025-02-04 19:19:38 -08:00
Koushik Dutta
05cb505783 server: hook cluster creation for electron 2025-02-04 19:19:30 -08:00
Koushik Dutta
07baddc9c3 sdk: update detection properties 2025-02-04 13:59:39 -08:00
Koushik Dutta
76ac260bf7 hikvision: fix unhandled rejection parsing camera object detection 2025-02-04 07:45:36 -08:00
Koushik Dutta
dfee7c6b09 Merge branch 'main' of github.com:koush/scrypted 2025-02-04 07:37:46 -08:00
Koushik Dutta
b3ce6a2af3 postbeta 2025-02-04 07:37:15 -08:00
Koushik Dutta
933c0cac0f postrelease 2025-02-04 07:37:02 -08:00
apocaliss92
1fb1334a00 snapshot: Sleeping cameras should not wake for periodic snapshots (#1718)
* Preserve battery on snapshots

* Don't force snapshot below 1 min

* Online interface changes

* Pr comments fix

* Interval removed

* Debounce restored

* Branching fixes

* Fix isBattery leftover

* Remove prebuffer check

* Remove comment

* Remove unused import

* Use Sleep interface

* Disable default prebuffer for Sleep devices

* Rollback default changes

* Unused import removed

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2025-02-03 10:55:58 -08:00
apocaliss92
cb45a00c25 reolink: Battery cams api fixes (#1719)
* Battery cams api fixes

* Update with new Sleep class

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2025-02-03 08:51:45 -08:00
Koushik Dutta
fec59af263 core: support cluster fork for terminal 2025-02-02 22:34:44 -08:00
Koushik Dutta
5d213a4c51 Merge branch 'main' of github.com:koush/scrypted 2025-02-02 22:33:28 -08:00
Koushik Dutta
d444c4ab7c sdk: update 2025-02-02 22:33:23 -08:00
Brett Jia
590f955ca9 core: terminalservice fork across cluster (#1721)
* core: terminalservice fork across cluster

* exit cluster fork on completion

* force terminate on errors

* make isClusterFork internal to prevent callers from killing core plugin

* implement forkInterface and share forks

* use correct native id

* use correct native id in primary device construction
2025-02-01 22:33:29 -08:00
Koushik Dutta
7df4bf2723 postbeta 2025-02-01 19:28:40 -08:00
Brett Jia
3416347a1f server/python: fix hash calculation (#1720) 2025-02-01 19:28:17 -08:00
Koushik Dutta
c669bb8902 snapshot: do not wake sleeping cameras for periodic snapshots 2025-02-01 10:51:46 -08:00
Koushik Dutta
ce5fd2d4fd Merge branch 'main' of github.com:koush/scrypted 2025-01-31 20:14:00 -08:00
Koushik Dutta
fa8a756059 sdk: critical alerts 2025-01-31 20:13:58 -08:00
apocaliss92
73b85e1cd0 homekit: Fix autoadd (#1716)
Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2025-01-31 14:49:12 -08:00
Koushik Dutta
1300073712 videoanalysis: publish audio sensor 2025-01-29 11:18:19 -08:00
Koushik Dutta
3e296e12a5 core: publish audio sensor ui 2025-01-29 11:11:19 -08:00
Koushik Dutta
bf98060a08 videoanalysis: fixup noisy startup 2025-01-29 11:02:13 -08:00
Koushik Dutta
d1cd380123 videoanalysis: initial implemnetation of audio sensor 2025-01-29 10:39:10 -08:00
Koushik Dutta
1a2aadfb52 rebroadcast: fix audio soft mute with adaptive bitrate and other downstream clients 2025-01-29 08:48:55 -08:00
Koushik Dutta
60c854a477 ha: publish beta 2025-01-27 13:08:45 -08:00
Koushik Dutta
0790b60122 postbeta 2025-01-27 13:03:14 -08:00
Koushik Dutta
a3caa09df4 server: fixup node modules search path on HA 2025-01-27 13:03:06 -08:00
Koushik Dutta
02ca8bd765 reolink: publish 2025-01-27 11:48:51 -08:00
apocaliss92
f9e1a94ab3 reolink: support additional trackmix (#1711)
* Add support for Trackmix Series W760

* settings restored

* Settings restored

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2025-01-27 11:45:52 -08:00
Koushik Dutta
dd0da26df3 ha: publish 2025-01-27 11:44:57 -08:00
Koushik Dutta
890f2e8daf postbeta 2025-01-26 22:26:56 -08:00
Koushik Dutta
2c8babe3ce postrelease 2025-01-26 22:26:48 -08:00
Koushik Dutta
8e31b5f970 homekit: fixup exports, publish 2025-01-24 10:52:26 -08:00
Nick Berardi
0873a72848 homekit: moved humidity settings to common and added characteristics to expose settings Home Assistant (#1699) 2025-01-24 10:51:28 -08:00
Koushik Dutta
145c66e1c8 doorbird: publish 2025-01-24 10:15:04 -08:00
r3dDoX
2b60b45113 doorbird: update underlying doorbird api package (#1705) 2025-01-24 10:12:51 -08:00
Koushik Dutta
6f63927e2f core: publish 2025-01-23 19:34:41 -08:00
Koushik Dutta
528eabdfc0 sdk: improve StorageSettings deviceFilter 2025-01-23 19:33:42 -08:00
Koushik Dutta
e201ea1fc1 doorbird: fix build 2025-01-23 13:11:31 -08:00
Koushik Dutta
7790810b86 server: cleanup launch.json 2025-01-23 09:23:13 -08:00
Koushik Dutta
e9ec78909b core: Fix missing buttons 2025-01-22 13:46:38 -08:00
Koushik Dutta
26245e17ca core: publish button support 2025-01-22 13:22:49 -08:00
Koushik Dutta
5d87a1b2dd sdk: PressButtons 2025-01-22 12:57:27 -08:00
Koushik Dutta
e1efde3868 postbeta 2025-01-22 12:00:35 -08:00
Koushik Dutta
525eb028c6 sdk: Buttons interface 2025-01-22 10:14:42 -08:00
Koushik Dutta
520c6a62a1 Merge branch 'main' of github.com:koush/scrypted 2025-01-21 13:48:43 -08:00
Koushik Dutta
6e6898ce33 common/rebroadcast: change rtp packet size to 32000 since that is what is supported on darwin for some reason 2025-01-21 13:48:38 -08:00
Koushik Dutta
1344c9112c server: fixup potential unhandled errors in sdk fork 2025-01-21 09:50:44 -08:00
Koushik Dutta
f2148ce26a hikvision: publish 2025-01-20 19:37:37 -08:00
Koushik Dutta
81b00195d6 Merge branch 'main' of github.com:koush/scrypted 2025-01-20 19:36:58 -08:00
Koushik Dutta
8f71778f05 core: publish 2025-01-20 19:36:54 -08:00
George Talusan
2e5b8d90aa hikvision: add ERI-K104-P4 to the list of NVRs that doesn't support channel cap checks (#1698) 2025-01-19 00:32:36 -08:00
Koushik Dutta
780182b94a fix npm-install.sh 2025-01-18 15:04:59 -08:00
Brett Jia
57480f7606 actions: add Linux arm64 runner to tests (#1696) 2025-01-17 16:50:35 -08:00
Koushik Dutta
1478684120 Update install-nvidia-container-toolkit.sh 2025-01-17 14:58:45 -08:00
Koushik Dutta
223b302bed core: publish new ui with lxc-docker update fix 2025-01-16 13:28:28 -08:00
Koushik Dutta
f56cef1b50 postbeta 2025-01-16 12:04:53 -08:00
Koushik Dutta
83bfa30d4b server: improve abi/server change detection 2025-01-16 12:04:43 -08:00
Koushik Dutta
611674af46 rebroadcast: publish 2025-01-16 08:24:01 -08:00
Koushik Dutta
941ea7f346 Update bug_report.md 2025-01-16 08:06:59 -08:00
Koushik Dutta
2b9c2956d6 Update bug_report.md 2025-01-16 08:05:28 -08:00
Koushik Dutta
266d5bf8a3 Update bug_report.md 2025-01-16 07:19:41 -08:00
Koushik Dutta
d0007fc7bb postbeta 2025-01-15 14:53:20 -08:00
Koushik Dutta
75f90b78eb postrelease 2025-01-15 14:53:20 -08:00
Simon Marty
1e8959413e Fix path in comment (#1694) 2025-01-15 14:43:37 -08:00
55 changed files with 929 additions and 316 deletions

View File

@@ -27,6 +27,11 @@ Created issues that do not meet these requirements or are improperly filled out
1. Delete this section and everything above it.
2. Fill out the sections below.
** Before You Submit**
- [ ] I checked that my issue isn't already filed: [Search open issues](https://github.com/koush/scrypted/issues).
- [ ] I checked the relevant camera/device and/or plugin `Log` in the `Management Console` for errors or warnings that may help identify and resolve the issue myself.
**Describe the bug**
A clear and concise description of what the bug is. The issue tracker is only for reporting bugs in Scrypted, for general support check Discord. Hardrware support requests or assistance requests will be immediately closed.
@@ -43,6 +48,9 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Include a `Log` from the device/camera in the management console (and if applicable, the affacted plugin, like HomeKit).
**Server (please complete the following information):**
- OS: [e.g. Ubuntu]
- Installation Method: [e.g. Desktop App, Docker, Local]

View File

@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runner: [ubuntu-latest, macos-14, macos-13, windows-latest]
runner: [ubuntu-latest, ubuntu-24.04-arm, macos-14, macos-13, windows-latest]
steps:
- name: Checkout repository

View File

@@ -247,7 +247,8 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
'tcp',
...(options?.vcodec || []),
...(options?.acodec || []),
'-pkt_size', '64000',
// linux and windows seem to support 64000 but darwin is 32000?
'-pkt_size', '32000',
'-f', 'rtsp',
],
findSyncFrame(streamChunks: StreamChunk[]) {

View File

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

@@ -55,7 +55,7 @@ services:
# Scrypted NVR Storage (Part 3 of 3)
# Modify to add the additional volume for Scrypted NVR.
# The following example would mount the /mnt/sda/video path on the host
# The following example would mount the /mnt/media/video path on the host
# to the /nvr path inside the docker container.
# - /mnt/media/video:/nvr

View File

@@ -36,9 +36,8 @@ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dea
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt -y update
# is there a way to get a versioned package automatically?
apt -y install nvidia-utils-560
apt -y install cuda-drivers
apt -y install nvidia-container-toolkit
nvidia-ctk runtime configure --runtime=docker
nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
systemctl restart docker

View File

@@ -27,7 +27,7 @@ echo "external/werift > npm install"
npm install
popd
for directory in rtsp amcrest onvif hikvision reolink unifi-protect webrtc homekit
for directory in rtsp ffmpeg-camera amcrest onvif hikvision reolink unifi-protect webrtc homekit
do
echo "$directory > npm install"
pushd plugins/$directory

View File

@@ -1,25 +1,25 @@
{
"name": "@scrypted/client",
"version": "1.3.9",
"version": "1.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.9",
"version": "1.3.10",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.3.92",
"engine.io-client": "^6.6.1",
"@scrypted/types": "^0.3.100",
"engine.io-client": "^6.6.2",
"follow-redirects": "^1.15.9",
"rimraf": "^6.0.1"
},
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^22.7.4",
"@types/node": "^22.10.7",
"@types/ws": "^8.5.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
"typescript": "^5.7.3"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -76,9 +76,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.92",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
"version": "0.3.100",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.100.tgz",
"integrity": "sha512-s/07QCxjMWqODgWj2UpLehzeo2cGFrCA9X8mvpG3owT/+q+sb8v/UUcw9TLHGSN6yIriNhceg3i9WO07kEIT6A==",
"license": "ISC"
},
"node_modules/@socket.io/component-emitter": {
@@ -120,12 +120,13 @@
}
},
"node_modules/@types/node": {
"version": "22.7.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
"version": "22.10.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
"undici-types": "~6.20.0"
}
},
"node_modules/@types/ws": {
@@ -272,9 +273,10 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/engine.io-client": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
@@ -623,10 +625,11 @@
}
},
"node_modules/typescript": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -636,10 +639,11 @@
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.3.9",
"version": "1.3.10",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -13,14 +13,14 @@
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^22.7.4",
"@types/node": "^22.10.7",
"@types/ws": "^8.5.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
"typescript": "^5.7.3"
},
"dependencies": {
"@scrypted/types": "^0.3.92",
"engine.io-client": "^6.6.1",
"@scrypted/types": "^0.3.100",
"engine.io-client": "^6.6.2",
"follow-redirects": "^1.15.9",
"rimraf": "^6.0.1"
}

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ import { MediaCore } from './media-core';
import { checkLegacyLxc, checkLxc } from './platform/lxc';
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
import { TerminalService, TerminalServiceNativeId, newTerminalService } from './terminal-service';
import { UsersCore, UsersNativeId } from './user';
import { ClusterCore, ClusterCoreNativeId } from './cluster';
@@ -140,7 +140,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
{
name: 'Terminal Service',
nativeId: TerminalServiceNativeId,
interfaces: [ScryptedInterface.StreamService, ScryptedInterface.TTY],
interfaces: [ScryptedInterface.StreamService, ScryptedInterface.TTY, ScryptedInterface.ClusterForkInterface],
type: ScryptedDeviceType.Builtin,
},
);
@@ -242,7 +242,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
if (nativeId === UsersNativeId)
return this.users ||= new UsersCore();
if (nativeId === TerminalServiceNativeId)
return this.terminalService ||= new TerminalService();
return this.terminalService ||= new TerminalService(TerminalServiceNativeId, false);
if (nativeId === ReplServiceNativeId)
return this.replService ||= new PluginSocketService(ReplServiceNativeId, 'repl');
if (nativeId === ConsoleServiceNativeId)
@@ -331,5 +331,6 @@ export async function fork() {
return {
tsCompile,
newScript,
newTerminalService,
}
}

View File

@@ -1,4 +1,4 @@
import sdk, { ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTYSettings } from "@scrypted/sdk";
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";
@@ -111,8 +111,11 @@ class NoninteractiveTerminal {
}
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer> {
constructor(nativeId?: ScryptedNativeId) {
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer>, ClusterForkInterface {
private forks: { [clusterWorkerId: string]: TerminalService } = {};
private forkClients: 0;
constructor(nativeId?: ScryptedNativeId, private isFork: boolean = false) {
super(nativeId);
}
@@ -134,6 +137,42 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
return extraPaths;
}
async forkInterface<StreamService>(forkInterface: ScryptedInterface, options?: ClusterForkInterfaceOptions): Promise<StreamService> {
if (forkInterface !== ScryptedInterface.StreamService) {
throw new Error('can only fork StreamService');
}
if (!options?.clusterWorkerId) {
throw new Error('clusterWorkerId required');
}
if (this.isFork) {
throw new Error('cannot fork a fork');
}
const clusterWorkerId = options.clusterWorkerId;
if (this.forks[clusterWorkerId]) {
return this.forks[clusterWorkerId] as StreamService;
}
const fork = sdk.fork<{
newTerminalService: typeof newTerminalService,
}>({ clusterWorkerId });
try {
const result = await fork.result;
const terminalService = await result.newTerminalService();
this.forks[clusterWorkerId] = terminalService;
fork.worker.on('exit', () => {
delete this.forks[clusterWorkerId];
});
return terminalService as StreamService;
}
catch (e) {
fork.worker.terminate();
throw e;
}
}
/*
* The input to this stream can send buffers for normal terminal data and strings
* for control messages. Control messages are JSON-formatted.
@@ -149,6 +188,19 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
const queue = createAsyncQueue<Buffer>();
const extraPaths = await this.getExtraPaths();
if (this.isFork) {
this.forkClients++;
}
queue.endPromise.then(() => {
if (this.isFork) {
this.forkClients--;
if (this.forkClients === 0) {
process.exit();
}
}
});
function registerChildListeners() {
cp.onExit(() => queue.end());
@@ -232,4 +284,8 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
return generator();
}
}
export async function newTerminalService(): Promise<TerminalService> {
return new TerminalService(TerminalServiceNativeId, true);
}

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.2",
"version": "0.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/doorbird",
"version": "0.0.2",
"version": "0.0.4",
"dependencies": {
"doorbird": "^2.1.2"
"doorbird": "2.6.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.15.11",
"@types/node": "^22.10.10",
"cross-env": "^7.0.3"
}
},
@@ -24,36 +24,41 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^5.0.1",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^5.3.3"
"typescript": "^5.5.3"
},
"devDependencies": {
"@types/node": "^20.10.8",
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.108",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
@@ -65,11 +70,9 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.11"
}
},
"node_modules/@scrypted/common": {
@@ -81,10 +84,14 @@
"link": true
},
"node_modules/@types/node": {
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
"dev": true
"version": "22.10.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
"integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
@@ -92,11 +99,12 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.4",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@@ -145,10 +153,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"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",
@@ -167,25 +176,29 @@
}
},
"node_modules/doorbird": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.1.2.tgz",
"integrity": "sha512-ivwwsS/nOslDnuLg3UB60Axo76w5LQuZ67mCPEeWFr5+HbGYRL7PCY3iLjWYaIakh5+IvZyFPHKR4yHAvAc1WQ==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.6.0.tgz",
"integrity": "sha512-HZBI5uFhwEVF8JFULQlpzXXvjSHmtQMJUNWfogq6vHe3kv7mCSmg0g/TDbeV5fVvisi8w7GxKD0/PpZCrtcGOg==",
"dependencies": {
"axios": "^1.2.1",
"axios": "^1.6.2",
"chacha-js": "^2.1.1",
"libsodium-wrappers-sumo": "^0.7.11"
"libsodium-wrappers-sumo": "^0.7.13"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@@ -225,16 +238,16 @@
"dev": true
},
"node_modules/libsodium-sumo": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz",
"integrity": "sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA=="
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz",
"integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw=="
},
"node_modules/libsodium-wrappers-sumo": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz",
"integrity": "sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ==",
"version": "0.7.15",
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz",
"integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==",
"dependencies": {
"libsodium-sumo": "^0.7.11"
"libsodium-sumo": "^0.7.15"
}
},
"node_modules/mime-db": {
@@ -307,6 +320,13 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.2",
"version": "0.0.4",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -33,12 +33,12 @@
]
},
"dependencies": {
"doorbird": "^2.1.2"
"doorbird": "2.6.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.15.11",
"@types/node": "^22.10.10",
"cross-env": "^7.0.3"
}
}

View File

@@ -1,13 +1,13 @@
import { listenZero } from '@scrypted/common/src/listen-cluster';
import sdk, { BinarySensor, Camera, DeviceProvider, DeviceCreator, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, MotionSensor } from '@scrypted/sdk';
import child_process, { ChildProcess } from 'child_process';
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
import net from 'net';
import { randomBytes } from 'crypto';
import { PassThrough, Readable } from "stream";
import { readLength } from "@scrypted/common/src/read-stream";
import { authHttpFetch } from "@scrypted/common/src/http-auth-fetch";
import { ApiRingEvent, ApiMotionEvent, DoorbirdAPI } from "./doorbird-api";
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";
const { deviceManager, mediaManager } = sdk;
@@ -384,7 +384,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
server.close();
}, 30000);
const port = await listenZero(server);
const port = await listenZero(server, '127.0.0.1');
return port;
}

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.160",
"version": "0.0.162",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/hikvision",
"version": "0.0.160",
"version": "0.0.162",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.160",
"version": "0.0.162",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -116,11 +116,15 @@ export class HikvisionCameraAPI implements HikvisionAPI {
}
async checkIsOldModel() {
// The old Hikvision DS-7608NI-E2 doesn't support channel capability checks, and the requests cause errors
// The old Hikvision NVRs don't support channel capability checks, and the requests cause errors
const oldModels = [
/DS-76098NI-E2/,
/ERI-K104-P4/
];
const model = await this.checkDeviceModel();
if (!model)
return;
return !!model?.match(/DS-7608NI-E2/);
return !!oldModels.find(oldModel => model?.match(oldModel));
}
async checkStreamSetup(channel: string, isOld: boolean): Promise<HikvisionCameraStreamSetup> {

View File

@@ -161,14 +161,9 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
const now = Date.now();
let detections: ObjectDetectionResult[] = xml.EventNotificationAlert?.DetectionRegionList?.map(region => {
const { DetectionRegionEntry } = region;
const dre = DetectionRegionEntry[0];
if (!DetectionRegionEntry)
const name = region?.DetectionRegionEntry?.[0]?.detectionTarget?.name;
if (!name)
return;
const { detectionTarget } = dre;
// const { TargetRect } = dre;
// const { X, Y, width, height } = TargetRect[0];
const [name] = detectionTarget;
return {
score: 1,
className: detectionMap[name] || name,

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/homekit",
"version": "1.2.62",
"version": "1.2.63",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/homekit",
"version": "1.2.62",
"version": "1.2.63",
"dependencies": {
"@koush/werift-src": "file:../../external/werift",
"check-disk-space": "^3.4.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/homekit",
"version": "1.2.62",
"version": "1.2.63",
"description": "HomeKit Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",

View File

@@ -216,6 +216,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
throw Error(`error in device reordering, expected ${uniqueDeviceIds.size} unique devices but only got ${uniqueReorderedIds.size} entries!`);
}
const autoAdd = this.storageSettings.values.autoAdd ?? true;
for (const id of reorderedDeviceIds) {
const device = systemManager.getDeviceById<Online>(id);
const supportedType = supportedTypes[device.type];
@@ -224,8 +225,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
try {
const mixins = (device.mixins || []).slice();
const autoAdd = this.storageSettings.values.autoAdd ?? true;
if (!mixins.includes(this.id) && autoAdd) {
if (!mixins.includes(this.id)) {
// don't sync this by default, as it's solely for automations
if (device.type === ScryptedDeviceType.Notifier)
continue;
@@ -235,6 +235,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
continue;
if (defaultIncluded[device.id] === includeToken)
continue;
if (!autoAdd)
continue;
mixins.push(this.id);
await device.setMixins(mixins);
defaultIncluded[device.id] = includeToken;

View File

@@ -1,4 +1,4 @@
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, HumiditySettingStatus, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
import { bindCharacteristic } from "../common";
import { Accessory, Characteristic, CharacteristicEventTypes, Service, uuid } from '../hap';
import type { HomeKitPlugin } from "../main";
@@ -96,24 +96,161 @@ export function addCarbonDioxideSensor(device: ScryptedDevice & CO2Sensor, acces
return co2Service;
}
export function addFan(device: ScryptedDevice & Fan & OnOff, accessory: Accessory): Service {
if (!device.interfaces.includes(ScryptedInterface.OnOff) && !device.interfaces.includes(ScryptedInterface.Fan))
function commonHumidifierDehumidifier(mode: HumidityMode, subtype: string, name: string, device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
function currentState(mode: HumidityMode) {
switch(mode) {
case HumidityMode.Humidify:
return Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING;
case HumidityMode.Dehumidify:
return Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING;
case HumidityMode.Off:
return Characteristic.CurrentHumidifierDehumidifierState.INACTIVE;
default:
return Characteristic.CurrentHumidifierDehumidifierState.IDLE;
}
}
function targetState(mode: HumidityMode) {
switch(mode) {
case HumidityMode.Humidify:
return Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER;
case HumidityMode.Dehumidify:
return Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER;
default:
return Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER;
}
}
const service = accessory.addService(Service.HumidifierDehumidifier, name, subtype);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.Active,
() => {
if (!device.humiditySetting?.mode)
return false;
if (device.humiditySetting.mode === mode)
return true;
if (device.humiditySetting.mode === HumidityMode.Auto)
return true;
return false;
});
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
mode: value ? mode : HumidityMode.Off
});
});
bindCharacteristic(device, ScryptedInterface.HumiditySensor, service, Characteristic.CurrentRelativeHumidity,
() => device.humidity);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.CurrentHumidifierDehumidifierState,
() => currentState(device.humiditySetting?.activeMode));
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetHumidifierDehumidifierState,
() => targetState(device.humiditySetting?.mode));
service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
mode: value === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
? HumidityMode.Humidify
: value === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
? HumidityMode.Dehumidify
: HumidityMode.Auto
});
});
function targetHumidity(setting: HumiditySettingStatus) {
if (!setting)
return 0;
if (setting?.availableModes.includes(HumidityMode.Humidify)
&& setting?.availableModes.includes(HumidityMode.Dehumidify)) {
if (setting?.activeMode === HumidityMode.Humidify)
return setting?.humidifierSetpoint;
if (setting?.activeMode === HumidityMode.Dehumidify)
return setting?.dehumidifierSetpoint;
return 0;
}
if (setting?.availableModes.includes(HumidityMode.Humidify))
return setting?.humidifierSetpoint;
if (setting?.availableModes.includes(HumidityMode.Dehumidify))
return setting?.dehumidifierSetpoint;
return 0;
}
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetRelativeHumidity,
() => targetHumidity(device.humiditySetting));
return service;
}
function addHumidifier(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
var service = commonHumidifierDehumidifier(HumidityMode.Humidify, "humidifier", device.name + " Humidifier", device, accessory);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.RelativeHumidityHumidifierThreshold,
() => device.humiditySetting?.humidifierSetpoint);
service.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
humidifierSetpoint: value as number,
});
});
return service;
}
function addDehumidifer(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
var service = commonHumidifierDehumidifier(HumidityMode.Dehumidify, "dehumidifier", device.name + " Dehumidifier", device, accessory);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.RelativeHumidityDehumidifierThreshold,
() => device.humiditySetting?.dehumidifierSetpoint);
service.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
dehumidifierSetpoint: value as number,
});
});
return service;
}
export function addHumiditySetting(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
if (!device.interfaces.includes(ScryptedInterface.HumiditySetting) && !device.interfaces.includes(ScryptedInterface.HumiditySensor))
return undefined;
var service;
if (device.humiditySetting?.availableModes.includes(HumidityMode.Humidify)) {
service = addHumidifier(device, accessory);
}
if (device.humiditySetting?.availableModes.includes(HumidityMode.Dehumidify)) {
service = addDehumidifer(device, accessory);
}
return service;
}
export function addFan(device: ScryptedDevice & Fan, accessory: Accessory): Service {
if (!device.interfaces.includes(ScryptedInterface.Fan))
return undefined;
const service = accessory.addService(Service.Fanv2, device.name);
if (device.interfaces.includes(ScryptedInterface.OnOff)) {
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.Active,
() => !!device.on);
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.Active,
() => device.fan?.active);
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
if (value)
device.turnOn();
else
device.turnOff();
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setFan({
mode: value ? FanMode.Auto : FanMode.Manual,
});
}
});
if (device.fan?.counterClockwise !== undefined) {
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.RotationDirection,

View File

@@ -1,7 +1,7 @@
import { Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, AirQualitySensor, AirQuality, PM10Sensor, PM25Sensor, VOCSensor, NOXSensor, CO2Sensor } from '@scrypted/sdk';
import { Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, AirQualitySensor, AirQuality, PM10Sensor, PM25Sensor, VOCSensor, NOXSensor, CO2Sensor, HumiditySettingStatus } from '@scrypted/sdk';
import { addSupportedType, bindCharacteristic, DummyDevice, } from '../common';
import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, Service } from '../hap';
import { addAirQualitySensor, addCarbonDioxideSensor, addFan, makeAccessory } from './common';
import { addAirQualitySensor, addCarbonDioxideSensor, addFan, addHumiditySetting, makeAccessory } from './common';
import type { HomeKitPlugin } from "../main";
addSupportedType({
@@ -178,72 +178,60 @@ addSupportedType({
() => device.humidity || 0);
}
if (device.interfaces.includes(ScryptedInterface.HumiditySetting) && device.interfaces.includes(ScryptedInterface.HumiditySensor)) {
const humidityService = accessory.addService(Service.HumidifierDehumidifier);
// add fan state to thermostat service even though it is not required or optional,
// in order to expose to Home Assistant HomeKit Controller under their climate entity
if (device.interfaces.includes(ScryptedInterface.Fan)) {
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.TargetFanState,
() => device.fan?.mode === FanMode.Manual
? Characteristic.TargetFanState.MANUAL
: Characteristic.TargetFanState.AUTO);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.Active,
() => {
if (!device.humiditySetting?.mode)
return false;
if (device.humiditySetting.mode === HumidityMode.Off)
return false;
return true;
});
humidityService.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
service.getCharacteristic(Characteristic.TargetFanState).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
mode: value ? HumidityMode.Auto : HumidityMode.Off
device.setFan({
mode: value === Characteristic.TargetFanState.MANUAL ? FanMode.Manual : FanMode.Auto,
});
});
bindCharacteristic(device, ScryptedInterface.HumiditySensor, humidityService, Characteristic.CurrentRelativeHumidity,
() => device.humidity || 0);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.CurrentHumidifierDehumidifierState,
() => !device.humiditySetting?.activeMode
? Characteristic.CurrentHumidifierDehumidifierState.INACTIVE
: device.humiditySetting.activeMode === HumidityMode.Dehumidify
? Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING
: device.humiditySetting.activeMode === HumidityMode.Humidify
? Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING
: Characteristic.CurrentHumidifierDehumidifierState.IDLE);
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.TargetHumidifierDehumidifierState,
() => !device.humiditySetting?.mode || device.humiditySetting?.mode === HumidityMode.Auto
? Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER
: device.humiditySetting?.mode === HumidityMode.Dehumidify
? Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
: Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER);
humidityService.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
mode: value === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
? HumidityMode.Humidify
: value === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
? HumidityMode.Dehumidify
: HumidityMode.Auto
});
});
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.RelativeHumidityHumidifierThreshold,
() => device.humiditySetting?.humidifierSetpoint || 0);
humidityService.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
humidifierSetpoint: value as number,
});
});
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.RelativeHumidityDehumidifierThreshold,
() => device.humiditySetting?.dehumidifierSetpoint || 0);
humidityService.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
callback();
device.setHumidity({
dehumidifierSetpoint: value as number,
});
});
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.CurrentFanState,
() => !device.fan?.active
? Characteristic.CurrentFanState.INACTIVE
: !device.fan.speed
? Characteristic.CurrentFanState.IDLE
: Characteristic.CurrentFanState.BLOWING_AIR);
}
// add relataive target humidity to thermostat service even though it is not required or optional,
// in order to expose to Home Assistant HomeKit Controller under their climate entity
if (device.interfaces.includes(ScryptedInterface.HumiditySetting)) {
function targetHumidity(setting: HumiditySettingStatus) {
if (!setting)
return 0;
if (setting?.availableModes.includes(HumidityMode.Humidify)
&& setting?.availableModes.includes(HumidityMode.Dehumidify)) {
if (setting?.activeMode === HumidityMode.Humidify)
return setting?.humidifierSetpoint;
if (setting?.activeMode === HumidityMode.Dehumidify)
return setting?.dehumidifierSetpoint;
return 0;
}
if (setting?.availableModes.includes(HumidityMode.Humidify))
return setting?.humidifierSetpoint;
if (setting?.availableModes.includes(HumidityMode.Dehumidify))
return setting?.dehumidifierSetpoint;
return 0;
}
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetRelativeHumidity,
() => targetHumidity(device.humiditySetting));
}
addHumiditySetting(device, accessory);
addFan(device, accessory);
addAirQualitySensor(device, accessory);
addCarbonDioxideSensor(device, accessory);

View File

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

View File

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

View File

@@ -0,0 +1,173 @@
import sdk, { AudioSensor, FFmpegInput, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SettingValue, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/sdk/settings-mixin";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { RtpPacket } from "../../../external/werift/packages/rtp/src/rtp/rtp";
import { sleep } from "@scrypted/common/src/sleep";
function pcmU8ToDb(payload: Uint8Array): number {
let sum = 0;
const count = payload.length;
if (count === 0) return 0; // Treat empty input as silence (0 dB)
for (let i = 0; i < count; i++) {
const sample = payload[i] - 128; // Convert to signed range (-128 to 127)
sum += sample * sample;
}
const rms = Math.sqrt(sum / count);
const minRMS = 1.0; // Define a minimum reference level to avoid log(0)
if (rms < minRMS) return 0; // Silence is 0 dB
const db = 20 * Math.log10(rms / minRMS); // Scale against the minimum audible level
return db;
}
class FFmpegAudioDetectionMixin extends SettingsMixinDeviceBase<AudioSensor> implements AudioSensor {
storageSettings = new StorageSettings(this, {
decibelThreshold: {
title: 'Decibel Threshold',
type: 'number',
description: 'The decibel level at which to trigger an event.',
defaultValue: 20,
},
audioTimeout: {
title: 'Audio Timeout',
type: 'number',
description: 'The number of seconds to wait after the last audio event before resetting the audio sensor.',
defaultValue: 10,
},
});
ensureInterval: NodeJS.Timeout;
forwarder: ReturnType<typeof startRtpForwarderProcess>;
audioResetInterval: NodeJS.Timeout;
constructor(options: SettingsMixinDeviceOptions<AudioSensor>) {
super(options);
this.ensureInterval = setInterval(() => this.ensureAudioSensor(), 60000);
this.ensureAudioSensor();
};
ensureAudioSensor() {
if (!this.ensureInterval)
return;
if (this.forwarder)
return;
this.audioDetected = false;
clearInterval(this.audioResetInterval);
this.audioResetInterval = undefined;
const fp = this.ensureAudioSensorInternal();
this.forwarder = fp;
fp.catch(() => {
if (this.forwarder === fp)
this.forwarder = undefined;
});
this.forwarder.then(f => {
f.killPromise.then(() => {
if (this.forwarder === fp)
this.forwarder = undefined;
});
})
}
async ensureAudioSensorInternal() {
await sleep(5000);
if (!this.forwarder)
throw new Error('released/killed');
const realDevice = sdk.systemManager.getDeviceById<VideoCamera>(this.id);
const mo = await realDevice.getVideoStream({
video: null,
audio: {},
});
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(mo, ScryptedMimeTypes.FFmpegInput);
let lastAudio = 0;
const forwarder = await startRtpForwarderProcess(this.console, ffmpegInput, {
video: null,
audio: {
codecCopy: 'pcm_u8',
encoderArguments: [
'-acodec', 'pcm_u8',
'-ac', '1',
'-ar', '8000',
],
onRtp: rtp => {
const now = Date.now();
// if this.audioDetected is true skip the processing unless the lastAudio time is halfway through the interval
if (this.audioDetected && now - lastAudio < this.storageSettings.values.audioTimeout * 500)
return;
const packet = RtpPacket.deSerialize(rtp);
const decibels = pcmU8ToDb(packet.payload);
if (decibels < this.storageSettings.values.decibelThreshold)
return;
this.audioDetected = true;
lastAudio = now;
},
}
});
this.audioResetInterval = setInterval(() => {
if (!this.audioDetected)
return;
if (Date.now() - lastAudio < this.storageSettings.values.audioTimeout * 1000)
return;
this.audioDetected = false;
}, this.storageSettings.values.audioTimeout * 1000);
return forwarder;
}
async getMixinSettings() {
return this.storageSettings.getSettings();
}
putMixinSetting(key: string, value: SettingValue) {
return this.storageSettings.putSetting(key, value);
}
async release() {
this.forwarder?.then(f => f.kill());
this.forwarder = undefined;
clearInterval(this.ensureInterval);
this.ensureInterval = undefined;
clearTimeout(this.audioResetInterval);
this.audioResetInterval = undefined;
}
}
export class FFmpegAudioDetectionMixinProvider extends ScryptedDeviceBase implements MixinProvider {
async canMixin(type: ScryptedDeviceType, interfaces: string[]) {
if (type !== ScryptedDeviceType.Camera && type !== ScryptedDeviceType.Doorbell)
return;
if (!interfaces.includes(ScryptedInterface.VideoCamera))
return;
return [ScryptedInterface.AudioSensor, ScryptedInterface.Settings];
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: WritableDeviceState): Promise<any> {
return new FFmpegAudioDetectionMixin({
group: 'Audio Detection',
groupKey: 'audio-detection',
mixinDevice,
mixinDeviceInterfaces,
mixinDeviceState,
mixinProviderNativeId: this.nativeId,
});
}
async releaseMixin(id: string, mixinDevice: any) {
await (mixinDevice as FFmpegAudioDetectionMixin)?.release();
}
}

View File

@@ -10,6 +10,7 @@ import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonInt
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
import { getAllDevices, safeParseJson } from './util';
import { FFmpegAudioDetectionMixinProvider } from './ffmpeg-audiosensor';
const { systemManager } = sdk;
@@ -1056,7 +1057,16 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
ScryptedInterface.VideoFrameGenerator,
],
nativeId: 'ffmpeg',
})
});
sdk.deviceManager.onDeviceDiscovered({
name: 'FFmpeg Audio Detection',
type: ScryptedDeviceType.Builtin,
interfaces: [
ScryptedInterface.MixinProvider,
],
nativeId: 'ffmpeg-audio-detection',
});
});
// on an interval check to see if system load allows squelched detectors to start up.
@@ -1195,6 +1205,8 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
let ret: any;
if (nativeId === 'ffmpeg')
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
if (nativeId === 'ffmpeg-audio-detection')
ret = this.devices.get(nativeId) || new FFmpegAudioDetectionMixinProvider('ffmpeg-audio-detection');
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.43",
"version": "0.10.46",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.43",
"version": "0.10.46",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.43",
"version": "0.10.46",
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -63,8 +63,6 @@ class PrebufferSession {
usingScryptedParser = false;
usingScryptedUdpParser = false;
audioDisabled = false;
mixinDevice: VideoCamera;
console: Console;
storage: Storage;
@@ -507,20 +505,15 @@ class PrebufferSession {
catch (e) {
}
// audio codecs are determined by probing the camera to see what it reports.
// if the camera does not specify a codec, rebroadcast will force audio off
// to determine the codec without causing a parse failure.
// camera may explicity request that its audio stream be muted via a null.
// respect that setting.
const audioSoftMuted = mso?.audio === null;
const advertisedAudioCodec = mso?.audio?.codec;
const advertisedAudioCodec = !audioSoftMuted && mso?.audio?.codec;
let detectedAudioCodec = this.storage.getItem(this.lastDetectedAudioCodecKey) || undefined;
if (detectedAudioCodec === 'null')
detectedAudioCodec = null;
this.audioDisabled = false;
const rbo: ParserOptions<PrebufferParsers> = {
console: this.console,
timeout: 60000,
@@ -604,7 +597,6 @@ class PrebufferSession {
if (audioSoftMuted) {
// no audio? explicitly disable it.
acodec = ['-an'];
this.audioDisabled = true;
}
else {
acodec = [
@@ -628,9 +620,6 @@ class PrebufferSession {
const extraInputArguments = userInputArguments || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
const extraOutputArguments = this.storage.getItem(this.ffmpegOutputArgumentsKey) || '';
ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
// ehh this seems to cause issues with frames being updated in the webassembly decoder..?
// if (!userInputArguments && (ffmpegInput.container === 'rtmp' || ffmpegInput.url?.startsWith('rtmp:')))
// ffmpegInput.inputArguments.unshift('-use_wallclock_as_timestamps', '1');
if (ffmpegInput.h264EncoderArguments?.length) {
vcodec = [...ffmpegInput.h264EncoderArguments];
@@ -1024,6 +1013,9 @@ class PrebufferSession {
mediaStreamOptions.video.h264Info = this.getLastH264Probe();
}
if (this.mixin.streamSettings.storageSettings.values.noAudio)
mediaStreamOptions.audio = null;
let socketPromise: Promise<Duplex>;
let url: string;
let urls: string[];
@@ -1134,10 +1126,7 @@ class PrebufferSession {
mediaStreamOptions.prebuffer = requestedPrebuffer;
if (this.audioDisabled) {
mediaStreamOptions.audio = null;
}
else if (audioSection) {
if (audioSection) {
mediaStreamOptions.audio ||= {};
mediaStreamOptions.audio.codec ||= audioSection.rtpmap.codec;
mediaStreamOptions.audio.sampleRate ||= audioSection.rtpmap.clock;

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/reolink",
"version": "0.0.100",
"version": "0.0.104",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/reolink",
"version": "0.0.100",
"version": "0.0.104",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -35,22 +35,29 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.67",
"version": "0.3.108",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.24.7",
"adm-zip": "^0.5.14",
"axios": "^1.7.3",
"babel-loader": "^9.1.3",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -63,11 +70,9 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^22.1.0",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.5"
"typedoc": "^0.26.11"
}
},
"../onvif/onvif": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/reolink",
"version": "0.0.100",
"version": "0.0.104",
"description": "Reolink Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,5 +1,5 @@
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { Brightness, Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
import sdk, { Sleep, Brightness, Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { EventEmitter } from "stream";
import { createRtspMediaStreamOptions, Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
@@ -78,7 +78,7 @@ class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brigh
}
}
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom {
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom, Sleep {
client: ReolinkCameraClient;
clientWithToken: ReolinkCameraClient;
onvifClient: OnvifCameraAPI;
@@ -362,7 +362,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
if (this.hasSiren() || this.hasFloodlight())
interfaces.push(ScryptedInterface.DeviceProvider);
if (this.hasBattery()) {
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Online);
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Sleep);
this.startBatteryCheckInterval();
}
@@ -378,14 +378,20 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
const api = this.getClientWithToken();
try {
const { batteryPercent, sleep } = await api.getBatteryInfo();
const { batteryPercent, sleeping } = await api.getBatteryInfo();
this.batteryLevel = batteryPercent;
this.online = !sleep;
if (sleeping !== this.sleeping) {
this.sleeping = sleeping;
}
if (batteryPercent !== this.batteryLevel) {
this.batteryLevel = batteryPercent;
}
}
catch (e) {
this.console.log('Error in getting battery info', e);
}
}, 1000 * 60 * 30);
}, 1000 * 10);
}
async reboot() {
@@ -557,10 +563,19 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
(async () => {
while (!killed) {
try {
const { value, data } = await client.getMotionState();
if (value)
triggerMotion();
ret.emit('data', JSON.stringify(data));
// Battey cameras do not have AI state, they just send events in case of PIR sensor triggered
// which equals a motion detected
if (this.hasBattery()) {
const { value, data } = await client.getPidActive();
if (value)
triggerMotion();
ret.emit('data', JSON.stringify(data));
} else {
const { value, data } = await client.getMotionState();
if (value)
triggerMotion();
ret.emit('data', JSON.stringify(data));
}
}
catch (e) {
ret.emit('error', e);
@@ -725,7 +740,8 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
[
"Reolink TrackMix PoE",
"Reolink TrackMix WiFi",
"RLC-81MA"
"RLC-81MA",
"Trackmix Series W760"
].includes(deviceInfo?.model)) {
streams.push({
name: '',

View File

@@ -492,7 +492,35 @@ export class ReolinkCameraClient {
return {
batteryPercent: batteryInfoEntry?.batteryPercent,
sleep: channelStatusEntry?.sleep === 1,
sleeping: channelStatusEntry?.sleep === 1,
}
}
async getPidActive() {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: "GetEvents",
action: 0,
param: { channel: this.channelId }
},
];
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
const error = response.body?.find(elem => elem.error)?.error;
if (error) {
this.console.error('error during call to getEvents', error);
}
return {
value: !!response.body?.[0]?.value?.ai?.other?.alarm_state,
data: response.body,
};
}
}

View File

@@ -2,7 +2,7 @@ import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-p
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { RefreshPromise, TimeoutError, createMapPromiseDebouncer, singletonPromise, timeoutPromise } from "@scrypted/common/src/promise-utils";
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, Sleep, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import https from 'https';
import os from 'os';
@@ -127,9 +127,8 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
return this.console;
}
async takePictureInternal(options?: RequestPictureOptions): Promise<Buffer> {
this.debugConsole?.log("Picture requested from camera", options);
const eventSnapshot = options?.reason === 'event';
async takePictureInternal(id: string, eventSnapshot: boolean): Promise<Buffer> {
this.debugConsole?.log("Picture requested from camera", { id, eventSnapshot });
const { snapshotsFromPrebuffer } = this.storageSettings.values;
let usePrebufferSnapshots: boolean;
switch (snapshotsFromPrebuffer) {
@@ -162,11 +161,12 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
}
}
const realDevice = systemManager.getDeviceById<VideoCamera & Sleep>(this.id);
let takePrebufferPicture: () => Promise<Buffer>;
const preparePrebufferSnapshot = async () => {
if (takePrebufferPicture)
return takePrebufferPicture;
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
const msos = await realDevice.getVideoStreamOptions();
let prebufferChannel = msos?.find(mso => mso.prebuffer);
if (prebufferChannel || !this.lastAvailablePicture) {
@@ -250,7 +250,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
let takePictureOptions: RequestPictureOptions;
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
if (!id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
try {
const psos = await this.getPictureOptions();
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
@@ -262,6 +262,9 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
}
}
try {
// consider waking the camera if
if (!eventSnapshot && this.mixinDeviceInterfaces.includes(ScryptedInterface.Sleep) && realDevice.sleeping)
throw new Error('Not waking sleeping camera for periodic snapshot.');
return await this.mixinDevice.takePicture(takePictureOptions).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
}
catch (e) {
@@ -289,7 +292,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
event: options?.reason === 'event',
}, eventSnapshot ? 0 : 4000, async () => {
const snapshotTimer = Date.now();
let picture = await this.takePictureInternal();
let picture = await this.takePictureInternal(undefined, eventSnapshot);
picture = await this.cropAndScale(picture);
this.clearCachedPictures();
const pictureTime = Date.now();

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.3.106",
"version": "0.3.112",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.3.106",
"version": "0.3.112",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.26.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.3.106",
"version": "0.3.112",
"description": "",
"main": "dist/src/index.js",
"exports": {

View File

@@ -1,4 +1,4 @@
import sdk, { ScryptedInterface, Setting, Settings, SettingValue } from ".";
import sdk, { ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from ".";
const { systemManager } = sdk;
@@ -57,9 +57,9 @@ function parseValue(value: string | null | undefined, setting: StorageSetting, r
return value || readDefaultValue();
}
export type HideFunction = (device: any) => boolean;
export interface StorageSetting extends Omit<Setting, 'deviceFilter'> {
deviceFilter?: string | ((test: { id: string, deviceInterface: string, interfaces: string[], type: ScryptedDeviceType, ScryptedDeviceType: typeof ScryptedDeviceType, ScryptedInterface: typeof ScryptedInterface }) => boolean);
export interface StorageSetting extends Setting {
defaultValue?: any;
persistedDefaultValue?: any;
onPut?: (oldValue: any, newValue: any) => void;
@@ -140,7 +140,9 @@ export class StorageSettings<T extends string> implements Settings {
continue;
s.key = key;
s.value = this.getItemInternal(key as T, s, true);
ret.push(s);
if (typeof s.deviceFilter === 'function')
s.deviceFilter = s.deviceFilter.toString();
ret.push(s as Setting);
delete s.onPut;
delete s.onGet;
delete s.mapPut;

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.3.98",
"version": "0.3.104",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.3.98",
"version": "0.3.104",
"license": "ISC"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/types",
"version": "0.3.98",
"version": "0.3.104",
"description": "",
"main": "dist/index.js",
"author": "",

View File

@@ -117,6 +117,7 @@ class ScryptedInterface(str, Enum):
BinarySensor = "BinarySensor"
Brightness = "Brightness"
BufferConverter = "BufferConverter"
Buttons = "Buttons"
Camera = "Camera"
Charger = "Charger"
ClusterForkInterface = "ClusterForkInterface"
@@ -166,6 +167,7 @@ class ScryptedInterface(str, Enum):
PM25Sensor = "PM25Sensor"
PositionSensor = "PositionSensor"
PowerSensor = "PowerSensor"
PressButtons = "PressButtons"
Program = "Program"
PushHandler = "PushHandler"
Readme = "Readme"
@@ -184,6 +186,7 @@ class ScryptedInterface(str, Enum):
ScryptedUser = "ScryptedUser"
SecuritySystem = "SecuritySystem"
Settings = "Settings"
Sleep = "Sleep"
StartStop = "StartStop"
StreamService = "StreamService"
TamperSensor = "TamperSensor"
@@ -199,6 +202,7 @@ class ScryptedInterface(str, Enum):
VideoFrameGenerator = "VideoFrameGenerator"
VideoRecorder = "VideoRecorder"
VideoRecorderManagement = "VideoRecorderManagement"
VideoTextOverlay = "VideoTextOverlay"
VOCSensor = "VOCSensor"
class ScryptedMimeTypes(str, Enum):
@@ -286,11 +290,6 @@ class ClipPath(TypedDict):
pass
class Point(TypedDict):
pass
class AudioStreamOptions(TypedDict):
bitrate: float
@@ -322,8 +321,8 @@ class ObjectDetectionResult(TypedDict):
boundingBox: tuple[float, float, float, float] # x, y, width, height
className: str # The detection class of the object.
clipPaths: list[ClipPath] # The detection clip paths that outlines various features or segments, like traced facial features.
clipped: bool # Flag that indicates whether the detection was clipped by the detection input and may not be a full bounding box.
cost: float # The certainty that this is correct tracked object.
descriptor: str # A base64 encoded Float32Array that represents the vector descriptor of the detection. Can be used to compute euclidian distance to determine similarity.
embedding: str # Base64 encoded embedding float32 vector.
history: ObjectDetectionHistory
id: str # The id of the tracked object.
@@ -468,6 +467,7 @@ class ClusterForkInterfaceOptions(TypedDict):
class ClusterWorker(TypedDict):
address: str
forks: list[ClusterFork]
id: str
labels: list[str]
@@ -667,6 +667,7 @@ class NotifierOptions(TypedDict):
badge: str
body: str
bodyWithSubtitle: str
critical: bool
data: Any
dir: NotificationDirection
image: str
@@ -946,12 +947,17 @@ class MediaConverterTypes(TypedDict):
pass
class Point(TypedDict):
pass
class TamperState(TypedDict):
pass
TYPES_VERSION = "0.3.98"
TYPES_VERSION = "0.3.104"
class AirPurifier:
@@ -1006,6 +1012,10 @@ class BufferConverter:
pass
class Buttons:
buttons: list[str]
class Camera:
"""Camera devices can take still photos."""
@@ -1376,6 +1386,12 @@ class PowerSensor:
powerDetected: bool
class PressButtons:
async def pressButton(self, button: str) -> None:
pass
class Program:
async def run(self, variables: Any = None) -> Any:
@@ -1523,6 +1539,10 @@ class Settings:
pass
class Sleep:
sleeping: bool
class StartStop:
"""StartStop represents a device that can be started, stopped, and possibly paused and resumed. Typically vacuum cleaners or washers."""
@@ -1649,6 +1669,12 @@ class VideoRecorderManagement:
pass
class VideoTextOverlay:
fontSize: float
origin: Point # The top left position of the overlay in the image, normalized to 0-1.
text: str
class VOCSensor:
vocDensity: float
@@ -1864,6 +1890,7 @@ class ScryptedInterfaceProperty(str, Enum):
colorTemperature = "colorTemperature"
rgb = "rgb"
hsv = "hsv"
buttons = "buttons"
running = "running"
paused = "paused"
docked = "docked"
@@ -1872,6 +1899,9 @@ class ScryptedInterfaceProperty(str, Enum):
temperatureUnit = "temperatureUnit"
humidity = "humidity"
audioVolumes = "audioVolumes"
fontSize = "fontSize"
origin = "origin"
text = "text"
recordingActive = "recordingActive"
ptzCapabilities = "ptzCapabilities"
lockState = "lockState"
@@ -1884,6 +1914,7 @@ class ScryptedInterfaceProperty(str, Enum):
converters = "converters"
binaryState = "binaryState"
tampered = "tampered"
sleeping = "sleeping"
powerDetected = "powerDetected"
audioDetected = "audioDetected"
motionDetected = "motionDetected"
@@ -1924,6 +1955,7 @@ class ScryptedInterfaceMethods(str, Enum):
setColorTemperature = "setColorTemperature"
setRgb = "setRgb"
setHsv = "setHsv"
pressButton = "pressButton"
sendNotification = "sendNotification"
start = "start"
stop = "stop"
@@ -2178,6 +2210,14 @@ class DeviceState:
def hsv(self, value: ColorHsv):
self.setScryptedProperty("hsv", value)
@property
def buttons(self) -> list[str]:
return self.getScryptedProperty("buttons")
@buttons.setter
def buttons(self, value: list[str]):
self.setScryptedProperty("buttons", value)
@property
def running(self) -> bool:
return self.getScryptedProperty("running")
@@ -2242,6 +2282,30 @@ class DeviceState:
def audioVolumes(self, value: AudioVolumes):
self.setScryptedProperty("audioVolumes", value)
@property
def fontSize(self) -> float:
return self.getScryptedProperty("fontSize")
@fontSize.setter
def fontSize(self, value: float):
self.setScryptedProperty("fontSize", value)
@property
def origin(self) -> Point:
return self.getScryptedProperty("origin")
@origin.setter
def origin(self, value: Point):
self.setScryptedProperty("origin", value)
@property
def text(self) -> str:
return self.getScryptedProperty("text")
@text.setter
def text(self, value: str):
self.setScryptedProperty("text", value)
@property
def recordingActive(self) -> bool:
return self.getScryptedProperty("recordingActive")
@@ -2338,6 +2402,14 @@ class DeviceState:
def tampered(self, value: TamperState):
self.setScryptedProperty("tampered", value)
@property
def sleeping(self) -> bool:
return self.getScryptedProperty("sleeping")
@sleeping.setter
def sleeping(self, value: bool):
self.setScryptedProperty("sleeping", value)
@property
def powerDetected(self) -> bool:
return self.getScryptedProperty("powerDetected")
@@ -2612,6 +2684,20 @@ ScryptedInterfaceDescriptors = {
"hsv"
]
},
"Buttons": {
"name": "Buttons",
"methods": [],
"properties": [
"buttons"
]
},
"PressButtons": {
"name": "PressButtons",
"methods": [
"pressButton"
],
"properties": []
},
"Notifier": {
"name": "Notifier",
"methods": [
@@ -2722,6 +2808,15 @@ ScryptedInterfaceDescriptors = {
],
"properties": []
},
"VideoTextOverlay": {
"name": "VideoTextOverlay",
"methods": [],
"properties": [
"fontSize",
"origin",
"text"
]
},
"VideoRecorder": {
"name": "VideoRecorder",
"methods": [
@@ -2938,6 +3033,13 @@ ScryptedInterfaceDescriptors = {
"tampered"
]
},
"Sleep": {
"name": "Sleep",
"methods": [],
"properties": [
"sleeping"
]
},
"PowerSensor": {
"name": "PowerSensor",
"methods": [],

View File

@@ -214,6 +214,14 @@ export interface ColorHsv {
v?: number;
}
export interface Buttons {
buttons?: ('doorbell' | string)[];
}
export interface PressButtons {
pressButton(button: string): Promise<void>;
}
export interface NotificationAction {
action: string;
icon?: string;
@@ -234,6 +242,7 @@ export interface NotifierOptions {
renotify?: boolean;
requireInteraction?: boolean;
silent?: boolean;
critical?: boolean;
/**
* Collapse key/id.
*/
@@ -953,6 +962,20 @@ export interface VideoCameraMask {
setPrivacyMasks(masks: PrivacyMasks): Promise<void>;
}
export interface VideoTextOverlay {
/**
* The top left position of the overlay in the image, normalized to 0-1.
*/
origin?: Point;
fontSize?: number;
text?: string;
}
export interface VideoTextOverlays {
getVideoTextOverlays(): Promise<Record<string, string>>;
setVideoTextOverlay(id: string, value: VideoTextOverlay): Promise<void>;
}
export enum PanTiltZoomMovement {
Absolute = "Absolute",
Relative = "Relative",
@@ -1259,6 +1282,10 @@ export interface Charger {
chargeState?: ChargeState;
}
export interface Sleep {
sleeping?: boolean;
}
export interface Reboot {
reboot(): Promise<void>;
}
@@ -1531,6 +1558,10 @@ export interface ObjectDetectionResult extends BoundingBoxResult {
* The certainty that this is correct tracked object.
*/
cost?: number;
/**
* Flag that indicates whether the detection was clipped by the detection input and may not be a full bounding box.
*/
clipped?: boolean;
/**
* The detection class of the object.
*/
@@ -1547,11 +1578,6 @@ export interface ObjectDetectionResult extends BoundingBoxResult {
* The score of the label.
*/
labelScore?: number;
/**
* A base64 encoded Float32Array that represents the vector descriptor of the detection.
* Can be used to compute euclidian distance to determine similarity.
*/
descriptor?: string;
/**
* The detection landmarks, like key points in a face landmarks.
*/
@@ -2255,6 +2281,8 @@ export enum ScryptedInterface {
ColorSettingTemperature = "ColorSettingTemperature",
ColorSettingRgb = "ColorSettingRgb",
ColorSettingHsv = "ColorSettingHsv",
Buttons = "Buttons",
PressButtons = "PressButtons",
Notifier = "Notifier",
StartStop = "StartStop",
Pause = "Pause",
@@ -2268,6 +2296,7 @@ export enum ScryptedInterface {
Display = "Display",
VideoCamera = "VideoCamera",
VideoCameraMask = "VideoCameraMask",
VideoTextOverlay = "VideoTextOverlay",
VideoRecorder = "VideoRecorder",
VideoRecorderManagement = "VideoRecorderManagement",
PanTiltZoom = "PanTiltZoom",
@@ -2294,6 +2323,7 @@ export enum ScryptedInterface {
Settings = "Settings",
BinarySensor = "BinarySensor",
TamperSensor = "TamperSensor",
Sleep = "Sleep",
PowerSensor = "PowerSensor",
AudioSensor = "AudioSensor",
MotionSensor = "MotionSensor",
@@ -2683,6 +2713,7 @@ export interface ClusterWorker {
labels: string[];
forks: ClusterFork[];
mode: 'server' | 'client';
address: string;
}
export interface ClusterManager {

View File

@@ -99,11 +99,6 @@
"env": {
"SCRYPTED_PYTHON310_PATH": "/opt/homebrew/bin/python3.10",
"DYLD_LIBRARY_PATH": "/usr/local/lib",
"SCRYPTED_CLUSTER_WORKER_NAME": "Macaroni 2",
"SCRYPTED_CLUSTER_LABELS": "@scrypted/coreml,@scrypted/tensorflow-lite,compute,compute.preferred",
"SCRYPTED_CLUSTER_MODE": "client",
"SCRYPTED_CLUSTER_SERVER": "192.168.2.130",
"SCRYPTED_CLUSTER_SECRET": "swordfish",
"SCRYPTED_CAN_RESTART": "true",
"SCRYPTED_VOLUME": "/Users/koush/.scrypted-cluster/volume-client",
}

View File

@@ -1,18 +1,18 @@
{
"name": "@scrypted/server",
"version": "0.125.3",
"version": "0.134.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.125.3",
"version": "0.134.1",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@scrypted/ffmpeg-static": "^6.1.0-build3",
"@scrypted/node-pty": "^1.0.22",
"@scrypted/types": "^0.3.92",
"@scrypted/types": "^0.3.104",
"adm-zip": "^0.5.16",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",
@@ -557,9 +557,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.92",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
"version": "0.3.104",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.104.tgz",
"integrity": "sha512-aFqB9mDmKoKLGF6O3+N71V+fPeMkIO2xC+2/oUF/xOvhG0D9fmQwTaV2gJtwVZJwx/ZgWGU85dIWqmxP8YfcDg==",
"license": "ISC"
},
"node_modules/@types/adm-zip": {

View File

@@ -1,11 +1,11 @@
{
"name": "@scrypted/server",
"version": "0.127.1",
"version": "0.135.0",
"description": "",
"dependencies": {
"@scrypted/ffmpeg-static": "^6.1.0-build3",
"@scrypted/node-pty": "^1.0.22",
"@scrypted/types": "^0.3.92",
"@scrypted/types": "^0.3.104",
"adm-zip": "^0.5.16",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",

View File

@@ -144,7 +144,11 @@ class ClusterSetup:
m = hashlib.sha256()
m.update(
bytes(
f"{o['id']}{o.get('address', '')}{o['port']}{o.get('sourceKey', '')}{o['proxyId']}{self.clusterSecret}",
# The use of ` o.get(key, None) or '' ` is to ensure that optional fields
# are omitted from the hash, matching the JS implementation. Otherwise, since
# the dict may contain the keys initialized to None, ` o.get(key, '') ` would
# return None instead of ''.
f"{o['id']}{o.get('address', None) or ''}{o['port']}{o.get('sourceKey', None) or ''}{o['proxyId']}{self.clusterSecret}",
"utf8",
)
)

View File

@@ -4,7 +4,6 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import process from 'process';
import semver from 'semver';
import { ensurePluginVolume } from "./plugin-volume";
export function defaultNpmExec(args: string[], options: child_process.SpawnOptions) {
@@ -26,8 +25,16 @@ export function setNpmExecFunction(f: typeof npmExecFunction) {
export function getPluginNodePath(name: string) {
const pluginVolume = ensurePluginVolume(name);
const nodeMajorVersion = semver.parse(process.version).major;
let nodeVersionedDirectory = `node${nodeMajorVersion}-${process.platform}-${process.arch}`;
const abi = process.versions.modules;
let runtime = process.env.npm_config_runtime;
if (!runtime && process.versions.electron)
runtime = 'electron';
if (!runtime)
runtime = 'node';
const { platform, arch } = process;
let nodeVersionedDirectory = `n-${runtime}-v${abi}-${platform}-${arch}`;
const scryptedBase = process.env.SCRYPTED_BASE_VERSION;
if (scryptedBase)
nodeVersionedDirectory += '-' + scryptedBase;
@@ -98,7 +105,7 @@ export async function installOptionalDependencies(console: Console, packageJson:
if (!de.isDirectory())
return;
if (de.name.startsWith('linux') || de.name.startsWith('darwin') || de.name.startsWith('win32')
|| de.name.startsWith('python') || de.name.startsWith('node')) {
|| de.name.startsWith('python') || de.name.startsWith('node') || de.name.startsWith('n-')) {
console.log('Removing old dependencies:', filePath);
try {
await fs.promises.rm(filePath, {

View File

@@ -24,6 +24,7 @@ import { NodeThreadWorker } from './runtime/node-thread-worker';
import { prepareZip } from './runtime/node-worker-common';
import { getBuiltinRuntimeHosts } from './runtime/runtime-host';
import { RuntimeWorker, RuntimeWorkerOptions } from './runtime/runtime-worker';
import { Deferred } from '../deferred';
const serverVersion = require('../../package.json').version;
@@ -291,6 +292,14 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
forkPeer = Promise.resolve(localPeer);
}
const exitDeferred = new Deferred<string>();
runtimeWorker.on('exit', () => {
exitDeferred.resolve('worker exited');
});
runtimeWorker.on('error', e => {
exitDeferred.resolve('worker error' + e);
});
// thread workers inherit main console. pipe anything else.
if (!(runtimeWorker instanceof NodeThreadWorker)) {
const console = options?.id ? getMixinConsole(options.id, options.nativeId) : undefined;
@@ -299,6 +308,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const result = (async () => {
const threadPeer = await forkPeer;
exitDeferred.promise.then(reason => {
threadPeer.kill(reason);
});
// todo: handle nested forks and skip wrap. this is probably buggy.
class PluginForkAPI extends PluginAPIProxy {
@@ -319,13 +331,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
const remote = await setupPluginRemote(threadPeer, forkApi, pluginId, { serverVersion }, () => systemManager.getSystemState());
forks.add(remote);
runtimeWorker.on('exit', () => {
threadPeer.kill('worker exited');
forkApi.removeListeners();
forks.delete(remote);
});
runtimeWorker.on('error', e => {
threadPeer.kill('worker error ' + e);
exitDeferred.promise.then(reason => {
forkApi.removeListeners();
forks.delete(remote);
});

View File

@@ -48,7 +48,12 @@ export class NodeForkWorker extends ChildProcessWorker {
this.pluginId
];
const nodePaths: string[] = [path.resolve(__dirname, '..', '..', '..', 'node_modules')];
const nodePaths: string[] = [
// /server/node_modules/@scrypted/server/node_modules
path.resolve(__dirname, '..', '..', '..', 'node_modules'),
// /server/node_modules
path.resolve(process.cwd(), 'node_modules'),
];
if (env?.NODE_PATH)
nodePaths.push(env.NODE_PATH);
if (process.env.NODE_PATH)

View File

@@ -12,6 +12,7 @@ import { computeClusterObjectHash } from './cluster/cluster-hash';
import { getClusterLabels, getClusterWorkerWeight } from './cluster/cluster-labels';
import { getScryptedClusterMode, InitializeCluster, setupCluster } from './cluster/cluster-setup';
import type { ClusterObject } from './cluster/connect-rpc-object';
import { getScryptedFFmpegPath } from './plugin/ffmpeg-path';
import { getPluginVolume, getScryptedVolume } from './plugin/plugin-volume';
import { prepareZip } from './plugin/runtime/node-worker-common';
import { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
@@ -23,15 +24,20 @@ import { EnvControl } from './services/env';
import { Info } from './services/info';
import { ServiceControl } from './services/service-control';
import { sleep } from './sleep';
import { getScryptedFFmpegPath } from './plugin/ffmpeg-path';
installSourceMapSupport({
environment: 'node',
});
async function start(mainFilename: string, serviceControl?: ServiceControl) {
serviceControl ||= new ServiceControl();
startClusterClient(mainFilename, serviceControl);
async function start(mainFilename: string, options?: {
onClusterWorkerCreated?: (options?: {
clusterPluginHosts?: ReturnType<typeof getBuiltinRuntimeHosts>,
}) => Promise<void>,
serviceControl?: ServiceControl;
}) {
options ||= {};
options.serviceControl ||= new ServiceControl();
startClusterClient(mainFilename, options);
}
export default start;
@@ -122,12 +128,11 @@ export interface ClusterForkResultInterface {
export type ClusterForkParam = (runtime: string, options: RuntimeWorkerOptions, peerLiveness: PeerLiveness, getZip: () => Promise<Buffer>) => Promise<ClusterForkResultInterface>;
function createClusterForkParam(mainFilename: string, clusterId: string, clusterSecret: string, clusterWorkerId: string) {
function createClusterForkParam(mainFilename: string, clusterId: string, clusterSecret: string, clusterWorkerId: string, clusterPluginHosts: ReturnType<typeof getBuiltinRuntimeHosts>) {
const clusterForkParam: ClusterForkParam = async (runtime, runtimeWorkerOptions, peerLiveness, getZip) => {
let runtimeWorker: RuntimeWorker;
const builtins = getBuiltinRuntimeHosts();
const rt = builtins.get(runtime);
const rt = clusterPluginHosts.get(runtime);
if (!rt)
throw new Error('unknown runtime ' + runtime);
@@ -205,7 +210,12 @@ function createClusterForkParam(mainFilename: string, clusterId: string, cluster
return clusterForkParam;
}
export function startClusterClient(mainFilename: string, serviceControl?: ServiceControl) {
export function startClusterClient(mainFilename: string, options?: {
onClusterWorkerCreated?: (options?: {
clusterPluginHosts?: ReturnType<typeof getBuiltinRuntimeHosts>,
}) => Promise<void>,
serviceControl?: ServiceControl;
}) {
console.log('Cluster client starting.');
const envControl = new EnvControl();
@@ -217,6 +227,10 @@ export function startClusterClient(mainFilename: string, serviceControl?: Servic
const clusterSecret = process.env.SCRYPTED_CLUSTER_SECRET;
const clusterMode = getScryptedClusterMode();
const [, host, port] = clusterMode;
const clusterPluginHosts = getBuiltinRuntimeHosts();
options?.onClusterWorkerCreated?.({ clusterPluginHosts });
(async () => {
while (true) {
// this sleep is here to prevent a tight loop if the server is down.
@@ -259,7 +273,7 @@ export function startClusterClient(mainFilename: string, serviceControl?: Servic
process.env.SCRYPTED_CLUSTER_ADDRESS = socket.localAddress;
const peer = preparePeer(socket, 'client');
peer.params['service-control'] = serviceControl;
peer.params['service-control'] = options?.serviceControl;
peer.params['env-control'] = envControl;
peer.params['info'] = new Info();
peer.params['fs.promises'] = {
@@ -294,7 +308,7 @@ export function startClusterClient(mainFilename: string, serviceControl?: Servic
const clusterPeerSetup = setupCluster(peer);
await clusterPeerSetup.initializeCluster({ clusterId, clusterSecret, clusterWorkerId });
peer.params['fork'] = createClusterForkParam(mainFilename, clusterId, clusterSecret, clusterWorkerId);
peer.params['fork'] = createClusterForkParam(mainFilename, clusterId, clusterSecret, clusterWorkerId, clusterPluginHosts);
await peer.killed;
}
@@ -316,7 +330,7 @@ export function createClusterServer(mainFilename: string, scryptedRuntime: Scryp
labels: getClusterLabels(),
id: scryptedRuntime.serverClusterWorkerId,
peer: undefined,
fork: Promise.resolve(createClusterForkParam(mainFilename, scryptedRuntime.clusterId, scryptedRuntime.clusterSecret, scryptedRuntime.serverClusterWorkerId)),
fork: Promise.resolve(createClusterForkParam(mainFilename, scryptedRuntime.clusterId, scryptedRuntime.clusterSecret, scryptedRuntime.serverClusterWorkerId, scryptedRuntime.pluginHosts)),
name: process.env.SCRYPTED_CLUSTER_WORKER_NAME || os.hostname(),
address: process.env.SCRYPTED_CLUSTER_ADDRESS,
weight: getClusterWorkerWeight(),

View File

@@ -8,6 +8,7 @@ import vm from 'vm';
import { getScryptedClusterMode } from './cluster/cluster-setup';
import { PluginError } from './plugin/plugin-error';
import { isNodePluginWorkerProcess } from './plugin/runtime/node-fork-worker';
import type { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
import type { Runtime } from './scrypted-server-main';
import { getDotEnvPath } from './services/env';
@@ -16,6 +17,9 @@ import type { ServiceControl } from './services/service-control';
function start(mainFilename: string, options?: {
serviceControl?: ServiceControl,
onRuntimeCreated?: (runtime: Runtime) => Promise<void>,
onClusterWorkerCreated?: (options?: {
clusterPluginHosts?: ReturnType<typeof getBuiltinRuntimeHosts>,
}) => Promise<void>,
}) {
// Allow including a custom file path for platforms that require
// compatibility hacks. For example, Android may need to patch
@@ -71,7 +75,7 @@ function start(mainFilename: string, options?: {
const clusterMode = getScryptedClusterMode();
if (clusterMode?.[0] === 'client') {
const start = require('./scrypted-cluster-main').default;
return start(mainFilename, options?.serviceControl);
return start(mainFilename, options);
}
else {
const start = require('./scrypted-server-main').default;

View File

@@ -109,6 +109,7 @@ export class ClusterForkService {
labels: worker.labels,
forks: [...worker.forks] as ClusterFork[],
mode: worker.mode,
address: worker.address,
};
}
return ret;