mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 07:52:12 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adcdd18497 | ||
|
|
a95b77fe26 | ||
|
|
3ff75f0fde | ||
|
|
eecd38d271 | ||
|
|
7128af20af | ||
|
|
c651c2164b | ||
|
|
6caafd73f5 | ||
|
|
05cb505783 | ||
|
|
07baddc9c3 | ||
|
|
76ac260bf7 | ||
|
|
dfee7c6b09 | ||
|
|
b3ce6a2af3 | ||
|
|
933c0cac0f | ||
|
|
1fb1334a00 | ||
|
|
cb45a00c25 | ||
|
|
fec59af263 | ||
|
|
5d213a4c51 | ||
|
|
d444c4ab7c | ||
|
|
590f955ca9 | ||
|
|
7df4bf2723 | ||
|
|
3416347a1f | ||
|
|
c669bb8902 | ||
|
|
ce5fd2d4fd | ||
|
|
fa8a756059 | ||
|
|
73b85e1cd0 | ||
|
|
1300073712 | ||
|
|
3e296e12a5 | ||
|
|
bf98060a08 | ||
|
|
d1cd380123 | ||
|
|
1a2aadfb52 | ||
|
|
60c854a477 | ||
|
|
0790b60122 | ||
|
|
a3caa09df4 | ||
|
|
02ca8bd765 | ||
|
|
f9e1a94ab3 | ||
|
|
dd0da26df3 | ||
|
|
890f2e8daf | ||
|
|
2c8babe3ce |
@@ -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"
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.111",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.111",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.111",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.161",
|
||||
"version": "0.0.162",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
173
plugins/objectdetector/src/ffmpeg-audiosensor.ts
Normal file
173
plugins/objectdetector/src/ffmpeg-audiosensor.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.45",
|
||||
"version": "0.10.46",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.45",
|
||||
"version": "0.10.46",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.45",
|
||||
"version": "0.10.46",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
plugins/reolink/package-lock.json
generated
31
plugins/reolink/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"description": "Reolink Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
4
sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.112",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.112",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.112",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.104",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.104",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.104",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
|
||||
@@ -186,6 +186,7 @@ class ScryptedInterface(str, Enum):
|
||||
ScryptedUser = "ScryptedUser"
|
||||
SecuritySystem = "SecuritySystem"
|
||||
Settings = "Settings"
|
||||
Sleep = "Sleep"
|
||||
StartStop = "StartStop"
|
||||
StreamService = "StreamService"
|
||||
TamperSensor = "TamperSensor"
|
||||
@@ -201,6 +202,7 @@ class ScryptedInterface(str, Enum):
|
||||
VideoFrameGenerator = "VideoFrameGenerator"
|
||||
VideoRecorder = "VideoRecorder"
|
||||
VideoRecorderManagement = "VideoRecorderManagement"
|
||||
VideoTextOverlay = "VideoTextOverlay"
|
||||
VOCSensor = "VOCSensor"
|
||||
|
||||
class ScryptedMimeTypes(str, Enum):
|
||||
@@ -288,11 +290,6 @@ class ClipPath(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AudioStreamOptions(TypedDict):
|
||||
|
||||
bitrate: float
|
||||
@@ -324,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.
|
||||
@@ -470,6 +467,7 @@ class ClusterForkInterfaceOptions(TypedDict):
|
||||
|
||||
class ClusterWorker(TypedDict):
|
||||
|
||||
address: str
|
||||
forks: list[ClusterFork]
|
||||
id: str
|
||||
labels: list[str]
|
||||
@@ -669,6 +667,7 @@ class NotifierOptions(TypedDict):
|
||||
badge: str
|
||||
body: str
|
||||
bodyWithSubtitle: str
|
||||
critical: bool
|
||||
data: Any
|
||||
dir: NotificationDirection
|
||||
image: str
|
||||
@@ -948,12 +947,17 @@ class MediaConverterTypes(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TamperState(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
TYPES_VERSION = "0.3.100"
|
||||
TYPES_VERSION = "0.3.104"
|
||||
|
||||
|
||||
class AirPurifier:
|
||||
@@ -1535,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."""
|
||||
|
||||
@@ -1661,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
|
||||
@@ -1885,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"
|
||||
@@ -1897,6 +1914,7 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
converters = "converters"
|
||||
binaryState = "binaryState"
|
||||
tampered = "tampered"
|
||||
sleeping = "sleeping"
|
||||
powerDetected = "powerDetected"
|
||||
audioDetected = "audioDetected"
|
||||
motionDetected = "motionDetected"
|
||||
@@ -2264,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")
|
||||
@@ -2360,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")
|
||||
@@ -2758,6 +2808,15 @@ ScryptedInterfaceDescriptors = {
|
||||
],
|
||||
"properties": []
|
||||
},
|
||||
"VideoTextOverlay": {
|
||||
"name": "VideoTextOverlay",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"fontSize",
|
||||
"origin",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"VideoRecorder": {
|
||||
"name": "VideoRecorder",
|
||||
"methods": [
|
||||
@@ -2974,6 +3033,13 @@ ScryptedInterfaceDescriptors = {
|
||||
"tampered"
|
||||
]
|
||||
},
|
||||
"Sleep": {
|
||||
"name": "Sleep",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"sleeping"
|
||||
]
|
||||
},
|
||||
"PowerSensor": {
|
||||
"name": "PowerSensor",
|
||||
"methods": [],
|
||||
|
||||
@@ -242,6 +242,7 @@ export interface NotifierOptions {
|
||||
renotify?: boolean;
|
||||
requireInteraction?: boolean;
|
||||
silent?: boolean;
|
||||
critical?: boolean;
|
||||
/**
|
||||
* Collapse key/id.
|
||||
*/
|
||||
@@ -961,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",
|
||||
@@ -1267,6 +1282,10 @@ export interface Charger {
|
||||
chargeState?: ChargeState;
|
||||
}
|
||||
|
||||
export interface Sleep {
|
||||
sleeping?: boolean;
|
||||
}
|
||||
|
||||
export interface Reboot {
|
||||
reboot(): Promise<void>;
|
||||
}
|
||||
@@ -1539,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.
|
||||
*/
|
||||
@@ -1555,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.
|
||||
*/
|
||||
@@ -2278,6 +2296,7 @@ export enum ScryptedInterface {
|
||||
Display = "Display",
|
||||
VideoCamera = "VideoCamera",
|
||||
VideoCameraMask = "VideoCameraMask",
|
||||
VideoTextOverlay = "VideoTextOverlay",
|
||||
VideoRecorder = "VideoRecorder",
|
||||
VideoRecorderManagement = "VideoRecorderManagement",
|
||||
PanTiltZoom = "PanTiltZoom",
|
||||
@@ -2304,6 +2323,7 @@ export enum ScryptedInterface {
|
||||
Settings = "Settings",
|
||||
BinarySensor = "BinarySensor",
|
||||
TamperSensor = "TamperSensor",
|
||||
Sleep = "Sleep",
|
||||
PowerSensor = "PowerSensor",
|
||||
AudioSensor = "AudioSensor",
|
||||
MotionSensor = "MotionSensor",
|
||||
@@ -2693,6 +2713,7 @@ export interface ClusterWorker {
|
||||
labels: string[];
|
||||
forks: ClusterFork[];
|
||||
mode: 'server' | 'client';
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface ClusterManager {
|
||||
|
||||
12
server/package-lock.json
generated
12
server/package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.128.3",
|
||||
"version": "0.134.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.128.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.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": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.128.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",
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -109,6 +109,7 @@ export class ClusterForkService {
|
||||
labels: worker.labels,
|
||||
forks: [...worker.forks] as ClusterFork[],
|
||||
mode: worker.mode,
|
||||
address: worker.address,
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
|
||||
Reference in New Issue
Block a user