mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -725,7 +725,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: '',
|
||||
|
||||
@@ -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, Online, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, 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 & Online>(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.Battery) && !realDevice.online)
|
||||
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.110",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.110",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.108",
|
||||
"version": "0.3.110",
|
||||
"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.102",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.102",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.102",
|
||||
"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
|
||||
@@ -669,6 +666,7 @@ class NotifierOptions(TypedDict):
|
||||
badge: str
|
||||
body: str
|
||||
bodyWithSubtitle: str
|
||||
critical: bool
|
||||
data: Any
|
||||
dir: NotificationDirection
|
||||
image: str
|
||||
@@ -948,12 +946,17 @@ class MediaConverterTypes(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TamperState(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
TYPES_VERSION = "0.3.100"
|
||||
TYPES_VERSION = "0.3.102"
|
||||
|
||||
|
||||
class AirPurifier:
|
||||
@@ -1535,6 +1538,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 +1668,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 +1898,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 +1913,7 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
converters = "converters"
|
||||
binaryState = "binaryState"
|
||||
tampered = "tampered"
|
||||
sleeping = "sleeping"
|
||||
powerDetected = "powerDetected"
|
||||
audioDetected = "audioDetected"
|
||||
motionDetected = "motionDetected"
|
||||
@@ -2264,6 +2281,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 +2401,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 +2807,15 @@ ScryptedInterfaceDescriptors = {
|
||||
],
|
||||
"properties": []
|
||||
},
|
||||
"VideoTextOverlay": {
|
||||
"name": "VideoTextOverlay",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"fontSize",
|
||||
"origin",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"VideoRecorder": {
|
||||
"name": "VideoRecorder",
|
||||
"methods": [
|
||||
@@ -2974,6 +3032,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>;
|
||||
}
|
||||
@@ -2278,6 +2297,7 @@ export enum ScryptedInterface {
|
||||
Display = "Display",
|
||||
VideoCamera = "VideoCamera",
|
||||
VideoCameraMask = "VideoCameraMask",
|
||||
VideoTextOverlay = "VideoTextOverlay",
|
||||
VideoRecorder = "VideoRecorder",
|
||||
VideoRecorderManagement = "VideoRecorderManagement",
|
||||
PanTiltZoom = "PanTiltZoom",
|
||||
@@ -2304,6 +2324,7 @@ export enum ScryptedInterface {
|
||||
Settings = "Settings",
|
||||
BinarySensor = "BinarySensor",
|
||||
TamperSensor = "TamperSensor",
|
||||
Sleep = "Sleep",
|
||||
PowerSensor = "PowerSensor",
|
||||
AudioSensor = "AudioSensor",
|
||||
MotionSensor = "MotionSensor",
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.128.3",
|
||||
"version": "0.129.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.128.3",
|
||||
"version": "0.129.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.128.3",
|
||||
"version": "0.130.3",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build3",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user