Compare commits

..

144 Commits

Author SHA1 Message Date
Koushik Dutta
15283e13f0 prepublish 2023-03-14 15:14:23 -07:00
Koushik Dutta
0cde5bf8e7 prepublish 2023-03-14 15:14:10 -07:00
Koushik Dutta
fe3a1a023d prepublish 2023-03-14 15:14:02 -07:00
Koushik Dutta
369dcff2bd server: support large file transfers on engine io 2023-03-14 14:50:47 -07:00
Koushik Dutta
ed341a12b1 predict: rgba to rgb conversion 2023-03-14 14:50:28 -07:00
Koushik Dutta
00e523e268 core: add object detection ui 2023-03-14 14:50:04 -07:00
Koushik Dutta
4e25aedbe7 python-codecs: multiprocessing decode 2023-03-14 10:22:01 -07:00
Koushik Dutta
45bd3cbb7c server: fix various python mutiprocesisng quirks 2023-03-14 10:21:45 -07:00
Koushik Dutta
8e34bc2130 server: fix dangling thread if glib main loop fails 2023-03-14 09:18:00 -07:00
Koushik Dutta
457fc96332 predict: support for new pipeline redetection 2023-03-14 09:16:54 -07:00
Koushik Dutta
e2186401bf videoanalysis: new working pipeline 2023-03-14 09:16:34 -07:00
Koushik Dutta
a19d916ef0 python-codecs: improve memory management 2023-03-14 09:16:08 -07:00
Koushik Dutta
42bc7dc644 rebroadcast: publish update, current version was using actual addresses? 2023-03-14 08:52:38 -07:00
Koushik Dutta
f9d6308154 rpc: python rpc should be killed on disconnect 2023-03-13 17:10:06 -07:00
Koushik Dutta
dcb6627fb1 predict: publish fix that validates settings input 2023-03-13 11:15:45 -07:00
Koushik Dutta
1d5c71d617 videoanalysis: publish 2023-03-13 10:28:07 -07:00
Koushik Dutta
d5157fb868 predict: new detection pipeline around 50% faster! 2023-03-13 10:17:38 -07:00
Koushik Dutta
98096845dc rtp: add utility method for adding timestamps 2023-03-13 10:17:09 -07:00
Koushik Dutta
28ac97f4c9 predict: new pipeline 2023-03-12 22:11:06 -07:00
Koushik Dutta
2fc39e3979 videoanalysis: new pipeline 2023-03-12 22:10:40 -07:00
Koushik Dutta
9c89c3c2b8 snapshot: vips fixes 2023-03-12 22:10:15 -07:00
Koushik Dutta
15c7747f48 sdk: update 2023-03-12 22:09:59 -07:00
Koushik Dutta
940d4b7fd4 rpc: various python fixes 2023-03-12 22:09:50 -07:00
Koushik Dutta
a1c8ce754e python-codecs: working prototype 2023-03-12 22:09:33 -07:00
Koushik Dutta
5e6364850a onvif: fix ptz causing creation issues 2023-03-12 10:44:21 -07:00
Koushik Dutta
8df52e7595 python-codecs: wip 2023-03-11 20:01:26 -08:00
Koushik Dutta
1e004d6700 rpc: fixup various async iterator bugs, add memoryview support to python 2023-03-11 19:38:43 -08:00
Koushik Dutta
4570f9cd38 python-codecs: wip 2023-03-11 00:17:50 -08:00
Koushik Dutta
601cd39ba4 rpc: fix proxied iterator proxy 2023-03-10 21:38:48 -08:00
Koushik Dutta
923475fab2 Merge branch 'main' of github.com:koush/scrypted 2023-03-10 19:46:59 -08:00
Koushik Dutta
21ce5dfad4 sdk: image support 2023-03-10 19:46:51 -08:00
Koushik Dutta
2bd3592aad server: fix mediaobject polymorphism 2023-03-10 19:46:38 -08:00
Koushik Dutta
44f083ca23 webrtc: remove potential converter with permission escalation 2023-03-10 19:46:03 -08:00
Koushik Dutta
cc7271f0a2 snapshot: use libvips 2023-03-10 19:45:37 -08:00
Koushik Dutta
11a1a1134d predict: validate args 2023-03-10 16:50:11 -08:00
Alex Leeds
70cfa13e67 eufy: motion, partial livestream removal & minor improvement in snapshots (#618) 2023-03-10 16:16:36 -08:00
Koushik Dutta
291f90b2b2 rtp: expose child process in rtp forwarder 2023-03-10 11:55:22 -08:00
Koushik Dutta
d0ae7eb841 eufy: all cleaned up 2023-03-10 11:11:13 -08:00
Koushik Dutta
8444102cca eufy: functional audio 2023-03-10 10:49:45 -08:00
Alex Leeds
5a1c052c77 eufy: support captcha (#616) 2023-03-10 09:46:42 -08:00
Koushik Dutta
fb7eeece54 eufy: more logging 2023-03-10 07:53:34 -08:00
Koushik Dutta
d479bcece9 eufy: fix encoder codecs 2023-03-10 07:34:04 -08:00
Koushik Dutta
deefac2347 eufy: fix encoder codecs 2023-03-10 07:33:04 -08:00
Koushik Dutta
53808a04b7 google-cloud-tts: move to org 2023-03-09 21:40:06 -08:00
Koushik Dutta
a1785c2658 tensorflow-legacy: remove 2023-03-09 21:37:56 -08:00
Koushik Dutta
601cf46b1e thermostat: move to org 2023-03-09 21:37:28 -08:00
Koushik Dutta
6bba1b1cbd eufy: fix output url 2023-03-09 20:56:22 -08:00
Koushik Dutta
ab0122420b eufy: codec copy 2023-03-09 20:55:48 -08:00
Koushik Dutta
74ae2aab91 eufy: try mpegts 2023-03-09 20:54:43 -08:00
Koushik Dutta
c5fa131a44 eufy: revert stream manager change 2023-03-09 20:44:00 -08:00
Koushik Dutta
8dcf4dda9f eufy: use ffmpeg and adts audio 2023-03-09 20:29:00 -08:00
Koushik Dutta
cd59125ada eufy: revert 2023-03-09 20:24:08 -08:00
Koushik Dutta
d284eb6738 eufy: mute audio 2023-03-09 19:07:42 -08:00
Koushik Dutta
a78cc943cc eufy: mark stream as scrypted parser safe 2023-03-09 19:07:22 -08:00
Koushik Dutta
7ddeda1595 eufy: add audio toggle 2023-03-09 19:06:20 -08:00
Koushik Dutta
f02dfa5e14 eufy: remove some logging 2023-03-09 18:56:31 -08:00
Koushik Dutta
b2a4f20381 eufy: audio maybe 2023-03-09 18:53:36 -08:00
Koushik Dutta
dec3c354f0 eufy: use per session live stream manager 2023-03-09 18:47:45 -08:00
Koushik Dutta
2ee581d48d Merge branch 'main' of github.com:koush/scrypted 2023-03-09 18:07:34 -08:00
Koushik Dutta
d74c3a3fc5 eufy: generate some timestamps 2023-03-09 18:07:29 -08:00
Nick Berardi
405d9f0c09 onvif: add absolute and speed support to movement (#612) 2023-03-09 17:17:08 -08:00
Koushik Dutta
db25c5babe Merge branch 'main' of github.com:koush/scrypted 2023-03-09 14:13:17 -08:00
Alex Leeds
d5c90ab8da eufy: add plugin (#614) 2023-03-09 14:13:03 -08:00
Koushik Dutta
81a5c143d6 snapshot: add/use sharp (libvips) 2023-03-09 09:29:28 -08:00
Koushik Dutta
ebf2176618 remote: wip 2023-03-08 13:36:55 -08:00
Koushik Dutta
f435f8eff5 sdk: update 2023-03-08 13:36:40 -08:00
Koushik Dutta
f8c16edaae Merge branch 'main' of github.com:koush/scrypted 2023-03-08 07:37:03 -08:00
Koushik Dutta
ea86065d99 tapo: add cloud password instructions 2023-03-08 07:36:57 -08:00
Alex Leeds
ed5c7b126c ring: update dependencies (#607) 2023-03-07 20:49:28 -08:00
Koushik Dutta
806e015823 tapo: make it searchable in plugin install 2023-03-07 16:55:15 -08:00
Koushik Dutta
41c4cbc96c client: update 2023-03-07 16:24:31 -08:00
Koushik Dutta
143a0b2c41 webrtc: startRtpForwarderProcess remove werift dependency 2023-03-07 16:24:22 -08:00
Koushik Dutta
f582db3f11 common: http message parsing helpers 2023-03-07 16:24:00 -08:00
Koushik Dutta
103855ca50 Merge branch 'main' of github.com:koush/scrypted 2023-03-07 16:07:22 -08:00
Koushik Dutta
70c6fe4c68 tapo: initial commit of two way audio 2023-03-07 16:07:15 -08:00
Nick Berardi
c85d45050f alexa: refactor code structure (#606) 2023-03-07 12:04:52 -08:00
Alex Leeds
16a39ac76a ring: update ring api client (#605) 2023-03-07 07:51:25 -08:00
Koushik Dutta
fdc7519db0 onvif: ptz 2023-03-06 18:17:54 -08:00
Koushik Dutta
83af0c5ec7 core: cleanup device discovery 2023-03-06 17:03:21 -08:00
Koushik Dutta
ee22686bff videoanalysis: prevent double motion detector or double object detector 2023-03-06 10:32:35 -08:00
Koushik Dutta
7dc1f9736a pam-diff: add support for motion objects 2023-03-06 10:10:16 -08:00
Koushik Dutta
6e2aa37d75 server: implement missing setMixins 2023-03-06 09:34:29 -08:00
Koushik Dutta
fbaa8a31cf predict: fix bug where memory can leak if detection fails
tf: request restart if detection fails
2023-03-06 09:34:04 -08:00
Koushik Dutta
fa89a5ad24 sort: fix crash if no detection id is provided 2023-03-06 09:33:38 -08:00
Koushik Dutta
464deaf35e cameras: fix bug where device creation fails when no name is provided 2023-03-06 09:33:15 -08:00
Koushik Dutta
9cc8f50ff7 client: update sdk 2023-03-06 09:32:53 -08:00
Koushik Dutta
c17a1184cc core: fix settings subgroup regression 2023-03-06 07:39:17 -08:00
Koushik Dutta
b5004739c3 core: fix wonky settings 2023-03-05 23:09:31 -08:00
Koushik Dutta
d01c0fa72b sdk: fix StorageSettings 'device' defaults 2023-03-05 22:39:04 -08:00
Koushik Dutta
bb9f3d5aab predict: revert object tracker changes until custom NVR detector with face recognition is in place 2023-03-05 22:38:36 -08:00
Koushik Dutta
b23daa6735 Merge branch 'main' of github.com:koush/scrypted 2023-03-05 21:36:54 -08:00
Koushik Dutta
bb8b0125b6 server/sdk: update 2023-03-05 21:36:50 -08:00
Koushik Dutta
8e5f44f998 server: add support for polymorphic media objects 2023-03-05 21:33:44 -08:00
Brett Jia
9015af4902 arlo: optimize event handling (#601)
* optimize event waiting by keying on properties

* bump 0.6.6

* interrupt cleanup for other tasks

* bump 0.6.7 for race condition fix
2023-03-05 19:25:47 -08:00
Koushik Dutta
7902a091a9 core: fix listener leak 2023-03-04 20:48:24 -08:00
Koushik Dutta
615357befb werift: update 2023-03-04 19:20:31 -08:00
Koushik Dutta
34b26c81dc server: fix bug where express sets Cache-Control: max-age=0 on all file responses 2023-03-04 19:19:52 -08:00
Koushik Dutta
ea99a54e1b cloud: cleanup logging 2023-03-04 19:18:36 -08:00
Koushik Dutta
f726826391 core: fix changing password escalating user privileges 2023-03-04 19:00:49 -08:00
Koushik Dutta
dc5148c856 rpc: dont throw on oneway methods even if the peer is closed 2023-03-04 18:59:55 -08:00
Koushik Dutta
373c11ffee webrtc: add connection logging 2023-03-04 18:34:45 -08:00
Koushik Dutta
bea1f019b4 server: update deps 2023-03-04 14:05:08 -08:00
Koushik Dutta
29c98777e9 server: add python plugin id to command line 2023-03-04 14:05:04 -08:00
Koushik Dutta
9eb5029128 cloud: Fix x-scrypted-cloud header to come from upstream proxy 2023-03-04 08:59:23 -08:00
Koushik Dutta
33607796d1 cloud: log incoming connections 2023-03-04 07:51:08 -08:00
Koushik Dutta
f23fa0c335 coreml: update deps 2023-03-03 23:39:43 -08:00
Koushik Dutta
e6cfecfc1a videoanalysis: configurable object tracker 2023-03-03 23:39:18 -08:00
Koushik Dutta
44346d5b33 server: fix python rpc connect 2023-03-03 23:34:15 -08:00
Koushik Dutta
19da68884b server: implement python connectRPCObject 2023-03-03 23:17:43 -08:00
Koushik Dutta
544349de8d snapshot: update sdk 2023-03-03 16:48:37 -08:00
Koushik Dutta
6f90b1a0e3 server: add support for direct ipc 2023-03-03 16:48:29 -08:00
Koushik Dutta
fbbb9163d7 sdk: add ipcObject 2023-03-03 14:56:40 -08:00
Koushik Dutta
445581eefa server: plugin worker cleanups 2023-03-03 11:36:15 -08:00
Koushik Dutta
096c036ea2 rpc: implement python async iterator 2023-03-02 21:03:29 -08:00
Koushik Dutta
b2e5801426 rpc: improve error serialization and handling 2023-03-02 16:02:48 -08:00
Koushik Dutta
41061854f1 rpc: add intrinsic support for async iterators 2023-03-02 13:49:20 -08:00
Koushik Dutta
d91e625973 sort-tracker: publish 2023-03-02 09:09:53 -08:00
Koushik Dutta
ec5b59a00c Merge branch 'main' of github.com:koush/scrypted 2023-03-01 21:34:12 -08:00
Koushik Dutta
172790b18f sdk: fix device StorageSetting deserialzation
predict: externalize tracker
2023-03-01 21:33:43 -08:00
Nick Berardi
de0e6ee955 unifi-protect: added new smart event and updated snapshot to use login (#595) 2023-03-01 20:13:01 -08:00
Koushik Dutta
69d7ff2ced Merge branch 'main' of github.com:koush/scrypted 2023-03-01 14:51:55 -08:00
Koushik Dutta
3c237eac91 tensorflow-lite: cleanup dead code 2023-03-01 14:51:52 -08:00
Koushik Dutta
694c195024 rpc: fixup WeakRef typing 2023-03-01 14:51:40 -08:00
Koushik Dutta
c1f0281030 core: add finer grain user permissions 2023-03-01 14:51:14 -08:00
Brett Jia
fa218cbcbd remote: cleanup remote hint now that rebroadcast uses external by default (#594) 2023-03-01 14:18:42 -08:00
Koushik Dutta
a89700acc2 cli/client: decouple, upgrade packages, publish 2023-03-01 13:55:40 -08:00
Koushik Dutta
82fb24e275 rebroadcast: move url expansion into separate file 2023-03-01 12:22:33 -08:00
Koushik Dutta
eef67a9383 cli: fix arg parsing 2023-03-01 11:56:37 -08:00
Koushik Dutta
1180d9fa2c cli: rebuild 2023-03-01 11:51:36 -08:00
Koushik Dutta
57734f1d3c videoanalysis: remove extra settings 2023-02-28 23:56:39 -08:00
Koushik Dutta
dace750829 predict: publish 2023-02-28 21:53:33 -08:00
Koushik Dutta
f359a7167a server: nuke python prefix prior to install to purge old conflicting deps 2023-02-28 21:53:15 -08:00
Koushik Dutta
39c0759d1b tensorflow-lite: add simd support 2023-02-28 21:34:06 -08:00
Koushik Dutta
fee90334fb videoanalysis: snapshot mode cleanups 2023-02-28 20:48:31 -08:00
Koushik Dutta
80db6e50ab rebroadcast: fix external url behavior 2023-02-28 20:44:57 -08:00
Koushik Dutta
1fa6c2d842 tensorflow: reduce several expensice cpu resizes 2023-02-28 20:44:21 -08:00
Koushik Dutta
8b39c4c22c snapshot: fix debug file logging 2023-02-28 20:26:15 -08:00
Koushik Dutta
4b6fd5b5a8 server: remove debug logging 2023-02-28 20:20:17 -08:00
Koushik Dutta
f2d1909b6d docker: gstreamer vaapi is apparently xplat 2023-02-28 19:42:21 -08:00
Koushik Dutta
7917fb96dc docker: incude ffmpeg 2023-02-28 19:39:12 -08:00
Koushik Dutta
ad5fae98f1 docker: incude ffmpeg 2023-02-28 19:35:33 -08:00
Koushik Dutta
8412eb36fe rebroadcast: fix erroneous external check. 2023-02-28 11:22:51 -08:00
Koushik Dutta
822455383b rebroadcast: include error in warning message 2023-02-28 11:20:27 -08:00
Koushik Dutta
2d4357e4c0 server: preserve MediaObject name in constructor 2023-02-28 11:17:58 -08:00
220 changed files with 12430 additions and 8219 deletions

9
.gitmodules vendored
View File

@@ -19,6 +19,7 @@
[submodule "external/ring-client-api"]
path = external/ring-client-api
url = ../../koush/ring
branch = fork
[submodule "plugins/vscode-typescript"]
path = plugins/vscode-typescript
url = ../../koush/scrypted-vscode-typescript/
@@ -28,20 +29,14 @@
[submodule "plugins/zwave/file-stream-rotator"]
path = plugins/zwave/file-stream-rotator
url = ../../koush/file-stream-rotator.git
[submodule "external/push-receiver"]
path = external/push-receiver
url = ../../koush/push-receiver.git
[submodule "sdk/developer.scrypted.app"]
path = sdk/developer.scrypted.app
url = ../../koush/developer.scrypted.app
[submodule "plugins/sample-cameraprovider"]
path = plugins/sample-cameraprovider
url = ../../koush/scrypted-sample-cameraprovider
[submodule "plugins/objectdetector/node-moving-things-tracker"]
path = plugins/objectdetector/node-moving-things-tracker
url = ../../koush/node-moving-things-tracker.git
[submodule "plugins/tensorflow-lite/sort_oh"]
path = plugins/tensorflow-lite/sort_oh
path = plugins/sort-tracker/sort_oh
url = ../../koush/sort_oh.git
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/node-nat-upnp

View File

@@ -2,14 +2,15 @@ import type { TranspileOptions } from "typescript";
import sdk, { ScryptedDeviceBase, MixinDeviceBase, ScryptedInterface, ScryptedDeviceType } from "@scrypted/sdk";
import vm from "vm";
import fs from 'fs';
import { newThread } from '@scrypted/server/src/threading';
import { ScriptDevice } from "./monaco/script-device";
import { ScryptedInterfaceDescriptors } from "@scrypted/sdk";
import fetch from 'node-fetch-commonjs';
import { PluginAPIProxy } from '../../../server/src/plugin/plugin-api';
import { SystemManagerImpl } from '../../../server/src/plugin/system';
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
function tsCompile(source: string, options: TranspileOptions = null): string {
export async function tsCompile(source: string, options: TranspileOptions = null): Promise<string> {
const ts = require("typescript");
const { ScriptTarget } = ts;
@@ -25,27 +26,6 @@ function tsCompile(source: string, options: TranspileOptions = null): string {
return ts.transpileModule(source, options).outputText;
}
async function tsCompileThread(source: string, options: TranspileOptions = null): Promise<string> {
return newThread({
source, options,
customRequire: '__webpack_require__',
}, ({ source, options }) => {
const ts = global.require("typescript");
const { ScriptTarget } = ts;
// Default options -- you could also perform a merge, or use the project tsconfig.json
if (null === options) {
options = {
compilerOptions: {
target: ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS
}
};
}
return ts.transpileModule(source, options).outputText;
});
}
function getTypeDefs() {
const scryptedTypesDefs = fs.readFileSync('@types/sdk/types.d.ts').toString();
const scryptedIndexDefs = fs.readFileSync('@types/sdk/index.d.ts').toString();
@@ -61,14 +41,27 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
}, extraLibs);
const allScripts = Object.values(libs).join('\n').toString() + script;
let compiled: string;
const worker = sdk.fork<{
tsCompile: typeof tsCompile,
}>();
worker.worker.on('error', () => { })
try {
compiled = await tsCompileThread(allScripts);
const result = await worker.result;
compiled = await result.tsCompile(allScripts);
}
catch (e) {
device.log.e('Error compiling typescript.');
device.console.error(e);
throw e;
}
finally {
worker.worker.terminate();
}
const smProxy = new SystemManagerImpl();
smProxy.state = systemManager.getSystemState();
const apiProxy = new PluginAPIProxy(sdk.pluginHostAPI);
smProxy.api = apiProxy;
const allParams = Object.assign({}, params, {
sdk,
@@ -76,7 +69,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
fetch,
ScryptedDeviceBase,
MixinDeviceBase,
systemManager,
systemManager: smProxy,
deviceManager,
endpointManager,
mediaManager,
@@ -111,6 +104,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
return {
value,
defaultExport,
apiProxy,
};
}
catch (e) {

View File

@@ -4,7 +4,7 @@ import { once } from 'events';
import { BASIC } from 'http-auth-utils/dist/index';
import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
import net from 'net';
import { Duplex, Readable } from 'stream';
import { Duplex, Readable, Writable } from 'stream';
import tls from 'tls';
import { Deferred } from './deferred';
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
@@ -47,6 +47,29 @@ export async function readMessage(client: Readable): Promise<string[]> {
}
}
export async function readBody(client: Readable, response: Headers) {
const cl = parseInt(response['content-length']);
if (cl)
return readLength(client, cl)
}
export function writeMessage(client: Writable, messageLine: string, body: Buffer, headers: Headers, console?: Console) {
let message = messageLine !== undefined ? `${messageLine}\r\n` : '';
if (body)
headers['Content-Length'] = body.length.toString();
for (const [key, value] of Object.entries(headers)) {
message += `${key}: ${value}\r\n`;
}
message += '\r\n';
client.write(message);
console?.log('rtsp outgoing message\n', message);
console?.log();
if (body)
client.write(body);
}
// https://yumichan.net/video-processing/video-compression/introduction-to-h264-nal-unit/
export const H264_NAL_TYPE_RESERVED0 = 0;
@@ -284,18 +307,7 @@ export class RtspBase {
}
write(messageLine: string, headers: Headers, body?: Buffer) {
let message = `${messageLine}\r\n`;
if (body)
headers['Content-Length'] = body.length.toString();
for (const [key, value] of Object.entries(headers)) {
message += `${key}: ${value}\r\n`;
}
message += '\r\n';
this.client.write(message);
this.console?.log('rtsp outgoing message\n', message);
this.console?.log();
if (body)
this.client.write(body);
writeMessage(this.client, messageLine, body, headers, this.console);
}
async readMessage(): Promise<string[]> {
@@ -590,9 +602,7 @@ export class RtspClient extends RtspBase {
}
async readBody(response: Headers) {
const cl = parseInt(response['content-length']);
if (cl)
return readLength(this.client, cl)
return readBody(this.client, response);
}
async request(method: string, headers?: Headers, path?: string, body?: Buffer, authenticating?: boolean): Promise<RtspServerResponse> {

View File

@@ -35,11 +35,14 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
RUN bash -c 'if [ $(uname -m) == "x86_64" ]; then apt-get -y install gstreamer1.0-vaapi; fi'
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \

View File

@@ -20,6 +20,11 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
# python native
RUN apt-get -y install \
python3 \

View File

@@ -19,6 +19,11 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
ENV SCRYPTED_DOCKER_SERVE="true"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"

View File

@@ -32,11 +32,14 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
RUN bash -c 'if [ $(uname -m) == "x86_64" ]; then apt-get -y install gstreamer1.0-vaapi; fi'
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \

Submodule external/push-receiver deleted from d054e083d6

View File

@@ -22,7 +22,8 @@
"args": [
"ffplay",
"Kitchen",
"getVideoStream"
"getRecordingStream",
"{\"startTime\":1677699495709}"
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,119 +1,183 @@
{
"name": "scrypted",
"version": "1.0.58",
"lockfileVersion": 2,
"version": "1.0.67",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.0.58",
"version": "1.0.67",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",
"adm-zip": "^0.5.9",
"@scrypted/client": "^1.1.43",
"@scrypted/types": "^0.2.66",
"adm-zip": "^0.5.10",
"axios": "^0.21.4",
"engine.io-client": "^5.2.0",
"ip": "^1.1.8",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"mkdirp": "^1.0.4",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tslib": "^2.3.1"
"semver": "^7.3.8",
"tslib": "^2.5.0"
},
"bin": {
"scrypted": "dist/packages/cli/src/main.js"
"scrypted": "dist/main.js"
},
"devDependencies": {
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.14.2",
"@types/readline-sync": "^1.4.4",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"ts-node": "^10.2.1",
"typescript": "^4.8.2"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"extraneous": true,
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
}
},
"node_modules/@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true,
"engines": {
"node": ">= 12"
"@types/semver": "^7.3.13",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-consumer": "0.8.0"
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@scrypted/client": {
"version": "1.1.43",
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.1.43.tgz",
"integrity": "sha512-qpeGdqFga/Fx51MoF3E0iBPCjE/SDEIVdGh8Ws5dqw38bxUJD264c9NsNyCguLKyYguErKTAWnQkzqhO0bUbaA==",
"dependencies": {
"@scrypted/types": "^0.2.66",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
}
},
"node_modules/@scrypted/client/node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"dependencies": {
"follow-redirects": "^1.14.7"
}
},
"node_modules/@scrypted/client/node_modules/engine.io-client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/@scrypted/client/node_modules/engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@scrypted/client/node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@scrypted/types": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
"version": "0.2.66",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.66.tgz",
"integrity": "sha512-AL2iD7OmpqZlQMlpZKUBHpzL7H1IHhwKOi9uhRbVwG7EIDwenTspqtziH2Hyu0+XeCLf+gN69uQB6Qlz+QPf9A=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
"dev": true,
"dependencies": {
"@types/minimatch": "*",
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"node_modules/@types/mkdirp": {
@@ -126,9 +190,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/@types/readline-sync": {
@@ -148,15 +212,15 @@
}
},
"node_modules/@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"node_modules/acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -175,9 +239,9 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"engines": {
"node": ">=6.0"
}
@@ -204,7 +268,7 @@
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
"engines": {
"node": ">= 0.6.0"
}
@@ -226,7 +290,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/create-require": {
"version": "1.1.1",
@@ -235,9 +299,9 @@
"dev": true
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -288,9 +352,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
@@ -306,25 +370,20 @@
}
}
},
"node_modules/fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -338,12 +397,12 @@
"node_modules/has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -359,11 +418,6 @@
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"node_modules/linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -381,17 +435,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"dependencies": {
"fs-monkey": "1.0.3"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -422,7 +465,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
@@ -440,7 +483,7 @@
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
@@ -468,9 +511,9 @@
}
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -482,12 +525,12 @@
}
},
"node_modules/ts-node": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "0.7.0",
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
@@ -498,11 +541,13 @@
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
@@ -523,14 +568,14 @@
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/typescript": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -540,10 +585,16 @@
"node": ">=4.2.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "7.4.6",
@@ -581,7 +632,7 @@
"node_modules/yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
},
"node_modules/yn": {
"version": "3.1.1",
@@ -592,413 +643,5 @@
"node": ">=6"
}
}
},
"dependencies": {
"@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true
},
"@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"dev": true,
"requires": {
"@cspotcode/source-map-consumer": "0.8.0"
}
},
"@scrypted/types": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
},
"@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"dev": true
},
"@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"dev": true
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"dev": true
},
"@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"dev": true,
"requires": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"dev": true
},
"@types/mkdirp": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"dev": true
},
"@types/readline-sync": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
"dev": true
},
"@types/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
"dev": true,
"requires": {
"@types/glob": "*",
"@types/node": "*"
}
},
"@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"dev": true
},
"acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"engine.io-client": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.2.0.tgz",
"integrity": "sha512-BcIBXGBkT7wKecwnfrSV79G2X5lSUSgeAGgoo60plXf8UsQEvCQww/KMwXSMhVjb98fFYNq20CC5eo8IOAPqsg==",
"requires": {
"base64-arraybuffer": "0.1.4",
"component-emitter": "~1.3.0",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.1",
"has-cors": "1.1.0",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~2.0.0",
"yeast": "0.1.2"
}
},
"engine.io-parser": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
"requires": {
"base64-arraybuffer": "0.1.4"
}
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"requires": {
"fs-monkey": "1.0.3"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
},
"parseuri": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"readline-sync": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"ts-node": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
"dev": true,
"requires": {
"@cspotcode/source-map-support": "0.7.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"yn": "3.1.1"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"typescript": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@@ -1,10 +1,10 @@
{
"name": "scrypted",
"version": "1.0.58",
"version": "1.0.67",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"main": "./dist/main.js",
"bin": {
"scrypted": "./dist/packages/cli/src/main.js"
"scrypted": "./dist/main.js"
},
"scripts": {
"prebuild": "rimraf dist",
@@ -16,25 +16,25 @@
"author": "",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",
"adm-zip": "^0.5.9",
"@scrypted/client": "^1.1.43",
"@scrypted/types": "^0.2.66",
"adm-zip": "^0.5.10",
"axios": "^0.21.4",
"engine.io-client": "^5.2.0",
"ip": "^1.1.8",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"mkdirp": "^1.0.4",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tslib": "^2.3.1"
"semver": "^7.3.8",
"tslib": "^2.5.0"
},
"devDependencies": {
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.14.2",
"@types/readline-sync": "^1.4.4",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"ts-node": "^10.2.1",
"typescript": "^4.8.2"
"@types/semver": "^7.3.13",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
}

View File

@@ -7,8 +7,8 @@ import readline from 'readline-sync';
import https from 'https';
import mkdirp from 'mkdirp';
import { installServe, serveMain } from './service';
import { connectScryptedClient } from '../../client/src/index';
import { ScryptedMimeTypes, FFmpegInput } from '../../../sdk/types/src/types.input';
import { connectScryptedClient } from '@scrypted/client';
import { ScryptedMimeTypes, FFmpegInput } from '@scrypted/types';
import semver from 'semver';
import child_process from 'child_process';
@@ -126,7 +126,7 @@ async function runCommand() {
if (!device)
throw new Error('device not found: ' + idOrName);
const method = process.argv[4];
const args = process.argv.slice(5).map(arg => () => {
const args = process.argv.slice(5).map(arg => {
try {
return JSON.parse(arg);
}

View File

@@ -1,15 +1,16 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"module": "commonjs",
"target": "ESNext",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
"sourceMap": true,
"declaration": true
"module": "commonjs",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
},
"include": [
"src/**/*"
"src/**/*"
],
}

View File

@@ -1,58 +1,29 @@
{
"name": "@scrypted/client",
"version": "1.1.40",
"lockfileVersion": 2,
"version": "1.1.43",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.40",
"version": "1.1.43",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.65",
"@scrypted/types": "^0.2.76",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"extraneous": true,
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
}
},
"../../sdk/types": {
"name": "@scrypted/types",
"version": "0.0.9",
"extraneous": true,
"license": "ISC",
"devDependencies": {}
},
"../common": {
"extraneous": true
},
"../sdk/types": {
"extraneous": true
},
"node_modules/@scrypted/types": {
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.65.tgz",
"integrity": "sha512-V/gfPy+xeRds6WMHwU6trt2YBkH9qcC/3Bx9q5hOxpE+rZSL4ru+nvlaumCRM3mSNWXBav4nbd23JCoGJ0F2eA=="
"version": "0.2.76",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.76.tgz",
"integrity": "sha512-/7n8ICkXj8TGba4cHvckLCgSNsOmOGQ8I+Jd8fX9sxkthgsZhF5At8PHhHdkCDS+yfSmfXHkcqluZZOfYPkpAg=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -69,9 +40,9 @@
}
},
"node_modules/@types/node": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/axios": {
@@ -99,12 +70,12 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -118,29 +89,29 @@
}
},
"node_modules/engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
@@ -159,17 +130,17 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -183,7 +154,7 @@
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -213,7 +184,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
@@ -221,7 +192,7 @@
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
@@ -241,9 +212,9 @@
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -256,12 +227,12 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
@@ -286,177 +257,5 @@
"node": ">=0.4.0"
}
}
},
"dependencies": {
"@scrypted/types": {
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.65.tgz",
"integrity": "sha512-V/gfPy+xeRds6WMHwU6trt2YBkH9qcC/3Bx9q5hOxpE+rZSL4ru+nvlaumCRM3mSNWXBav4nbd23JCoGJ0F2eA=="
},
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@types/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"dev": true
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"requires": {
"follow-redirects": "^1.14.7"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.40",
"version": "1.1.43",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -13,13 +13,13 @@
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
},
"dependencies": {
"@scrypted/types": "^0.2.65",
"@scrypted/types": "^0.2.76",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import { RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import axios, { AxiosRequestConfig } from 'axios';
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
@@ -504,7 +504,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
} = scrypted;
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async (data, mimeType, options) => {
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
const mo: MediaObjectRemote & {
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
@@ -520,7 +520,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
return data;
},
};
return mo;
return mo as any;
}
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "127.0.0.1",
"scrypted.debugHost": "10.10.0.50",
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.1.0",
"version": "0.2.0",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -21,11 +21,12 @@
"amazon"
],
"scrypted": {
"name": "Alexa Plugin",
"name": "Alexa",
"type": "API",
"interfaces": [
"HttpRequestHandler",
"MixinProvider"
"MixinProvider",
"Settings"
],
"pluginDependencies": [
"@scrypted/cloud",
@@ -33,14 +34,11 @@
]
},
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/server": "file:../../server"
"@types/node": "^18.4.2",
"@scrypted/sdk": "^0.2.70"
}
}

221
plugins/alexa/src/alexa.ts Normal file
View File

@@ -0,0 +1,221 @@
export declare type DisplayCategory = 'ACTIVITY_TRIGGER' | 'CAMERA' | 'CONTACT_SENSOR' | 'DOOR' | 'DOORBELL' | 'GARAGE_DOOR' | 'LIGHT' | 'MICROWAVE' | 'MOTION_SENSOR' | 'OTHER' | 'SCENE_TRIGGER' | 'SECURITY_PANEL' | 'SMARTLOCK' | 'SMARTPLUG' | 'SPEAKER' | 'SWITCH' | 'TEMPERATURE_SENSOR' | 'THERMOSTAT' | 'TV';
/*
COMMON DIRECTIVES AND RESPONSES
*/
export interface AddOrUpdateReport {
event: {
header: Header<"Alexa.Discovery", "AddOrUpdateReport">;
payload: AddOrUpdateReportPayload;
}
}
export interface DeleteReport {
event: {
header: Header<"Alexa.Discovery", "DeleteReport">;
payload: DeleteReportPayload;
}
}
export interface StateReport extends Report<"Alexa", "StateReport"> { }
export interface ChangeReport extends Report<"Alexa", "ChangeReport", ChangePayload> { }
export interface Response {
event: Event<"Alexa", "Response">;
context?: Context;
}
export interface DeferredResponse {
event: Event<"Alexa", "DeferredResponse", DeferredPayload>;
}
export interface ErrorResponse {
event: Event<"Alexa", "ErrorResponse", ErrorPayload>;
}
/*
DEVICE EVENTS
*/
export interface WebRTCAnswerGeneratedForSessionEvent extends Report<"Alexa.RTCSessionController", "AnswerGeneratedForSession", WebRTCAnswerGeneratedForSessionPayload> { }
export interface WebRTCSessionConnectedEvent extends Report<"Alexa.RTCSessionController", "SessionConnected", WebRTCSessionPayload> { }
export interface WebRTCSessionDisconnectedEvent extends Report<"Alexa.RTCSessionController", "SessionDisconnected", WebRTCSessionPayload> { }
export interface ObjectDetectionEvent extends Report<"Alexa.SmartVision.ObjectDetectionSensor", "ObjectDetection", ObjectDetectionPayload> { }
export interface DoorbellPressEvent extends Report<"Alexa.DoorbellEventSource", "DoorbellPress", DoorbellPressPayload> { }
/*
IMPLIMENTATION TYPES
*/
export interface Header<NS = string, N = string> {
namespace: NS;
name: N;
messageId: string;
correlationToken?: string;
payloadVersion: string;
}
export interface Scope {
type: string;
token: string;
partition?: string;
userId?: string;
}
export interface Endpoint {
endpointId: string;
scope?: Scope;
cookie?: any;
}
export interface Payload { }
export interface Directive<NS = string, N = string, P = Payload> {
header: Header<NS, N>;
endpoint: Endpoint;
payload: P;
}
export interface Event<NS = string, N = string, P = Payload> {
header: Header<NS, N>;
endpoint: Endpoint;
payload: P;
}
export interface Property {
namespace: string;
instance?: string;
name: string;
value: any;
timeOfSample: string;
uncertaintyInMilliseconds?: number;
}
export interface Context {
properties: Property[];
}
export interface Report<NS = string, N = string, P = Payload> {
event: Event<NS, N, P>;
context: Context;
}
export interface DeferredPayload {
estimatedDeferralInSeconds: number;
}
export interface ErrorPayload {
type: string;
message: string;
}
export interface ChangePayload {
change: {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION" | "RULE_TRIGGER";
},
properties: Property[];
}
}
export interface WebRTCSessionPayload {
sessionId: string;
}
export interface WebRTCAnswerGeneratedForSessionPayload {
answer: {
format: string;
value: string;
}
}
export interface ObjectDetectionPayloadEvent {
eventIdenifier: string;
imageNetClass: string;
timeOfSample: string;
uncertaintyInMilliseconds: number;
objectIdentifier: string;
frameImageUri: string;
croppedImageUri: string;
}
export interface ObjectDetectionPayload {
events: ObjectDetectionPayloadEvent[]
}
export interface DoorbellPressPayload {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION";
},
timestamp: string;
}
export interface DiscoveryProperty {
supported: any[];
proactivelyReported: boolean;
retrievable: boolean;
}
export interface DiscoveryCapability {
type: string;
interface: string;
instance?: string;
version: string;
properties?: DiscoveryProperty;
capabilityResources?: any;
configuration?: any;
semantics?: any;
}
export interface DiscoveryEndpoint {
endpointId: string;
manufacturerName: string;
description: string;
friendlyName: string;
displayCategories: DisplayCategory[];
additionalAttributes?: {
"manufacturer"?: string;
"model"?: string;
"serialNumber"?: string;
"firmwareVersion"? : string;
"softwareVersion"?: string;
"customIdentifier"?: string;
};
capabilities?: DiscoveryCapability[];
connections?: any[];
relationships?: any;
cookie?: any;
}
export interface DiscoverPayload {
endpoints: DiscoveryEndpoint[]
}
export interface Discovery {
event: {
header: Header<"Alexa.Discovery", "Discover.Response">;
payload: DiscoverPayload;
}
}
export interface AddOrUpdateReportPayload {
endpoints: DiscoveryEndpoint[]
scope: Scope;
}
export interface DeleteReportEndpoint {
endpointId: string;
}
export interface DeleteReportPayload {
endpoints: DeleteReportEndpoint[]
scope: Scope;
}

131
plugins/alexa/src/common.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Battery, Online, PowerSensor, ScryptedDevice, ScryptedInterface, HttpResponse } from "@scrypted/sdk";
import { v4 as createMessageId } from 'uuid';
export interface AlexaHttpResponse extends HttpResponse {
send(body: any, options?: any): void;
}
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
if (!device.interfaces.includes(ScryptedInterface.Online))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": device.online ? "OK" : "UNREACHABLE",
"reason": device.online ? undefined : "INTERNET_UNREACHABLE"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
if (!device.interfaces.includes(ScryptedInterface.Battery))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
const lowPower = device.batteryLevel < 20;
let health = undefined;
if (lowPower) {
health = {
"state": "WARNING",
"reasons": [
"LOW_CHARGE"
]
};
}
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "battery",
"value": {
health,
"levelPercentage": device.batteryLevel,
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function authErrorResponse(errorType: string, errorMessage: string, directive: any): any {
const { header } = directive;
const data = {
"event": {
header,
"payload": {
"type": errorType,
"message": errorMessage
}
}
};
data.event.header.name = "ErrorResponse";
data.event.header.messageId = createMessageId();
return data;
}
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-errorresponse.html#error-types
export function deviceErrorResponse (errorType: string, errorMessage: string, directive: any): any{
const { header, endpoint } = directive;
const data = {
"event": {
header,
endpoint,
"payload": {
"type": errorType,
"message": errorMessage
}
}
};
data.event.header.name = "ErrorResponse";
data.event.header.messageId = createMessageId();
return data;
}
export function mirroredResponse (directive: any): any {
const { header, endpoint, payload } = directive;
const data = {
"event": {
header,
endpoint,
payload
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
return data;
}
export function sendDeviceResponse(data: any, response: any, device: ScryptedDevice) {
data = addBattery(data, device);
data = addOnline(data, device);
response.send(data);
}

View File

@@ -0,0 +1,34 @@
import { HttpRequest, ScryptedDevice } from "@scrypted/sdk";
import { AlexaHttpResponse, sendDeviceResponse } from "./common";
import { supportedTypes } from "./types";
import { v4 as createMessageId } from 'uuid';
import { Directive, StateReport } from "./alexa";
export type AlexaHandler = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive) => Promise<void>
export type AlexaDeviceHandler<T> = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
export const alexaDeviceHandlers = new Map<string, AlexaDeviceHandler<any>>();
export const alexaHandlers = new Map<string, AlexaHandler>();
alexaDeviceHandlers.set('Alexa/ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const report = await supportedType.sendReport(device);
let data = {
"event": {
header,
endpoint,
payload
},
context: report?.context
} as StateReport;
data.event.header.name = "StateReport";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
});

View File

@@ -1,18 +1,21 @@
import axios from 'axios';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
import sdk, { HttpRequest, HttpRequestHandler, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, EventDetails, Setting, SettingValue, Settings, HttpResponseOptions, HttpResponse } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
import { isSupported } from './types';
import { DiscoveryEndpoint, DiscoverEvent } from 'alexa-smarthome-ts';
import { AlexaHandler, addBattery, addOnline, addPowerSensor, capabilityHandlers, supportedTypes } from './types/common';
import { createMessageId } from './message';
import { addBattery, addOnline, deviceErrorResponse, mirroredResponse, authErrorResponse, AlexaHttpResponse } from './common';
import { supportedTypes } from './types';
import { v4 as createMessageId } from 'uuid';
import { ChangeReport, Discovery, DiscoveryEndpoint } from './alexa';
import { alexaHandlers, alexaDeviceHandlers } from './handlers';
const { systemManager, deviceManager } = sdk;
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
const includeToken = 4;
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
export let DEBUG = false;
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
hide: true,
@@ -22,6 +25,10 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
multiple: true,
hide: true
},
defaultIncluded: {
hide: true,
json: true
},
apiEndpoint: {
title: 'Alexa Endpoint',
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
@@ -30,88 +37,169 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
});
handlers = new Map<string, AlexaHandler>();
accessToken: Promise<string>;
validAuths = new Set<string>();
devices = new Map<string, ScryptedDevice>();
constructor(nativeId?: string) {
super(nativeId);
this.handlers.set('Alexa.Authorization', this.alexaAuthorization);
this.handlers.set('Alexa.Discovery', this.alexaDiscovery);
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
this.syncDevices();
this.start();
}
systemManager.listen(async (eventSource, eventDetails, eventData) => {
if (!eventSource)
return;
async start() {
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
await this.tryEnableMixin(device);
}
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType) {
this.console.warn(`${eventSource.name} no longer supported type?`);
return;
systemManager.listen((async (eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) => {
const status = await this.tryEnableMixin(eventSource);
// sync new devices when added or removed
if (status === DeviceMixinStatus.Setup)
await this.syncEndpoints();
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
if (!this.devices.has(eventSource.id)) {
this.devices.set(eventSource.id, eventSource);
eventSource.listen(ScryptedInterface.ObjectDetector, this.deviceListen.bind(this));
}
this.deviceListen(eventSource, eventDetails, eventData);
}
}).bind(this));
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
let data = {
"event": {
"header": {
"messageId": createMessageId(),
"namespace": report?.namespace ?? "Alexa",
"name": report?.name ?? "ChangeReport",
"payloadVersion": "3"
},
"endpoint": {
"endpointId": eventSource.id,
"scope": undefined
},
"payload": report?.payload,
await this.syncEndpoints();
}
private async tryEnableMixin(device: ScryptedDevice): Promise<DeviceMixinStatus> {
if (!device)
return DeviceMixinStatus.NotSupported;
const mixins = (device.mixins || []).slice();
if (mixins.includes(this.id))
return DeviceMixinStatus.AlreadySetup;
const defaultIncluded = this.storageSettings.values.defaultIncluded || {};
if (defaultIncluded[device.id] === includeToken)
return DeviceMixinStatus.AlreadySetup;
if (!supportedTypes.has(device.type))
return DeviceMixinStatus.NotSupported;
mixins.push(this.id);
const plugins = await systemManager.getComponent('plugins');
await plugins.setMixins(device.id, mixins);
defaultIncluded[device.id] = includeToken;
this.storageSettings.values.defaultIncluded = defaultIncluded;
return DeviceMixinStatus.Setup;
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
const available = supportedTypes.has(type);
if (available)
return [];
return;
}
async getMixin(device: ScryptedDevice, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): Promise<any> {
return device;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
const device = systemManager.getDeviceById(id);
const mixins = (device.mixins || []).slice();
if (mixins.includes(this.id))
return;
this.log.i(`Device removed from Alexa: ${device.name}. Requesting sync.`);
await this.syncEndpoints();
}
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
if (!eventSource)
return;
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice)
return;
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType)
return;
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
let data = {
"event": {
"header": {
"messageId": createMessageId(),
"namespace": report?.event?.header?.namespace ?? "Alexa",
"name": report?.event?.header?.name ?? "ChangeReport",
"payloadVersion": "3"
},
"context": report?.context
}
"endpoint": {
"endpointId": eventSource.id,
},
payload: report?.event?.payload
},
context: report?.context
} as ChangeReport;
data = addOnline(data, eventSource);
data = addBattery(data, eventSource);
data = addPowerSensor(data, eventSource);
data = addOnline(data, eventSource);
data = addBattery(data, eventSource);
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
const accessToken = await this.getAccessToken();
data.event.endpoint.scope = {
"type": "BearerToken",
"token": accessToken,
};
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
data = await this.addAccessToken(data);
await this.postEvent(data);
});
await this.postEvent(data);
}
private async addAccessToken(data: any) : Promise<any> {
const accessToken = await this.getAccessToken();
if (data.event === undefined)
data.event = {};
if (data.event.endpoint === undefined)
data.event.endpoint = [];
data.event.endpoint.scope = {
"type": "BearerToken",
"token": accessToken,
};
return data;
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
return mixinDevice;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
const device = systemManager.getDeviceById(id);
if (device.mixins?.includes(this.id)) {
return;
}
this.console.log('release mixin', id);
this.log.a(`${device.name} was removed. The Alexa plugin will reload momentarily.`);
deviceManager.requestRestart();
}
readonly endpoints: string[] = [
'api.amazonalexa.com',
'api.eu.amazonalexa.com',
@@ -146,6 +234,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
const endpoint = await this.getAlexaEndpoint();
const self = this;
this.console.assert(!DEBUG, `event:`, data);
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
'Authorization': 'Bearer ' + accessToken,
@@ -160,25 +250,59 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
});
}
async syncDevices() {
const endpoints = await this.addOrUpdateReport();
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
const endpoints: DiscoveryEndpoint[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
if (!device.mixins?.includes(this.id))
continue;
const endpoint = await this.getEndpointForDevice(device);
if (endpoint)
endpoints.push(endpoint);
}
return endpoints;
}
async onDiscoverEndpoints(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
const endpoints = await this.getEndpoints();
const data = {
"event": {
"header": {
"namespace": 'Alexa.Discovery',
"name": 'Discover.Response',
"payloadVersion": '3',
"messageId": createMessageId()
},
"payload": {
endpoints
}
}
} as Discovery;
response.send(data);
await this.saveEndpoints(endpoints);
}
async addOrUpdateReport() {
const endpoints = this.getDiscoveryEndpoints();
async syncEndpoints() {
const endpoints = await this.getEndpoints();
if (!endpoints.length)
return [];
return [];
const accessToken = await this.getAccessToken();
await this.postEvent({
const data = {
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "AddOrUpdateReport",
"payloadVersion": "3",
"messageId": createMessageId(),
"messageId": createMessageId()
},
"payload": {
endpoints,
@@ -188,12 +312,35 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
}
}
});
};
return endpoints;
await this.postEvent(data);
await this.saveEndpoints(endpoints);
}
async deleteReport(...ids: string[]) {
async saveEndpoints(endpoints: DiscoveryEndpoint[]) {
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
const deleted = new Set(existingEndpoints);
for (const id of newEndpoints) {
deleted.delete(id);
}
const all = new Set([...existingEndpoints, ...newEndpoints]);
// save all the endpoints
this.storageSettings.values.syncedDevices = [...all];
// delete leftover endpoints
await this.deleteEndpoints(...deleted);
// prune if the delete report completed successfully
this.storageSettings.values.syncedDevices = newEndpoints;
}
async deleteEndpoints(...ids: string[]) {
if (!ids.length)
return;
@@ -219,17 +366,6 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
})
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
const discovery = isSupported({
type,
interfaces,
} as any);
if (!discovery)
return;
return [];
}
getAccessToken(): Promise<string> {
if (this.accessToken)
return this.accessToken;
@@ -306,9 +442,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
return this.accessToken;
}
async alexaAuthorization(request: HttpRequest, response: HttpResponse) {
const json = JSON.parse(request.body);
const { grant } = json.directive.payload;
async onAlexaAuthorization(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
const { grant } = directive.payload;
this.storageSettings.values.tokenInfo = grant;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
@@ -321,27 +456,14 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
response.send(JSON.stringify({
"event": {
"header": {
"namespace": "Alexa.Authorization",
"name": "ErrorResponse",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
"type": "ACCEPT_GRANT_FAILED",
"message": `Failed to handle the AcceptGrant directive because ${reason}`
}
}
}));
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
return undefined;
});
if (accessToken !== undefined) {
try {
response.send(JSON.stringify({
response.send({
"event": {
"header": {
"namespace": "Alexa.Authorization",
@@ -351,7 +473,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
},
"payload": {}
}
}));
});
} catch (error) {
this.console.error(`AcceptGrant.Response failed because ${error}`);
@@ -363,14 +485,15 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
}
createEndpoint(device: ScryptedDevice): DiscoveryEndpoint<any> {
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
if (!device)
return;
const discovery = isSupported(device);
const discovery = await supportedTypes.get(device.type)?.discover(device);
if (!discovery)
return;
const ret = Object.assign({
const data: DiscoveryEndpoint = {
endpointId: device.id,
manufacturerName: "Scrypted",
description: `${device.info?.manufacturer ?? 'Unknown'} ${device.info?.model ?? `device of type ${device.type}`}, connected via Scrypted`,
@@ -380,13 +503,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
model: device.info?.model || undefined,
serialNumber: device.info?.serialNumber || undefined,
firmwareVersion: device.info?.firmware || undefined,
//softwareVersion: device.info?.version || undefined
}
}, discovery);
softwareVersion: device.info?.version || undefined
},
displayCategories: discovery.displayCategories,
capabilities: discovery.capabilities
};
let supportedEndpointHealths: any[] = [];
if (device.interfaces.includes(ScryptedInterface.Online)) {
supportedEndpointHealths.push({
"name": "connectivity"
});
}
let supportedEndpointHealths = [{
"name": "connectivity"
}];
// {
// "name": "radioDiagnostics"
// },
@@ -400,17 +530,22 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
})
}
ret.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.2" as any,
"properties": {
"supported": supportedEndpointHealths,
"proactivelyReported": true,
"retrievable": true
if (supportedEndpointHealths.length > 0) {
data.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.2",
"properties": {
"supported": supportedEndpointHealths,
"proactivelyReported": true,
"retrievable": true
}
}
},
);
}
data.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa",
@@ -418,76 +553,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
);
//if (device.info?.mac !== undefined)
// ret.connections.push(
// {
// "type": "TCP_IP",
// "macAddress": device.info?.mac || undefined
// }
// );
return ret as any;
}
async saveEndpoints(endpoints: DiscoveryEndpoint<any>[]) {
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
const deleted = new Set(existingEndpoints);
for (const id of newEndpoints) {
deleted.delete(id);
}
const all = new Set([...existingEndpoints, ...newEndpoints]);
// save all the endpoints
this.storageSettings.values.syncedDevices = [...all];
// delete leftover endpoints
await this.deleteReport(...deleted);
// prune if the delete report completed successfully
this.storageSettings.values.syncedDevices = newEndpoints;
}
getDiscoveryEndpoints() {
const endpoints: DiscoveryEndpoint<any>[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
if (!device.mixins?.includes(this.id))
continue;
const endpoint = this.createEndpoint(device);
if (endpoint)
endpoints.push(endpoint);
}
return endpoints;
}
async alexaDiscovery(request: HttpRequest, response: HttpResponse) {
const endpoints = this.getDiscoveryEndpoints();
const ret: DiscoverEvent<any> = {
event: {
header: {
namespace: 'Alexa.Discovery',
name: 'Discover.Response',
messageId: createMessageId(),
payloadVersion: '3',
},
payload: {
endpoints,
if (device.info?.mac !== undefined)
data.connections = [
{
"type": "TCP_IP",
"macAddress": device.info.mac
}
}
}
];
response.send(JSON.stringify(ret));
this.saveEndpoints(endpoints);
return data as any;
}
async onRequest(request: HttpRequest, response: HttpResponse) {
async onRequest(request: HttpRequest, rawResponse: HttpResponse) {
const response = new HttpResponseLoggingImpl(rawResponse, this.console);
const { authorization } = request.headers;
if (!this.validAuths.has(authorization)) {
try {
@@ -501,42 +580,81 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
catch (e) {
this.console.error(`request failed due to invalid authorization`, e);
response.send(e.message, {
code: 500
code: 500,
});
return;
}
}
try {
const body = JSON.parse(request.body);
const { directive } = body;
const { namespace } = directive.header;
const handler = this.handlers.get(namespace);
if (handler)
return handler.apply(this, arguments);
const body = JSON.parse(request.body);
const { directive } = body;
const { namespace, name } = directive.header;
const capHandler = capabilityHandlers.get(namespace);
if (capHandler) {
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
if (!device) {
response.send('Not Found', {
code: 404,
});
return;
}
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
return capHandler.apply(this, [request, response, directive, device]);
const mapName = `${namespace}/${name}`;
const handler = alexaHandlers.get(mapName);
if (handler)
return handler.apply(this, [request, response, directive]);
const deviceHandler = alexaDeviceHandlers.get(mapName);
if (deviceHandler) {
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
if (!device) {
response.send(deviceErrorResponse("NO_SUCH_ENDPOINT", "The device doesn't exist in Scrypted", directive));
return;
}
response.send('Not Found', {
code: 404,
});
}
catch (e) {
response.send(e.message, {
code: 500,
});
return deviceHandler.apply(this, [request, response, directive, device]);
} else {
this.console.error(`no handler for: ${mapName}`);
}
// it is better to send a non-specific response than an error, as the API might get rate throttled
response.send(mirroredResponse(directive));
}
}
enum DeviceMixinStatus {
NotSupported = 0,
Setup = 1,
AlreadySetup = 2
}
class HttpResponseLoggingImpl implements AlexaHttpResponse {
constructor(private response: HttpResponse, private console: Console) {
}
send(body: string): void;
send(body: string, options: HttpResponseOptions): void;
send(body: Buffer): void;
send(body: Buffer, options: HttpResponseOptions): void;
send(body: any, options?: any): void {
if (!options)
options = {};
if (!options.code)
options.code = 200;
if (options.code !== 200)
this.console.error(`response error ${options.code}:`, body);
else
this.console.assert(!DEBUG, `response ${options.code}:`, body);
if (typeof body === 'object')
body = JSON.stringify(body);
this.response.send(body, options);
}
sendFile(path: string): void;
sendFile(path: string, options: HttpResponseOptions): void;
sendFile(path: any, options?: any): void {
this.response.sendFile(path, options);
}
sendSocket(socket: any, options: HttpResponseOptions): void {
this.response.sendSocket(socket, options);
}
}

View File

@@ -1,5 +0,0 @@
import {v4 as uuidv4} from 'uuid';
export function createMessageId() {
return uuidv4();
}

View File

@@ -1,190 +1,24 @@
import { HttpResponse, MotionSensor, RTCAVSignalingSetup, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VideoCamera } from "@scrypted/sdk";
import { addSupportedType, AlexaCapabilityHandler, capabilityHandlers, EventReport, StateReport } from "./common";
import { createMessageId } from "../message";
import { Capability } from "alexa-smarthome-ts/lib/skill/Capability";
import { DisplayCategory } from "alexa-smarthome-ts";
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, Report } from "../alexa";
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
import { supportedTypes } from ".";
export function getCameraCapabilities(device: ScryptedDevice): Capability<any>[] {
const capabilities: Capability<any>[] = [
{
"type": "AlexaInterface",
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
}
} as any,
];
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
)
}
return capabilities;
}
addSupportedType(ScryptedDeviceType.Camera, {
probe(device) {
supportedTypes.set(ScryptedDeviceType.Camera, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel))
return;
const capabilities = getCameraCapabilities(device);
const capabilities = await getCameraCapabilities(device);
return {
displayCategories: ['CAMERA'],
capabilities
}
},
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport> {
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
};
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
return reportCameraState(device);
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor, eventDetails, eventData): Promise<EventReport> {
if (eventDetails.eventInterface !== ScryptedInterface.MotionSensor)
return undefined;
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
},
};
sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
return sendCameraEvent(eventSource, eventDetails, eventData);
}
});
export const rtcHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: HttpResponse, public directive: any) {
}
async getOptions(): Promise<RTCSignalingOptions> {
return {
proxy: true,
offer: {
type: 'offer',
sdp: this.directive.payload.offer.value,
},
disableTrickle: true,
// this could be a low resolution screen, no way of knowing, so never send a
// 1080p+ stream.
screen: {
devicePixelRatio: 1, // TODO: get this from the device
width: 1280,
height: 720,
}
}
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
return {
type: 'offer',
sdp: this.directive.payload.offer.value,
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
throw new Error("Alexa does not support trickle ICE");
}
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
this.response.send(JSON.stringify({
"event": {
"header": {
"namespace": "Alexa.RTCSessionController",
"name": "AnswerGeneratedForSession",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
"answer": {
"format": "SDP",
"value": description.sdp,
}
}
}
}));
}
}
rtcHandlers.set('InitiateSessionWithOffer', async (request, response, directive: any,
device: ScryptedDevice & RTCSignalingChannel) => {
const session = new AlexaSignalingSession(response, directive);
const control = await device.startRTCSignalingSession(session);
control.setPlayback({
audio: true,
video: false,
})
});
capabilityHandlers.set('Alexa.RTCSessionController', async (request, response, directive: any, device: ScryptedDevice & VideoCamera) => {
const { name } = directive.header;
const handler = rtcHandlers.get(name);
if (handler)
return handler.apply(this, [request, response, directive, device]);
const { sessionId } = directive.payload;
const body = {
"event": {
"header": {
"namespace": "Alexa.RTCSessionController",
name,
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
sessionId,
}
}
};
response.send(JSON.stringify(body));
});

View File

@@ -0,0 +1,194 @@
import sdk, { MediaObject, MotionSensor, ObjectDetector, ScryptedDevice, ScryptedInterface } from "@scrypted/sdk";
import { ChangeReport, DiscoveryCapability, ObjectDetectionEvent, Report, StateReport, Property } from "../../alexa";
const { mediaManager } = sdk;
export async function reportCameraState(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
data.context.properties.push({
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": classNames.map(type => ({
"imageNetClass": type
})),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
data.context.properties.push({
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
};
export async function sendCameraEvent (eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.ObjectDetector) {
// ring and motion are not valid objects
if (eventData.detections.has('ring') || eventData.detections.has('motion'))
return undefined;
console.debug('ObjectDetector event', eventData);
let mediaObj: MediaObject = undefined;
let frameImageUri: string = undefined;
try {
mediaObj = await eventSource.getDetectionInput(eventData.detectionId, eventData.eventId);
frameImageUri = await mediaManager.convertMediaObjectToUrl(mediaObj, 'image/jpeg');
} catch (e) { }
let data = {
event: {
header: {
namespace: 'Alexa.SmartVision.ObjectDetectionSensor',
name: 'ObjectDetection'
},
payload: {
"events": [eventData.detections.map(detection => {
let event = {
"eventIdentifier": eventData.eventId,
"imageNetClass": detection.className,
"timeOfSample": new Date(eventData.timestamp).toISOString(),
"uncertaintyInMilliseconds": 500
};
if (detection.id) {
event["objectIdentifier"] = detection.id;
}
if (frameImageUri) {
event["frameImageUri"] = frameImageUri;
}
return event;
})]
}
}
} as Partial<ObjectDetectionEvent>;
return data;
}
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
},
}
} as Partial<ChangeReport>;
return undefined;
};
export async function getCameraCapabilities(device: ScryptedDevice): Promise<DiscoveryCapability[]> {
const capabilities = [
{
"type": "AlexaInterface",
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
}
} as DiscoveryCapability
];
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.SmartVision.ObjectDetectionSensor",
"version": "1.0",
"properties": {
"supported": [{
"name": "objectDetectionClasses"
}],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"objectDetectionConfiguration": classNames.map(type => ({
"imageNetClass": type
}))
}
} as DiscoveryCapability
);
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DataController",
"instance": "Camera.SmartVisionData",
"version": "1.0",
"properties": undefined,
"configuration": {
"targetCapability": {
"name": "Alexa.SmartVision.ObjectDetectionSensor",
"version": "1.0"
},
"dataRetrievalSchema": {
"type": "JSON",
"schema": "SmartVisionData"
},
"supportedAccess": ["BY_IDENTIFIER", "BY_TIMESTAMP_RANGE"]
}
} as DiscoveryCapability
);
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
}
return capabilities;
};

View File

@@ -0,0 +1,152 @@
import { ObjectDetector, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { v4 as createMessageId } from 'uuid';
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: AlexaHttpResponse, public directive: any) {
}
async getOptions(): Promise<RTCSignalingOptions> {
return {
proxy: true,
offer: {
type: 'offer',
sdp: this.directive.payload.offer.value,
},
disableTrickle: true,
}
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
return {
type: type,
sdp: this.directive.payload.offer.value,
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
throw new Error("Alexa does not support trickle ICE");
}
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
const { header, endpoint, payload } = this.directive;
const data: WebRTCAnswerGeneratedForSessionEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.name = "AnswerGeneratedForSession";
data.event.header.messageId = createMessageId();
data.event.payload.answer = {
format: 'SDP',
value: description.sdp,
};
this.response.send(data);
}
}
const sessionCache = new Map<string, RTCSessionControl>();
alexaDeviceHandlers.set('Alexa.RTCSessionController/InitiateSessionWithOffer', async (request, response, directive: any, device: ScryptedDevice & RTCSignalingChannel) => {
const { header, endpoint, payload } = directive;
const { sessionId } = payload;
const session = new AlexaSignalingSession(response, directive);
const control = await device.startRTCSignalingSession(session);
control.setPlayback({
audio: true,
video: false,
})
sessionCache.set(sessionId, control);
});
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (request, response, directive: any, device: ScryptedDevice) => {
const { header, endpoint, payload } = directive;
const data: WebRTCSessionConnectedEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async (request, response, directive: any, device: ScryptedDevice) => {
const { header, endpoint, payload } = directive;
const { sessionId } = payload;
const session = sessionCache.get(sessionId);
if (session) {
sessionCache.delete(sessionId);
await session.endSession();
}
const data: WebRTCSessionDisconnectedEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetectionClasses', async (request, response, directive: any, device: ScryptedDevice & ObjectDetector) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const detectionTypes = await device.getObjectTypes();
const data: Response = {
"event": {
header,
endpoint,
payload: {}
},
"context": {
"properties": [{
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": detectionTypes.classes.map(type => ({
"imageNetClass": type
})),
timeOfSample: new Date().toISOString(),
uncertaintyInMilliseconds: 0
}]
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
});

View File

@@ -1,180 +0,0 @@
import { Battery, EventDetails, HttpRequest, HttpResponse, Online, PowerSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import {DiscoveryEndpoint, Directive} from 'alexa-smarthome-ts';
import { createMessageId } from "../message";
export type AlexaHandler = (request: HttpRequest, response: HttpResponse, directive: Directive) => Promise<void>
export type AlexaCapabilityHandler<T> = (request: HttpRequest, response: HttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
export const capabilityHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export const alexaHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export interface EventReport {
type: 'event';
payload?: any;
context?: any;
namespace?: string;
name?: string;
}
export interface StateReport {
type: 'state';
payload?: any;
context?: any;
namespace?: string;
name?: string;
}
export interface SupportedType {
probe(device: ScryptedDevice): Partial<DiscoveryEndpoint<any>>;
sendEvent(eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<EventReport>;
reportState(device: ScryptedDevice): Promise<StateReport>;
}
export function addSupportedType(type: ScryptedDeviceType, supportedType: SupportedType) {
supportedTypes.set(type, supportedType);
}
export function isSupported(device: ScryptedDevice) {
return supportedTypes.get(device.type)?.probe(device);
}
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
if (!device.interfaces.includes(ScryptedInterface.Online))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": device.online ? "OK" : "UNREACHABLE",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addPowerSensor(data: any, device: ScryptedDevice & PowerSensor) : any {
if (!device.interfaces.includes(ScryptedInterface.PowerSensor))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": device.powerDetected ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
if (!device.interfaces.includes(ScryptedInterface.Battery))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
const lowPower = device.batteryLevel < 20;
let health = undefined;
if (lowPower) {
health = {
"state": "WARNING",
"reasons": [
"LOW_CHARGE"
]
};
}
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "battery",
"value": {
health,
"levelPercentage": device.batteryLevel,
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
function sendResponse(data: any, response: any, device: ScryptedDevice) {
data = addBattery(data, device);
data = addOnline(data, device);
data = addPowerSensor(data, device);
response.send(JSON.stringify(data));
}
alexaHandlers.set('ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint } = directive;
const report = await supportedType.reportState(device);
if (report.type === 'state') {
const data = {
"event": {
header,
endpoint,
payload: report.payload,
},
"context": report.context
};
data.event.header.name = "StateReport";
data.event.header.messageId = createMessageId();
sendResponse(data, response, device);
}
});
capabilityHandlers.set('Alexa', async (request, response, directive: any, device: ScryptedDevice) => {
const { name } = directive.header;
let handler = alexaHandlers.get(name);
if (handler)
return handler.apply(this, [request, response, directive, device]);
const { header, endpoint, payload } = directive;
const data = {
"event": {
header,
endpoint,
payload
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendResponse(data, response, device);
});

View File

@@ -1,81 +1,57 @@
import { BinarySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities } from "./camera";
import { addSupportedType, EventReport, StateReport } from "./common";
import { DisplayCategory } from "alexa-smarthome-ts";
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
import { DiscoveryEndpoint, DisplayCategory, Report, DoorbellPressEvent } from "../alexa";
import { supportedTypes } from ".";
addSupportedType(ScryptedDeviceType.Doorbell, {
probe(device) {
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel) || !device.interfaces.includes(ScryptedInterface.BinarySensor))
return;
supportedTypes.set(ScryptedDeviceType.Doorbell, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
let capabilities: any[] = [];
let category: DisplayCategory = 'DOORBELL';
const capabilities = getCameraCapabilities(device);
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
} as any,
);
return {
displayCategories: ['CAMERA'],
capabilities
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
capabilities = await getCameraCapabilities(device);
category = 'CAMERA';
}
},
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport>{
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
} as any,
);
}
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
displayCategories: [category],
capabilities
};
},
async sendEvent(eventSource: ScryptedDevice, eventDetails, eventData): Promise<EventReport> {
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
},
};
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
return reportCameraState(device);
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
let response = await sendCameraEvent(eventSource, eventDetails, eventData);
if (response)
return response;
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
return {
type: 'event',
namespace: 'Alexa.DoorbellEventSource',
name: 'DoorbellPress',
payload: {
"cause": {
"type": "PHYSICAL_INTERACTION"
event: {
header: {
namespace: 'Alexa.DoorbellEventSource',
name: 'DoorbellPress'
},
"timestamp": new Date().toISOString(),
}
};
payload: {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"timestamp": new Date(eventDetails.eventTime).toISOString(),
}
}
} as Partial<DoorbellPressEvent>;
}
});

View File

@@ -1,14 +1,13 @@
import { BinarySensor, Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities } from "./camera";
import { addSupportedType, EventReport, StateReport } from "./common";
import { DisplayCategory } from "alexa-smarthome-ts";
import { Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report } from "../alexa";
import { supportedTypes } from ".";
addSupportedType(ScryptedDeviceType.Garage, {
probe(device) {
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
supportedTypes.set(ScryptedDeviceType.Garage, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
return;
const capabilities = getCameraCapabilities(device);
const capabilities: DiscoveryCapability[] = [];
capabilities.push(
{
"type": "AlexaInterface",
@@ -115,19 +114,16 @@ addSupportedType(ScryptedDeviceType.Garage, {
}
]
}
} as any,
},
);
return {
displayCategories: ['GARAGE_DOOR'] as any,
displayCategories: ['GARAGE_DOOR'],
capabilities
}
},
async reportState(eventSource: ScryptedDevice & EntrySensor): Promise<StateReport> {
async sendReport(eventSource: ScryptedDevice & EntrySensor): Promise<Partial<Report>> {
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
@@ -142,14 +138,12 @@ addSupportedType(ScryptedDeviceType.Garage, {
}
};
},
async sendEvent(eventSource: ScryptedDevice & EntrySensor, eventDetails, eventData): Promise<EventReport> {
async sendEvent(eventSource: ScryptedDevice & Entry & EntrySensor, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.EntrySensor)
return undefined;
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
event: {
payload: {
change: {
cause: {
@@ -167,6 +161,30 @@ addSupportedType(ScryptedDeviceType.Garage, {
]
}
},
};
}
}
} as Partial<ChangeReport>;
},
async setState(eventSource: ScryptedDevice & Entry & EntrySensor, payload: any): Promise<Partial<Report>> {
if (payload.mode === 'Position.Up') {
await eventSource.openEntry();
}
else if (payload.mode === 'Position.Down') {
await eventSource.closeEntry();
}
return {
context: {
"properties": [
{
"namespace": "Alexa.ModeController",
"instance": "GarageDoor.Position",
"name": "mode",
"value": payload.mode,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
};
}
});

View File

@@ -0,0 +1,31 @@
import { ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { v4 as createMessageId } from 'uuid';
import { Response } from "../../alexa";
async function sendResponse (request, response, directive: any, device: ScryptedDevice) {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const report = await supportedType.setState(device, payload);
const data = {
"event": {
header,
endpoint,
payload
},
context: report?.context
} as Response;
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.ModeController/SetMode', sendResponse);
alexaDeviceHandlers.set('Alexa.ModeController/AdjustMode', sendResponse);

View File

@@ -1,6 +1,21 @@
import { ScryptedDeviceType, ScryptedDevice, EventDetails } from '@scrypted/sdk';
import { DiscoveryEndpoint, Report } from '../alexa';
export interface SupportedType {
discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>>;
sendEvent(device: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<Partial<Report>>;
sendReport(device: ScryptedDevice): Promise<Partial<Report>>;
setState?(device: ScryptedDevice, payload: any): Promise<Partial<Report>>;
}
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
import '../handlers';
import './camera';
import './camera/handlers';
import './doorbell';
import './garagedoor';
export { isSupported} from './common';
import './switch';
import './switch/handlers';
import './sensor';
import './securitysystem';

View File

@@ -0,0 +1,179 @@
import { EventDetails, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SecuritySystem, SecuritySystemMode } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
import { supportedTypes } from ".";
function getArmState(mode: SecuritySystemMode): string {
switch(mode) {
case SecuritySystemMode.AwayArmed:
return 'ARMED_AWAY';
case SecuritySystemMode.HomeArmed:
return 'ARMED_STAY';
case SecuritySystemMode.NightArmed:
return 'ARMED_NIGHT';
case SecuritySystemMode.Disarmed:
return 'DISARMED';
}
}
supportedTypes.set(ScryptedDeviceType.SecuritySystem, {
async discover(device: ScryptedDevice & SecuritySystem): Promise<Partial<DiscoveryEndpoint>> {
const capabilities: DiscoveryCapability[] = [];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.SecuritySystem)) {
const supportedModes = device.securitySystemState.supportedModes;
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.SecurityPanelController",
"version": "3",
"properties": {
"supported": [
{
"name": "armState"
},
{
"name": "burglaryAlarm"
},
//{
// "name": "waterAlarm"
//},
//{
// "name": "fireAlarm"
//},
//{
// "name": "carbonMonoxideAlarm"
//}
],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"supportedArmStates": supportedModes.map(mode => {
return {
"value": getArmState(mode)
}
}),
"supportedAuthorizationTypes": [
{
"type": "FOUR_DIGIT_PIN"
}
]
}
} as DiscoveryCapability
);
displayCategories.push('SECURITY_PANEL');
}
if (capabilities.length === 0)
return;
return {
displayCategories,
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & SecuritySystem): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.SecuritySystem)) {
data.context.properties.push({
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventSource.securitySystemState.mode),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property);
data.context.properties.push({
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property);
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & SecuritySystem, eventDetails: EventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "mode") {
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventData),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
},
context: {
properties: [{
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property]
}
} as Partial<ChangeReport>;
}
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "triggered") {
return {
event: {
payload: {
change: {
cause: {
type: "RULE_TRIGGER"
},
properties: [
{
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventData ? "ALARM" : "OK"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
},
context: {
properties: [{
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventSource.securitySystemState.mode),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property]
}
} as Partial<ChangeReport>;
}
return undefined;
}
});

View File

@@ -0,0 +1,196 @@
import { EntrySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Thermometer } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Sensor, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
const capabilities: DiscoveryCapability[] = [];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.Thermometer)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.TemperatureSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "temperature"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('TEMPERATURE_SENSOR');
}
if (device.interfaces.includes(ScryptedInterface.EntrySensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.ContactSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('CONTACT_SENSOR');
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('MOTION_SENSOR');
}
if (capabilities.length === 0)
return;
return {
displayCategories: displayCategories,
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.Thermometer)) {
data.context.properties.push({
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": eventSource.temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.EntrySensor)) {
data.context.properties.push({
"namespace": "Alexa.ContactSensor",
"name": "detectionState",
"value": eventSource.entryOpen ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.MotionSensor)) {
data.context.properties.push({
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventSource.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface === ScryptedInterface.EntrySensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.ContactSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface === ScryptedInterface.Thermometer)
return {
event: {
payload: {
change: {
cause: {
type: "PERIODIC_POLL"
},
properties: [
{
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": eventSource.temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
return undefined;
}
});

View File

@@ -0,0 +1,71 @@
import { OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, ChangeReport, Report, Property, ChangePayload, DiscoveryCapability } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Switch, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.OnOff))
return;
const capabilities: DiscoveryCapability[] = [];
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
});
return {
displayCategories: ['SWITCH'],
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & OnOff): Promise<Partial<Report>> {
return {
context: {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventSource.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
};
},
async sendEvent(eventSource: ScryptedDevice & OnOff, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.OnOff)
return undefined;
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventData ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
}
});

View File

@@ -0,0 +1,55 @@
import { OnOff, ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { sendDeviceResponse } from "../../common";
import { v4 as createMessageId } from 'uuid';
import { alexaDeviceHandlers } from "../../handlers";
import { Directive, Response } from "../../alexa";
function commonResponse(header, endpoint, payload, response, device: ScryptedDevice & OnOff) {
const data : Response = {
"event": {
header,
endpoint,
payload
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": device.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
};
data.event.header.namespace = "Alexa";
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.PowerController/TurnOn', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.turnOn();
commonResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.PowerController/TurnOff', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.turnOff();
commonResponse(header, endpoint, payload, response, device);
});

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.63",
"version": "0.2.78",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",

View File

@@ -27,7 +27,7 @@ from .request import Request
from .mqtt_stream_async import MQTTStream
from .sse_stream_async import EventStream
from .logging import logger
# Import all of the other stuff.
from datetime import datetime
@@ -227,7 +227,7 @@ class Arlo(object):
when subsequent calls to /notify are made.
"""
async def heartbeat(self, basestations, interval=30):
while self.event_stream and self.event_stream.connected:
while self.event_stream and self.event_stream.active:
for basestation in basestations:
try:
self.Ping(basestation)
@@ -378,7 +378,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
)
def SubscribeToBatteryEvents(self, basestation, camera, callback):
"""
@@ -403,7 +405,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'batteryLevel')], callbackwrapper)
)
def SubscribeToDoorbellEvents(self, basestation, doorbell, callback):
"""
@@ -437,7 +441,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'buttonPressed')], callbackwrapper)
)
def SubscribeToSDPAnswers(self, basestation, camera, callback):
"""
@@ -456,14 +462,16 @@ class Arlo(object):
def callbackwrapper(self, event):
properties = event.get("properties", {})
stop = None
stop = None
if properties.get("type") == "answerSdp":
stop = callback(properties.get("data"))
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
)
def SubscribeToCandidateAnswers(self, basestation, camera, callback):
"""
@@ -482,14 +490,16 @@ class Arlo(object):
def callbackwrapper(self, event):
properties = event.get("properties", {})
stop = None
stop = None
if properties.get("type") == "answerCandidate":
stop = callback(properties.get("data"))
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
)
async def HandleEvents(self, basestation, resource, actions, callback):
"""
@@ -502,9 +512,17 @@ class Arlo(object):
await self.Subscribe()
async def loop_action_listener(action):
# in this function, action can either be a tuple or a string
# if it is a tuple, we expect there to be a property key in the tuple
property = None
if isinstance(action, tuple):
action, property = action
if not isinstance(action, str):
raise Exception('Actions must be either a tuple or a str')
seen_events = {}
while self.event_stream.active:
event, _ = await self.event_stream.get(resource, [action], seen_events)
event, _ = await self.event_stream.get(resource, action, property, seen_events)
if event is None or self.event_stream is None \
or self.event_stream.event_stream_stop_event.is_set():
@@ -514,7 +532,7 @@ class Arlo(object):
response = callback(self, event.item)
# always requeue so other listeners can see the event too
self.event_stream.requeue(event, resource, action)
self.event_stream.requeue(event, resource, action, property)
if response is not None:
return response
@@ -606,7 +624,13 @@ class Arlo(object):
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
return None
return await self.TriggerAndHandleEvent(basestation, resource, ["is"], trigger, callback)
return await self.TriggerAndHandleEvent(
basestation,
resource,
[("is", "activityState")],
trigger,
callback,
)
def StartPushToTalk(self, basestation, camera):
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
@@ -644,8 +668,6 @@ class Arlo(object):
async def TriggerFullFrameSnapshot(self, basestation, camera):
"""
This function causes the camera to record a fullframe snapshot.
The presignedFullFrameSnapshotUrl url is returned.
Use DownloadSnapshot() to download the actual image file.
"""
resource = f"cameras/{camera.get('deviceId')}"
@@ -676,4 +698,14 @@ class Arlo(object):
return url
return None
return await self.TriggerAndHandleEvent(basestation, resource, ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"], trigger, callback)
return await self.TriggerAndHandleEvent(
basestation,
resource,
[
(action, property)
for action in ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"]
for property in ["presignedFullFrameSnapshotUrl", "presignedLastImageUrl"]
],
trigger,
callback,
)

View File

@@ -28,7 +28,7 @@ from .logging import logger
class Stream:
"""This class provides a queue-based EventStream object."""
def __init__(self, arlo, expire=10):
def __init__(self, arlo, expire=5):
self.event_stream = None
self.initializing = True
self.connected = False
@@ -43,7 +43,7 @@ class Stream:
self.event_loop = asyncio.get_event_loop()
self.event_loop.create_task(self._clean_queues())
self.event_loop.create_task(self._refresh_interval())
def __del__(self):
self.disconnect()
@@ -83,11 +83,16 @@ class Stream:
self.refresh_loop_signal.put_nowait(object())
async def _clean_queues(self):
interval = self.expire * 2
interval = self.expire * 4
await asyncio.sleep(interval)
while not self.event_stream_stop_event.is_set():
for key, q in self.queues.items():
# since we interrupt the cleanup loop after every queue, there's
# a chance the self.queues dict is modified during iteration.
# so, we first make a copy of all the items of the dict and any
# new queues will be processed on the next loop through
queue_items = [i for i in self.queues.items()]
for key, q in queue_items:
if q.empty():
continue
@@ -114,81 +119,47 @@ class Stream:
if num_dropped > 0:
logger.debug(f"Cleaned {num_dropped} events from queue {key}")
# cleanup is not urgent, so give other tasks a chance
await asyncio.sleep(0.1)
await asyncio.sleep(interval)
async def get(self, resource, actions, skip_uuids={}):
if len(actions) == 1:
action = actions[0]
async def get(self, resource, action, property=None, skip_uuids={}):
if not property:
key = f"{resource}/{action}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
first_requeued = None
while True:
event = await q.get()
q.task_done()
if not event:
# exit signal received
return None, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so sleep and give the next
# subscriber a chance
q.put_nowait(event)
await asyncio.sleep(random.uniform(0, 0.01))
continue
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
else:
while True:
for action in actions:
key = f"{resource}/{action}"
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
if q.empty():
continue
first_requeued = None
while True:
event = await q.get()
q.task_done()
first_requeued = None
while not q.empty():
event = q.get_nowait()
q.task_done()
if not event:
# exit signal received
return None, action
if not event:
# exit signal received
return None, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so go to the next queue
q.put_nowait(event)
break
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so sleep and give the next
# subscriber a chance
q.put_nowait(event)
await asyncio.sleep(random.uniform(0, 0.01))
continue
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
async def start(self):
raise NotImplementedError()
@@ -203,15 +174,31 @@ class Stream:
resource = response.get('resource')
action = response.get('action')
key = f"{resource}/{action}"
now = time.time()
event = StreamEvent(response, now, now + self.expire)
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
now = time.time()
q.put_nowait(StreamEvent(response, now, now + self.expire))
q.put_nowait(event)
def requeue(self, event, resource, action):
key = f"{resource}/{action}"
# for optimized lookups, notify listeners of individual properties
properties = response.get('properties', {})
for property in properties.keys():
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
q.put_nowait(event)
def requeue(self, event, resource, action, property=None):
if not property:
key = f"{resource}/{action}"
else:
key = f"{resource}/{action}/{property}"
self.queues[key].put_nowait(event)
def disconnect(self):

View File

@@ -1,26 +1,28 @@
{
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.13",
"dependencies": {
"@eneris/push-receiver": "../../external/push-receiver",
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
}
@@ -40,39 +42,9 @@
"@types/node": "^16.9.0"
}
},
"../../external/push-receiver": {
"name": "@eneris/push-receiver",
"version": "3.0.2",
"license": "MIT",
"dependencies": {
"axios": "^0.27.1",
"http_ece": "^1.0.5",
"long": "^5.2.0",
"protobufjs": "^6.11.2",
"request-promise": "^4.2.6"
},
"devDependencies": {
"@types/jest": "^28.1.0",
"@types/long": "^4.0.1",
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.4.6",
"http-proxy": "^1.16.2",
"husky": "^7.0.4",
"jest": "^28.0.2",
"ts-jest": "^28.0.4",
"typescript": "^4.4.3",
"yargs": "^17.2.1"
},
"engines": {
"node": ">=16"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.56",
"version": "0.2.82",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -108,8 +80,82 @@
}
},
"node_modules/@eneris/push-receiver": {
"resolved": "../../external/push-receiver",
"link": true
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
"dependencies": {
"axios": "^1.2.1",
"http_ece": "^1.0.5",
"long": "^5.2.1",
"protobufjs": "^7.1.2"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@eneris/push-receiver/node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
@@ -137,6 +183,12 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -155,8 +207,7 @@
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"node_modules/ansi-regex": {
"version": "1.1.1",
@@ -730,6 +781,17 @@
"he": "bin/he"
}
},
"node_modules/http_ece": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
"dependencies": {
"urlsafe-base64": "~1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
@@ -1160,6 +1222,11 @@
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
"dev": true
},
"node_modules/long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1432,6 +1499,29 @@
"node": ">= 0.6.6"
}
},
"node_modules/protobufjs": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -1636,6 +1726,11 @@
"node": ">=0.8.0"
}
},
"node_modules/urlsafe-base64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
},
"node_modules/utile": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
@@ -1889,28 +1984,82 @@
},
"dependencies": {
"@eneris/push-receiver": {
"version": "file:../../external/push-receiver",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
"requires": {
"@types/jest": "^28.1.0",
"@types/long": "^4.0.1",
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"axios": "^0.27.1",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.4.6",
"axios": "^1.2.1",
"http_ece": "^1.0.5",
"http-proxy": "^1.16.2",
"husky": "^7.0.4",
"jest": "^28.0.2",
"long": "^5.2.0",
"protobufjs": "^6.11.2",
"request-promise": "^4.2.6",
"ts-jest": "^28.0.4",
"typescript": "^4.4.3",
"yargs": "^17.2.1"
"long": "^5.2.1",
"protobufjs": "^7.1.2"
},
"dependencies": {
"axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
}
}
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"requires": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@scrypted/common": {
"version": "file:../../common",
"requires": {
@@ -1964,6 +2113,12 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -1982,8 +2137,7 @@
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"ansi-regex": {
"version": "1.1.1",
@@ -2399,6 +2553,14 @@
"integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
"dev": true
},
"http_ece": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
"requires": {
"urlsafe-base64": "~1.0.0"
}
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
@@ -2737,6 +2899,11 @@
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
"dev": true
},
"long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -2966,6 +3133,25 @@
"winston": "0.8.x"
}
},
"protobufjs": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3109,6 +3295,11 @@
"integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==",
"dev": true
},
"urlsafe-base64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
},
"utile": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",

View File

@@ -37,21 +37,23 @@
]
},
"dependencies": {
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@eneris/push-receiver": "../../external/push-receiver",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
},
"version": "0.1.11"
"version": "0.1.13"
}

View File

@@ -1,23 +1,23 @@
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios from 'axios';
import bpmux from 'bpmux';
import crypto from 'crypto';
import { once } from 'events';
import http from 'http';
import https from 'https';
import HttpProxy from 'http-proxy';
import https from 'https';
import throttle from "lodash/throttle";
import upnp from 'nat-upnp';
import net from 'net';
import net, { AddressInfo } from 'net';
import os from 'os';
import path from 'path';
import qs from 'query-string';
import { Duplex } from 'stream';
import tls from 'tls';
import Url from 'url';
import type { CORSControlLegacy } from '../../../server/src/services/cors';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { PushManager } from './push';
import tls from 'tls';
const { deviceManager, endpointManager, systemManager } = sdk;
@@ -547,6 +547,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
};
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
this.console.log(req.socket?.remoteAddress, req.url);
const url = Url.parse(req.url);
if (url.path.startsWith('/web/oauth/callback') && url.query) {
const query = qs.parse(url.query);
@@ -620,7 +622,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
});
this.proxy.on('error', () => { });
this.proxy.on('proxyRes', (res, req) => {
res.headers['X-Scrypted-Cloud'] = 'true';
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
});

View File

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

View File

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

View File

@@ -47,12 +47,11 @@ export class Scheduler {
throw new Error('sunrise/sunset clock not supported');
}
const ret: ScryptedDevice = {
async setName() { },
async setType() { },
async setRoom() { },
async setMixins() { },
async probe() { return true },
listen(event: EventListenerOptions, callback, source?: ScryptedDeviceBase) {
function reschedule(): Date {

View File

@@ -1,8 +1,7 @@
import { BufferConverter, BufferConvertorOptions, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import sdk from "@scrypted/sdk";
import sdk, { BufferConverter, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import crypto from 'crypto';
import mime from "mime/lite";
import path from 'path';
import crypto from 'crypto';
const { endpointManager } = sdk;

View File

@@ -1,3 +1,4 @@
import { tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
import sdk, { DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk';
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import fs from 'fs';
@@ -237,3 +238,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
}
export default ScryptedCore;
export async function fork() {
return {
tsCompile,
}
}

View File

@@ -3,10 +3,13 @@ import { scryptedEval } from "./scrypted-eval";
import { monacoEvalDefaults } from "./monaco";
import { createScriptDevice, ScriptDeviceImpl } from "@scrypted/common/src/eval/scrypted-eval";
import { ScriptCoreNativeId } from "./script-core";
import { PluginAPIProxy } from "../../../server/src/plugin/plugin-api";
const { log, deviceManager, systemManager } = sdk;
export class Script extends ScryptedDeviceBase implements Scriptable, Program, ScriptDeviceImpl {
apiProxy: PluginAPIProxy;
constructor(nativeId: string) {
super(nativeId);
}
@@ -67,6 +70,8 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
}
prepareScript() {
this.apiProxy?.removeListeners();
Object.assign(this, createScriptDevice([
ScryptedInterface.Scriptable,
ScryptedInterface.Program,
@@ -79,10 +84,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
try {
const data = JSON.parse(this.storage.getItem('data'));
const { value, defaultExport } = await scryptedEval(this, data['script.ts'], Object.assign({
const { value, defaultExport, apiProxy } = await scryptedEval(this, data['script.ts'], Object.assign({
device: this,
}, variables));
this.apiProxy = apiProxy;
await this.postRunScript(defaultExport);
return value;
}
@@ -95,10 +102,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
async eval(source: ScriptSource, variables?: { [name: string]: any }) {
this.prepareScript();
const { value, defaultExport } = await scryptedEval(this, source.script, Object.assign({
const { value, defaultExport, apiProxy } = await scryptedEval(this, source.script, Object.assign({
device: this,
}, variables));
this.apiProxy = apiProxy;
await this.postRunScript(defaultExport);
return value;
}

View File

@@ -3,14 +3,20 @@ import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
export const UsersNativeId = 'users';
type DBUser = { username: string, aclId: string };
type DBUser = { username: string, admin: boolean };
export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
storageSettings = new StorageSettings(this, {
devices: {
title: 'Devices',
description: 'The devices this user can access. Admin users can access all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
type: 'device',
defaultAccess: {
title: 'Default Access',
description: 'Grant access to @scrypted/core and @scrypted/webrtc',
defaultValue: true,
type: 'boolean',
},
interfaces: {
title: 'Interfaces',
description: 'The interfaces this user can access. Admin users can access all interfaces on all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
type: 'interface',
multiple: true,
defaultValue: [],
},
@@ -19,26 +25,27 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
const self = sdk.deviceManager.getDeviceState(this.nativeId);
const ret: ScryptedUserAccessControl = {
const ret: ScryptedUserAccessControl = {
devicesAccessControls: [
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
...this.storageSettings.values.devices.map((id: string) => ({
id,
})),
...this.storageSettings.values.defaultAccess
? [
// grant this? not sure.
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
]
: [],
...this.storageSettings.values.interfaces.map((deviceInterface: string) => {
const [id, scryptedInterface] = deviceInterface.split('#');
return addAccessControlsForInterface(id, ScryptedInterface.ScryptedDevice, scryptedInterface as ScryptedInterface);
}),
]
};
if (self) {
}
return ret;
}
@@ -72,7 +79,19 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
const user = users.find(user => user.username === this.username);
if (!user)
return;
await usersService.addUser(user.username, value.toString(), user.aclId);
const { username, admin } = user;
const nativeId = `user:${username}`;
const aclId = await sdk.deviceManager.onDeviceDiscovered({
providerNativeId: this.nativeId,
name: username.toString(),
nativeId,
interfaces: [
ScryptedInterface.ScryptedUser,
ScryptedInterface.Settings,
],
type: ScryptedDeviceType.Person,
})
await usersService.addUser(user.username, value.toString(), admin ? undefined : aclId);
}
}

View File

@@ -5,7 +5,7 @@
color="primary" icon="mdi-vuetify" border="left">
<template v-slot:prepend>
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
getAlertIcon(alert)
getAlertIcon(alert)
}}</v-icon>
</template>
<div class="caption">{{ alert.title }}</div>
@@ -185,7 +185,8 @@
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
</v-flex>
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<v-flex xs12
v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<Settings :device="device"></Settings>
</v-flex>
</v-layout>
@@ -240,6 +241,7 @@ import Scene from "../interfaces/Scene.vue";
import TemperatureSetting from "../interfaces/TemperatureSetting.vue";
import PositionSensor from "../interfaces/sensors/PositionSensor.vue";
import DeviceProvider from "../interfaces/DeviceProvider.vue";
import ObjectDetection from "../interfaces/ObjectDetection.vue";
import MixinProvider from "../interfaces/MixinProvider.vue";
import Readme from "../interfaces/Readme.vue";
import Scriptable from "../interfaces/automation/Scriptable.vue";
@@ -286,7 +288,9 @@ const leftInterfaces = [
ScryptedInterface.DeviceProvider,
ScryptedInterface.Readme,
];
const leftAboveInterfaces = [ScryptedInterface.Camera];
const leftAboveInterfaces = [
ScryptedInterface.Camera,
];
const noCardInterfaces = [
ScryptedInterface.Camera,
@@ -294,7 +298,10 @@ const noCardInterfaces = [
ScryptedInterface.Scriptable,
];
const aboveInterfaces = [ScryptedInterface.Scriptable];
const aboveInterfaces = [
ScryptedInterface.ObjectDetection,
ScryptedInterface.Scriptable
];
const cardActionInterfaces = [
ScryptedInterface.OauthClient,
@@ -379,6 +386,8 @@ export default {
Automation,
Program,
Scriptable,
ObjectDetection,
},
mixins: [Mixin],
data() {

View File

@@ -23,6 +23,9 @@ export function createSystemSettingsDevice(systemManager: SystemManager): Scrypt
},
async probe() {
return true;
},
async setMixins() {
},
listen(event, callback) {
let listeners = systemSettings.map(d => d.listen(event, callback));

View File

@@ -24,22 +24,45 @@
Devices</v-btn>
</v-card-actions>
<v-simple-table v-if="discoveredDevices && discoveredDevices.length">
<thead>
<tr>
<th style="width: 10px;"></th>
<th>Discovered</th>
<th></th>
</tr>
</thead>
<tbody v-if="discoveredDevices.length">
<tr v-for="device in discoveredDevices" :key="device.id">
<td>
<v-btn x-small outlined fab color="info" @click="openDeviceAdoptionDialog(device)">
<v-icon>fa-solid
fa-plus</v-icon>
</v-btn>
</td>
<td>
{{ device.name }}
</td>
<td> {{ device.description }}</td>
</tr>
</tbody>
<tbody v-else>
<td></td>
<td>None found.</td>
<td></td>
</tbody>
</v-simple-table>
<v-card-text>These things were created by {{ device.name }}.</v-card-text>
<v-text-field v-model="search" append-icon="search" label="Search" single-line hide-details></v-text-field>
<v-data-table :headers="headers" :items="providerDevices.devices" :items-per-page="10" :search="search">
<v-text-field v-if="managedDevices.devices.length > 10" v-model="search" append-icon="search" label="Search"
single-line hide-details></v-text-field>
<v-data-table v-if="managedDevices.devices.length > 10" :headers="headers" :items="managedDevices.devices"
:items-per-page="10" :search="search">
<template v-slot:[`item.icon`]="{ item }">
<v-icon v-if="!item.nativeId" x-small color="grey">
<v-icon x-small color="grey">
{{ typeToIcon(item.type) }}
</v-icon>
<v-tooltip bottom v-else>
<template v-slot:activator="{ on }">
<v-btn x-small outlined fab v-on="on" color="info" @click="openDeviceAdoptionDialog(item)"><v-icon>fa-solid
fa-plus</v-icon></v-btn>
</template>
<span>Add Discovered Device</span>
</v-tooltip>
</template>
<template v-slot:[`item.name`]="{ item }">
<a v-if="!item.nativeId" link :href="'#' + getDeviceViewPath(item.id)">{{ item.name }}</a>
@@ -48,7 +71,7 @@
</template>
</v-data-table>
<!-- <DeviceGroup v-else :deviceGroup="providerDevices"></DeviceGroup> -->
<DeviceGroup v-else :deviceGroup="managedDevices"></DeviceGroup>
</v-flex>
</template>
<script>
@@ -167,8 +190,8 @@ export default {
});
return ret;
},
providerDevices() {
const currentDevices = this.$store.state.scrypted.devices
managedDevices() {
const devices = this.$store.state.scrypted.devices
.filter(
(id) =>
this.$store.state.systemState[id].providerId.value ===
@@ -181,7 +204,7 @@ export default {
}));
return {
devices: [...this.discoveredDevices || [], ...currentDevices],
devices,
};
},
},

View File

@@ -0,0 +1,91 @@
<template>
<v-sheet :height="600" width="100%" class="d-flex align-center justify-center flex-wrap text-center mx-auto"
@drop="onDrop" @dragover="allowDrop">
<div v-if="!img">Drag and Drop a JPG or PNG to analyze.</div>
<div v-else style="position: relative; height: 100%;">
<img :src="img" style="height: 100%">
<svg v-if="lastDetection" :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
top: 0;
left: 0;
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
" v-html="svgContents"></svg>
</div>
</v-sheet>
</template>
<script>
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
data() {
return {
img: null,
lastDetection: null,
}
},
mounted() {
},
computed: {
svgWidth() {
return this.lastDetection?.inputDimensions?.[0] || 1920;
},
svgHeight() {
return this.lastDetection?.inputDimensions?.[1] || 1080;
},
svgContents() {
if (!this.lastDetection) return "";
let contents = "";
for (const detection of this.lastDetection.detections || []) {
if (!detection.boundingBox) continue;
const svgScale = this.svgWidth / 1080;
const sw = 6 * svgScale;
const s = "red";
const x = detection.boundingBox[0];
const y = detection.boundingBox[1];
const w = detection.boundingBox[2];
const h = detection.boundingBox[3];
let t = ``;
let toffset = 0;
if (detection.score && detection.className !== 'motion') {
t += `<tspan x='${x}' dy='${toffset}em'>${Math.round(detection.score * 100) / 100}</tspan>`
toffset -= 1.2;
}
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
const fs = 30 * svgScale;
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
`;
contents += box;
}
return contents;
},
},
methods: {
async onDrop(ev) {
ev.preventDefault()
const file = ev.dataTransfer.files[0];
this.img = URL.createObjectURL(file);
const buffer = Buffer.from(await file.arrayBuffer());
const mediaManager = this.$scrypted.mediaManager;
const mo = await mediaManager.createMediaObject(buffer, 'image/*');
const detected = await this.rpc().detectObjects(mo);
this.lastDetection = detected;
},
allowDrop(ev) {
ev.preventDefault();
}
}
}
</script>

View File

@@ -102,6 +102,7 @@ export default {
},
set(value) {
this.rawSettingsGroupName = value;
this.rawSettingsSubgroupName = undefined;
},
},
settingsSubgroupName: {
@@ -110,7 +111,7 @@ export default {
return;
if (this.settingsSubgroups.findIndex(sg => sg === this.rawSettingsSubgroupName) !== -1)
return this.rawSettingsSubgroupName;
return Object.keys(this.settingsSubgroups)?.[0];
return Object.values(this.settingsSubgroups)?.[0];
},
set(value) {
this.rawSettingsSubgroupName = value;

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/coreml",
"version": "0.0.21",
"version": "0.0.24",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.0.21",
"version": "0.0.24",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.57",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -41,5 +41,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.21"
"version": "0.0.24"
}

View File

@@ -3,8 +3,3 @@ Pillow>=5.4.1
PyGObject>=3.30.4
coremltools~=6.1
av>=10.0.0; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
# sort_oh
scipy
filterpy
numpy

View File

@@ -7,4 +7,5 @@ src
.vscode
dist/*.js
dist/*.txt
face-api.js
HAP-NodeJS
.gitattributes

View File

@@ -10,6 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",
@@ -19,4 +20,4 @@
"type": "pwa-node"
}
]
}
}

View File

@@ -1,3 +1,3 @@
{
"scrypted.debugHost": "127.0.0.1",
}
}

1
plugins/eufy/README.md Normal file
View File

@@ -0,0 +1 @@
# Eufy Plugin for Scrypted

1333
plugins/eufy/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
plugins/eufy/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@scrypted/eufy",
"description": "Eufy Plugin for Scrypted",
"version": "0.0.1",
"keywords": [
"scrypted",
"plugin",
"eufy",
"camera"
],
"scripts": {
"build": "scrypted-webpack",
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
"prescrypted-vscode-launch": "scrypted-webpack",
"scrypted-vscode-launch": "scrypted-deploy-debug",
"scrypted-deploy-debug": "scrypted-deploy-debug",
"scrypted-debug": "scrypted-debug",
"scrypted-deploy": "scrypted-deploy",
"scrypted-readme": "scrypted-readme",
"scrypted-package-json": "scrypted-package-json",
"scrypted-webpack": "scrypted-webpack"
},
"scrypted": {
"name": "Eufy",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"Settings"
]
},
"dependencies": {
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@scrypted/h264-repacketizer": "file:../../packages/h264-repacketizer ",
"@types/node": "^18.14.6"
},
"optionalDependencies": {
"eufy-security-client": "^2.4.2"
}
}

319
plugins/eufy/src/main.ts Normal file
View File

@@ -0,0 +1,319 @@
import { listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
import sdk, { Battery, Camera, Device, DeviceProvider, FFmpegInput, MediaObject, MotionSensor, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import eufy, { CaptchaOptions, EufySecurity } from 'eufy-security-client';
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { Deferred } from '@scrypted/common/src/deferred';
import { Writable } from 'stream';
import { LocalLivestreamManager } from './stream';
const { deviceManager, mediaManager, systemManager } = sdk;
class EufyCamera extends ScryptedDeviceBase implements Camera, VideoCamera, Battery, MotionSensor {
client: EufySecurity;
device: eufy.Camera;
livestreamManager: LocalLivestreamManager
constructor(nativeId: string, client: EufySecurity, device: eufy.Camera) {
super(nativeId);
this.client = client;
this.device = device;
this.livestreamManager = new LocalLivestreamManager(this.client, this.device, this.console);
this.batteryLevel = this.device.getBatteryValue() as number;
this.setupMotionDetection();
}
setupMotionDetection() {
const handle = (device: eufy.Device, state: boolean) => {
this.motionDetected = state;
};
this.device.on('motion detected', handle);
this.device.on('person detected', handle);
this.device.on('pet detected', handle);
this.device.on('vehicle detected', handle);
this.device.on('dog detected', handle);
this.device.on('radar motion detected', handle);
}
async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
// if this stream is prebuffered, its safe to use the prebuffer to generate an image
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
try {
const msos = await realDevice.getVideoStreamOptions();
const prebuffered: RequestMediaStreamOptions = msos.find(mso => mso.prebuffer);
if (prebuffered) {
prebuffered.refresh = false;
return realDevice.getVideoStream(prebuffered);
}
} catch (e) {}
// try to fetch the cloud image if one exists
const url = this.device.getLastCameraImageURL();
if (url) {
return mediaManager.createMediaObjectFromUrl(url.toString());
}
throw new Error("snapshot unavailable");
}
getPictureOptions(): Promise<ResponsePictureOptions[]> {
return;
}
getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
return this.createVideoStream(options);
}
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
return [
{
container: 'rtsp',
id: 'p2p',
name: 'P2P',
video: {
codec: 'h264',
},
audio: {
codec: 'aac',
},
tool: 'scrypted',
userConfigurable: false,
}
];
}
async createVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
const kill = new Deferred<void>();
kill.promise.finally(() => {
this.console.log('video stream exited');
this.livestreamManager.stopLocalLiveStream();
});
const rtspServer = await listenSingleRtspClient();
rtspServer.rtspServerPromise.then(async rtsp => {
kill.promise.finally(() => rtsp.client.destroy());
rtsp.client.on('close', () => kill.resolve());
try {
const process = await startRtpForwarderProcess(this.console, {
inputArguments: [
'-f', 'h264', '-i', 'pipe:4',
'-f', 'aac', '-i', 'pipe:5',
]
}, {
video: {
onRtp: rtp => {
if (videoTrack)
rtsp.sendTrack(videoTrack.control, rtp, false);
},
encoderArguments: [
'-vcodec', 'copy',
]
},
audio: {
onRtp: rtp => {
if (audioTrack)
rtsp.sendTrack(audioTrack.control, rtp, false);
},
encoderArguments: [
'-acodec', 'copy',
'-rtpflags', 'latm',
]
}
});
process.killPromise.finally(() => kill.resolve());
kill.promise.finally(() => process.kill());
let parsedSdp: ReturnType<typeof parseSdp>;
let videoTrack: typeof parsedSdp.msections[0]
let audioTrack: typeof parsedSdp.msections[0]
process.sdpContents.then(async sdp => {
sdp = addTrackControls(sdp);
rtsp.sdp = sdp;
parsedSdp = parseSdp(sdp);
videoTrack = parsedSdp.msections.find(msection => msection.type === 'video');
audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio');
await rtsp.handlePlayback();
});
const proxyStream = await this.livestreamManager.getLocalLivestream();
proxyStream.videostream.pipe(process.cp.stdio[4] as Writable);
proxyStream.audiostream.pipe((process.cp.stdio as any)[5] as Writable);
}
catch (e) {
rtsp.client.destroy();
}
});
const input: FFmpegInput = {
url: rtspServer.url,
mediaStreamOptions: options,
inputArguments: [
'-i', rtspServer.url,
]
};
return mediaManager.createFFmpegMediaObject(input);
}
}
class EufyPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings {
client: EufySecurity;
devices = new Map<string, any>();
storageSettings = new StorageSettings(this, {
country: {
title: 'Country',
defaultValue: 'US',
},
email: {
title: 'Email',
onPut: async () => this.tryLogin(),
},
password: {
title: 'Password',
type: 'password',
onPut: async () => this.tryLogin(),
},
twoFactorCode: {
title: 'Two Factor Code',
description: 'Optional: If 2FA is enabled on your account, enter the code sent to your email or phone number.',
onPut: async (oldValue, newValue) => {
await this.tryLogin(newValue);
},
noStore: true,
},
captcha: {
title: 'Captcha',
description: 'Optional: If a captcha request is recieved, enter the code in the image.',
onPut: async (oldValue, newValue) => {
await this.tryLogin(undefined, newValue);
},
noStore: true,
},
captchaId: {
title: 'Captcha Id',
hide: true,
}
});
constructor() {
super();
this.tryLogin()
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async tryLogin(twoFactorCode?: string, captchaCode?: string) {
this.log.clearAlerts();
if (!this.storageSettings.values.email || !this.storageSettings.values.email) {
this.log.a('Enter your Eufy email and password to complete setup.');
throw new Error('Eufy email and password are missing.');
}
await this.initializeClient();
var captchaOptions: CaptchaOptions = undefined
if (captchaCode) {
captchaOptions = {
captchaCode: captchaCode,
captchaId: this.storageSettings.values.captchaId,
}
}
await this.client.connect({ verifyCode: twoFactorCode, captcha: captchaOptions, force: false });
}
private async initializeClient() {
const config = {
username: this.storageSettings.values.email,
password: this.storageSettings.values.password,
country: this.storageSettings.values.country,
language: 'en',
p2pConnectionSetup: 2,
pollingIntervalMinutes: 10,
eventDurationSeconds: 10
}
this.client = await EufySecurity.initialize(config);
this.client.on('device added', this.deviceAdded.bind(this));
this.client.on('station added', this.stationAdded.bind(this));
this.client.on('tfa request', () => {
this.log.a('Login failed: 2FA is enabled, check your email or texts for your code, then enter it into the Two Factor Code setting to conplete login.');
});
this.client.on('captcha request', (id, captcha) => {
this.log.a(`Login failed: Captcha was requested, fill out the Captcha setting to conplete login. </br> <img src="${captcha}" />`);
this.storageSettings.putSetting('captchaId', id);
});
this.client.on('connect', () => {
this.console.debug(`[${this.name}] (${new Date().toLocaleString()}) Client connected.`);
this.log.clearAlerts();
});
this.client.on('push connect', () => {
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Connected.`);
});
this.client.on('push close', () => {
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Closed.`);
});
}
private async deviceAdded(eufyDevice: eufy.Device) {
if (!eufyDevice.isCamera) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Ignoring unsupported discovered device: `, eufyDevice.getName(), eufyDevice.getModel());
return;
}
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device discovered: `, eufyDevice.getName(), eufyDevice.getModel());
const nativeId = eufyDevice.getSerial();
const interfaces = [
ScryptedInterface.Camera,
ScryptedInterface.VideoCamera
];
if (eufyDevice.hasBattery())
interfaces.push(ScryptedInterface.Battery);
if (eufyDevice.hasProperty('motionDetection'))
interfaces.push(ScryptedInterface.MotionSensor);
const device: Device = {
info: {
model: eufyDevice.getModel(),
manufacturer: 'Eufy',
firmware: eufyDevice.getSoftwareVersion(),
serialNumber: nativeId
},
nativeId,
name: eufyDevice.getName(),
type: ScryptedDeviceType.Camera,
interfaces,
};
this.devices.set(nativeId, new EufyCamera(nativeId, this.client, eufyDevice as eufy.Camera))
await deviceManager.onDeviceDiscovered(device);
}
private async stationAdded(station: eufy.Station) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Station discovered: `, station.getName(), station.getModel(), `but stations are not currently supported.`);
}
async getDevice(nativeId: string): Promise<any> {
return this.devices.get(nativeId);
}
async releaseDevice(id: string, nativeId: string) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device with id '${nativeId}' was removed.`);
}
}
export default new EufyPlugin();

129
plugins/eufy/src/stream.ts Normal file
View File

@@ -0,0 +1,129 @@
// Based off of https://github.com/homebridge-eufy-security/plugin/blob/master/src/plugin/controller/LocalLivestreamManager.ts
import { EventEmitter, Readable } from 'stream';
import { Station, Device, StreamMetadata, Camera, EufySecurity } from 'eufy-security-client';
type StationStream = {
station: Station;
device: Device;
metadata: StreamMetadata;
videostream: Readable;
audiostream: Readable;
createdAt: number;
};
export class LocalLivestreamManager extends EventEmitter {
private stationStream: StationStream | null;
private console: Console;
private livestreamStartedAt: number | null;
private livestreamIsStarting = false;
private readonly client: EufySecurity;
private readonly device: Camera;
constructor(client: EufySecurity, device: Camera, console: Console) {
super();
this.console = console;
this.client = client;
this.device = device;
this.stationStream = null;
this.livestreamStartedAt = null;
this.initialize();
this.client.on('station livestream stop', (station: Station, device: Device) => {
this.onStationLivestreamStop(station, device);
});
this.client.on('station livestream start',
(station: Station, device: Device, metadata: StreamMetadata, videostream: Readable, audiostream: Readable) => {
this.onStationLivestreamStart(station, device, metadata, videostream, audiostream);
});
}
private initialize() {
if (this.stationStream) {
this.stationStream.audiostream.unpipe();
this.stationStream.audiostream.destroy();
this.stationStream.videostream.unpipe();
this.stationStream.videostream.destroy();
}
this.stationStream = null;
this.livestreamStartedAt = null;
}
public async getLocalLivestream(): Promise<StationStream> {
this.console.debug(this.device.getName(), 'New instance requests livestream.');
if (this.stationStream) {
const runtime = (Date.now() - this.livestreamStartedAt!) / 1000;
this.console.debug(this.device.getName(), 'Using livestream that was started ' + runtime + ' seconds ago.');
return this.stationStream;
} else {
return await this.startAndGetLocalLiveStream();
}
}
private async startAndGetLocalLiveStream(): Promise<StationStream> {
return new Promise((resolve, reject) => {
this.console.debug(this.device.getName(), 'Start new station livestream (P2P Session)...');
if (!this.livestreamIsStarting) { // prevent multiple stream starts from eufy station
this.livestreamIsStarting = true;
this.client.startStationLivestream(this.device.getSerial());
} else {
this.console.debug(this.device.getName(), 'stream is already starting. waiting...');
}
this.once('livestream start', async () => {
if (this.stationStream !== null) {
this.console.debug(this.device.getName(), 'New livestream started.');
this.livestreamIsStarting = false;
resolve(this.stationStream);
} else {
reject('no started livestream found');
}
});
});
}
public stopLocalLiveStream(): void {
this.console.debug(this.device.getName(), 'Stopping station livestream.');
this.client.stopStationLivestream(this.device.getSerial());
this.initialize();
}
private onStationLivestreamStop(station: Station, device: Device) {
if (device.getSerial() === this.device.getSerial()) {
this.console.info(station.getName() + ' station livestream for ' + device.getName() + ' has stopped.');
this.initialize();
}
}
private async onStationLivestreamStart(
station: Station,
device: Device,
metadata: StreamMetadata,
videostream: Readable,
audiostream: Readable,
) {
if (device.getSerial() === this.device.getSerial()) {
if (this.stationStream) {
const diff = (Date.now() - this.stationStream.createdAt) / 1000;
if (diff < 5) {
this.console.warn(this.device.getName(), 'Second livestream was started from station. Ignore.');
return;
}
}
this.initialize(); // important to prevent unwanted behaviour when the eufy station emits the 'livestream start' event multiple times
this.console.info(station.getName() + ' station livestream (P2P session) for ' + device.getName() + ' has started.');
this.livestreamStartedAt = Date.now();
const createdAt = Date.now();
this.stationStream = {station, device, metadata, videostream, audiostream, createdAt};
this.console.debug(this.device.getName(), 'Stream metadata: ' + JSON.stringify(this.stationStream.metadata));
this.emit('livestream start');
}
}
}

View File

@@ -10,4 +10,4 @@
"include": [
"src/**/*"
]
}
}

View File

@@ -174,7 +174,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
async createDevice(settings: DeviceCreatorSettings, nativeId?: ScryptedNativeId): Promise<string> {
nativeId ||= randomBytes(4).toString('hex');
const name = settings.newCamera.toString();
const name = settings.newCamera?.toString() || 'New Camera';
await this.updateDevice(nativeId, name, this.getInterfaces());
return nativeId;
}

View File

@@ -1,4 +0,0 @@
.DS_Store
out/
node_modules/
dist/*.map

View File

@@ -1,15 +0,0 @@
# Google Cloud Text to Speech plugin
## npm commands
* npm run scrypted-webpack
* npm run scrypted-deploy <ipaddress>
* npm run scrypted-debug <ipaddress>
## scrypted distribution via npm
1. Ensure package.json is set up properly for publishing on npm.
2. npm publish
## Visual Studio Code configuration
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
* Launch Scrypted Debugger from the launch menu.

View File

@@ -1,141 +0,0 @@
{
"name": "@scrypted/google-cloud-tts",
"version": "0.0.21",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/google-cloud-tts",
"version": "0.0.21",
"hasInstallScript": true,
"dependencies": {
"axios": "^0.24.0"
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^17.0.8"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.199",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
},
"bin": {
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"../sdk": {
"extraneous": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/node": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
"dev": true
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
}
},
"dependencies": {
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@types/node": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
"dev": true
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
}
}
}

View File

@@ -1,95 +0,0 @@
// https://developer.scrypted.app/#getting-started
import axios from 'axios';
import sdk, { BufferConverter, ScryptedDeviceBase, Settings, Setting } from "@scrypted/sdk";
class GoogleCloudTts extends ScryptedDeviceBase implements BufferConverter, Settings {
constructor() {
super();
this.fromMimeType = 'text/plain';
this.toMimeType = 'audio/mpeg';
if (!this.getApiKey())
this.log.a('API key missing.');
}
getApiKey() {
const apiKey = this.storage.getItem('api_key');
return apiKey;
}
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer> {
const voice_name = this.storage.getItem("voice_name") || "en-GB-Standard-A";
const voice_gender = this.storage.getItem("voice_gender") || "FEMALE";
const voice_language_code = this.storage.getItem("voice_language_code") || "en-GB";
const from = Buffer.from(data);
var json = {
"input": {
"text": from.toString()
},
"voice": {
"languageCode": voice_language_code,
"name": voice_name,
"ssmlGender": voice_gender
},
"audioConfig": {
"audioEncoding": "MP3"
}
};
var result = await axios.post(`https://texttospeech.googleapis.com/v1/text:synthesize?key=${this.getApiKey()}`, json);
console.log(JSON.stringify(result.data, null, 2));
const buffer = Buffer.from(result.data.audioContent, 'base64');
return buffer;
}
voices: any;
async getSettings(): Promise<Setting[]> {
const ret: Setting[] = [
{
title: 'API Key',
description: 'API Key used by Google Cloud TTS.',
key: 'api_key',
value: this.storage.getItem('api_key'),
}
];
if (!this.getApiKey())
return ret;
try {
if (!this.voices) {
const response = await axios.get(`https://texttospeech.googleapis.com/v1/voices?key=${this.getApiKey()}`)
this.voices = response.data;
}
}
catch (e) {
this.log.a('Error retrieving settings from Google Cloud Text to Speech. Is your API Key correct?');
return ret;
}
ret.push({
title: "Voice",
choices: this.voices.voices.map(voice => voice.name),
key: "voice",
value: this.storage.getItem("voice_name"),
});
return ret;
}
async putSetting(key: string, value: string | number | boolean) {
if (key !== 'voice') {
this.storage.setItem(key, value.toString());
return;
}
const found = this.voices.voices.find((voice: any) => voice.name === value);
if (!found) {
console.error('Voice not found.');
return;
}
localStorage.setItem('voice_name', found.name);
localStorage.setItem('voice_language_code', found.languageCodes[0]);
localStorage.setItem('voice_gender', found.ssmlGender);
}
}
export default new GoogleCloudTts();

View File

@@ -32,6 +32,11 @@ export function nextSequenceNumber(current: number, increment = 1) {
return (current + increment + 0x10000) % 0x10000;
}
const maxRtpTimestamp = BigInt(0xFFFFFFFF);
export function addRtpTimestamp(current: number, adjust: number) {
return Number(maxRtpTimestamp & (BigInt(current) + BigInt(adjust)));
}
export function isNextSequenceNumber(current: number, next: number) {
return nextSequenceNumber(current) === next;
}

View File

@@ -10,7 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"**/plugin-console.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",

View File

@@ -1,19 +1,18 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.92",
"version": "0.0.102",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/objectdetector",
"version": "0.0.92",
"version": "0.0.102",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"jpeg-js": "^0.4.3",
"lodash": "^4.17.21",
"node-moving-things-tracker": "file:./node-moving-things-tracker",
"point-inside-polygon": "^1.0.3",
"polygon-overlap": "^1.0.5",
"semver": "^7.3.8"
@@ -42,7 +41,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.51",
"version": "0.2.71",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -61,11 +60,11 @@
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
@@ -191,28 +190,6 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -228,67 +205,6 @@
"node": ">=0.3.1"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/jasmine": {
"version": "3.99.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz",
"integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==",
"dev": true,
"dependencies": {
"glob": "^7.1.6",
"jasmine-core": "~3.99.0"
},
"bin": {
"jasmine": "bin/jasmine.js"
}
},
"node_modules/jasmine-core": {
"version": "3.99.1",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
"integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
"dev": true
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@@ -304,11 +220,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -326,53 +237,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/munkres-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz",
"integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ=="
},
"node_modules/node-moving-things-tracker": {
"resolved": "node-moving-things-tracker",
"link": true
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/point-inside-polygon": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
@@ -463,27 +327,12 @@
"node": ">=4.2.0"
}
},
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -500,6 +349,7 @@
},
"node-moving-things-tracker": {
"version": "0.9.1",
"extraneous": true,
"license": "MIT",
"dependencies": {
"lodash.isequal": "^4.5.0",
@@ -642,28 +492,6 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -676,58 +504,6 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"jasmine": {
"version": "3.99.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz",
"integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==",
"dev": true,
"requires": {
"glob": "^7.1.6",
"jasmine-core": "~3.99.0"
}
},
"jasmine-core": {
"version": "3.99.1",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
"integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
"dev": true
},
"jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@@ -743,11 +519,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -762,50 +533,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
},
"munkres-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz",
"integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ=="
},
"node-moving-things-tracker": {
"version": "file:node-moving-things-tracker",
"requires": {
"jasmine": "^3.6.1",
"lodash.isequal": "^4.5.0",
"minimist": "^1.2.0",
"munkres-js": "^1.2.2",
"uuid": "^3.2.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true
},
"point-inside-polygon": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
@@ -863,23 +590,12 @@
"dev": true,
"peer": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.92",
"version": "0.0.102",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",
@@ -35,6 +35,7 @@
"name": "Video Analysis Plugin",
"type": "API",
"interfaces": [
"Settings",
"MixinProvider"
],
"realfs": true
@@ -42,9 +43,7 @@
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"jpeg-js": "^0.4.3",
"lodash": "^4.17.21",
"node-moving-things-tracker": "file:./node-moving-things-tracker",
"point-inside-polygon": "^1.0.3",
"polygon-overlap": "^1.0.5",
"semver": "^7.3.8"

View File

@@ -1,5 +1,3 @@
const Tracker = require('node-moving-things-tracker').Tracker;
export class DenoisedDetectionEntry<T> {
id?: string;
boundingBox?: [number, number, number, number];
@@ -24,75 +22,18 @@ export interface DenoisedDetectionOptions<T> {
now?: number;
}
export interface TrackerItem<T> {
x: number,
y: number,
w: number,
h: number,
confidence: number,
name: string,
};
export interface TrackedItem<T> extends TrackerItem<T> {
id: string;
isZombie: boolean;
bearing: number;
frameUnmatchedLeftBeforeDying: number;
velocity: {
dx: number,
dy: number,
}
}
export interface DenoisedDetectionState<T> {
previousDetections?: DenoisedDetectionEntry<T>[];
tracker?: any;
tracked?: TrackedItem<T>[];
frameCount?: number;
lastDetection?: number;
// id to time
externallyTracked?: Map<string, DenoisedDetectionEntry<T>>;
}
type Rectangle = {
xmin: number;
xmax: number;
ymin: number;
ymax: number;
};
function intersect_area(a: Rectangle, b: Rectangle) {
const dx = Math.min(a.xmax, b.xmax) - Math.max(a.xmin, b.xmin)
const dy = Math.min(a.ymax, b.ymax) - Math.max(a.ymin, b.ymin)
if (dx >= 0 && dy >= 0)
return dx * dy
}
function trackedItemToRectangle(item: TrackedItem<any>): Rectangle {
return {
xmin: item.x,
xmax: item.x + item.w,
ymin: item.y,
ymax: item.y + item.h,
};
}
export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
currentDetections: DenoisedDetectionEntry<T>[],
options?: DenoisedDetectionOptions<T>
) {
if (!state.tracker) {
state.frameCount = 0;
const tracker = Tracker.newTracker();
tracker.reset();
tracker.setParams({
fastDelete: true,
unMatchedFramesTolerance: Number.MAX_SAFE_INTEGER,
iouLimit: 0.05
});
state.tracker = tracker;
}
if (!state.previousDetections)
state.previousDetections = [];
@@ -100,157 +41,52 @@ export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
const lastDetection = state.lastDetection || now;
const sinceLastDetection = now - lastDetection;
const externallyTracked = currentDetections.filter(d => d.id);
if (externallyTracked.length) {
if (!state.externallyTracked)
state.externallyTracked = new Map();
if (!state.externallyTracked)
state.externallyTracked = new Map();
for (const tracked of currentDetections) {
tracked.durationGone = 0;
tracked.lastSeen = now;
tracked.lastBox = tracked.boundingBox;
for (const tracked of currentDetections) {
tracked.durationGone = 0;
tracked.lastSeen = now;
tracked.lastBox = tracked.boundingBox;
if (!tracked.id) {
const id = tracked.id = `untracked-${tracked.name}`;
if (!state.externallyTracked.get(id)) {
// crappy track untracked objects for 1 minute.
setTimeout(() => state.externallyTracked.delete(id), 60000);
}
}
let previous = state.externallyTracked.get(tracked.id);
if (previous) {
state.externallyTracked.delete(tracked.id);
tracked.firstSeen = previous.firstSeen;
tracked.firstBox = previous.firstBox;
previous.durationGone = 0;
previous.lastSeen = now;
previous.lastBox = tracked.boundingBox;
options?.retained(tracked, previous);
}
else {
tracked.firstSeen = now;
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
options?.added(tracked);
}
}
for (const previous of state.externallyTracked.values()) {
if (now - previous.lastSeen) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
options?.expiring(previous);
}
if (!tracked.id) {
const id = tracked.id = `untracked-${tracked.name}`;
if (!state.externallyTracked.get(id)) {
// crappy track untracked objects for 1 minute.
setTimeout(() => state.externallyTracked.delete(id), 60000);
}
}
for (const tracked of currentDetections) {
state.externallyTracked.set(tracked.id, tracked);
}
}
let previous = state.externallyTracked.get(tracked.id);
if (previous) {
state.externallyTracked.delete(tracked.id);
tracked.firstSeen = previous.firstSeen;
tracked.firstBox = previous.firstBox;
if (state.externallyTracked)
return;
const { tracker, previousDetections } = state;
const items: TrackerItem<T>[] = currentDetections.filter(cd => cd.boundingBox).map(cd => {
const [x, y, w, h] = cd.boundingBox;
return {
x, y, w, h,
confidence: cd.score,
name: cd.name,
}
});
tracker.updateTrackedItemsWithNewFrame(items, state.frameCount);
// console.log(tracker.getAllTrackedItems());
const trackedObjects: TrackedItem<T>[] = [...tracker.getTrackedItems().values()];
// for (const to of trackedObjects) {
// console.log(to.velocity);
// }
const previousCopy = previousDetections.slice();
previousDetections.splice(0, previousDetections.length);
const map = new Map<string, DenoisedDetectionEntry<T>>();
for (const pd of previousCopy) {
map.set(pd.id, pd);
}
for (const trackedObject of trackedObjects) {
map.delete(trackedObject.id);
const previous = previousCopy.find(d => d.id === trackedObject.id);
const current = currentDetections.find(d => {
const [x, y, w, h] = d.boundingBox;
return !d.id && x === trackedObject.x && y === trackedObject.y && w === trackedObject.w && h === trackedObject.h;
});
if (current) {
current.id = trackedObject.id;
current.lastSeen = now;
current.durationGone = 0;
if (previous) {
previous.lastSeen = now;
current.firstSeen = previous.firstSeen;
current.firstBox = previous.firstBox;
current.lastBox = previous.boundingBox;
previous.lastBox = current.boundingBox;
previous.durationGone = 0;
options.retained?.(current, previous);
}
else {
current.firstSeen = now;
current.firstBox = current.boundingBox;
current.lastBox = current.boundingBox;
options.added?.(current);
}
previousDetections.push(current);
}
else if (previous) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
let foundContainer = false;
// the detector may combine multiple detections into one.
// handle that scenario by not expiring the individual detections that
// are globbed into a larger one.
for (const other of trackedObjects) {
if (other === trackedObject || other.isZombie)
continue;
const area = intersect_area(trackedItemToRectangle(trackedObject), trackedItemToRectangle(other));
if (area) {
const trackedObjectArea = trackedObject.w * trackedObject.h;
if (area / trackedObjectArea > .5) {
foundContainer = true;
break;
}
}
}
if (!foundContainer)
trackedObject.frameUnmatchedLeftBeforeDying = -1;
// else
// console.log('globbed!');
}
else {
options.expiring?.(previous);
previousDetections.push(previous);
}
previous.durationGone = 0;
previous.lastSeen = now;
previous.lastBox = tracked.boundingBox;
options?.retained(tracked, previous);
}
else {
// console.warn('unprocessed denoised detection?', trackedObject);
tracked.firstSeen = now;
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
options?.added(tracked);
}
}
for (const previous of state.externallyTracked.values()) {
if (now - previous.lastSeen) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
options?.expiring(previous);
}
}
}
// should never reach here?
for (const r of map.values()) {
options.removed?.(r)
for (const tracked of currentDetections) {
state.externallyTracked.set(tracked.id, tracked);
}
state.tracked = trackedObjects;
state.lastDetection = now;
state.frameCount++;
}

View File

@@ -1,4 +1,4 @@
import sdk, { Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, VideoCamera } from '@scrypted/sdk';
import sdk, { VideoFrameGenerator, Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import cloneDeep from 'lodash/cloneDeep';
@@ -7,7 +7,7 @@ import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
import { DenoisedDetectionEntry, DenoisedDetectionState, denoiseDetections } from './denoise';
import { serverSupportsMixinEventMasking } from './server-version';
import { sleep } from './sleep';
import { safeParseJson } from './util';
import { getAllDevices, safeParseJson } from './util';
const polygonOverlap = require('polygon-overlap');
const insidePolygon = require('point-inside-polygon');
@@ -24,6 +24,8 @@ const defaultSecondScoreThreshold = .7;
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
type ClipPath = [number, number][];
type Zones = { [zone: string]: ClipPath };
interface ZoneInfo {
@@ -126,7 +128,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
analyzeStop = 0;
lastDetectionInput = 0;
constructor(mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public hasMotionType: boolean, public settings: Setting[]) {
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public hasMotionType: boolean, public settings: Setting[]) {
super({
mixinDevice, mixinDeviceState,
mixinProviderNativeId: providerNativeId,
@@ -187,16 +189,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|| setting.value;
}
if (this.hasMotionType)
ret['motionAsObjects'] = this.storageSettings.values.motionAsObjects;
return ret;
}
async snapshotDetection() {
const picture = await this.cameraDevice.takePicture();
const detections = await this.objectDetection.detectObjects(picture, {
let detections = await this.objectDetection.detectObjects(picture, {
detectionId: this.detectionId,
settings: this.getCurrentSettings(),
});
this.trackObjects(detections, true);
detections = await this.trackObjects(detections, true);
this.reportObjectDetections(detections);
}
@@ -212,6 +217,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
this.detectorRunning = false;
this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
settings: this.getCurrentSettings(),
});
}
@@ -308,8 +314,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
async handleDetectionEvent(detection: ObjectsDetected, redetect?: (boundingBox: [number, number, number, number]) => Promise<ObjectDetectionResult[]>, mediaObject?: MediaObject) {
this.detectorRunning = detection.running;
// track the objects on a pre-zoned set.
this.trackObjects(detection);
detection = await this.trackObjects(detection);
// apply the zones to the detections and get a shallow copy list of detections after
// exclusion zones have applied
@@ -347,7 +352,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
// retain passing the second pass threshold for first time.
if (d.bestSecondPassScore < this.secondScoreThreshold && secondPassScore >= this.secondScoreThreshold) {
this.console.log('improved', d.id, d.bestSecondPassScore, d.score);
this.console.log('improved', d.id, secondPassScore, d.score);
better = true;
retainImage = true;
}
@@ -401,7 +406,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (this.lastDetectionInput + this.storageSettings.values.detectionTimeout * 1000 < Date.now())
retainImage = true;
if (retainImage) {
if (retainImage && mediaObject) {
this.lastDetectionInput = now;
this.console.log('retaining detection image');
this.setDetection(detection, mediaObject);
@@ -436,6 +441,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
this.detectorRunning = true;
this.analyzeStop = Date.now() + this.getDetectionDuration();
while (this.detectorRunning) {
const now = Date.now();
if (now > this.analyzeStop)
@@ -445,12 +452,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
reason: 'event',
});
const found = await this.objectDetection.detectObjects(mo, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
settings: this.getCurrentSettings(),
});
found.running = this.detectorRunning;
this.handleDetectionEvent(found, undefined, mo);
}, this);
}
catch (e) {
this.console.error('snapshot detection error', e);
}
// cameras tend to only refresh every 1s at best.
// maybe get this value from somewhere? or sha the jpeg?
@@ -458,17 +466,82 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (diff > 0)
await sleep(diff);
}
this.detectorRunning = false;
this.handleDetectionEvent({
detectionId: this.detectionId,
running: false,
detections: [],
timestamp: Date.now(),
}, undefined, undefined);
this.endObjectDetection();
}
async startPipelineAnalysis() {
if (this.detectorRunning)
return;
const stream = await this.cameraDevice.getVideoStream({
destination: 'local-recorder',
// ask rebroadcast to mute audio, not needed.
audio: null,
});
this.detectorRunning = true;
this.analyzeStop = Date.now() + this.getDetectionDuration();
const videoFrameGenerator = this.newPipeline as VideoFrameGenerator;
try {
const start = Date.now();
let detections = 0;
for await (const detected
of await this.objectDetection.generateObjectDetections(await videoFrameGenerator.generateVideoFrames(stream), {
settings: this.getCurrentSettings(),
})) {
if (!this.detectorRunning) {
break;
}
const now = Date.now();
if (now > this.analyzeStop) {
break;
}
// apply the zones to the detections and get a shallow copy list of detections after
// exclusion zones have applied
const zonedDetections = this.applyZones(detected.detected);
const filteredDetections = zonedDetections
.filter(d => {
if (!d.zones?.length)
return d.score >= this.scoreThreshold;
for (const zone of d.zones || []) {
const zi = this.zoneInfos[zone];
const scoreThreshold = zi?.scoreThreshold || this.scoreThreshold;
if (d.score >= scoreThreshold)
return true;
}
});
detected.detected.detections = filteredDetections;
detections++;
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
if (detected.detected.detectionId) {
const jpeg = await detected.videoFrame.toBuffer({
format: 'jpg',
});
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
this.setDetection(detected.detected, mo);
// this.console.log('image saved', detected.detected.detections);
}
this.reportObjectDetections(detected.detected);
// this.handleDetectionEvent(detected.detected);
}
}
finally {
this.endObjectDetection();
}
}
async startStreamAnalysis() {
if (!this.hasMotionType && this.storageSettings.values.captureMode === 'Snapshot') {
if (this.newPipeline) {
await this.startPipelineAnalysis();
}
else if (!this.hasMotionType && this.storageSettings.values.captureMode === 'Snapshot') {
await this.startSnapshotAnalysis();
}
else {
@@ -487,6 +560,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
await this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
settings: this.getCurrentSettings(),
}, this);
}
catch (e) {
@@ -545,7 +619,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
applyZones(detection: ObjectsDetected) {
// determine zones of the objects, if configured.
if (!detection.detections)
return;
return [];
let copy = detection.detections.slice();
for (const o of detection.detections) {
if (!o.boundingBox)
@@ -641,15 +715,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
}
trackObjects(detectionResult: ObjectsDetected, showAll?: boolean) {
async trackObjects(detectionResult: ObjectsDetected, showAll?: boolean) {
// do not denoise
if (this.hasMotionType) {
return;
return detectionResult;
}
if (!detectionResult?.detections) {
// detection session ended.
return;
return detectionResult;
}
const { detections } = detectionResult;
@@ -719,6 +793,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (found.length || showAll) {
this.console.log('current detections:', this.detectionState.previousDetections.map(d => `${d.detection.className} (${d.detection.score}, ${d.detection.boundingBox?.join(', ')})`).join(', '));
}
return detectionResult;
}
setDetection(detection: ObjectsDetected, detectionInput: MediaObject) {
@@ -772,9 +848,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return BUILTIN_MOTION_SENSOR_REPLACE;
}
get newPipeline() {
return this.plugin.storageSettings.values.newPipeline;
}
async getMixinSettings(): Promise<Setting[]> {
const settings: Setting[] = [];
try {
this.settings = (await this.objectDetection.getDetectionModel(this.getCurrentSettings())).settings;
}
catch (e) {
}
if (this.settings) {
settings.push(...this.settings.map(setting =>
Object.assign({}, setting, {
@@ -785,15 +871,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
);
}
settings.push(...await this.storageSettings.getSettings());
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
this.storageSettings.settings.captureMode.hide = this.hasMotionType;
this.storageSettings.settings.captureMode.hide = this.hasMotionType || !!this.newPipeline;
this.storageSettings.settings.detectionDuration.hide = this.hasMotionType;
this.storageSettings.settings.detectionTimeout.hide = this.hasMotionType;
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
this.storageSettings.settings.motionAsObjects.hide = !this.hasMotionType;
settings.push(...await this.storageSettings.getSettings());
let hideThreshold = true;
if (!this.hasMotionType) {
let hasInclusionZone = false;
@@ -829,7 +915,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
settings.push({
subgroup,
key: `zone-${name}`,
title: `Edit Zone`,
title: `Open Zone Editor`,
type: 'clippath',
value: JSON.stringify(value),
});
@@ -987,8 +1073,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements MixinProvider {
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, mixinProviderNativeId: ScryptedNativeId, public model: ObjectDetectionModel) {
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId });
currentMixins = new Set<ObjectDetectionMixin>();
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, public plugin: ObjectDetectionPlugin, public model: ObjectDetectionModel) {
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId: plugin.nativeId });
// trigger mixin creation. todo: fix this to not be stupid hack.
for (const id of Object.keys(systemManager.getSystemState())) {
@@ -1000,55 +1088,94 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
// filter out
for (const iface of interfaces) {
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
const deviceMatch = this.mixinDeviceInterfaces.find(miface => miface.startsWith(iface));
if (deviceMatch)
continue;
return null;
}
}
const hasMotionType = this.model.classes.includes('motion');
const prefix = `${objectDetectionPrefix}${hasMotionType}`;
const thisPrefix = `${prefix}:${this.id}`;
const found = interfaces.find(iface => iface.startsWith(prefix) && iface !== thisPrefix);
if (found)
return;
// this.console.log('found', found);
if ((type === ScryptedDeviceType.Camera || type === ScryptedDeviceType.Doorbell) && (interfaces.includes(ScryptedInterface.VideoCamera) || interfaces.includes(ScryptedInterface.Camera))) {
const ret: string[] = [ScryptedInterface.ObjectDetector, ScryptedInterface.Settings];
const ret: string[] = [
ScryptedInterface.ObjectDetector,
ScryptedInterface.Settings,
thisPrefix,
];
const model = await this.mixinDevice.getDetectionModel();
if (model.classes?.includes('motion')) {
// const vamotion = 'mixin:@scrypted/objectdetector:motion';
// if (interfaces.includes(vamotion))
// return;
if (model.classes?.includes('motion')) {
ret.push(
ScryptedInterface.MotionSensor,
// vamotion,
);
}
return ret;
return ret;
}
return null;
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
let objectDetection = systemManager.getDeviceById<ObjectDetection>(this.id);
const group = objectDetection.name.replace('Plugin', '').trim();
const hasMotionType = this.model.classes.includes('motion');
const group = hasMotionType ? 'Motion Detection' : 'Object Detection';
// const group = objectDetection.name.replace('Plugin', '').trim();
const settings = this.model.settings;
return new ObjectDetectionMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model.name, group, hasMotionType, settings);
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model.name, group, hasMotionType, settings);
this.currentMixins.add(ret);
return ret;
}
async releaseMixin(id: string, mixinDevice: any) {
this.currentMixins.delete(mixinDevice);
return mixinDevice.release();
}
}
class ObjectDetectionPlugin extends AutoenableMixinProvider {
class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings {
currentMixins = new Set<ObjectDetectorMixin>();
storageSettings = new StorageSettings(this, {
newPipeline: {
title: 'New Video Pipeline',
description: 'WARNING! DO NOT ENABLE: Use the new video pipeline. Leave blank to use the legacy pipeline.',
type: 'device',
deviceFilter: `interfaces.includes('${ScryptedInterface.VideoFrameGenerator}')`,
},
activeMotionDetections: {
title: 'Active Motion Detection Sessions',
readonly: true,
mapGet: () => {
return [...this.currentMixins.values()]
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
.reduce((c2, v2) => c2 + (v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
}
},
activeObjectDetections: {
title: 'Active Object Detection Sessions',
readonly: true,
mapGet: () => {
return [...this.currentMixins.values()]
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
.reduce((c2, v2) => c2 + (!v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
}
}
})
constructor(nativeId?: ScryptedNativeId) {
super(nativeId);
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (!interfaces.includes(ScryptedInterface.ObjectDetection))
return;
@@ -1057,12 +1184,15 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider {
async getMixin(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
const model = await mixinDevice.getDetectionModel();
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model);
const ret = new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this, model);
this.currentMixins.add(ret);
return ret;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
// what does this mean to make a mixin provider no longer available?
// just ignore it until reboot?
this.currentMixins.delete(mixinDevice);
}
}

View File

@@ -1,3 +1,5 @@
import sdk from '@scrypted/sdk';
export function safeParseJson(value: string) {
try {
return JSON.parse(value);
@@ -5,3 +7,7 @@ export function safeParseJson(value: string) {
catch (e) {
}
}
export function getAllDevices() {
return Object.keys(sdk.systemManager.getSystemState()).map(id => sdk.systemManager.getDeviceById(id));
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/onvif",
"version": "0.0.114",
"version": "0.0.117",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/onvif",
"version": "0.0.114",
"version": "0.0.117",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/onvif",
"version": "0.0.114",
"version": "0.0.117",
"description": "ONVIF Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,4 +1,4 @@
import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PanTiltZoom, PanTiltZoomCommand, PictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import { AddressInfo } from "net";
import onvif from 'onvif';
import { Stream } from "stream";
@@ -6,6 +6,7 @@ import xml2js from 'xml2js';
import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { connectCameraAPI, OnvifCameraAPI, OnvifEvent } from "./onvif-api";
import { OnvifIntercom } from "./onvif-intercom";
import { OnvifPTZMixinProvider } from "./onvif-ptz";
const { mediaManager, systemManager, deviceManager } = sdk;
@@ -393,7 +394,7 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
}
async putSetting(key: string, value: string) {
async putSetting(key: string, value: any) {
this.client = undefined;
this.rtspMediaStreamOptions = undefined;
@@ -429,6 +430,17 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
constructor(nativeId?: string) {
super(nativeId);
process.nextTick(() => {
deviceManager.onDeviceDiscovered({
name: 'ONVIF PTZ',
type: ScryptedDeviceType.Builtin,
nativeId: 'ptz',
interfaces: [
ScryptedInterface.MixinProvider,
]
})
})
onvif.Discovery.on('device', (cam: any, rinfo: AddressInfo, xml: any) => {
// Function will be called as soon as the NVT responses
@@ -511,6 +523,12 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
})
}
async getDevice(nativeId: string) {
if (nativeId === 'ptz')
return new OnvifPTZMixinProvider('ptz');
return super.getDevice(nativeId);
}
getAdditionalInterfaces() {
return [
ScryptedInterface.Camera,
@@ -531,6 +549,7 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
let ptzCapabilities: string[];
if (!skipValidate) {
try {
const api = await connectCameraAPI(httpAddress, username, password, this.console, undefined);
@@ -545,6 +564,13 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
}
settings.newCamera = info.model;
if (api.cam?.services?.find((s: any) => s.namespace === 'http://www.onvif.org/ver20/ptz/wsdl')) {
ptzCapabilities = [
'Pan',
'Tilt',
];
}
}
catch (e) {
this.console.error('Error adding ONVIF camera', e);
@@ -576,6 +602,16 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
intercom.intercomClient?.client.destroy();
}
if (ptzCapabilities) {
try {
const rd = sdk.systemManager.getDeviceById(device.id);
const ptz = await this.getDevice('ptz');
rd.setMixins([...(rd.mixins || []), ptz.id]);
}
catch (e) {
}
}
return nativeId;
}

View File

@@ -0,0 +1,133 @@
import { DeviceState, MixinDeviceBase, MixinDeviceOptions, MixinProvider, PanTiltZoom, PanTiltZoomCommand, PanTiltZoomMovement, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { connectCameraAPI } from "./onvif-api";
import {SettingsMixinDeviceBase, SettingsMixinDeviceOptions} from '../../../common/src/settings-mixin';
export class OnvifPtzMixin extends SettingsMixinDeviceBase<Settings> implements PanTiltZoom, Settings {
storageSettings = new StorageSettings(this, {
ptz: {
title: 'Pan/Tilt/Zoom',
type: 'string',
multiple: true,
choices: [
'Pan',
'Tilt',
'Zoom',
],
persistedDefaultValue: [
'Pan',
'Tilt',
],
onPut: (ov, ptz: string[]) => {
this.ptzCapabilities = {
pan: ptz.includes('Pan'),
tilt: ptz.includes('Tilt'),
zoom: ptz.includes('Zoom'),
}
}
}
});
constructor(options: SettingsMixinDeviceOptions<Settings>) {
super(options);
// force a read to set the state.
this.storageSettings.values.ptz;
}
getMixinSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putMixinSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async ptzCommand(command: PanTiltZoomCommand) {
const client = await this.getClient();
let speed: any;
if (command.speed) {
speed = {
x: command.speed.pan,
y: command.speed.tilt,
zoom: command.speed.zoom
};
}
if (command.movement === PanTiltZoomMovement.Relative) {
return new Promise<void>((r, f) => {
client.cam.relativeMove({
x: command.pan,
y: command.tilt,
zoom: command.zoom,
speed: speed
}, (e, result, xml) => {
if (e)
return f(e);
r();
})
})
}
else if (command.movement === PanTiltZoomMovement.Absolute) {
return new Promise<void>((r, f) => {
client.cam.absoluteMove({
x: command.pan,
y: command.tilt,
zoom: command.zoom,
speed: speed
}, (e, result, xml) => {
if (e)
return f(e);
r();
})
})
}
}
async getClient() {
const creds = await this.getCredentials();
return connectCameraAPI(creds.ipAndPort, creds.username, creds.password, this.console, undefined)
}
async getCredentials() {
const settings = await this.mixinDevice.getSettings();
const username = settings.find(s => s.key === 'username')?.value?.toString();
const password = settings.find(s => s.key === 'password')?.value?.toString();
const ip = settings.find(s => s.key === 'ip')?.value?.toString();
const httpPort = settings.find(s => s.key === 'httpPort')?.value?.toString();
const ipAndPort = `${ip}:${httpPort || 80}`;
return {
ipAndPort,
username,
password,
}
}
}
export class OnvifPTZMixinProvider extends ScryptedDeviceBase implements MixinProvider {
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (type !== ScryptedDeviceType.Camera || !interfaces.includes(ScryptedInterface.VideoCamera) || !interfaces.includes(ScryptedInterface.Settings))
return;
return [
ScryptedInterface.PanTiltZoom,
ScryptedInterface.Settings,
];
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState): Promise<any> {
return new OnvifPtzMixin({
group: 'ONVIF PTZ',
groupKey: 'ptz',
mixinDevice,
mixinDeviceInterfaces,
mixinDeviceState,
mixinProviderNativeId: this.nativeId,
})
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/pam-diff",
"version": "0.0.15",
"version": "0.0.16",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/pam-diff",
"version": "0.0.15",
"version": "0.0.16",
"hasInstallScript": true,
"dependencies": {
"@types/node": "^16.6.1",

View File

@@ -43,5 +43,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.15"
"version": "0.0.16"
}

View File

@@ -1,4 +1,4 @@
import { FFmpegInput, MediaObject, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionSession, ObjectsDetected, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes } from '@scrypted/sdk';
import { ObjectDetectionResult, FFmpegInput, MediaObject, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionSession, ObjectsDetected, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes } from '@scrypted/sdk';
import sdk from '@scrypted/sdk';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "../../../common/src/media-helpers";
@@ -121,22 +121,42 @@ class PamDiff extends ScryptedDeviceBase implements ObjectDetection {
const p2p = new P2P();
const pamDiff = new PD({
difference: 9,
percent: 75,
response: 'percent',
difference: session.settings?.difference || defaultDifference,
percent: session.settings?.percent || defaultPercentage,
response: session?.settings?.motionAsObjects ? 'blobs' : 'percent',
});
pamDiff.on('diff', async (data: any) => {
const trigger = data.trigger[0];
// console.log(trigger.blobs.length);
const { blobs } = trigger;
const detections: ObjectDetectionResult[] = [];
if (blobs?.length) {
for (const blob of blobs) {
detections.push(
{
className: 'motion',
score: trigger.percent / 100,
boundingBox: [blob.minX, blob.minY, blob.maxX - blob.minX, blob.maxY - blob.minY],
}
)
}
}
else {
detections.push(
{
className: 'motion',
score: trigger.percent / 100,
}
)
}
const event: ObjectsDetected = {
timestamp: Date.now(),
running: true,
detectionId: pds.id,
detections: [
{
className: 'motion',
score: data.trigger[0].percent / 100,
}
]
inputDimensions: [640, 360],
detections,
}
if (pds.callbacks) {
pds.callbacks.onDetection(event);
@@ -149,10 +169,13 @@ class PamDiff extends ScryptedDeviceBase implements ObjectDetection {
const console = sdk.deviceManager.getMixinConsole(mediaObject.sourceId, this.nativeId);
pds.pamDiff = pamDiff;
pds.pamDiff.setDifference(session.settings?.difference || defaultDifference).setPercent(session.settings?.percent || defaultPercentage);
pds.pamDiff
.setDifference(session.settings?.difference || defaultDifference)
.setPercent(session.settings?.percent || defaultPercentage)
.setResponse(session?.settings?.motionAsObjects ? 'blobs' : 'percent');;
safePrintFFmpegArguments(console, args);
pds.cp = child_process.spawn(ffmpeg, args, {
stdio:[ 'inherit', 'pipe', 'pipe', 'pipe']
stdio: ['inherit', 'pipe', 'pipe', 'pipe']
});
let pamTimeout: NodeJS.Timeout;
const resetTimeout = () => {

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
import sdk from '@scrypted/sdk';
export async function getUrlLocalAdresses(console: Console, url: string) {
let urls: string[];
try {
const addresses = await sdk.endpointManager.getLocalAddresses();
if (addresses) {
const [address] = addresses;
if (address) {
const u = new URL(url);
u.hostname = address;
url = u.toString();
}
urls = addresses.map(address => {
const u = new URL(url);
u.hostname = address;
return u.toString();
});
}
}
catch (e) {
console.warn('Error determining external addresses. Is Scrypted Server Address configured?', e);
}
return urls;
}

View File

@@ -19,6 +19,7 @@ import semver from 'semver';
import { Duplex } from 'stream';
import { Worker } from 'worker_threads';
import { FileRtspServer } from './file-rtsp-server';
import { getUrlLocalAdresses } from './local-addresses';
import { REBROADCAST_MIXIN_INTERFACE_TOKEN } from './rebroadcast-mixin-token';
import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
import { RtspSessionParserSpecific, startRtspSession } from './rtsp-session';
@@ -1154,23 +1155,7 @@ class PrebufferSession {
url = clientPromise.url;
if (hostname) {
try {
const addresses = await sdk.endpointManager.getLocalAddresses();
const [address] = addresses;
if (address && options?.route === 'external') {
const u = new URL(url);
u.hostname = address;
url = u.toString();
}
urls = addresses.map(address => {
const u = new URL(url);
u.hostname = address;
return u.toString();
})
}
catch (e) {
this.console.warn('Error determining external addresses. Is Scrypted Server Address configured?');
}
urls = await getUrlLocalAdresses(this.console, url);
}
}
else {

View File

@@ -2,3 +2,4 @@
out/
node_modules/
dist/
.venv

View File

@@ -0,0 +1,9 @@
.DS_Store
out/
node_modules/
*.map
fs
src
.vscode
dist/*.js
.venv

View File

@@ -0,0 +1,41 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Scrypted Debugger",
"type": "python",
"request": "attach",
"connect": {
"host": "${config:scrypted.debugHost}",
"port": 10081
},
"justMyCode": false,
"preLaunchTask": "scrypted: deploy+debug",
"pathMappings": [
{
"localRoot": "/Volumes/Dev/scrypted/server/python/",
"remoteRoot": "/Volumes/Dev/scrypted/server/python/",
},
{
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
},
]
},
{
"name": "Python: Test",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/test.py",
"console": "internalConsole",
"justMyCode": true,
"env": {
"GST_PLUGIN_PATH": "/opt/homebrew/lib/gstreamer-1.0"
}
}
]
}

View File

@@ -0,0 +1,19 @@
{
// docker installation
// "scrypted.debugHost": "koushik-thin",
// "scrypted.serverRoot": "/server",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// local checkout
"scrypted.debugHost": "127.0.0.1",
"scrypted.serverRoot": "/Users/koush/.scrypted",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}

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