Compare commits

..

15 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
17 changed files with 115 additions and 57 deletions

View File

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

View File

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

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,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);

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, Online, 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';
@@ -161,7 +161,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
}
}
const realDevice = systemManager.getDeviceById<VideoCamera & Online>(this.id);
const realDevice = systemManager.getDeviceById<VideoCamera & Sleep>(this.id);
let takePrebufferPicture: () => Promise<Buffer>;
const preparePrebufferSnapshot = async () => {
@@ -263,7 +263,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
}
try {
// consider waking the camera if
if (!eventSnapshot && this.mixinDeviceInterfaces.includes(ScryptedInterface.Battery) && !realDevice.online)
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'))
}

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.3.110",
"version": "0.3.112",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.3.110",
"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.110",
"version": "0.3.112",
"description": "",
"main": "dist/src/index.js",
"exports": {

View File

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

View File

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

View File

@@ -321,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.
@@ -467,6 +467,7 @@ class ClusterForkInterfaceOptions(TypedDict):
class ClusterWorker(TypedDict):
address: str
forks: list[ClusterFork]
id: str
labels: list[str]
@@ -956,7 +957,7 @@ class TamperState(TypedDict):
pass
TYPES_VERSION = "0.3.102"
TYPES_VERSION = "0.3.104"
class AirPurifier:

View File

@@ -1558,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.
*/
@@ -1574,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.
*/
@@ -2714,6 +2713,7 @@ export interface ClusterWorker {
labels: string[];
forks: ClusterFork[];
mode: 'server' | 'client';
address: string;
}
export interface ClusterManager {

View File

@@ -1,18 +1,18 @@
{
"name": "@scrypted/server",
"version": "0.129.0",
"version": "0.134.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.129.0",
"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.100",
"@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.100",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.100.tgz",
"integrity": "sha512-s/07QCxjMWqODgWj2UpLehzeo2cGFrCA9X8mvpG3owT/+q+sb8v/UUcw9TLHGSN6yIriNhceg3i9WO07kEIT6A==",
"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.130.3",
"version": "0.135.0",
"description": "",
"dependencies": {
"@scrypted/ffmpeg-static": "^6.1.0-build3",
"@scrypted/node-pty": "^1.0.22",
"@scrypted/types": "^0.3.100",
"@scrypted/types": "^0.3.104",
"adm-zip": "^0.5.16",
"body-parser": "^1.20.3",
"cookie-parser": "^1.4.7",

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;