mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 14:13:28 +00:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7128af20af | ||
|
|
c651c2164b | ||
|
|
6caafd73f5 | ||
|
|
05cb505783 | ||
|
|
07baddc9c3 | ||
|
|
76ac260bf7 | ||
|
|
dfee7c6b09 | ||
|
|
b3ce6a2af3 | ||
|
|
933c0cac0f | ||
|
|
1fb1334a00 | ||
|
|
cb45a00c25 | ||
|
|
fec59af263 | ||
|
|
5d213a4c51 | ||
|
|
d444c4ab7c | ||
|
|
590f955ca9 | ||
|
|
7df4bf2723 | ||
|
|
3416347a1f | ||
|
|
c669bb8902 | ||
|
|
ce5fd2d4fd | ||
|
|
fa8a756059 | ||
|
|
73b85e1cd0 | ||
|
|
1300073712 | ||
|
|
3e296e12a5 | ||
|
|
bf98060a08 | ||
|
|
d1cd380123 | ||
|
|
1a2aadfb52 | ||
|
|
60c854a477 | ||
|
|
0790b60122 | ||
|
|
a3caa09df4 | ||
|
|
02ca8bd765 | ||
|
|
f9e1a94ab3 | ||
|
|
dd0da26df3 | ||
|
|
890f2e8daf | ||
|
|
2c8babe3ce | ||
|
|
8e31b5f970 | ||
|
|
0873a72848 | ||
|
|
145c66e1c8 | ||
|
|
2b60b45113 | ||
|
|
6f63927e2f | ||
|
|
528eabdfc0 | ||
|
|
e201ea1fc1 | ||
|
|
7790810b86 | ||
|
|
e9ec78909b | ||
|
|
26245e17ca | ||
|
|
5d87a1b2dd | ||
|
|
e1efde3868 | ||
|
|
525eb028c6 | ||
|
|
520c6a62a1 | ||
|
|
6e6898ce33 | ||
|
|
1344c9112c | ||
|
|
f2148ce26a | ||
|
|
81b00195d6 | ||
|
|
8f71778f05 | ||
|
|
2e5b8d90aa | ||
|
|
780182b94a | ||
|
|
57480f7606 | ||
|
|
1478684120 | ||
|
|
223b302bed | ||
|
|
f56cef1b50 | ||
|
|
83bfa30d4b | ||
|
|
611674af46 | ||
|
|
941ea7f346 | ||
|
|
2b9c2956d6 | ||
|
|
266d5bf8a3 | ||
|
|
d0007fc7bb | ||
|
|
75f90b78eb | ||
|
|
1e8959413e | ||
|
|
1301247ea3 | ||
|
|
2798fe4d3d | ||
|
|
55a76a86dc | ||
|
|
cebd49fadb | ||
|
|
90adb11f27 | ||
|
|
cea5c95c82 | ||
|
|
0405e13181 | ||
|
|
5659499c16 | ||
|
|
d272a4b86f | ||
|
|
f8a8ed4241 | ||
|
|
892b978065 | ||
|
|
c81c55c12e | ||
|
|
bb9d98921b | ||
|
|
4c66efc4af | ||
|
|
0547ed9a32 | ||
|
|
b046822282 | ||
|
|
b033d24451 | ||
|
|
15464229ad | ||
|
|
93ad50db73 | ||
|
|
427139e8df | ||
|
|
b1100398ec | ||
|
|
b40a2eaf6e | ||
|
|
17c9440fd9 | ||
|
|
ea63a96444 | ||
|
|
0f02f96b89 | ||
|
|
6ce538bb23 | ||
|
|
29ab0e79de | ||
|
|
e07cd13ef3 | ||
|
|
0cbb26051c | ||
|
|
fcb8d938ee | ||
|
|
98fe1d412a | ||
|
|
c19ec63f98 | ||
|
|
a41e915f69 | ||
|
|
f0db59f6d2 | ||
|
|
8e691ff2ee | ||
|
|
42e0810bc0 | ||
|
|
68e91ad996 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -27,6 +27,11 @@ Created issues that do not meet these requirements or are improperly filled out
|
||||
1. Delete this section and everything above it.
|
||||
2. Fill out the sections below.
|
||||
|
||||
** Before You Submit**
|
||||
|
||||
- [ ] I checked that my issue isn't already filed: [Search open issues](https://github.com/koush/scrypted/issues).
|
||||
- [ ] I checked the relevant camera/device and/or plugin `Log` in the `Management Console` for errors or warnings that may help identify and resolve the issue myself.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. The issue tracker is only for reporting bugs in Scrypted, for general support check Discord. Hardrware support requests or assistance requests will be immediately closed.
|
||||
|
||||
@@ -43,6 +48,9 @@ A clear and concise description of what you expected to happen.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
Include a `Log` from the device/camera in the management console (and if applicable, the affacted plugin, like HomeKit).
|
||||
|
||||
**Server (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu]
|
||||
- Installation Method: [e.g. Desktop App, Docker, Local]
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-latest, macos-14, macos-13, windows-latest]
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm, macos-14, macos-13, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -70,11 +70,7 @@ export function getH264DecoderArgs(): CodecArgs {
|
||||
],
|
||||
};
|
||||
|
||||
if (isRaspberryPi()) {
|
||||
ret['Raspberry Pi'] = ['-c:v', 'h264_mmal'];
|
||||
ret[V4L2] = ['-c:v', 'h264_v4l2m2m'];
|
||||
}
|
||||
else if (os.platform() === 'linux') {
|
||||
if (os.platform() === 'linux') {
|
||||
ret[V4L2] = ['-c:v', 'h264_v4l2m2m'];
|
||||
}
|
||||
else if (os.platform() === 'win32') {
|
||||
|
||||
@@ -247,6 +247,8 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'tcp',
|
||||
...(options?.vcodec || []),
|
||||
...(options?.acodec || []),
|
||||
// linux and windows seem to support 64000 but darwin is 32000?
|
||||
'-pkt_size', '32000',
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
@@ -394,7 +396,7 @@ export class RtspClient extends RtspBase {
|
||||
hasGetParameter = true;
|
||||
contentBase: string;
|
||||
|
||||
constructor(public url: string) {
|
||||
constructor(public readonly url: string) {
|
||||
super();
|
||||
const u = new URL(url);
|
||||
const port = parseInt(u.port) || 554;
|
||||
@@ -511,6 +513,42 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
|
||||
async *handleStream(): AsyncGenerator<{
|
||||
rtcp: boolean,
|
||||
header: Buffer,
|
||||
packet: Buffer,
|
||||
channel: number,
|
||||
}> {
|
||||
while (true) {
|
||||
const header = await readLength(this.client, 4);
|
||||
// can this even happen? since the RTSP request method isn't a fixed
|
||||
// value like the "RTSP" in the RTSP response, I don't think so?
|
||||
if (header[0] !== RTSP_FRAME_MAGIC) {
|
||||
if (header.toString() !== 'RTSP')
|
||||
throw this.createBadHeader(header);
|
||||
|
||||
this.client.unshift(header);
|
||||
|
||||
// do what with this?
|
||||
const message = await super.readMessage();
|
||||
const body = await this.readBody(parseHeaders(message));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const length = header.readUInt16BE(2);
|
||||
const packet = await readLength(this.client, length);
|
||||
const id = header.readUInt8(1);
|
||||
|
||||
yield {
|
||||
channel: id,
|
||||
rtcp: id % 2 === 1,
|
||||
header,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readLoop() {
|
||||
const deferred = new Deferred<void>();
|
||||
|
||||
@@ -613,7 +651,8 @@ export class RtspClient extends RtspBase {
|
||||
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
|
||||
|
||||
if (this.wwwAuthenticate.includes('Basic')) {
|
||||
const hash = BASIC.computeHash(url);
|
||||
const parsedUrl = new URL(this.url);
|
||||
const hash = BASIC.computeHash({ username: parsedUrl.username, password: parsedUrl.password });
|
||||
return `Basic ${hash}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.120.0-jammy-full"
|
||||
version: "v0.130.1-noble-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -16,6 +16,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
ENV SCRYPTED_BASE_VERSION="20250101"
|
||||
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -46,6 +46,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
ENV SCRYPTED_BASE_VERSION="20250101"
|
||||
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
# The following example would mount the /mnt/sda/video path on the host
|
||||
# The following example would mount the /mnt/media/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/media/video:/nvr
|
||||
|
||||
|
||||
@@ -36,9 +36,8 @@ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --yes --dea
|
||||
tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
apt -y update
|
||||
# is there a way to get a versioned package automatically?
|
||||
apt -y install nvidia-utils-560
|
||||
apt -y install cuda-drivers
|
||||
apt -y install nvidia-container-toolkit
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
|
||||
systemctl restart docker
|
||||
|
||||
@@ -27,7 +27,7 @@ echo "external/werift > npm install"
|
||||
npm install
|
||||
popd
|
||||
|
||||
for directory in rtsp amcrest onvif hikvision reolink unifi-protect webrtc homekit
|
||||
for directory in rtsp ffmpeg-camera amcrest onvif hikvision reolink unifi-protect webrtc homekit
|
||||
do
|
||||
echo "$directory > npm install"
|
||||
pushd plugins/$directory
|
||||
|
||||
50
packages/client/package-lock.json
generated
50
packages/client/package-lock.json
generated
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"engine.io-client": "^6.6.2",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -76,9 +76,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.92",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
|
||||
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
|
||||
"version": "0.3.100",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.100.tgz",
|
||||
"integrity": "sha512-s/07QCxjMWqODgWj2UpLehzeo2cGFrCA9X8mvpG3owT/+q+sb8v/UUcw9TLHGSN6yIriNhceg3i9WO07kEIT6A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
@@ -120,12 +120,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz",
|
||||
"integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
@@ -272,9 +273,10 @@
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
|
||||
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
|
||||
"integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
@@ -623,10 +625,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -636,10 +639,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -13,14 +13,14 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"engine.io-client": "^6.6.2",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
}
|
||||
|
||||
4
plugins/cloud/package-lock.json
generated
4
plugins/cloud/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.2.48",
|
||||
"version": "0.2.49",
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^4.3.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -52,5 +52,5 @@
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"version": "0.2.48"
|
||||
"version": "0.2.49"
|
||||
}
|
||||
|
||||
@@ -1046,12 +1046,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
args['--url'] = tunnelUrl;
|
||||
}
|
||||
|
||||
// if error messages are detected after 10 minutes from tunnel attempt start,
|
||||
// kill the tunnel.
|
||||
const tenMinutesMs = 10 * 60 * 1000;
|
||||
const tunnelStart = Date.now();
|
||||
const deferred = new Deferred<string>();
|
||||
|
||||
const cloudflareTunnel = cloudflared.tunnel(args);
|
||||
deferred.resolvePromise(cloudflareTunnel.url);
|
||||
|
||||
const processData = (string: string) => {
|
||||
this.console.error(string);
|
||||
|
||||
const lines = string.split('\n');
|
||||
for (const line of lines) {
|
||||
if ((line.includes('Unregistered tunnel connection')
|
||||
@@ -1059,12 +1063,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
|| line.includes('Register tunnel error')
|
||||
|| line.includes('Failed to serve tunnel')
|
||||
|| line.includes('Failed to get tunnel'))
|
||||
&& deferred.finished) {
|
||||
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');
|
||||
&& (deferred.finished || Date.now() - tunnelStart > tenMinutesMs)) {
|
||||
this.console.warn('Cloudflare registration failure detected. Terminating.');
|
||||
cloudflareTunnel.child.kill();
|
||||
}
|
||||
if (line.includes('hostname'))
|
||||
this.console.log(line);
|
||||
const match = /config=(".*?}")/gm.exec(line)
|
||||
if (match) {
|
||||
const json = match[1];
|
||||
@@ -1109,7 +1111,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
throw e;
|
||||
}
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
|
||||
return cloudflareTunnel;
|
||||
return {
|
||||
url: deferred.promise,
|
||||
child: cloudflareTunnel.child,
|
||||
};
|
||||
}, {
|
||||
startingDelay: 60000,
|
||||
timeMultiple: 1.2,
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.111",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.111",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.111",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { MediaCore } from './media-core';
|
||||
import { checkLegacyLxc, checkLxc } from './platform/lxc';
|
||||
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
|
||||
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
|
||||
import { TerminalService, TerminalServiceNativeId, newTerminalService } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
|
||||
@@ -140,7 +140,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
{
|
||||
name: 'Terminal Service',
|
||||
nativeId: TerminalServiceNativeId,
|
||||
interfaces: [ScryptedInterface.StreamService, ScryptedInterface.TTY],
|
||||
interfaces: [ScryptedInterface.StreamService, ScryptedInterface.TTY, ScryptedInterface.ClusterForkInterface],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
@@ -242,7 +242,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
if (nativeId === UsersNativeId)
|
||||
return this.users ||= new UsersCore();
|
||||
if (nativeId === TerminalServiceNativeId)
|
||||
return this.terminalService ||= new TerminalService();
|
||||
return this.terminalService ||= new TerminalService(TerminalServiceNativeId, false);
|
||||
if (nativeId === ReplServiceNativeId)
|
||||
return this.replService ||= new PluginSocketService(ReplServiceNativeId, 'repl');
|
||||
if (nativeId === ConsoleServiceNativeId)
|
||||
@@ -331,5 +331,6 @@ export async function fork() {
|
||||
return {
|
||||
tsCompile,
|
||||
newScript,
|
||||
newTerminalService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function checkLegacyLxc() {
|
||||
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC)
|
||||
return;
|
||||
|
||||
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/installation.html#proxmox-ve-container-reset');
|
||||
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/install/proxmox-ve.html#proxmox-ve-container-reset');
|
||||
}
|
||||
|
||||
const DOCKER_COMPOSE_SH_PATH = '/root/.scrypted/docker-compose.sh';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTYSettings } from "@scrypted/sdk";
|
||||
import sdk, { ClusterForkInterface, ClusterForkInterfaceOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedNativeId, StreamService, TTYSettings } from "@scrypted/sdk";
|
||||
import type { IPty, spawn as ptySpawn } from 'node-pty';
|
||||
import { createAsyncQueue } from '@scrypted/common/src/async-queue'
|
||||
import { ChildProcess, spawn as childSpawn } from "child_process";
|
||||
@@ -111,8 +111,11 @@ class NoninteractiveTerminal {
|
||||
}
|
||||
|
||||
|
||||
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer> {
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
export class TerminalService extends ScryptedDeviceBase implements StreamService<Buffer | string, Buffer>, ClusterForkInterface {
|
||||
private forks: { [clusterWorkerId: string]: TerminalService } = {};
|
||||
private forkClients: 0;
|
||||
|
||||
constructor(nativeId?: ScryptedNativeId, private isFork: boolean = false) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
@@ -134,6 +137,42 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
return extraPaths;
|
||||
}
|
||||
|
||||
async forkInterface<StreamService>(forkInterface: ScryptedInterface, options?: ClusterForkInterfaceOptions): Promise<StreamService> {
|
||||
if (forkInterface !== ScryptedInterface.StreamService) {
|
||||
throw new Error('can only fork StreamService');
|
||||
}
|
||||
|
||||
if (!options?.clusterWorkerId) {
|
||||
throw new Error('clusterWorkerId required');
|
||||
}
|
||||
|
||||
if (this.isFork) {
|
||||
throw new Error('cannot fork a fork');
|
||||
}
|
||||
|
||||
const clusterWorkerId = options.clusterWorkerId;
|
||||
if (this.forks[clusterWorkerId]) {
|
||||
return this.forks[clusterWorkerId] as StreamService;
|
||||
}
|
||||
|
||||
const fork = sdk.fork<{
|
||||
newTerminalService: typeof newTerminalService,
|
||||
}>({ clusterWorkerId });
|
||||
try {
|
||||
const result = await fork.result;
|
||||
const terminalService = await result.newTerminalService();
|
||||
this.forks[clusterWorkerId] = terminalService;
|
||||
fork.worker.on('exit', () => {
|
||||
delete this.forks[clusterWorkerId];
|
||||
});
|
||||
return terminalService as StreamService;
|
||||
}
|
||||
catch (e) {
|
||||
fork.worker.terminate();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The input to this stream can send buffers for normal terminal data and strings
|
||||
* for control messages. Control messages are JSON-formatted.
|
||||
@@ -149,6 +188,19 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
const queue = createAsyncQueue<Buffer>();
|
||||
const extraPaths = await this.getExtraPaths();
|
||||
|
||||
if (this.isFork) {
|
||||
this.forkClients++;
|
||||
}
|
||||
|
||||
queue.endPromise.then(() => {
|
||||
if (this.isFork) {
|
||||
this.forkClients--;
|
||||
if (this.forkClients === 0) {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function registerChildListeners() {
|
||||
cp.onExit(() => queue.end());
|
||||
|
||||
@@ -232,4 +284,8 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
|
||||
return generator();
|
||||
}
|
||||
}
|
||||
|
||||
export async function newTerminalService(): Promise<TerminalService> {
|
||||
return new TerminalService(TerminalServiceNativeId, true);
|
||||
}
|
||||
124
plugins/doorbird/package-lock.json
generated
124
plugins/doorbird/package-lock.json
generated
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"dependencies": {
|
||||
"doorbird": "^2.1.2"
|
||||
"doorbird": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node": "^22.10.10",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
},
|
||||
@@ -24,36 +24,41 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.8",
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.108",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -65,11 +70,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -81,10 +84,14 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true
|
||||
"version": "22.10.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
|
||||
"integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
@@ -92,11 +99,12 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz",
|
||||
"integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==",
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -145,10 +153,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -167,25 +176,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/doorbird": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.1.2.tgz",
|
||||
"integrity": "sha512-ivwwsS/nOslDnuLg3UB60Axo76w5LQuZ67mCPEeWFr5+HbGYRL7PCY3iLjWYaIakh5+IvZyFPHKR4yHAvAc1WQ==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.6.0.tgz",
|
||||
"integrity": "sha512-HZBI5uFhwEVF8JFULQlpzXXvjSHmtQMJUNWfogq6vHe3kv7mCSmg0g/TDbeV5fVvisi8w7GxKD0/PpZCrtcGOg==",
|
||||
"dependencies": {
|
||||
"axios": "^1.2.1",
|
||||
"axios": "^1.6.2",
|
||||
"chacha-js": "^2.1.1",
|
||||
"libsodium-wrappers-sumo": "^0.7.11"
|
||||
"libsodium-wrappers-sumo": "^0.7.13"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -225,16 +238,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/libsodium-sumo": {
|
||||
"version": "0.7.11",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.11.tgz",
|
||||
"integrity": "sha512-bY+7ph7xpk51Ez2GbE10lXAQ5sJma6NghcIDaSPbM/G9elfrjLa0COHl/7P6Wb/JizQzl5UQontOOP1z0VwbLA=="
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz",
|
||||
"integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw=="
|
||||
},
|
||||
"node_modules/libsodium-wrappers-sumo": {
|
||||
"version": "0.7.11",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.11.tgz",
|
||||
"integrity": "sha512-DGypHOmJbB1nZn89KIfGOAkDgfv5N6SBGC3Qvmy/On0P0WD1JQvNRS/e3UL3aFF+xC0m+MYz5M+MnRnK2HMrKQ==",
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz",
|
||||
"integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==",
|
||||
"dependencies": {
|
||||
"libsodium-sumo": "^0.7.11"
|
||||
"libsodium-sumo": "^0.7.15"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
@@ -307,6 +320,13 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/doorbird",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -33,12 +33,12 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"doorbird": "^2.1.2"
|
||||
"doorbird": "2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node": "^22.10.10",
|
||||
"cross-env": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { listenZero } from '@scrypted/common/src/listen-cluster';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, DeviceCreator, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, MotionSensor } from '@scrypted/sdk';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
import net from 'net';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import { authHttpFetch } from "@scrypted/common/src/http-auth-fetch";
|
||||
import { ApiRingEvent, ApiMotionEvent, DoorbirdAPI } from "./doorbird-api";
|
||||
import { listenZero } from '@scrypted/common/src/listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
|
||||
import { readLength } from "@scrypted/common/src/read-stream";
|
||||
import sdk, { BinarySensor, Camera, DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, FFmpegInput, Intercom, MediaObject, MotionSensor, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera } from '@scrypted/sdk';
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { randomBytes } from 'crypto';
|
||||
import net from 'net';
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { ApiMotionEvent, ApiRingEvent, DoorbirdAPI } from "./doorbird-api";
|
||||
|
||||
const { deviceManager, mediaManager } = sdk;
|
||||
|
||||
@@ -384,7 +384,7 @@ class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, Vid
|
||||
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
|
||||
server.close();
|
||||
}, 30000);
|
||||
const port = await listenZero(server);
|
||||
const port = await listenZero(server, '127.0.0.1');
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
|
||||
2
plugins/dummy-switch/.vscode/settings.json
vendored
2
plugins/dummy-switch/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
130
plugins/dummy-switch/package-lock.json
generated
130
plugins/dummy-switch/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/dummy-switch",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.25",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/dummy-switch",
|
||||
"version": "0.0.24",
|
||||
"version": "0.0.25",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
"axios": "^1.3.6"
|
||||
@@ -23,35 +23,41 @@
|
||||
"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"
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.97",
|
||||
"version": "0.3.106",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -63,11 +69,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -92,11 +96,11 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz",
|
||||
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==",
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -121,9 +125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -182,35 +186,39 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -224,11 +232,11 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz",
|
||||
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==",
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -247,9 +255,9 @@
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.24"
|
||||
"version": "0.0.25"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,60 @@ import { BinarySensor, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Loc
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { ReplaceMotionSensor, ReplaceMotionSensorNativeId } from './replace-motion-sensor';
|
||||
import { ReplaceBinarySensor, ReplaceBinarySensorNativeId } from './replace-binary-sensor';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
|
||||
const { log, deviceManager } = sdk;
|
||||
|
||||
class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop, OccupancySensor, MotionSensor, BinarySensor, Settings {
|
||||
timeout: NodeJS.Timeout;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
reset: {
|
||||
title: 'Reset Sensor',
|
||||
description: 'Reset the motion sensor and binary sensor after the given seconds. Enter 0 to never reset.',
|
||||
defaultValue: 10,
|
||||
type: 'number',
|
||||
placeholder: '10',
|
||||
onPut: () => {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
},
|
||||
actionTypes: {
|
||||
title: 'Action Types',
|
||||
description: 'Select the action types to expose.',
|
||||
defaultValue: [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.StartStop,
|
||||
ScryptedInterface.Lock,
|
||||
],
|
||||
multiple: true,
|
||||
choices: [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.StartStop,
|
||||
ScryptedInterface.Lock,
|
||||
],
|
||||
onPut: () => {
|
||||
this.reportInterfaces();
|
||||
},
|
||||
},
|
||||
sensorTypes: {
|
||||
title: 'Sensor Types',
|
||||
description: 'Select the sensor types to expose.',
|
||||
defaultValue: [
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.OccupancySensor,
|
||||
],
|
||||
multiple: true,
|
||||
choices: [
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.OccupancySensor,
|
||||
],
|
||||
onPut: () => {
|
||||
this.reportInterfaces();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
@@ -19,6 +68,22 @@ class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop,
|
||||
this.occupied = false;
|
||||
}
|
||||
|
||||
async reportInterfaces() {
|
||||
const interfaces: ScryptedInterface[] = this.storageSettings.values.sensorTypes || [];
|
||||
if (!interfaces.length)
|
||||
interfaces.push(ScryptedInterface.MotionSensor, ScryptedInterface.BinarySensor, ScryptedInterface.OccupancySensor);
|
||||
const actionTyoes = this.storageSettings.values.actionTypes || [];
|
||||
if (!actionTyoes.length)
|
||||
actionTyoes.push(ScryptedInterface.OnOff, ScryptedInterface.StartStop, ScryptedInterface.Lock);
|
||||
|
||||
await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId: this.nativeId,
|
||||
interfaces: [...interfaces, ...actionTyoes, ScryptedInterface.Settings],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
name: this.providedName,
|
||||
});
|
||||
}
|
||||
|
||||
lock(): Promise<void> {
|
||||
return this.turnOff();
|
||||
}
|
||||
@@ -31,20 +96,12 @@ class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop,
|
||||
stop(): Promise<void> {
|
||||
return this.turnOff();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'reset',
|
||||
title: 'Reset Sensor',
|
||||
description: 'Reset the motion sensor and binary sensor after the given seconds. Enter 0 to never reset.',
|
||||
value: this.storage.getItem('reset') || '10',
|
||||
placeholder: '10',
|
||||
}
|
||||
]
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
clearTimeout(this.timeout);
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
// note that turnOff locks the lock
|
||||
@@ -131,12 +188,6 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
const nativeId = 'shell:' + Math.random().toString();
|
||||
const name = settings.name?.toString();
|
||||
|
||||
await this.onDiscovered(nativeId, name);
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async onDiscovered(nativeId: string, name: string) {
|
||||
await deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
@@ -151,6 +202,8 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
});
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
@@ -163,11 +216,6 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
if (!ret) {
|
||||
ret = new DummyDevice(nativeId);
|
||||
|
||||
// remove legacy scriptable interface
|
||||
if (ret.interfaces.includes(ScryptedInterface.Scriptable)) {
|
||||
setTimeout(() => this.onDiscovered(ret.nativeId, ret.providedName), 2000);
|
||||
}
|
||||
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "Node16",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.160",
|
||||
"version": "0.0.162",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.160",
|
||||
"version": "0.0.162",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.160",
|
||||
"version": "0.0.162",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -116,11 +116,15 @@ export class HikvisionCameraAPI implements HikvisionAPI {
|
||||
}
|
||||
|
||||
async checkIsOldModel() {
|
||||
// The old Hikvision DS-7608NI-E2 doesn't support channel capability checks, and the requests cause errors
|
||||
// The old Hikvision NVRs don't support channel capability checks, and the requests cause errors
|
||||
const oldModels = [
|
||||
/DS-76098NI-E2/,
|
||||
/ERI-K104-P4/
|
||||
];
|
||||
const model = await this.checkDeviceModel();
|
||||
if (!model)
|
||||
return;
|
||||
return !!model?.match(/DS-7608NI-E2/);
|
||||
return !!oldModels.find(oldModel => model?.match(oldModel));
|
||||
}
|
||||
|
||||
async checkStreamSetup(channel: string, isOld: boolean): Promise<HikvisionCameraStreamSetup> {
|
||||
|
||||
@@ -161,14 +161,9 @@ export class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom
|
||||
|
||||
const now = Date.now();
|
||||
let detections: ObjectDetectionResult[] = xml.EventNotificationAlert?.DetectionRegionList?.map(region => {
|
||||
const { DetectionRegionEntry } = region;
|
||||
const dre = DetectionRegionEntry[0];
|
||||
if (!DetectionRegionEntry)
|
||||
const name = region?.DetectionRegionEntry?.[0]?.detectionTarget?.name;
|
||||
if (!name)
|
||||
return;
|
||||
const { detectionTarget } = dre;
|
||||
// const { TargetRect } = dre;
|
||||
// const { X, Y, width, height } = TargetRect[0];
|
||||
const [name] = detectionTarget;
|
||||
return {
|
||||
score: 1,
|
||||
className: detectionMap[name] || name,
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.61",
|
||||
"version": "1.2.63",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.61",
|
||||
"version": "1.2.63",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.61",
|
||||
"version": "1.2.63",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -216,6 +216,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
throw Error(`error in device reordering, expected ${uniqueDeviceIds.size} unique devices but only got ${uniqueReorderedIds.size} entries!`);
|
||||
}
|
||||
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
for (const id of reorderedDeviceIds) {
|
||||
const device = systemManager.getDeviceById<Online>(id);
|
||||
const supportedType = supportedTypes[device.type];
|
||||
@@ -224,8 +225,7 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
|
||||
try {
|
||||
const mixins = (device.mixins || []).slice();
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
if (!mixins.includes(this.id) && autoAdd) {
|
||||
if (!mixins.includes(this.id)) {
|
||||
// don't sync this by default, as it's solely for automations
|
||||
if (device.type === ScryptedDeviceType.Notifier)
|
||||
continue;
|
||||
@@ -235,6 +235,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
continue;
|
||||
if (defaultIncluded[device.id] === includeToken)
|
||||
continue;
|
||||
if (!autoAdd)
|
||||
continue;
|
||||
mixins.push(this.id);
|
||||
await device.setMixins(mixins);
|
||||
defaultIncluded[device.id] = includeToken;
|
||||
|
||||
@@ -519,11 +519,15 @@ export class H264Repacketizer {
|
||||
// after the codec information. so codec information can be changed between
|
||||
// idr and non-idr? maybe it is not applied until next idr?
|
||||
}
|
||||
else if (nalType === NAL_TYPE_IDR) {
|
||||
// this is uncommon but has been seen on tapo.
|
||||
// i have no clue how they can fit an idr frame into a single packet stapa.
|
||||
}
|
||||
else if (nalType === 0) {
|
||||
// nal delimiter or something. usually empty.
|
||||
}
|
||||
else {
|
||||
this.console.warn('Skipped a stapa type. Please report this to @koush on Discord.', nalType)
|
||||
this.console.warn('Skipped a stapa type.', nalType)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
|
||||
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, HumiditySettingStatus, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
|
||||
import { bindCharacteristic } from "../common";
|
||||
import { Accessory, Characteristic, CharacteristicEventTypes, Service, uuid } from '../hap';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
@@ -96,24 +96,161 @@ export function addCarbonDioxideSensor(device: ScryptedDevice & CO2Sensor, acces
|
||||
return co2Service;
|
||||
}
|
||||
|
||||
export function addFan(device: ScryptedDevice & Fan & OnOff, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.OnOff) && !device.interfaces.includes(ScryptedInterface.Fan))
|
||||
function commonHumidifierDehumidifier(mode: HumidityMode, subtype: string, name: string, device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
function currentState(mode: HumidityMode) {
|
||||
switch(mode) {
|
||||
case HumidityMode.Humidify:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING;
|
||||
case HumidityMode.Dehumidify:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING;
|
||||
case HumidityMode.Off:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.INACTIVE;
|
||||
default:
|
||||
return Characteristic.CurrentHumidifierDehumidifierState.IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
function targetState(mode: HumidityMode) {
|
||||
switch(mode) {
|
||||
case HumidityMode.Humidify:
|
||||
return Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER;
|
||||
case HumidityMode.Dehumidify:
|
||||
return Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER;
|
||||
default:
|
||||
return Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER;
|
||||
}
|
||||
}
|
||||
|
||||
const service = accessory.addService(Service.HumidifierDehumidifier, name, subtype);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.Active,
|
||||
() => {
|
||||
if (!device.humiditySetting?.mode)
|
||||
return false;
|
||||
if (device.humiditySetting.mode === mode)
|
||||
return true;
|
||||
if (device.humiditySetting.mode === HumidityMode.Auto)
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value ? mode : HumidityMode.Off
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySensor, service, Characteristic.CurrentRelativeHumidity,
|
||||
() => device.humidity);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.CurrentHumidifierDehumidifierState,
|
||||
() => currentState(device.humiditySetting?.activeMode));
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetHumidifierDehumidifierState,
|
||||
() => targetState(device.humiditySetting?.mode));
|
||||
|
||||
service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
|
||||
? HumidityMode.Humidify
|
||||
: value === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
|
||||
? HumidityMode.Dehumidify
|
||||
: HumidityMode.Auto
|
||||
});
|
||||
});
|
||||
|
||||
function targetHumidity(setting: HumiditySettingStatus) {
|
||||
if (!setting)
|
||||
return 0;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify)
|
||||
&& setting?.availableModes.includes(HumidityMode.Dehumidify)) {
|
||||
if (setting?.activeMode === HumidityMode.Humidify)
|
||||
return setting?.humidifierSetpoint;
|
||||
if (setting?.activeMode === HumidityMode.Dehumidify)
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify))
|
||||
return setting?.humidifierSetpoint;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Dehumidify))
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetRelativeHumidity,
|
||||
() => targetHumidity(device.humiditySetting));
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
function addHumidifier(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
var service = commonHumidifierDehumidifier(HumidityMode.Humidify, "humidifier", device.name + " Humidifier", device, accessory);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.RelativeHumidityHumidifierThreshold,
|
||||
() => device.humiditySetting?.humidifierSetpoint);
|
||||
service.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
humidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
function addDehumidifer(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
var service = commonHumidifierDehumidifier(HumidityMode.Dehumidify, "dehumidifier", device.name + " Dehumidifier", device, accessory);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.RelativeHumidityDehumidifierThreshold,
|
||||
() => device.humiditySetting?.dehumidifierSetpoint);
|
||||
service.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
dehumidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
export function addHumiditySetting(device: ScryptedDevice & HumiditySetting & HumiditySensor, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.HumiditySetting) && !device.interfaces.includes(ScryptedInterface.HumiditySensor))
|
||||
return undefined;
|
||||
|
||||
var service;
|
||||
|
||||
if (device.humiditySetting?.availableModes.includes(HumidityMode.Humidify)) {
|
||||
service = addHumidifier(device, accessory);
|
||||
}
|
||||
|
||||
if (device.humiditySetting?.availableModes.includes(HumidityMode.Dehumidify)) {
|
||||
service = addDehumidifer(device, accessory);
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
export function addFan(device: ScryptedDevice & Fan, accessory: Accessory): Service {
|
||||
if (!device.interfaces.includes(ScryptedInterface.Fan))
|
||||
return undefined;
|
||||
|
||||
const service = accessory.addService(Service.Fanv2, device.name);
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.OnOff)) {
|
||||
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.Active,
|
||||
() => !!device.on);
|
||||
bindCharacteristic(device, ScryptedInterface.OnOff, service, Characteristic.Active,
|
||||
() => device.fan?.active);
|
||||
|
||||
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
if (value)
|
||||
device.turnOn();
|
||||
else
|
||||
device.turnOff();
|
||||
service.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setFan({
|
||||
mode: value ? FanMode.Auto : FanMode.Manual,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (device.fan?.counterClockwise !== undefined) {
|
||||
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.RotationDirection,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, AirQualitySensor, AirQuality, PM10Sensor, PM25Sensor, VOCSensor, NOXSensor, CO2Sensor } from '@scrypted/sdk';
|
||||
import { Fan, FanMode, HumidityMode, HumiditySensor, HumiditySetting, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, AirQualitySensor, AirQuality, PM10Sensor, PM25Sensor, VOCSensor, NOXSensor, CO2Sensor, HumiditySettingStatus } from '@scrypted/sdk';
|
||||
import { addSupportedType, bindCharacteristic, DummyDevice, } from '../common';
|
||||
import { Characteristic, CharacteristicEventTypes, CharacteristicSetCallback, CharacteristicValue, Service } from '../hap';
|
||||
import { addAirQualitySensor, addCarbonDioxideSensor, addFan, makeAccessory } from './common';
|
||||
import { addAirQualitySensor, addCarbonDioxideSensor, addFan, addHumiditySetting, makeAccessory } from './common';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
|
||||
addSupportedType({
|
||||
@@ -178,72 +178,60 @@ addSupportedType({
|
||||
() => device.humidity || 0);
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.HumiditySetting) && device.interfaces.includes(ScryptedInterface.HumiditySensor)) {
|
||||
const humidityService = accessory.addService(Service.HumidifierDehumidifier);
|
||||
// add fan state to thermostat service even though it is not required or optional,
|
||||
// in order to expose to Home Assistant HomeKit Controller under their climate entity
|
||||
if (device.interfaces.includes(ScryptedInterface.Fan)) {
|
||||
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.TargetFanState,
|
||||
() => device.fan?.mode === FanMode.Manual
|
||||
? Characteristic.TargetFanState.MANUAL
|
||||
: Characteristic.TargetFanState.AUTO);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.Active,
|
||||
() => {
|
||||
if (!device.humiditySetting?.mode)
|
||||
return false;
|
||||
if (device.humiditySetting.mode === HumidityMode.Off)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
humidityService.getCharacteristic(Characteristic.Active).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
service.getCharacteristic(Characteristic.TargetFanState).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value ? HumidityMode.Auto : HumidityMode.Off
|
||||
device.setFan({
|
||||
mode: value === Characteristic.TargetFanState.MANUAL ? FanMode.Manual : FanMode.Auto,
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySensor, humidityService, Characteristic.CurrentRelativeHumidity,
|
||||
() => device.humidity || 0);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.CurrentHumidifierDehumidifierState,
|
||||
() => !device.humiditySetting?.activeMode
|
||||
? Characteristic.CurrentHumidifierDehumidifierState.INACTIVE
|
||||
: device.humiditySetting.activeMode === HumidityMode.Dehumidify
|
||||
? Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING
|
||||
: device.humiditySetting.activeMode === HumidityMode.Humidify
|
||||
? Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING
|
||||
: Characteristic.CurrentHumidifierDehumidifierState.IDLE);
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.TargetHumidifierDehumidifierState,
|
||||
() => !device.humiditySetting?.mode || device.humiditySetting?.mode === HumidityMode.Auto
|
||||
? Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER
|
||||
: device.humiditySetting?.mode === HumidityMode.Dehumidify
|
||||
? Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
|
||||
: Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER);
|
||||
humidityService.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
mode: value === Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER
|
||||
? HumidityMode.Humidify
|
||||
: value === Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER
|
||||
? HumidityMode.Dehumidify
|
||||
: HumidityMode.Auto
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.RelativeHumidityHumidifierThreshold,
|
||||
() => device.humiditySetting?.humidifierSetpoint || 0);
|
||||
humidityService.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
humidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, humidityService, Characteristic.RelativeHumidityDehumidifierThreshold,
|
||||
() => device.humiditySetting?.dehumidifierSetpoint || 0);
|
||||
humidityService.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold).on(CharacteristicEventTypes.SET, (value, callback) => {
|
||||
callback();
|
||||
device.setHumidity({
|
||||
dehumidifierSetpoint: value as number,
|
||||
});
|
||||
});
|
||||
bindCharacteristic(device, ScryptedInterface.Fan, service, Characteristic.CurrentFanState,
|
||||
() => !device.fan?.active
|
||||
? Characteristic.CurrentFanState.INACTIVE
|
||||
: !device.fan.speed
|
||||
? Characteristic.CurrentFanState.IDLE
|
||||
: Characteristic.CurrentFanState.BLOWING_AIR);
|
||||
}
|
||||
|
||||
// add relataive target humidity to thermostat service even though it is not required or optional,
|
||||
// in order to expose to Home Assistant HomeKit Controller under their climate entity
|
||||
if (device.interfaces.includes(ScryptedInterface.HumiditySetting)) {
|
||||
function targetHumidity(setting: HumiditySettingStatus) {
|
||||
if (!setting)
|
||||
return 0;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify)
|
||||
&& setting?.availableModes.includes(HumidityMode.Dehumidify)) {
|
||||
if (setting?.activeMode === HumidityMode.Humidify)
|
||||
return setting?.humidifierSetpoint;
|
||||
if (setting?.activeMode === HumidityMode.Dehumidify)
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Humidify))
|
||||
return setting?.humidifierSetpoint;
|
||||
|
||||
if (setting?.availableModes.includes(HumidityMode.Dehumidify))
|
||||
return setting?.dehumidifierSetpoint;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bindCharacteristic(device, ScryptedInterface.HumiditySetting, service, Characteristic.TargetRelativeHumidity,
|
||||
() => targetHumidity(device.humiditySetting));
|
||||
}
|
||||
|
||||
addHumiditySetting(device, accessory);
|
||||
addFan(device, accessory);
|
||||
addAirQualitySensor(device, accessory);
|
||||
addCarbonDioxideSensor(device, accessory);
|
||||
|
||||
198
plugins/objectdetector/package-lock.json
generated
198
plugins/objectdetector/package-lock.json
generated
@@ -1,22 +1,19 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.66",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.66",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"semver": "^7.5.4"
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.6"
|
||||
"@types/node": "^20.11.0"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -25,34 +22,40 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.12",
|
||||
"version": "0.3.106",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -64,11 +67,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -88,67 +89,12 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/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==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/polygon-clipping": {
|
||||
"version": "0.15.7",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
|
||||
"integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2",
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"extraneous": true,
|
||||
@@ -172,35 +118,39 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -212,57 +162,11 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"polygon-clipping": {
|
||||
"version": "0.15.7",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
|
||||
"integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==",
|
||||
"requires": {
|
||||
"robust-predicates": "^3.0.2",
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.60",
|
||||
"version": "0.1.66",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -46,12 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"semver": "^7.5.4"
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.6"
|
||||
"@types/node": "^20.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
173
plugins/objectdetector/src/ffmpeg-audiosensor.ts
Normal file
173
plugins/objectdetector/src/ffmpeg-audiosensor.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import sdk, { AudioSensor, FFmpegInput, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SettingValue, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/sdk/settings-mixin";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
|
||||
import { RtpPacket } from "../../../external/werift/packages/rtp/src/rtp/rtp";
|
||||
import { sleep } from "@scrypted/common/src/sleep";
|
||||
|
||||
function pcmU8ToDb(payload: Uint8Array): number {
|
||||
let sum = 0;
|
||||
const count = payload.length;
|
||||
|
||||
if (count === 0) return 0; // Treat empty input as silence (0 dB)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const sample = payload[i] - 128; // Convert to signed range (-128 to 127)
|
||||
sum += sample * sample;
|
||||
}
|
||||
|
||||
const rms = Math.sqrt(sum / count);
|
||||
const minRMS = 1.0; // Define a minimum reference level to avoid log(0)
|
||||
|
||||
if (rms < minRMS) return 0; // Silence is 0 dB
|
||||
|
||||
const db = 20 * Math.log10(rms / minRMS); // Scale against the minimum audible level
|
||||
return db;
|
||||
}
|
||||
|
||||
class FFmpegAudioDetectionMixin extends SettingsMixinDeviceBase<AudioSensor> implements AudioSensor {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
decibelThreshold: {
|
||||
title: 'Decibel Threshold',
|
||||
type: 'number',
|
||||
description: 'The decibel level at which to trigger an event.',
|
||||
defaultValue: 20,
|
||||
},
|
||||
audioTimeout: {
|
||||
title: 'Audio Timeout',
|
||||
type: 'number',
|
||||
description: 'The number of seconds to wait after the last audio event before resetting the audio sensor.',
|
||||
defaultValue: 10,
|
||||
},
|
||||
});
|
||||
ensureInterval: NodeJS.Timeout;
|
||||
forwarder: ReturnType<typeof startRtpForwarderProcess>;
|
||||
audioResetInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(options: SettingsMixinDeviceOptions<AudioSensor>) {
|
||||
super(options);
|
||||
this.ensureInterval = setInterval(() => this.ensureAudioSensor(), 60000);
|
||||
this.ensureAudioSensor();
|
||||
};
|
||||
|
||||
ensureAudioSensor() {
|
||||
if (!this.ensureInterval)
|
||||
return;
|
||||
|
||||
if (this.forwarder)
|
||||
return;
|
||||
|
||||
this.audioDetected = false;
|
||||
clearInterval(this.audioResetInterval);
|
||||
this.audioResetInterval = undefined;
|
||||
|
||||
const fp = this.ensureAudioSensorInternal();
|
||||
this.forwarder = fp;
|
||||
|
||||
fp.catch(() => {
|
||||
if (this.forwarder === fp)
|
||||
this.forwarder = undefined;
|
||||
});
|
||||
|
||||
this.forwarder.then(f => {
|
||||
f.killPromise.then(() => {
|
||||
if (this.forwarder === fp)
|
||||
this.forwarder = undefined;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async ensureAudioSensorInternal() {
|
||||
await sleep(5000);
|
||||
if (!this.forwarder)
|
||||
throw new Error('released/killed');
|
||||
const realDevice = sdk.systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
const mo = await realDevice.getVideoStream({
|
||||
video: null,
|
||||
audio: {},
|
||||
});
|
||||
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
|
||||
let lastAudio = 0;
|
||||
|
||||
const forwarder = await startRtpForwarderProcess(this.console, ffmpegInput, {
|
||||
video: null,
|
||||
audio: {
|
||||
codecCopy: 'pcm_u8',
|
||||
encoderArguments: [
|
||||
'-acodec', 'pcm_u8',
|
||||
'-ac', '1',
|
||||
'-ar', '8000',
|
||||
],
|
||||
onRtp: rtp => {
|
||||
const now = Date.now();
|
||||
// if this.audioDetected is true skip the processing unless the lastAudio time is halfway through the interval
|
||||
if (this.audioDetected && now - lastAudio < this.storageSettings.values.audioTimeout * 500)
|
||||
return;
|
||||
|
||||
const packet = RtpPacket.deSerialize(rtp);
|
||||
const decibels = pcmU8ToDb(packet.payload);
|
||||
if (decibels < this.storageSettings.values.decibelThreshold)
|
||||
return;
|
||||
|
||||
this.audioDetected = true;
|
||||
lastAudio = now;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
this.audioResetInterval = setInterval(() => {
|
||||
if (!this.audioDetected)
|
||||
return;
|
||||
if (Date.now() - lastAudio < this.storageSettings.values.audioTimeout * 1000)
|
||||
return;
|
||||
this.audioDetected = false;
|
||||
}, this.storageSettings.values.audioTimeout * 1000);
|
||||
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
async getMixinSettings() {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putMixinSetting(key: string, value: SettingValue) {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async release() {
|
||||
this.forwarder?.then(f => f.kill());
|
||||
this.forwarder = undefined;
|
||||
|
||||
clearInterval(this.ensureInterval);
|
||||
this.ensureInterval = undefined;
|
||||
|
||||
clearTimeout(this.audioResetInterval);
|
||||
this.audioResetInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class FFmpegAudioDetectionMixinProvider extends ScryptedDeviceBase implements MixinProvider {
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]) {
|
||||
if (type !== ScryptedDeviceType.Camera && type !== ScryptedDeviceType.Doorbell)
|
||||
return;
|
||||
if (!interfaces.includes(ScryptedInterface.VideoCamera))
|
||||
return;
|
||||
return [ScryptedInterface.AudioSensor, ScryptedInterface.Settings];
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: WritableDeviceState): Promise<any> {
|
||||
return new FFmpegAudioDetectionMixin({
|
||||
group: 'Audio Detection',
|
||||
groupKey: 'audio-detection',
|
||||
mixinDevice,
|
||||
mixinDeviceInterfaces,
|
||||
mixinDeviceState,
|
||||
mixinProviderNativeId: this.nativeId,
|
||||
});
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any) {
|
||||
await (mixinDevice as FFmpegAudioDetectionMixin)?.release();
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ import crypto from 'crypto';
|
||||
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
|
||||
import { fixLegacyClipPath, insidePolygon, normalizeBoxToClipPath, polygonOverlap } from './polygon';
|
||||
import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonIntersectsBoundingBox } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
import { FFmpegAudioDetectionMixinProvider } from './ffmpeg-audiosensor';
|
||||
|
||||
|
||||
const { systemManager } = sdk;
|
||||
@@ -545,7 +546,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!o.boundingBox)
|
||||
continue;
|
||||
|
||||
const box = normalizeBoxToClipPath(o.boundingBox, detection.inputDimensions);
|
||||
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
|
||||
let included: boolean;
|
||||
// need a way to explicitly include package zone.
|
||||
@@ -572,13 +573,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
let match = false;
|
||||
if (zoneInfo?.type === 'Contain') {
|
||||
match = insidePolygon(box[0] as Point, zoneValue) &&
|
||||
insidePolygon(box[1], zoneValue) &&
|
||||
insidePolygon(box[2], zoneValue) &&
|
||||
insidePolygon(box[3], zoneValue);
|
||||
match = polygonContainsBoundingBox(zoneValue, box);
|
||||
}
|
||||
else {
|
||||
match = polygonOverlap(box, zoneValue);
|
||||
match = polygonIntersectsBoundingBox(zoneValue, box);
|
||||
}
|
||||
|
||||
const classes = zoneInfo?.classes?.length ? zoneInfo?.classes : this.model?.classes || [];
|
||||
@@ -604,7 +602,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// prevents errant motion from the on screen time changing every second.
|
||||
if (this.hasMotionType && included === undefined) {
|
||||
const defaultInclusionZone: ClipPath = [[0, .1], [1, .1], [1, .9], [0, .9]];
|
||||
included = polygonOverlap(box, defaultInclusionZone);
|
||||
included = polygonIntersectsBoundingBox(defaultInclusionZone, box);
|
||||
}
|
||||
|
||||
// if there are inclusion zones and this object
|
||||
@@ -868,8 +866,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
if (value && this.model.settings?.find(s => s.key === key)?.multiple) {
|
||||
vs = JSON.stringify(value);
|
||||
if (value) {
|
||||
const found = this.model.settings?.find(s => s.key === key);
|
||||
if (found?.multiple || found?.type === 'clippath')
|
||||
vs = JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (key === 'analyzeButton') {
|
||||
@@ -1057,7 +1057,16 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
ScryptedInterface.VideoFrameGenerator,
|
||||
],
|
||||
nativeId: 'ffmpeg',
|
||||
})
|
||||
});
|
||||
|
||||
sdk.deviceManager.onDeviceDiscovered({
|
||||
name: 'FFmpeg Audio Detection',
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
interfaces: [
|
||||
ScryptedInterface.MixinProvider,
|
||||
],
|
||||
nativeId: 'ffmpeg-audio-detection',
|
||||
});
|
||||
});
|
||||
|
||||
// on an interval check to see if system load allows squelched detectors to start up.
|
||||
@@ -1196,6 +1205,8 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
let ret: any;
|
||||
if (nativeId === 'ffmpeg')
|
||||
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
|
||||
if (nativeId === 'ffmpeg-audio-detection')
|
||||
ret = this.devices.get(nativeId) || new FFmpegAudioDetectionMixinProvider('ffmpeg-audio-detection');
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
|
||||
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))
|
||||
|
||||
@@ -1,17 +1,99 @@
|
||||
import type { ClipPath, Point } from '@scrypted/sdk';
|
||||
import polygonClipping from 'polygon-clipping';
|
||||
|
||||
// const polygonOverlap = require('polygon-overlap');
|
||||
// const insidePolygon = require('point-inside-polygon');
|
||||
// x y w h
|
||||
export type BoundingBox = [number, number, number, number];
|
||||
/**
|
||||
* Checks if a line segment intersects with another line segment
|
||||
*/
|
||||
function lineIntersects(
|
||||
[x1, y1]: Point,
|
||||
[x2, y2]: Point,
|
||||
[x3, y3]: Point,
|
||||
[x4, y4]: Point
|
||||
): boolean {
|
||||
// Calculate the denominators for intersection check
|
||||
const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
||||
if (denom === 0) return false; // Lines are parallel
|
||||
|
||||
export function polygonOverlap(p1: Point[], p2: Point[]) {
|
||||
const intersect = polygonClipping.intersection([p1], [p2]);
|
||||
return !!intersect.length;
|
||||
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
|
||||
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
|
||||
|
||||
// Check if intersection point lies within both line segments
|
||||
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
|
||||
}
|
||||
|
||||
export function insidePolygon(point: Point, polygon: Point[]) {
|
||||
const intersect = polygonClipping.intersection([polygon], [[point, [point[0] + 1, point[1]], [point[0] + 1, point[1] + 1]]]);
|
||||
return !!intersect.length;
|
||||
/**
|
||||
* Checks if a point is inside a polygon using ray casting algorithm
|
||||
*/
|
||||
function pointInPolygon([x, y]: Point, polygon: ClipPath): boolean {
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const [xi, yi] = polygon[i];
|
||||
const [xj, yj] = polygon[j];
|
||||
|
||||
const intersect = ((yi > y) !== (yj > y)) &&
|
||||
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
|
||||
if (intersect) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a bounding box to an array of its corner points
|
||||
*/
|
||||
function boundingBoxToPoints([x, y, w, h]: BoundingBox): Point[] {
|
||||
return [
|
||||
[x, y], // top-left
|
||||
[x + w, y], // top-right
|
||||
[x + w, y + h], // bottom-right
|
||||
[x, y + h] // bottom-left
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a polygon intersects with a bounding box
|
||||
*/
|
||||
export function polygonIntersectsBoundingBox(polygon: ClipPath, boundingBox: BoundingBox): boolean {
|
||||
// Get bounding box corners
|
||||
const boxPoints = boundingBoxToPoints(boundingBox);
|
||||
|
||||
// Check if any polygon edge intersects with any bounding box edge
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const nextI = (i + 1) % polygon.length;
|
||||
const polygonPoint1 = polygon[i];
|
||||
const polygonPoint2 = polygon[nextI];
|
||||
|
||||
// Check against all bounding box edges
|
||||
for (let j = 0; j < boxPoints.length; j++) {
|
||||
const nextJ = (j + 1) % boxPoints.length;
|
||||
const boxPoint1 = boxPoints[j];
|
||||
const boxPoint2 = boxPoints[nextJ];
|
||||
|
||||
if (lineIntersects(polygonPoint1, polygonPoint2, boxPoint1, boxPoint2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no edges intersect, check if either shape contains a point from the other
|
||||
if (pointInPolygon(polygon[0], boxPoints) || pointInPolygon(boxPoints[0], polygon))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a polygon completely contains a bounding box
|
||||
*/
|
||||
export function polygonContainsBoundingBox(polygon: ClipPath, boundingBox: BoundingBox): boolean {
|
||||
// Check if all corners of the bounding box are inside the polygon
|
||||
const boxPoints = boundingBoxToPoints(boundingBox);
|
||||
return boxPoints.every(point => pointInPolygon(point, polygon));
|
||||
}
|
||||
|
||||
|
||||
export function normalizeBox(box: BoundingBox, dims: Point): BoundingBox {
|
||||
return [box[0] / dims[0], box[1] / dims[1], box[2] / dims[0], box[3] / dims[1]];
|
||||
}
|
||||
|
||||
export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
|
||||
@@ -34,26 +116,3 @@ export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
|
||||
|
||||
return clipPath.map(p => p.map(c => c / 100)) as ClipPath;
|
||||
}
|
||||
|
||||
export function normalizeBoxToClipPath(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x / inputDimensions[0];
|
||||
y = y / inputDimensions[1];
|
||||
x2 = x2 / inputDimensions[0];
|
||||
y2 = y2 / inputDimensions[1];
|
||||
return [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
}
|
||||
|
||||
export function polygonArea(p: Point[]): number {
|
||||
let area = 0;
|
||||
const n = p.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
area += p[i][0] * p[j][1];
|
||||
area -= p[j][0] * p[i][1];
|
||||
}
|
||||
return Math.abs(area / 2);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
|
||||
this.storageSettings.settings.detections.onGet = async () => {
|
||||
const objectDetector: ObjectDetector = this.storageSettings.values.objectDetector;
|
||||
const choices = (await objectDetector?.getObjectTypes())?.classes || [];
|
||||
const choices = (await objectDetector?.getObjectTypes?.())?.classes || [];
|
||||
return {
|
||||
hide: !objectDetector,
|
||||
choices,
|
||||
|
||||
@@ -2,7 +2,7 @@ import sdk, { Camera, ClipPath, EventListenerRegister, Image, ObjectDetection, O
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { levenshteinDistance } from "./edit-distance";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
import { normalizeBoxToClipPath, polygonOverlap } from "./polygon";
|
||||
import { normalizeBox, polygonIntersectsBoundingBox } from "./polygon";
|
||||
|
||||
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
|
||||
|
||||
@@ -150,8 +150,8 @@ export class SmartOccupancySensor extends ScryptedDeviceBase implements Settings
|
||||
if (zone?.length >= 3) {
|
||||
if (!d.boundingBox)
|
||||
return false;
|
||||
const detectionBoxPath = normalizeBoxToClipPath(d.boundingBox, detected.inputDimensions);
|
||||
if (!polygonOverlap(detectionBoxPath, zone))
|
||||
const detectionBox = normalizeBox(d.boundingBox, detected.inputDimensions);
|
||||
if (!polygonIntersectsBoundingBox(zone, detectionBox))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
4
plugins/openvino/package-lock.json
generated
4
plugins/openvino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.148",
|
||||
"version": "0.1.153",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.148",
|
||||
"version": "0.1.153",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.148"
|
||||
"version": "0.1.153"
|
||||
}
|
||||
|
||||
@@ -25,8 +25,12 @@ try:
|
||||
except:
|
||||
OpenVINOTextRecognition = None
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="OpenVINO-Predict")
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="OpenVINO-Prepare")
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Predict"
|
||||
)
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Prepare"
|
||||
)
|
||||
|
||||
availableModels = [
|
||||
"Default",
|
||||
@@ -132,7 +136,7 @@ class OpenVINOPlugin(
|
||||
gpu = True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# AUTO mode can cause conflicts or hide errors with NPU and GPU
|
||||
# so try to be explicit and fall back accordingly.
|
||||
mode = self.storage.getItem("mode") or "Default"
|
||||
@@ -140,7 +144,7 @@ class OpenVINOPlugin(
|
||||
mode = "AUTO"
|
||||
|
||||
if npu:
|
||||
mode = 'NPU'
|
||||
mode = "NPU"
|
||||
elif len(dgpus):
|
||||
mode = f"AUTO:{','.join(dgpus)},CPU"
|
||||
# forcing GPU can cause crashes on older GPU.
|
||||
@@ -242,12 +246,42 @@ class OpenVINOPlugin(
|
||||
self.requestRestart()
|
||||
|
||||
self.infer_queue = ov.AsyncInferQueue(self.compiled_model)
|
||||
|
||||
def predict(output):
|
||||
if not self.yolo:
|
||||
objs = []
|
||||
for values in output[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
r = torelative(r)
|
||||
b = torelative(b)
|
||||
|
||||
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output[0])
|
||||
if self.scrypted_yolo_nas:
|
||||
return yolo.parse_yolo_nas([output[1], output[0]])
|
||||
return yolo.parse_yolov9(output[0])
|
||||
|
||||
def callback(infer_request, future: asyncio.Future):
|
||||
try:
|
||||
output = infer_request.get_output_tensor(0)
|
||||
self.loop.call_soon_threadsafe(future.set_result, output)
|
||||
output = infer_request.get_output_tensor(0).data
|
||||
objs = predict(output)
|
||||
self.loop.call_soon_threadsafe(future.set_result, objs)
|
||||
except Exception as e:
|
||||
self.loop.call_soon_threadsafe(future.set_exception, e)
|
||||
|
||||
self.infer_queue.set_callback(callback)
|
||||
|
||||
print(
|
||||
@@ -323,39 +357,6 @@ class OpenVINOPlugin(
|
||||
return super().get_input_format()
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
async def predict(input_tensor):
|
||||
f = asyncio.Future(loop = self.loop)
|
||||
self.infer_queue.start_async(input_tensor, f)
|
||||
|
||||
output_tensors = await f
|
||||
|
||||
if not self.yolo:
|
||||
output = output_tensors
|
||||
for values in output.data[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
r = torelative(r)
|
||||
b = torelative(b)
|
||||
|
||||
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
output = output_tensors.data
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output[0])
|
||||
if self.scrypted_yolo_nas:
|
||||
return yolo.parse_yolo_nas([output[1], output[0]])
|
||||
return yolo.parse_yolov9(output[0])
|
||||
|
||||
def prepare():
|
||||
# the input_tensor can be created with the shared_memory=True parameter,
|
||||
# but that seems to cause issues on some platforms.
|
||||
@@ -376,21 +377,19 @@ class OpenVINOPlugin(
|
||||
im = im.reshape((1, 3, self.model_dim, self.model_dim))
|
||||
im = im.astype(np.float32) / 255.0
|
||||
im = np.ascontiguousarray(im) # contiguous
|
||||
input_tensor = ov.Tensor(array=im)
|
||||
elif self.yolo:
|
||||
input_tensor = ov.Tensor(
|
||||
array=np.expand_dims(np.array(input), axis=0).astype(np.float32)
|
||||
)
|
||||
im = np.expand_dims(np.array(input), axis=0).astype(np.float32)
|
||||
else:
|
||||
input_tensor = ov.Tensor(array=np.expand_dims(np.array(input), axis=0))
|
||||
return input_tensor
|
||||
im = np.expand_dims(np.array(input), axis=0)
|
||||
return im
|
||||
|
||||
try:
|
||||
input_tensor = await asyncio.get_event_loop().run_in_executor(
|
||||
prepareExecutor, lambda: prepare()
|
||||
)
|
||||
objs = await predict(input_tensor)
|
||||
|
||||
f = asyncio.Future(loop=self.loop)
|
||||
self.infer_queue.start_async(input_tensor, f)
|
||||
objs = await f
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
@@ -17,7 +17,3 @@ Medium: 720p (500 Kbps)
|
||||
Low (if available): 320p (100 Kbps)
|
||||
|
||||
The `Key Frame (IDR) Interval` should be set to `4` seconds. This setting is usually configured in frames. So if the camera frame rate is `30`, the interval would be `120`. If the camera frame rate is `15` the interval would be `60`. The value can be calculated as `IDR Interval = FPS * 4`.
|
||||
|
||||
## Transcoding
|
||||
|
||||
Some cameras may not allow configuration of the video codec (h264) or IDR Interval. The camera may also only have a single high bitrate stream which will fail to stream when viewing on low bandwidth remote connections. In this case, Transcoding should be enabled for `Remote Stream` and `Remote Recording Stream` to ensure there isn't a bandwidth issue.
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.39",
|
||||
"version": "0.10.46",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.39",
|
||||
"version": "0.10.46",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.39",
|
||||
"version": "0.10.46",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "Rebroadcast Plugin",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings",
|
||||
"MixinProvider",
|
||||
"BufferConverter"
|
||||
],
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
|
||||
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
|
||||
import { ListenZeroSingleClientTimeoutError, closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
@@ -8,7 +7,7 @@ import { addTrackControls, getSpsPps, parseSdp } from '@scrypted/common/src/sdp-
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
||||
import sdk, { BufferConverter, ChargeState, DeviceProvider, EventListenerRegister, FFmpegInput, ForkWorker, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoCameraConfiguration, WritableDeviceState } from '@scrypted/sdk';
|
||||
import sdk, { BufferConverter, ChargeState, EventListenerRegister, FFmpegInput, ForkWorker, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoCameraConfiguration, WritableDeviceState } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import { once } from 'events';
|
||||
@@ -24,7 +23,6 @@ import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
||||
import { RtspSessionParserSpecific, startRtspSession } from './rtsp-session';
|
||||
import { getSpsResolution } from './sps-resolution';
|
||||
import { createStreamSettings } from './stream-settings';
|
||||
import { TRANSCODE_MIXIN_PROVIDER_NATIVE_ID, TranscodeMixinProvider, getTranscodeMixinProviderId } from './transcode-settings';
|
||||
|
||||
const { mediaManager, log, systemManager, deviceManager } = sdk;
|
||||
|
||||
@@ -65,15 +63,13 @@ class PrebufferSession {
|
||||
usingScryptedParser = false;
|
||||
usingScryptedUdpParser = false;
|
||||
|
||||
audioDisabled = false;
|
||||
|
||||
mixinDevice: VideoCamera;
|
||||
console: Console;
|
||||
storage: Storage;
|
||||
|
||||
activeClients = 0;
|
||||
inactivityTimeout: NodeJS.Timeout;
|
||||
audioConfigurationKey: string;
|
||||
syntheticInputIdKey: string;
|
||||
ffmpegInputArgumentsKey: string;
|
||||
ffmpegOutputArgumentsKey: string;
|
||||
lastDetectedAudioCodecKey: string;
|
||||
@@ -89,7 +85,7 @@ class PrebufferSession {
|
||||
this.storage = mixin.storage;
|
||||
this.console = mixin.console;
|
||||
this.mixinDevice = mixin.mixinDevice;
|
||||
this.audioConfigurationKey = 'audioConfiguration-' + this.streamId;
|
||||
this.syntheticInputIdKey = 'syntheticInputIdKey-' + this.streamId;
|
||||
this.ffmpegInputArgumentsKey = 'ffmpegInputArguments-' + this.streamId;
|
||||
this.ffmpegOutputArgumentsKey = 'ffmpegOutputArguments-' + this.streamId;
|
||||
this.lastDetectedAudioCodecKey = 'lastDetectedAudioCodec-' + this.streamId;
|
||||
@@ -117,10 +113,6 @@ class PrebufferSession {
|
||||
return !this.enabled || this.shouldDisableBatteryPrebuffer();
|
||||
}
|
||||
|
||||
get canPrebuffer() {
|
||||
return (this.advertisedMediaStreamOptions.container !== 'rawvideo' && this.advertisedMediaStreamOptions.container !== 'ffmpeg') || this.storage.getItem(this.ffmpegOutputArgumentsKey);
|
||||
}
|
||||
|
||||
getLastH264Probe(): H264Info {
|
||||
const str = this.storage.getItem(this.lastH264ProbeKey);
|
||||
if (!str) {
|
||||
@@ -230,12 +222,20 @@ class PrebufferSession {
|
||||
|
||||
getParser(mediaStreamOptions: MediaStreamOptions) {
|
||||
let parser: string;
|
||||
const rtspParser = this.storage.getItem(this.rtspParserKey);
|
||||
let rtspParser = this.storage.getItem(this.rtspParserKey);
|
||||
|
||||
let isDefault = !rtspParser || rtspParser === 'Default';
|
||||
|
||||
if (!this.canUseRtspParser(mediaStreamOptions)) {
|
||||
parser = STRING_DEFAULT;
|
||||
isDefault = true;
|
||||
rtspParser = undefined;
|
||||
}
|
||||
else {
|
||||
if (isDefault) {
|
||||
// use the plugin default
|
||||
rtspParser = localStorage.getItem('defaultRtspParser');
|
||||
}
|
||||
switch (rtspParser) {
|
||||
case FFMPEG_PARSER_TCP:
|
||||
case FFMPEG_PARSER_UDP:
|
||||
@@ -251,7 +251,7 @@ class PrebufferSession {
|
||||
|
||||
return {
|
||||
parser,
|
||||
isDefault: !rtspParser || rtspParser === 'Default',
|
||||
isDefault,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +326,19 @@ class PrebufferSession {
|
||||
const group = "Streams";
|
||||
const subgroup = `Stream: ${this.streamName}`;
|
||||
|
||||
if (this.mixin.streamSettings.storageSettings.values.synthenticStreams.includes(this.streamId)) {
|
||||
const nonSynthetic = [...this.mixin.sessions.keys()].filter(s => s && !s.startsWith('synthetic:'));
|
||||
settings.push({
|
||||
group,
|
||||
subgroup,
|
||||
key: this.syntheticInputIdKey,
|
||||
title: 'Synthetic Stream Source',
|
||||
description: 'The source stream to transcode.',
|
||||
choices: nonSynthetic,
|
||||
value: this.storage.getItem(this.syntheticInputIdKey),
|
||||
});
|
||||
}
|
||||
|
||||
const addFFmpegInputSettings = () => {
|
||||
settings.push(
|
||||
{
|
||||
@@ -351,7 +364,7 @@ class PrebufferSession {
|
||||
key: this.ffmpegOutputArgumentsKey,
|
||||
value: this.storage.getItem(this.ffmpegOutputArgumentsKey),
|
||||
choices: [
|
||||
'-c:v libx264 -pix_fmt yuvj420p -preset ultrafast -bf 0'
|
||||
'-c:v libx264 -pix_fmt yuvj420p -preset ultrafast -bf 0 -g 60 -r 15 -b:v 500000 -bufsize 1000000 -maxrate 500000'
|
||||
],
|
||||
combobox: true,
|
||||
},
|
||||
@@ -364,11 +377,6 @@ class PrebufferSession {
|
||||
const parser = this.getParser(this.advertisedMediaStreamOptions);
|
||||
const defaultValue = parser.parser;
|
||||
|
||||
const scryptedOptions = [
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
];
|
||||
|
||||
const currentParser = parser.isDefault ? STRING_DEFAULT : parser.parser;
|
||||
|
||||
settings.push(
|
||||
@@ -381,7 +389,8 @@ class PrebufferSession {
|
||||
value: currentParser,
|
||||
choices: [
|
||||
STRING_DEFAULT,
|
||||
...scryptedOptions,
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
FFMPEG_PARSER_TCP,
|
||||
FFMPEG_PARSER_UDP,
|
||||
],
|
||||
@@ -496,20 +505,15 @@ class PrebufferSession {
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
// audio codecs are determined by probing the camera to see what it reports.
|
||||
// if the camera does not specify a codec, rebroadcast will force audio off
|
||||
// to determine the codec without causing a parse failure.
|
||||
// camera may explicity request that its audio stream be muted via a null.
|
||||
// respect that setting.
|
||||
const audioSoftMuted = mso?.audio === null;
|
||||
const advertisedAudioCodec = mso?.audio?.codec;
|
||||
const advertisedAudioCodec = !audioSoftMuted && mso?.audio?.codec;
|
||||
|
||||
let detectedAudioCodec = this.storage.getItem(this.lastDetectedAudioCodecKey) || undefined;
|
||||
if (detectedAudioCodec === 'null')
|
||||
detectedAudioCodec = null;
|
||||
|
||||
this.audioDisabled = false;
|
||||
|
||||
const rbo: ParserOptions<PrebufferParsers> = {
|
||||
console: this.console,
|
||||
timeout: 60000,
|
||||
@@ -518,7 +522,19 @@ class PrebufferSession {
|
||||
};
|
||||
this.parsers = rbo.parsers;
|
||||
|
||||
const mo = await this.mixinDevice.getVideoStream(mso);
|
||||
let mo: MediaObject;
|
||||
if (this.mixin.streamSettings.storageSettings.values.synthenticStreams.includes(this.streamId)) {
|
||||
const syntheticInputId = this.storage.getItem(this.syntheticInputIdKey);
|
||||
if (!syntheticInputId)
|
||||
throw new Error('synthetic stream has not been configured with an input');
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.mixin.id);
|
||||
mo = await realDevice.getVideoStream({
|
||||
id: syntheticInputId,
|
||||
});
|
||||
}
|
||||
else {
|
||||
mo = await this.mixinDevice.getVideoStream(mso);
|
||||
}
|
||||
const isRfc4571 = mo.mimeType === 'x-scrypted/x-rfc4571';
|
||||
|
||||
let session: ParserSession<PrebufferParsers>;
|
||||
@@ -581,7 +597,6 @@ class PrebufferSession {
|
||||
if (audioSoftMuted) {
|
||||
// no audio? explicitly disable it.
|
||||
acodec = ['-an'];
|
||||
this.audioDisabled = true;
|
||||
}
|
||||
else {
|
||||
acodec = [
|
||||
@@ -605,9 +620,6 @@ class PrebufferSession {
|
||||
const extraInputArguments = userInputArguments || DEFAULT_FFMPEG_INPUT_ARGUMENTS;
|
||||
const extraOutputArguments = this.storage.getItem(this.ffmpegOutputArgumentsKey) || '';
|
||||
ffmpegInput.inputArguments.unshift(...extraInputArguments.split(' '));
|
||||
// ehh this seems to cause issues with frames being updated in the webassembly decoder..?
|
||||
// if (!userInputArguments && (ffmpegInput.container === 'rtmp' || ffmpegInput.url?.startsWith('rtmp:')))
|
||||
// ffmpegInput.inputArguments.unshift('-use_wallclock_as_timestamps', '1');
|
||||
|
||||
if (ffmpegInput.h264EncoderArguments?.length) {
|
||||
vcodec = [...ffmpegInput.h264EncoderArguments];
|
||||
@@ -1001,6 +1013,9 @@ class PrebufferSession {
|
||||
mediaStreamOptions.video.h264Info = this.getLastH264Probe();
|
||||
}
|
||||
|
||||
if (this.mixin.streamSettings.storageSettings.values.noAudio)
|
||||
mediaStreamOptions.audio = null;
|
||||
|
||||
let socketPromise: Promise<Duplex>;
|
||||
let url: string;
|
||||
let urls: string[];
|
||||
@@ -1111,10 +1126,7 @@ class PrebufferSession {
|
||||
|
||||
mediaStreamOptions.prebuffer = requestedPrebuffer;
|
||||
|
||||
if (this.audioDisabled) {
|
||||
mediaStreamOptions.audio = null;
|
||||
}
|
||||
else if (audioSection) {
|
||||
if (audioSection) {
|
||||
mediaStreamOptions.audio ||= {};
|
||||
mediaStreamOptions.audio.codec ||= audioSection.rtpmap.codec;
|
||||
mediaStreamOptions.audio.sampleRate ||= audioSection.rtpmap.clock;
|
||||
@@ -1282,9 +1294,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
|
||||
if (options?.route === 'direct')
|
||||
return this.mixinDevice.getVideoStream(options);
|
||||
|
||||
await this.ensurePrebufferSessions();
|
||||
|
||||
let id = options?.id;
|
||||
@@ -1294,8 +1303,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
let videoFilterArguments: string;
|
||||
let destinationVideoBitrate: number;
|
||||
|
||||
const transcodingEnabled = this.mixins?.includes(getTranscodeMixinProviderId());
|
||||
|
||||
const msos = await this.mixinDevice.getVideoStreamOptions();
|
||||
let result: {
|
||||
stream: ResponseMediaStreamOptions,
|
||||
@@ -1355,58 +1362,23 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
id = result.stream.id;
|
||||
// this.console.log('Selected stream', result.stream.name);
|
||||
// transcoding video should never happen transparently since it is CPU intensive.
|
||||
// encourage users at every step to configure proper codecs.
|
||||
// for this reason, do not automatically supply h264 encoder arguments
|
||||
// even if h264 is requested, to force a visible failure.
|
||||
if (transcodingEnabled && this.streamSettings.storageSettings.values.transcodeStreams?.includes(result.title)) {
|
||||
h264EncoderArguments = transcodeStorageSettings.h264EncoderArguments?.split(' ');
|
||||
if (this.streamSettings.storageSettings.values.videoFilterArguments)
|
||||
videoFilterArguments = this.streamSettings.storageSettings.values.videoFilterArguments;
|
||||
}
|
||||
}
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
let ffmpegInput: FFmpegInput;
|
||||
if (!session.canPrebuffer) {
|
||||
this.console.log('Source container can not be prebuffered. Using a direct media stream.');
|
||||
session = undefined;
|
||||
}
|
||||
if (!session) {
|
||||
const mo = await this.mixinDevice.getVideoStream(options);
|
||||
if (!transcodingEnabled)
|
||||
return mo;
|
||||
ffmpegInput = await mediaManager.convertMediaObjectToJSON(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
}
|
||||
else {
|
||||
// ffmpeg probing works better if the stream does NOT start on a sync frame. the pre-sps/pps data is used
|
||||
// as part of the stream analysis, and sync frame is immediately used. otherwise the sync frame is
|
||||
// read and tossed during rtsp analysis.
|
||||
// if ffmpeg is not in used (ie, not transcoding or implicitly rtsp),
|
||||
// trust that downstream is not using ffmpeg and start with a sync frame.
|
||||
const findSyncFrame = !transcodingEnabled
|
||||
&& (!options?.container || options?.container === 'rtsp')
|
||||
&& options?.tool !== 'ffmpeg';
|
||||
ffmpegInput = await session.getVideoStream(findSyncFrame, options);
|
||||
}
|
||||
if (!session)
|
||||
throw new Error('stream not found');
|
||||
|
||||
ffmpegInput = await session.getVideoStream(true, options);
|
||||
|
||||
ffmpegInput.h264EncoderArguments = h264EncoderArguments;
|
||||
ffmpegInput.destinationVideoBitrate = destinationVideoBitrate;
|
||||
|
||||
if (transcodingEnabled && this.streamSettings.storageSettings.values.missingCodecParameters) {
|
||||
if (!ffmpegInput.mediaStreamOptions)
|
||||
ffmpegInput.mediaStreamOptions = { id };
|
||||
ffmpegInput.mediaStreamOptions.oobCodecParameters = true;
|
||||
}
|
||||
|
||||
if (ffmpegInput.h264FilterArguments && videoFilterArguments)
|
||||
addVideoFilterArguments(ffmpegInput.h264FilterArguments, videoFilterArguments)
|
||||
else if (videoFilterArguments)
|
||||
ffmpegInput.h264FilterArguments = ['-filter_complex', videoFilterArguments];
|
||||
|
||||
if (transcodingEnabled)
|
||||
ffmpegInput.videoDecoderArguments = this.streamSettings.storageSettings.values.videoDecoderArguments?.split(' ');
|
||||
return mediaManager.createFFmpegMediaObject(ffmpegInput, {
|
||||
sourceId: this.id,
|
||||
});
|
||||
@@ -1489,6 +1461,22 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
})();
|
||||
}
|
||||
|
||||
for (const synthetic of this.streamSettings.storageSettings.values.synthenticStreams) {
|
||||
const id = `synthetic:${synthetic}`;
|
||||
toRemove.delete(id);
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
|
||||
if (session)
|
||||
continue;
|
||||
|
||||
session = new PrebufferSession(this, {
|
||||
id: synthetic,
|
||||
}, false, false);
|
||||
this.sessions.set(id, session);
|
||||
this.console.log('stream', synthetic, 'is synthetic and will be rebroadcast on demand.');
|
||||
}
|
||||
|
||||
if (!this.sessions.has(undefined)) {
|
||||
const defaultStreamName = this.streamSettings.storageSettings.values.defaultStream;
|
||||
let defaultSession = this.sessions.get(msos?.find(mso => mso.name === defaultStreamName)?.id);
|
||||
@@ -1617,30 +1605,39 @@ function millisUntilMidnight() {
|
||||
return (midnight.getTime() - new Date().getTime());
|
||||
}
|
||||
|
||||
export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, DeviceProvider {
|
||||
export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, Settings {
|
||||
// no longer in use, but kept for future use.
|
||||
storageSettings = new StorageSettings(this, {});
|
||||
storageSettings = new StorageSettings(this, {
|
||||
defaultRtspParser: {
|
||||
group: 'Advanced',
|
||||
title: 'Default RTSP Parser',
|
||||
description: `Experimental: The Default parser used to read RTSP streams. The default is "${SCRYPTED_PARSER_TCP}".`,
|
||||
defaultValue: STRING_DEFAULT,
|
||||
choices: [
|
||||
STRING_DEFAULT,
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
FFMPEG_PARSER_TCP,
|
||||
FFMPEG_PARSER_UDP,
|
||||
],
|
||||
onPut: () => {
|
||||
this.log.a('Rebroadcast Plugin will restart momentarily.');
|
||||
sdk.deviceManager.requestRestart();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
transcodeStorageSettings = new StorageSettings(this, {
|
||||
remoteStreamingBitrate: {
|
||||
group: 'Advanced',
|
||||
title: 'Remote Streaming Bitrate',
|
||||
type: 'number',
|
||||
defaultValue: 1000000,
|
||||
defaultValue: 500000,
|
||||
description: 'The bitrate to use when remote streaming. This setting will only be used when transcoding or adaptive bitrate is enabled on a camera.',
|
||||
onPut() {
|
||||
sdk.deviceManager.onDeviceEvent('transcode', ScryptedInterface.Settings, undefined);
|
||||
},
|
||||
},
|
||||
h264EncoderArguments: {
|
||||
title: 'H264 Encoder Arguments',
|
||||
description: 'FFmpeg arguments used to encode h264 video. This is not camera specific and is used to setup the hardware accelerated encoder on your Scrypted server. This setting will only be used when transcoding is enabled on a camera.',
|
||||
choices: Object.keys(getH264EncoderArgs()),
|
||||
defaultValue: getDebugModeH264EncoderArgs().join(' '),
|
||||
combobox: true,
|
||||
mapPut: (oldValue, newValue) => getH264EncoderArgs()[newValue]?.join(' ') || newValue || getDebugModeH264EncoderArgs().join(' '),
|
||||
onPut() {
|
||||
sdk.deviceManager.onDeviceEvent('transcode', ScryptedInterface.Settings, undefined);
|
||||
},
|
||||
}
|
||||
});
|
||||
currentMixins = new Map<PrebufferMixin, {
|
||||
worker: ForkWorker,
|
||||
@@ -1650,6 +1647,8 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
this.fromMimeType = 'x-scrypted/x-rfc4571';
|
||||
this.toMimeType = ScryptedMimeTypes.FFmpegInput;
|
||||
|
||||
@@ -1669,40 +1668,24 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
||||
}
|
||||
});
|
||||
|
||||
// schedule restarts at 2am
|
||||
// removed as the mp4 containerization leak used way back when is defunct.
|
||||
// const midnight = millisUntilMidnight();
|
||||
// const twoAM = midnight + 2 * 60 * 60 * 1000;
|
||||
// this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(twoAM / 1000 / 60)} minutes`)
|
||||
// setTimeout(() => deviceManager.requestRestart(), twoAM);
|
||||
|
||||
process.nextTick(() => {
|
||||
deviceManager.onDeviceDiscovered({
|
||||
nativeId: TRANSCODE_MIXIN_PROVIDER_NATIVE_ID,
|
||||
name: 'Transcoding',
|
||||
interfaces: [
|
||||
"SystemSettings",
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.MixinProvider,
|
||||
],
|
||||
type: ScryptedDeviceType.API,
|
||||
// legacy transcode extension that needs to be removed.
|
||||
if (sdk.deviceManager.getNativeIds().includes('transcode')) {
|
||||
process.nextTick(() => {
|
||||
deviceManager.onDeviceRemoved('transcode');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
if (nativeId === TRANSCODE_MIXIN_PROVIDER_NATIVE_ID)
|
||||
return new TranscodeMixinProvider(this);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
...await this.storageSettings.getSettings(),
|
||||
...await this.transcodeStorageSettings.getSettings(),
|
||||
];
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
if (this.transcodeStorageSettings.keys[key])
|
||||
return this.transcodeStorageSettings.putSetting(key, value);
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getH264DecoderArgs } from "@scrypted/common/src/ffmpeg-hardware-acceleration";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { MixinDeviceBase, ResponseMediaStreamOptions, VideoCamera } from "@scrypted/sdk";
|
||||
import { getTranscodeMixinProviderId } from "./transcode-settings";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
|
||||
export type StreamStorageSetting = StorageSetting & {
|
||||
prefersPrebuffer: boolean,
|
||||
@@ -102,45 +101,16 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
type: 'number',
|
||||
hide: false,
|
||||
},
|
||||
transcodeStreams: {
|
||||
group: 'Transcoding',
|
||||
title: 'Transcode Streams',
|
||||
description: 'The media streams to transcode. Transcoding audio and video is not recommended and should only be used when necessary. The Rebroadcast Plugin manages the system-wide Transcode settings. See the Rebroadcast Readme for optimal configuration.',
|
||||
synthenticStreams: {
|
||||
subgroup,
|
||||
title: 'Synthetic Streams',
|
||||
description: 'Create additional streams by transcoding the existing streams. This can be useful for creating streams with different resolutions or bitrates.',
|
||||
immediate: true,
|
||||
multiple: true,
|
||||
choices: Object.values(streamTypes).map(st => st.title),
|
||||
hide: true,
|
||||
},
|
||||
videoDecoderArguments: {
|
||||
group: 'Transcoding',
|
||||
title: 'Video Decoder Arguments',
|
||||
description: 'FFmpeg arguments used to decode input video when transcoding a stream.',
|
||||
placeholder: '-hwaccel auto',
|
||||
choices: Object.keys(getH264DecoderArgs()),
|
||||
combobox: true,
|
||||
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue || '',
|
||||
hide: true,
|
||||
},
|
||||
videoFilterArguments: {
|
||||
group: 'Transcoding',
|
||||
title: 'Video Filter Arguments',
|
||||
description: 'FFmpeg arguments used to filter input video when transcoding a stream. This can be used to crops, scale, rotates, etc.',
|
||||
placeholder: 'transpose=1',
|
||||
hide: true,
|
||||
},
|
||||
// 3/6/2022
|
||||
// Ran into an issue where the RTSP source had SPS/PPS in the SDP,
|
||||
// and none in the bitstream. Codec copy will not add SPS/PPS before IDR frames
|
||||
// unless this flag is used.
|
||||
// 3/7/2022
|
||||
// This flag was enabled by default, but I believe this is causing issues with some users.
|
||||
// Make it a setting.
|
||||
missingCodecParameters: {
|
||||
group: 'Transcoding',
|
||||
title: 'Out of Band Codec Parameters',
|
||||
description: 'Some cameras do not include H264 codec parameters in the stream and this causes live streaming to always fail (but recordings may be working). This is a inexpensive video filter and does not perform a transcode. Enable this setting only as necessary.',
|
||||
type: 'boolean',
|
||||
hide: true,
|
||||
},
|
||||
choices: [],
|
||||
defaultValue: [],
|
||||
}
|
||||
});
|
||||
|
||||
function getDefaultPrebufferedStreams(msos: ResponseMediaStreamOptions[]) {
|
||||
@@ -177,10 +147,18 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
const v: StreamStorageSetting = storageSettings.settings[key];
|
||||
const value = storageSettings.values[key];
|
||||
let isDefault = value === 'Default';
|
||||
|
||||
let stream = msos?.find(mso => mso.name === value);
|
||||
if (isDefault || !stream) {
|
||||
isDefault = true;
|
||||
stream = getDefaultMediaStream(v, msos);
|
||||
if (storageSettings.values.synthenticStreams.includes(value)) {
|
||||
stream = {
|
||||
id: `synthetic:${value}`,
|
||||
};
|
||||
}
|
||||
else {
|
||||
if (isDefault || !stream) {
|
||||
isDefault = true;
|
||||
stream = getDefaultMediaStream(v, msos);
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: streamTypes[key].title,
|
||||
@@ -193,6 +171,7 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
const choices = [
|
||||
'Default',
|
||||
...msos.map(mso => mso.name),
|
||||
...storageSettings.values.synthenticStreams,
|
||||
];
|
||||
const defaultValue = getDefaultMediaStream(v, msos).name;
|
||||
|
||||
@@ -209,16 +188,6 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
onGet: async () => {
|
||||
let enabledStreams: StorageSetting;
|
||||
|
||||
const hideTranscode = device.mixins?.includes(getTranscodeMixinProviderId()) ? {
|
||||
hide: false,
|
||||
} : {};
|
||||
const hideTranscodes = {
|
||||
transcodeStreams: hideTranscode,
|
||||
missingCodecParameters: hideTranscode,
|
||||
videoDecoderArguments: hideTranscode,
|
||||
videoFilterArguments: hideTranscode,
|
||||
};
|
||||
|
||||
try {
|
||||
const msos = await device.mixinDevice.getVideoStreamOptions();
|
||||
|
||||
@@ -236,13 +205,11 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
lowResolutionStream: createStreamOptions(streamTypes.lowResolutionStream, msos),
|
||||
recordingStream: createStreamOptions(streamTypes.recordingStream, msos),
|
||||
remoteRecordingStream: createStreamOptions(streamTypes.remoteRecordingStream, msos),
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
enabledStreams,
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,7 +218,6 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
}
|
||||
|
||||
return {
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import sdk, { MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import { RebroadcastPlugin } from "./main";
|
||||
import { REBROADCAST_MIXIN_INTERFACE_TOKEN } from "./rebroadcast-mixin-token";
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export const TRANSCODE_MIXIN_PROVIDER_NATIVE_ID = 'transcode';
|
||||
|
||||
export function getTranscodeMixinProviderId() {
|
||||
if (!deviceManager.getNativeIds().includes(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID))
|
||||
return;
|
||||
const transcodeMixin = deviceManager.getDeviceState(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID);
|
||||
return transcodeMixin?.id;
|
||||
}
|
||||
|
||||
export class TranscodeMixinProvider extends ScryptedDeviceBase implements MixinProvider, Settings {
|
||||
constructor(public plugin: RebroadcastPlugin) {
|
||||
super(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.plugin.transcodeStorageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.plugin.transcodeStorageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
if (!interfaces.includes(REBROADCAST_MIXIN_INTERFACE_TOKEN))
|
||||
return;
|
||||
return [
|
||||
ScryptedInterface.Settings,
|
||||
];
|
||||
}
|
||||
|
||||
invalidateSettings(id: string) {
|
||||
process.nextTick(async () => {
|
||||
for (const [mixin, v] of this.plugin.currentMixins.entries()) {
|
||||
if (v.id === id)
|
||||
mixin?.onDeviceEvent(ScryptedInterface.Settings, undefined)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
this.invalidateSettings(mixinDeviceState.id);
|
||||
return mixinDevice;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
this.invalidateSettings(id);
|
||||
}
|
||||
}
|
||||
31
plugins/reolink/package-lock.json
generated
31
plugins/reolink/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -35,22 +35,29 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.67",
|
||||
"version": "0.3.108",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -63,11 +70,9 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.5"
|
||||
"typedoc": "^0.26.11"
|
||||
}
|
||||
},
|
||||
"../onvif/onvif": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.100",
|
||||
"version": "0.0.104",
|
||||
"description": "Reolink Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Brightness, Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
|
||||
import sdk, { Sleep, Brightness, Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import { EventEmitter } from "stream";
|
||||
import { createRtspMediaStreamOptions, Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
@@ -78,7 +78,7 @@ class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brigh
|
||||
}
|
||||
}
|
||||
|
||||
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom {
|
||||
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom, Sleep {
|
||||
client: ReolinkCameraClient;
|
||||
clientWithToken: ReolinkCameraClient;
|
||||
onvifClient: OnvifCameraAPI;
|
||||
@@ -362,7 +362,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
if (this.hasSiren() || this.hasFloodlight())
|
||||
interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
if (this.hasBattery()) {
|
||||
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Online);
|
||||
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Sleep);
|
||||
this.startBatteryCheckInterval();
|
||||
}
|
||||
|
||||
@@ -378,14 +378,20 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
const api = this.getClientWithToken();
|
||||
|
||||
try {
|
||||
const { batteryPercent, sleep } = await api.getBatteryInfo();
|
||||
const { batteryPercent, sleeping } = await api.getBatteryInfo();
|
||||
this.batteryLevel = batteryPercent;
|
||||
this.online = !sleep;
|
||||
|
||||
if (sleeping !== this.sleeping) {
|
||||
this.sleeping = sleeping;
|
||||
}
|
||||
if (batteryPercent !== this.batteryLevel) {
|
||||
this.batteryLevel = batteryPercent;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.log('Error in getting battery info', e);
|
||||
}
|
||||
}, 1000 * 60 * 30);
|
||||
}, 1000 * 10);
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
@@ -557,10 +563,19 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
(async () => {
|
||||
while (!killed) {
|
||||
try {
|
||||
const { value, data } = await client.getMotionState();
|
||||
if (value)
|
||||
triggerMotion();
|
||||
ret.emit('data', JSON.stringify(data));
|
||||
// Battey cameras do not have AI state, they just send events in case of PIR sensor triggered
|
||||
// which equals a motion detected
|
||||
if (this.hasBattery()) {
|
||||
const { value, data } = await client.getPidActive();
|
||||
if (value)
|
||||
triggerMotion();
|
||||
ret.emit('data', JSON.stringify(data));
|
||||
} else {
|
||||
const { value, data } = await client.getMotionState();
|
||||
if (value)
|
||||
triggerMotion();
|
||||
ret.emit('data', JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
ret.emit('error', e);
|
||||
@@ -725,7 +740,8 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
[
|
||||
"Reolink TrackMix PoE",
|
||||
"Reolink TrackMix WiFi",
|
||||
"RLC-81MA"
|
||||
"RLC-81MA",
|
||||
"Trackmix Series W760"
|
||||
].includes(deviceInfo?.model)) {
|
||||
streams.push({
|
||||
name: '',
|
||||
|
||||
@@ -492,7 +492,35 @@ export class ReolinkCameraClient {
|
||||
|
||||
return {
|
||||
batteryPercent: batteryInfoEntry?.batteryPercent,
|
||||
sleep: channelStatusEntry?.sleep === 1,
|
||||
sleeping: channelStatusEntry?.sleep === 1,
|
||||
}
|
||||
}
|
||||
|
||||
async getPidActive() {
|
||||
const url = new URL(`http://${this.host}/api.cgi`);
|
||||
|
||||
const body = [
|
||||
{
|
||||
cmd: "GetEvents",
|
||||
action: 0,
|
||||
param: { channel: this.channelId }
|
||||
},
|
||||
];
|
||||
|
||||
const response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
method: 'POST',
|
||||
}, this.createReadable(body));
|
||||
|
||||
const error = response.body?.find(elem => elem.error)?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getEvents', error);
|
||||
}
|
||||
|
||||
return {
|
||||
value: !!response.body?.[0]?.value?.ai?.other?.alarm_state,
|
||||
data: response.body,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AutoenableMixinProvider } from "@scrypted/common/src/autoenable-mixin-p
|
||||
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { RefreshPromise, TimeoutError, createMapPromiseDebouncer, singletonPromise, timeoutPromise } from "@scrypted/common/src/promise-utils";
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
|
||||
import sdk, { BufferConverter, Camera, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, MediaObject, MediaObjectOptions, MixinProvider, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, Sleep, VideoCamera, WritableDeviceState } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import https from 'https';
|
||||
import os from 'os';
|
||||
@@ -127,9 +127,8 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
return this.console;
|
||||
}
|
||||
|
||||
async takePictureInternal(options?: RequestPictureOptions): Promise<Buffer> {
|
||||
this.debugConsole?.log("Picture requested from camera", options);
|
||||
const eventSnapshot = options?.reason === 'event';
|
||||
async takePictureInternal(id: string, eventSnapshot: boolean): Promise<Buffer> {
|
||||
this.debugConsole?.log("Picture requested from camera", { id, eventSnapshot });
|
||||
const { snapshotsFromPrebuffer } = this.storageSettings.values;
|
||||
let usePrebufferSnapshots: boolean;
|
||||
switch (snapshotsFromPrebuffer) {
|
||||
@@ -162,11 +161,12 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}
|
||||
}
|
||||
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera & Sleep>(this.id);
|
||||
|
||||
let takePrebufferPicture: () => Promise<Buffer>;
|
||||
const preparePrebufferSnapshot = async () => {
|
||||
if (takePrebufferPicture)
|
||||
return takePrebufferPicture;
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.id);
|
||||
const msos = await realDevice.getVideoStreamOptions();
|
||||
let prebufferChannel = msos?.find(mso => mso.prebuffer);
|
||||
if (prebufferChannel || !this.lastAvailablePicture) {
|
||||
@@ -250,7 +250,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
|
||||
if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Camera)) {
|
||||
let takePictureOptions: RequestPictureOptions;
|
||||
if (!options?.id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
|
||||
if (!id && this.storageSettings.values.defaultSnapshotChannel !== 'Camera Default') {
|
||||
try {
|
||||
const psos = await this.getPictureOptions();
|
||||
const pso = psos.find(pso => pso.name === this.storageSettings.values.defaultSnapshotChannel);
|
||||
@@ -262,6 +262,9 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
}
|
||||
}
|
||||
try {
|
||||
// consider waking the camera if
|
||||
if (!eventSnapshot && this.mixinDeviceInterfaces.includes(ScryptedInterface.Sleep) && realDevice.sleeping)
|
||||
throw new Error('Not waking sleeping camera for periodic snapshot.');
|
||||
return await this.mixinDevice.takePicture(takePictureOptions).then(mo => mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg'))
|
||||
}
|
||||
catch (e) {
|
||||
@@ -289,7 +292,7 @@ class SnapshotMixin extends SettingsMixinDeviceBase<Camera> implements Camera {
|
||||
event: options?.reason === 'event',
|
||||
}, eventSnapshot ? 0 : 4000, async () => {
|
||||
const snapshotTimer = Date.now();
|
||||
let picture = await this.takePictureInternal();
|
||||
let picture = await this.takePictureInternal(undefined, eventSnapshot);
|
||||
picture = await this.cropAndScale(picture);
|
||||
this.clearCachedPictures();
|
||||
const pictureTime = Date.now();
|
||||
|
||||
4
plugins/webrtc/package-lock.json
generated
4
plugins/webrtc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.58",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.58",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/webrtc",
|
||||
"version": "0.2.57",
|
||||
"version": "0.2.58",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
2
plugins/wyze/.vscode/settings.json
vendored
2
plugins/wyze/.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
|
||||
4
plugins/wyze/package-lock.json
generated
4
plugins/wyze/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/wyze",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.57",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/wyze",
|
||||
"version": "0.0.56",
|
||||
"version": "0.0.57",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.56"
|
||||
"version": "0.0.57"
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
|
||||
except:
|
||||
if default:
|
||||
return "Default"
|
||||
return 120 if self.camera.is_2k else 60
|
||||
return 240 if self.camera.is_2k else 160
|
||||
|
||||
async def getSettings(self):
|
||||
ret: List[Setting] = []
|
||||
@@ -152,8 +152,9 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
|
||||
"500",
|
||||
"750",
|
||||
"1000",
|
||||
"1200",
|
||||
"1400",
|
||||
"1800",
|
||||
"2000",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,8 +7,6 @@ const axios = require('axios').create({
|
||||
const process = require('process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
|
||||
|
||||
function getUserHome() {
|
||||
const ret = process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
|
||||
@@ -127,7 +125,7 @@ exports.deploy = function (debugHost, noRebind) {
|
||||
.catch((err) => {
|
||||
console.error(err.message);
|
||||
if (err.response && err.response.data) {
|
||||
console.log(chalk.red(err.response.data));
|
||||
console.log('\x1b[31m%s\x1b[0m', err.response.data);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
@@ -160,7 +158,7 @@ exports.debug = function (debugHost, entryPoint) {
|
||||
.catch((err) => {
|
||||
console.error(err.message);
|
||||
if (err.response && err.response.data) {
|
||||
console.log(chalk.red(err.response.data));
|
||||
console.log('\x1b[31m%s\x1b[0m', err.response.data);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
|
||||
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.111",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.111",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.3.111",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
|
||||
@@ -266,7 +266,7 @@ try {
|
||||
}
|
||||
|
||||
try {
|
||||
(systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, ScryptedInterfaceDescriptors)?.catch(() => { });
|
||||
(sdk.systemManager as any).setScryptedInterfaceDescriptors?.(TYPES_VERSION, ScryptedInterfaceDescriptors)?.catch(() => { });
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { ScryptedInterface, Setting, Settings, SettingValue } from ".";
|
||||
import sdk, { ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from ".";
|
||||
|
||||
const { systemManager } = sdk;
|
||||
|
||||
@@ -57,9 +57,9 @@ function parseValue(value: string | null | undefined, setting: StorageSetting, r
|
||||
return value || readDefaultValue();
|
||||
}
|
||||
|
||||
export type HideFunction = (device: any) => boolean;
|
||||
export interface StorageSetting extends Omit<Setting, 'deviceFilter'> {
|
||||
deviceFilter?: string | ((test: { id: string, deviceInterface: string, interfaces: string[], type: ScryptedDeviceType, ScryptedDeviceType: typeof ScryptedDeviceType, ScryptedInterface: typeof ScryptedInterface }) => boolean);
|
||||
|
||||
export interface StorageSetting extends Setting {
|
||||
defaultValue?: any;
|
||||
persistedDefaultValue?: any;
|
||||
onPut?: (oldValue: any, newValue: any) => void;
|
||||
@@ -140,7 +140,9 @@ export class StorageSettings<T extends string> implements Settings {
|
||||
continue;
|
||||
s.key = key;
|
||||
s.value = this.getItemInternal(key as T, s, true);
|
||||
ret.push(s);
|
||||
if (typeof s.deviceFilter === 'function')
|
||||
s.deviceFilter = s.deviceFilter.toString();
|
||||
ret.push(s as Setting);
|
||||
delete s.onPut;
|
||||
delete s.onGet;
|
||||
delete s.mapPut;
|
||||
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.103",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.103",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.3.94",
|
||||
"version": "0.3.103",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"author": "",
|
||||
|
||||
@@ -117,6 +117,7 @@ class ScryptedInterface(str, Enum):
|
||||
BinarySensor = "BinarySensor"
|
||||
Brightness = "Brightness"
|
||||
BufferConverter = "BufferConverter"
|
||||
Buttons = "Buttons"
|
||||
Camera = "Camera"
|
||||
Charger = "Charger"
|
||||
ClusterForkInterface = "ClusterForkInterface"
|
||||
@@ -166,6 +167,7 @@ class ScryptedInterface(str, Enum):
|
||||
PM25Sensor = "PM25Sensor"
|
||||
PositionSensor = "PositionSensor"
|
||||
PowerSensor = "PowerSensor"
|
||||
PressButtons = "PressButtons"
|
||||
Program = "Program"
|
||||
PushHandler = "PushHandler"
|
||||
Readme = "Readme"
|
||||
@@ -184,6 +186,7 @@ class ScryptedInterface(str, Enum):
|
||||
ScryptedUser = "ScryptedUser"
|
||||
SecuritySystem = "SecuritySystem"
|
||||
Settings = "Settings"
|
||||
Sleep = "Sleep"
|
||||
StartStop = "StartStop"
|
||||
StreamService = "StreamService"
|
||||
TamperSensor = "TamperSensor"
|
||||
@@ -199,6 +202,7 @@ class ScryptedInterface(str, Enum):
|
||||
VideoFrameGenerator = "VideoFrameGenerator"
|
||||
VideoRecorder = "VideoRecorder"
|
||||
VideoRecorderManagement = "VideoRecorderManagement"
|
||||
VideoTextOverlay = "VideoTextOverlay"
|
||||
VOCSensor = "VOCSensor"
|
||||
|
||||
class ScryptedMimeTypes(str, Enum):
|
||||
@@ -286,11 +290,6 @@ class ClipPath(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AudioStreamOptions(TypedDict):
|
||||
|
||||
bitrate: float
|
||||
@@ -322,8 +321,8 @@ class ObjectDetectionResult(TypedDict):
|
||||
boundingBox: tuple[float, float, float, float] # x, y, width, height
|
||||
className: str # The detection class of the object.
|
||||
clipPaths: list[ClipPath] # The detection clip paths that outlines various features or segments, like traced facial features.
|
||||
clipped: bool # Flag that indicates whether the detection was clipped by the detection input and may not be a full bounding box.
|
||||
cost: float # The certainty that this is correct tracked object.
|
||||
descriptor: str # A base64 encoded Float32Array that represents the vector descriptor of the detection. Can be used to compute euclidian distance to determine similarity.
|
||||
embedding: str # Base64 encoded embedding float32 vector.
|
||||
history: ObjectDetectionHistory
|
||||
id: str # The id of the tracked object.
|
||||
@@ -667,6 +666,7 @@ class NotifierOptions(TypedDict):
|
||||
badge: str
|
||||
body: str
|
||||
bodyWithSubtitle: str
|
||||
critical: bool
|
||||
data: Any
|
||||
dir: NotificationDirection
|
||||
image: str
|
||||
@@ -676,7 +676,7 @@ class NotifierOptions(TypedDict):
|
||||
requireInteraction: bool
|
||||
silent: bool
|
||||
subtitle: str
|
||||
tag: str
|
||||
tag: str # Collapse key/id.
|
||||
timestamp: float
|
||||
vibrate: VibratePattern
|
||||
|
||||
@@ -946,12 +946,17 @@ class MediaConverterTypes(TypedDict):
|
||||
pass
|
||||
|
||||
|
||||
class Point(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TamperState(TypedDict):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
TYPES_VERSION = "0.3.94"
|
||||
TYPES_VERSION = "0.3.103"
|
||||
|
||||
|
||||
class AirPurifier:
|
||||
@@ -1006,6 +1011,10 @@ class BufferConverter:
|
||||
pass
|
||||
|
||||
|
||||
class Buttons:
|
||||
|
||||
buttons: list[str]
|
||||
|
||||
class Camera:
|
||||
"""Camera devices can take still photos."""
|
||||
|
||||
@@ -1376,6 +1385,12 @@ class PowerSensor:
|
||||
|
||||
powerDetected: bool
|
||||
|
||||
class PressButtons:
|
||||
|
||||
async def pressButton(self, button: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Program:
|
||||
|
||||
async def run(self, variables: Any = None) -> Any:
|
||||
@@ -1523,6 +1538,10 @@ class Settings:
|
||||
pass
|
||||
|
||||
|
||||
class Sleep:
|
||||
|
||||
sleeping: bool
|
||||
|
||||
class StartStop:
|
||||
"""StartStop represents a device that can be started, stopped, and possibly paused and resumed. Typically vacuum cleaners or washers."""
|
||||
|
||||
@@ -1649,6 +1668,12 @@ class VideoRecorderManagement:
|
||||
pass
|
||||
|
||||
|
||||
class VideoTextOverlay:
|
||||
|
||||
fontSize: float
|
||||
origin: Point # The top left position of the overlay in the image, normalized to 0-1.
|
||||
text: str
|
||||
|
||||
class VOCSensor:
|
||||
|
||||
vocDensity: float
|
||||
@@ -1864,6 +1889,7 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
colorTemperature = "colorTemperature"
|
||||
rgb = "rgb"
|
||||
hsv = "hsv"
|
||||
buttons = "buttons"
|
||||
running = "running"
|
||||
paused = "paused"
|
||||
docked = "docked"
|
||||
@@ -1872,6 +1898,9 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
temperatureUnit = "temperatureUnit"
|
||||
humidity = "humidity"
|
||||
audioVolumes = "audioVolumes"
|
||||
fontSize = "fontSize"
|
||||
origin = "origin"
|
||||
text = "text"
|
||||
recordingActive = "recordingActive"
|
||||
ptzCapabilities = "ptzCapabilities"
|
||||
lockState = "lockState"
|
||||
@@ -1884,6 +1913,7 @@ class ScryptedInterfaceProperty(str, Enum):
|
||||
converters = "converters"
|
||||
binaryState = "binaryState"
|
||||
tampered = "tampered"
|
||||
sleeping = "sleeping"
|
||||
powerDetected = "powerDetected"
|
||||
audioDetected = "audioDetected"
|
||||
motionDetected = "motionDetected"
|
||||
@@ -1924,6 +1954,7 @@ class ScryptedInterfaceMethods(str, Enum):
|
||||
setColorTemperature = "setColorTemperature"
|
||||
setRgb = "setRgb"
|
||||
setHsv = "setHsv"
|
||||
pressButton = "pressButton"
|
||||
sendNotification = "sendNotification"
|
||||
start = "start"
|
||||
stop = "stop"
|
||||
@@ -2178,6 +2209,14 @@ class DeviceState:
|
||||
def hsv(self, value: ColorHsv):
|
||||
self.setScryptedProperty("hsv", value)
|
||||
|
||||
@property
|
||||
def buttons(self) -> list[str]:
|
||||
return self.getScryptedProperty("buttons")
|
||||
|
||||
@buttons.setter
|
||||
def buttons(self, value: list[str]):
|
||||
self.setScryptedProperty("buttons", value)
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self.getScryptedProperty("running")
|
||||
@@ -2242,6 +2281,30 @@ class DeviceState:
|
||||
def audioVolumes(self, value: AudioVolumes):
|
||||
self.setScryptedProperty("audioVolumes", value)
|
||||
|
||||
@property
|
||||
def fontSize(self) -> float:
|
||||
return self.getScryptedProperty("fontSize")
|
||||
|
||||
@fontSize.setter
|
||||
def fontSize(self, value: float):
|
||||
self.setScryptedProperty("fontSize", value)
|
||||
|
||||
@property
|
||||
def origin(self) -> Point:
|
||||
return self.getScryptedProperty("origin")
|
||||
|
||||
@origin.setter
|
||||
def origin(self, value: Point):
|
||||
self.setScryptedProperty("origin", value)
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self.getScryptedProperty("text")
|
||||
|
||||
@text.setter
|
||||
def text(self, value: str):
|
||||
self.setScryptedProperty("text", value)
|
||||
|
||||
@property
|
||||
def recordingActive(self) -> bool:
|
||||
return self.getScryptedProperty("recordingActive")
|
||||
@@ -2338,6 +2401,14 @@ class DeviceState:
|
||||
def tampered(self, value: TamperState):
|
||||
self.setScryptedProperty("tampered", value)
|
||||
|
||||
@property
|
||||
def sleeping(self) -> bool:
|
||||
return self.getScryptedProperty("sleeping")
|
||||
|
||||
@sleeping.setter
|
||||
def sleeping(self, value: bool):
|
||||
self.setScryptedProperty("sleeping", value)
|
||||
|
||||
@property
|
||||
def powerDetected(self) -> bool:
|
||||
return self.getScryptedProperty("powerDetected")
|
||||
@@ -2612,6 +2683,20 @@ ScryptedInterfaceDescriptors = {
|
||||
"hsv"
|
||||
]
|
||||
},
|
||||
"Buttons": {
|
||||
"name": "Buttons",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"buttons"
|
||||
]
|
||||
},
|
||||
"PressButtons": {
|
||||
"name": "PressButtons",
|
||||
"methods": [
|
||||
"pressButton"
|
||||
],
|
||||
"properties": []
|
||||
},
|
||||
"Notifier": {
|
||||
"name": "Notifier",
|
||||
"methods": [
|
||||
@@ -2722,6 +2807,15 @@ ScryptedInterfaceDescriptors = {
|
||||
],
|
||||
"properties": []
|
||||
},
|
||||
"VideoTextOverlay": {
|
||||
"name": "VideoTextOverlay",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"fontSize",
|
||||
"origin",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"VideoRecorder": {
|
||||
"name": "VideoRecorder",
|
||||
"methods": [
|
||||
@@ -2938,6 +3032,13 @@ ScryptedInterfaceDescriptors = {
|
||||
"tampered"
|
||||
]
|
||||
},
|
||||
"Sleep": {
|
||||
"name": "Sleep",
|
||||
"methods": [],
|
||||
"properties": [
|
||||
"sleeping"
|
||||
]
|
||||
},
|
||||
"PowerSensor": {
|
||||
"name": "PowerSensor",
|
||||
"methods": [],
|
||||
|
||||
@@ -214,6 +214,14 @@ export interface ColorHsv {
|
||||
v?: number;
|
||||
}
|
||||
|
||||
export interface Buttons {
|
||||
buttons?: ('doorbell' | string)[];
|
||||
}
|
||||
|
||||
export interface PressButtons {
|
||||
pressButton(button: string): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
action: string;
|
||||
icon?: string;
|
||||
@@ -234,6 +242,10 @@ export interface NotifierOptions {
|
||||
renotify?: boolean;
|
||||
requireInteraction?: boolean;
|
||||
silent?: boolean;
|
||||
critical?: boolean;
|
||||
/**
|
||||
* Collapse key/id.
|
||||
*/
|
||||
tag?: string;
|
||||
timestamp?: number;
|
||||
vibrate?: VibratePattern;
|
||||
@@ -950,6 +962,20 @@ export interface VideoCameraMask {
|
||||
setPrivacyMasks(masks: PrivacyMasks): Promise<void>;
|
||||
}
|
||||
|
||||
export interface VideoTextOverlay {
|
||||
/**
|
||||
* The top left position of the overlay in the image, normalized to 0-1.
|
||||
*/
|
||||
origin?: Point;
|
||||
fontSize?: number;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface VideoTextOverlays {
|
||||
getVideoTextOverlays(): Promise<Record<string, string>>;
|
||||
setVideoTextOverlay(id: string, value: VideoTextOverlay): Promise<void>;
|
||||
}
|
||||
|
||||
export enum PanTiltZoomMovement {
|
||||
Absolute = "Absolute",
|
||||
Relative = "Relative",
|
||||
@@ -1256,6 +1282,10 @@ export interface Charger {
|
||||
chargeState?: ChargeState;
|
||||
}
|
||||
|
||||
export interface Sleep {
|
||||
sleeping?: boolean;
|
||||
}
|
||||
|
||||
export interface Reboot {
|
||||
reboot(): Promise<void>;
|
||||
}
|
||||
@@ -1528,6 +1558,10 @@ export interface ObjectDetectionResult extends BoundingBoxResult {
|
||||
* The certainty that this is correct tracked object.
|
||||
*/
|
||||
cost?: number;
|
||||
/**
|
||||
* Flag that indicates whether the detection was clipped by the detection input and may not be a full bounding box.
|
||||
*/
|
||||
clipped?: boolean;
|
||||
/**
|
||||
* The detection class of the object.
|
||||
*/
|
||||
@@ -1544,11 +1578,6 @@ export interface ObjectDetectionResult extends BoundingBoxResult {
|
||||
* The score of the label.
|
||||
*/
|
||||
labelScore?: number;
|
||||
/**
|
||||
* A base64 encoded Float32Array that represents the vector descriptor of the detection.
|
||||
* Can be used to compute euclidian distance to determine similarity.
|
||||
*/
|
||||
descriptor?: string;
|
||||
/**
|
||||
* The detection landmarks, like key points in a face landmarks.
|
||||
*/
|
||||
@@ -2252,6 +2281,8 @@ export enum ScryptedInterface {
|
||||
ColorSettingTemperature = "ColorSettingTemperature",
|
||||
ColorSettingRgb = "ColorSettingRgb",
|
||||
ColorSettingHsv = "ColorSettingHsv",
|
||||
Buttons = "Buttons",
|
||||
PressButtons = "PressButtons",
|
||||
Notifier = "Notifier",
|
||||
StartStop = "StartStop",
|
||||
Pause = "Pause",
|
||||
@@ -2265,6 +2296,7 @@ export enum ScryptedInterface {
|
||||
Display = "Display",
|
||||
VideoCamera = "VideoCamera",
|
||||
VideoCameraMask = "VideoCameraMask",
|
||||
VideoTextOverlay = "VideoTextOverlay",
|
||||
VideoRecorder = "VideoRecorder",
|
||||
VideoRecorderManagement = "VideoRecorderManagement",
|
||||
PanTiltZoom = "PanTiltZoom",
|
||||
@@ -2291,6 +2323,7 @@ export enum ScryptedInterface {
|
||||
Settings = "Settings",
|
||||
BinarySensor = "BinarySensor",
|
||||
TamperSensor = "TamperSensor",
|
||||
Sleep = "Sleep",
|
||||
PowerSensor = "PowerSensor",
|
||||
AudioSensor = "AudioSensor",
|
||||
MotionSensor = "MotionSensor",
|
||||
|
||||
5
server/.vscode/launch.json
vendored
5
server/.vscode/launch.json
vendored
@@ -99,11 +99,6 @@
|
||||
"env": {
|
||||
"SCRYPTED_PYTHON310_PATH": "/opt/homebrew/bin/python3.10",
|
||||
"DYLD_LIBRARY_PATH": "/usr/local/lib",
|
||||
"SCRYPTED_CLUSTER_WORKER_NAME": "Macaroni 2",
|
||||
"SCRYPTED_CLUSTER_LABELS": "@scrypted/coreml,@scrypted/tensorflow-lite,compute,compute.preferred",
|
||||
"SCRYPTED_CLUSTER_MODE": "client",
|
||||
"SCRYPTED_CLUSTER_SERVER": "192.168.2.130",
|
||||
"SCRYPTED_CLUSTER_SECRET": "swordfish",
|
||||
"SCRYPTED_CAN_RESTART": "true",
|
||||
"SCRYPTED_VOLUME": "/Users/koush/.scrypted-cluster/volume-client",
|
||||
}
|
||||
|
||||
12
server/package-lock.json
generated
12
server/package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.125.2",
|
||||
"version": "0.132.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.125.2",
|
||||
"version": "0.132.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build3",
|
||||
"@scrypted/node-pty": "^1.0.22",
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -557,9 +557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.92",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
|
||||
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
|
||||
"version": "0.3.100",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.100.tgz",
|
||||
"integrity": "sha512-s/07QCxjMWqODgWj2UpLehzeo2cGFrCA9X8mvpG3owT/+q+sb8v/UUcw9TLHGSN6yIriNhceg3i9WO07kEIT6A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.125.3",
|
||||
"version": "0.132.3",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@scrypted/ffmpeg-static": "^6.1.0-build3",
|
||||
"@scrypted/node-pty": "^1.0.22",
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"@scrypted/types": "^0.3.100",
|
||||
"adm-zip": "^0.5.16",
|
||||
"body-parser": "^1.20.3",
|
||||
"cookie-parser": "^1.4.7",
|
||||
|
||||
@@ -144,7 +144,11 @@ class ClusterSetup:
|
||||
m = hashlib.sha256()
|
||||
m.update(
|
||||
bytes(
|
||||
f"{o['id']}{o.get('address', '')}{o['port']}{o.get('sourceKey', '')}{o['proxyId']}{self.clusterSecret}",
|
||||
# The use of ` o.get(key, None) or '' ` is to ensure that optional fields
|
||||
# are omitted from the hash, matching the JS implementation. Otherwise, since
|
||||
# the dict may contain the keys initialized to None, ` o.get(key, '') ` would
|
||||
# return None instead of ''.
|
||||
f"{o['id']}{o.get('address', None) or ''}{o['port']}{o.get('sourceKey', None) or ''}{o['proxyId']}{self.clusterSecret}",
|
||||
"utf8",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -13,6 +13,15 @@ export async function getScryptedFFmpegPath(): Promise<string> {
|
||||
return f;
|
||||
}
|
||||
|
||||
// strange behavior on synology and possibly unraid
|
||||
// where environment variables are not necessarily kept
|
||||
// in their container manager thing.
|
||||
// so even if the Dockerfile sets SCRYPTED_FFMPEG_PATH,
|
||||
// it is not gauranteed to be present in the environment.
|
||||
// this causes issues with @scrypted/ffmpeg-static,
|
||||
// which looks at that environment variable at build time
|
||||
// to determine whether to install ffmpeg.
|
||||
|
||||
// try to get the ffmpeg path from a variable
|
||||
// ie:
|
||||
// export SCRYPTED_FFMPEG_PATH=/usr/local/bin/ffmpeg
|
||||
@@ -21,5 +30,8 @@ export async function getScryptedFFmpegPath(): Promise<string> {
|
||||
return f;
|
||||
|
||||
const defaultPath = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
||||
return getFfmpegPath() || defaultPath;
|
||||
const scryptedFfmpegStatic = getFfmpegPath();
|
||||
if (scryptedFfmpegStatic && fs.existsSync(scryptedFfmpegStatic))
|
||||
return scryptedFfmpegStatic;
|
||||
return defaultPath;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import semver from 'semver';
|
||||
import { ensurePluginVolume } from "./plugin-volume";
|
||||
|
||||
export function defaultNpmExec(args: string[], options: child_process.SpawnOptions) {
|
||||
@@ -26,8 +25,16 @@ export function setNpmExecFunction(f: typeof npmExecFunction) {
|
||||
|
||||
export function getPluginNodePath(name: string) {
|
||||
const pluginVolume = ensurePluginVolume(name);
|
||||
const nodeMajorVersion = semver.parse(process.version).major;
|
||||
let nodeVersionedDirectory = `node${nodeMajorVersion}-${process.platform}-${process.arch}`;
|
||||
|
||||
const abi = process.versions.modules;
|
||||
let runtime = process.env.npm_config_runtime;
|
||||
if (!runtime && process.versions.electron)
|
||||
runtime = 'electron';
|
||||
if (!runtime)
|
||||
runtime = 'node';
|
||||
const { platform, arch } = process;
|
||||
let nodeVersionedDirectory = `n-${runtime}-v${abi}-${platform}-${arch}`;
|
||||
|
||||
const scryptedBase = process.env.SCRYPTED_BASE_VERSION;
|
||||
if (scryptedBase)
|
||||
nodeVersionedDirectory += '-' + scryptedBase;
|
||||
@@ -98,7 +105,7 @@ export async function installOptionalDependencies(console: Console, packageJson:
|
||||
if (!de.isDirectory())
|
||||
return;
|
||||
if (de.name.startsWith('linux') || de.name.startsWith('darwin') || de.name.startsWith('win32')
|
||||
|| de.name.startsWith('python') || de.name.startsWith('node')) {
|
||||
|| de.name.startsWith('python') || de.name.startsWith('node') || de.name.startsWith('n-')) {
|
||||
console.log('Removing old dependencies:', filePath);
|
||||
try {
|
||||
await fs.promises.rm(filePath, {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { NodeThreadWorker } from './runtime/node-thread-worker';
|
||||
import { prepareZip } from './runtime/node-worker-common';
|
||||
import { getBuiltinRuntimeHosts } from './runtime/runtime-host';
|
||||
import { RuntimeWorker, RuntimeWorkerOptions } from './runtime/runtime-worker';
|
||||
import { Deferred } from '../deferred';
|
||||
|
||||
const serverVersion = require('../../package.json').version;
|
||||
|
||||
@@ -291,6 +292,14 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
forkPeer = Promise.resolve(localPeer);
|
||||
}
|
||||
|
||||
const exitDeferred = new Deferred<string>();
|
||||
runtimeWorker.on('exit', () => {
|
||||
exitDeferred.resolve('worker exited');
|
||||
});
|
||||
runtimeWorker.on('error', e => {
|
||||
exitDeferred.resolve('worker error' + e);
|
||||
});
|
||||
|
||||
// thread workers inherit main console. pipe anything else.
|
||||
if (!(runtimeWorker instanceof NodeThreadWorker)) {
|
||||
const console = options?.id ? getMixinConsole(options.id, options.nativeId) : undefined;
|
||||
@@ -299,6 +308,9 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
|
||||
const result = (async () => {
|
||||
const threadPeer = await forkPeer;
|
||||
exitDeferred.promise.then(reason => {
|
||||
threadPeer.kill(reason);
|
||||
});
|
||||
|
||||
// todo: handle nested forks and skip wrap. this is probably buggy.
|
||||
class PluginForkAPI extends PluginAPIProxy {
|
||||
@@ -319,13 +331,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
|
||||
|
||||
const remote = await setupPluginRemote(threadPeer, forkApi, pluginId, { serverVersion }, () => systemManager.getSystemState());
|
||||
forks.add(remote);
|
||||
runtimeWorker.on('exit', () => {
|
||||
threadPeer.kill('worker exited');
|
||||
forkApi.removeListeners();
|
||||
forks.delete(remote);
|
||||
});
|
||||
runtimeWorker.on('error', e => {
|
||||
threadPeer.kill('worker error ' + e);
|
||||
exitDeferred.promise.then(reason => {
|
||||
forkApi.removeListeners();
|
||||
forks.delete(remote);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,12 @@ export class NodeForkWorker extends ChildProcessWorker {
|
||||
this.pluginId
|
||||
];
|
||||
|
||||
const nodePaths: string[] = [path.resolve(__dirname, '..', '..', '..', 'node_modules')];
|
||||
const nodePaths: string[] = [
|
||||
// /server/node_modules/@scrypted/server/node_modules
|
||||
path.resolve(__dirname, '..', '..', '..', 'node_modules'),
|
||||
// /server/node_modules
|
||||
path.resolve(process.cwd(), 'node_modules'),
|
||||
];
|
||||
if (env?.NODE_PATH)
|
||||
nodePaths.push(env.NODE_PATH);
|
||||
if (process.env.NODE_PATH)
|
||||
|
||||
@@ -132,7 +132,7 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
|
||||
|
||||
const strippedPythonVersion = pluginPythonVersion.replace('.', '');
|
||||
const envPython = !process.env.SCRYPTED_PORTABLE_PYTHON && process.env[`SCRYPTED_PYTHON${strippedPythonVersion}_PATH`];
|
||||
if (envPython) {
|
||||
if (envPython && fs.existsSync(envPython)) {
|
||||
pythonPath = envPython;
|
||||
setup();
|
||||
this.peerin = this.worker.stdio[3] as Writable;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { computeClusterObjectHash } from './cluster/cluster-hash';
|
||||
import { getClusterLabels, getClusterWorkerWeight } from './cluster/cluster-labels';
|
||||
import { getScryptedClusterMode, InitializeCluster, setupCluster } from './cluster/cluster-setup';
|
||||
import type { ClusterObject } from './cluster/connect-rpc-object';
|
||||
import { getScryptedFFmpegPath } from './plugin/ffmpeg-path';
|
||||
import { getPluginVolume, getScryptedVolume } from './plugin/plugin-volume';
|
||||
import { prepareZip } from './plugin/runtime/node-worker-common';
|
||||
import { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
|
||||
@@ -23,15 +24,20 @@ import { EnvControl } from './services/env';
|
||||
import { Info } from './services/info';
|
||||
import { ServiceControl } from './services/service-control';
|
||||
import { sleep } from './sleep';
|
||||
import { getScryptedFFmpegPath } from './plugin/ffmpeg-path';
|
||||
|
||||
installSourceMapSupport({
|
||||
environment: 'node',
|
||||
});
|
||||
|
||||
async function start(mainFilename: string, serviceControl?: ServiceControl) {
|
||||
serviceControl ||= new ServiceControl();
|
||||
startClusterClient(mainFilename, serviceControl);
|
||||
async function start(mainFilename: string, options?: {
|
||||
onClusterWorkerCreated?: (options?: {
|
||||
clusterPluginHosts?: ReturnType<typeof getBuiltinRuntimeHosts>,
|
||||
}) => Promise<void>,
|
||||
serviceControl?: ServiceControl;
|
||||
}) {
|
||||
options ||= {};
|
||||
options.serviceControl ||= new ServiceControl();
|
||||
startClusterClient(mainFilename, options);
|
||||
}
|
||||
|
||||
export default start;
|
||||
@@ -122,12 +128,11 @@ export interface ClusterForkResultInterface {
|
||||
|
||||
export type ClusterForkParam = (runtime: string, options: RuntimeWorkerOptions, peerLiveness: PeerLiveness, getZip: () => Promise<Buffer>) => Promise<ClusterForkResultInterface>;
|
||||
|
||||
function createClusterForkParam(mainFilename: string, clusterId: string, clusterSecret: string, clusterWorkerId: string) {
|
||||
function createClusterForkParam(mainFilename: string, clusterId: string, clusterSecret: string, clusterWorkerId: string, clusterPluginHosts: ReturnType<typeof getBuiltinRuntimeHosts>) {
|
||||
const clusterForkParam: ClusterForkParam = async (runtime, runtimeWorkerOptions, peerLiveness, getZip) => {
|
||||
let runtimeWorker: RuntimeWorker;
|
||||
|
||||
const builtins = getBuiltinRuntimeHosts();
|
||||
const rt = builtins.get(runtime);
|
||||
const rt = clusterPluginHosts.get(runtime);
|
||||
if (!rt)
|
||||
throw new Error('unknown runtime ' + runtime);
|
||||
|
||||
@@ -205,7 +210,12 @@ function createClusterForkParam(mainFilename: string, clusterId: string, cluster
|
||||
return clusterForkParam;
|
||||
}
|
||||
|
||||
export function startClusterClient(mainFilename: string, serviceControl?: ServiceControl) {
|
||||
export function startClusterClient(mainFilename: string, options?: {
|
||||
onClusterWorkerCreated?: (options?: {
|
||||
clusterPluginHosts?: ReturnType<typeof getBuiltinRuntimeHosts>,
|
||||
}) => Promise<void>,
|
||||
serviceControl?: ServiceControl;
|
||||
}) {
|
||||
console.log('Cluster client starting.');
|
||||
|
||||
const envControl = new EnvControl();
|
||||
@@ -217,6 +227,10 @@ export function startClusterClient(mainFilename: string, serviceControl?: Servic
|
||||
const clusterSecret = process.env.SCRYPTED_CLUSTER_SECRET;
|
||||
const clusterMode = getScryptedClusterMode();
|
||||
const [, host, port] = clusterMode;
|
||||
|
||||
const clusterPluginHosts = getBuiltinRuntimeHosts();
|
||||
options?.onClusterWorkerCreated?.({ clusterPluginHosts });
|
||||
|
||||
(async () => {
|
||||
while (true) {
|
||||
// this sleep is here to prevent a tight loop if the server is down.
|
||||
@@ -259,7 +273,7 @@ export function startClusterClient(mainFilename: string, serviceControl?: Servic
|
||||
process.env.SCRYPTED_CLUSTER_ADDRESS = socket.localAddress;
|
||||
|
||||
const peer = preparePeer(socket, 'client');
|
||||
peer.params['service-control'] = serviceControl;
|
||||
peer.params['service-control'] = options?.serviceControl;
|
||||
peer.params['env-control'] = envControl;
|
||||
peer.params['info'] = new Info();
|
||||
peer.params['fs.promises'] = {
|
||||
@@ -294,7 +308,7 @@ export function startClusterClient(mainFilename: string, serviceControl?: Servic
|
||||
const clusterPeerSetup = setupCluster(peer);
|
||||
await clusterPeerSetup.initializeCluster({ clusterId, clusterSecret, clusterWorkerId });
|
||||
|
||||
peer.params['fork'] = createClusterForkParam(mainFilename, clusterId, clusterSecret, clusterWorkerId);
|
||||
peer.params['fork'] = createClusterForkParam(mainFilename, clusterId, clusterSecret, clusterWorkerId, clusterPluginHosts);
|
||||
|
||||
await peer.killed;
|
||||
}
|
||||
@@ -316,7 +330,7 @@ export function createClusterServer(mainFilename: string, scryptedRuntime: Scryp
|
||||
labels: getClusterLabels(),
|
||||
id: scryptedRuntime.serverClusterWorkerId,
|
||||
peer: undefined,
|
||||
fork: Promise.resolve(createClusterForkParam(mainFilename, scryptedRuntime.clusterId, scryptedRuntime.clusterSecret, scryptedRuntime.serverClusterWorkerId)),
|
||||
fork: Promise.resolve(createClusterForkParam(mainFilename, scryptedRuntime.clusterId, scryptedRuntime.clusterSecret, scryptedRuntime.serverClusterWorkerId, scryptedRuntime.pluginHosts)),
|
||||
name: process.env.SCRYPTED_CLUSTER_WORKER_NAME || os.hostname(),
|
||||
address: process.env.SCRYPTED_CLUSTER_ADDRESS,
|
||||
weight: getClusterWorkerWeight(),
|
||||
@@ -371,6 +385,7 @@ export function createClusterServer(mainFilename: string, scryptedRuntime: Scryp
|
||||
console.log('Cluster client authenticated.', socket.remoteAddress, socket.remotePort, properties);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Cluster client authentication failed.', socket.remoteAddress, socket.remotePort, e);
|
||||
peer.kill(e);
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import vm from 'vm';
|
||||
import { getScryptedClusterMode } from './cluster/cluster-setup';
|
||||
import { PluginError } from './plugin/plugin-error';
|
||||
import { isNodePluginWorkerProcess } from './plugin/runtime/node-fork-worker';
|
||||
import type { getBuiltinRuntimeHosts } from './plugin/runtime/runtime-host';
|
||||
import { RPCResultError, startPeriodicGarbageCollection } from './rpc';
|
||||
import type { Runtime } from './scrypted-server-main';
|
||||
import { getDotEnvPath } from './services/env';
|
||||
@@ -16,6 +17,9 @@ import type { ServiceControl } from './services/service-control';
|
||||
function start(mainFilename: string, options?: {
|
||||
serviceControl?: ServiceControl,
|
||||
onRuntimeCreated?: (runtime: Runtime) => Promise<void>,
|
||||
onClusterWorkerCreated?: (options?: {
|
||||
clusterPluginHosts?: ReturnType<typeof getBuiltinRuntimeHosts>,
|
||||
}) => Promise<void>,
|
||||
}) {
|
||||
// Allow including a custom file path for platforms that require
|
||||
// compatibility hacks. For example, Android may need to patch
|
||||
@@ -71,7 +75,7 @@ function start(mainFilename: string, options?: {
|
||||
const clusterMode = getScryptedClusterMode();
|
||||
if (clusterMode?.[0] === 'client') {
|
||||
const start = require('./scrypted-cluster-main').default;
|
||||
return start(mainFilename, options?.serviceControl);
|
||||
return start(mainFilename, options);
|
||||
}
|
||||
else {
|
||||
const start = require('./scrypted-server-main').default;
|
||||
|
||||
Reference in New Issue
Block a user