Compare commits

...

124 Commits

Author SHA1 Message Date
Koushik Dutta
c492c15081 rpc: async generator should throw if yielded and when the peer has been killed. garbage collection does not trigger async generator return or throw. 2023-04-19 21:35:46 -07:00
Koushik Dutta
ee7076384b prebeta 2023-04-19 21:17:59 -07:00
Koushik Dutta
717cac721a detect: connect to rpc object for every videoframe 2023-04-19 12:18:02 -07:00
Koushik Dutta
af41c853bc Merge branch 'main' of github.com:koush/scrypted 2023-04-19 12:17:27 -07:00
Koushik Dutta
109b716753 sdk: update 2023-04-19 12:16:56 -07:00
Qasim Mehmood
07930508fe Publish mutable docker tags for all variants (#738)
This should add mutable docker tags for all variants that allow for updating via docker pull
2023-04-19 12:12:29 -07:00
nanosonde
a291abe375 Initial version of Doorbird plugin (#736)
save work

Add audio-transmit part

Fetch VGA JPEG snapshots from the camera

save work

Use fixed doorbird module 2.1.2

save work

Add doorbell and motion events

Clean up.

Improved initial camera setup like amcrest plugin

Update README
2023-04-19 12:12:18 -07:00
Koushik Dutta
f4f34b2da8 server: fix script 2023-04-18 10:47:32 -07:00
Koushik Dutta
3b4de526ba postrelease 2023-04-18 10:45:28 -07:00
Koushik Dutta
5de67fca86 server: fix python 3.8 issues 2023-04-18 10:45:20 -07:00
Koushik Dutta
98dc0b1b6d postrelease 2023-04-18 10:44:57 -07:00
Koushik Dutta
a05595ecc7 pam-diff/videoanalysis: fix performance, remove sharp dependency 2023-04-18 00:26:29 -07:00
Koushik Dutta
87be4648f1 prebeta 2023-04-17 22:14:36 -07:00
Koushik Dutta
60e51adb41 postrelease 2023-04-17 14:24:33 -07:00
Koushik Dutta
ace7720fe1 videoanalysis: fix snapshot hangs caused by HOL jpeg 2023-04-17 12:52:46 -07:00
Koushik Dutta
b9eb74d403 videoanalysis: add prebuffer hint 2023-04-17 09:55:20 -07:00
Koushik Dutta
fb7353383d predict: rollback rpc change until server is published 2023-04-17 08:46:32 -07:00
Koushik Dutta
bee119b486 python-codecs: handle vips rgba conversion 2023-04-17 08:46:24 -07:00
Koushik Dutta
0b6ffc2b87 predict: strip out allow list 2023-04-16 20:50:49 -07:00
Koushik Dutta
3863527b4d server: fix publish scripts 2023-04-16 13:11:44 -07:00
Koushik Dutta
51c48f4a1c prebeta 2023-04-16 13:10:41 -07:00
Koushik Dutta
4c138e9b4c prebeta 2023-04-16 12:11:25 -07:00
Koushik Dutta
e762c305a3 server: implement various python apis 2023-04-16 12:10:20 -07:00
Koushik Dutta
5bce335288 server: implement various python apis 2023-04-16 12:08:48 -07:00
Koushik Dutta
8201e9883a sdk: update python sdk 2023-04-16 11:43:02 -07:00
Koushik Dutta
74e5884285 videoanalysis: fix generator leak 2023-04-16 10:13:31 -07:00
Koushik Dutta
9cffd9ffbe server: fix noop cluster connect 2023-04-15 21:35:36 -07:00
Koushik Dutta
d8b617f2ae prebeta 2023-04-15 21:35:07 -07:00
Koushik Dutta
aeb564aa5d python-codecs: fix rgb->grasycale conversion 2023-04-15 21:22:52 -07:00
Koushik Dutta
45f672883a sdk: improve moving object metadata 2023-04-15 20:14:05 -07:00
Koushik Dutta
c0ff857a1b server: improve cluster resolution 2023-04-15 20:13:43 -07:00
Koushik Dutta
64f7e31f54 prebeta 2023-04-15 20:11:40 -07:00
Koushik Dutta
6b55f8876e prebeta 2023-04-15 15:12:10 -07:00
Koushik Dutta
718a31f2c5 prebeta 2023-04-15 15:02:32 -07:00
Koushik Dutta
c1e1d50fa5 sdk: publish 2023-04-15 10:14:42 -07:00
Koushik Dutta
75c4a1939f server: publish beta 2023-04-15 09:33:23 -07:00
Koushik Dutta
0d703c2aff predict: remove filter options 2023-04-15 09:33:10 -07:00
Koushik Dutta
0a6e4fda75 sdk: add support for designating object is moving 2023-04-14 22:29:05 -07:00
Koushik Dutta
4c2de9e443 server: add getDevice convenience method for pluginId/nativeId 2023-04-14 22:24:12 -07:00
Koushik Dutta
b8a4fedf1a client: publish 2023-04-14 22:08:58 -07:00
Koushik Dutta
79d9f1d4a1 server: add getDevice convenience method for pluginId/nativeId 2023-04-14 22:07:49 -07:00
Koushik Dutta
983213c578 sort-tracker: deprecate 2023-04-14 16:06:06 -07:00
Koushik Dutta
7dd3d71ebd videoanalysis: remove problematic ffmpeg video generator 2023-04-14 13:36:56 -07:00
Koushik Dutta
493f8deeef Revert "server: watch for dangling python processes"
This reverts commit b29f2d5ee1.
2023-04-14 13:02:51 -07:00
Koushik Dutta
b29f2d5ee1 server: watch for dangling python processes 2023-04-14 12:52:16 -07:00
Koushik Dutta
96bda10123 sort-tracker: remove average area check 2023-04-14 09:58:55 -07:00
Koushik Dutta
3294700d31 core: fix ui refresh issue 2023-04-14 08:07:12 -07:00
Koushik Dutta
0cf77d4c76 core: add support for date./time settings 2023-04-13 19:43:20 -07:00
Koushik Dutta
953841e3a5 update samples 2023-04-13 19:40:02 -07:00
Koushik Dutta
393c1017df sdk: add date/time/datetime types 2023-04-13 13:05:44 -07:00
Koushik Dutta
f50176d14a server: fix CPU usage being lost on fork exit 2023-04-13 12:53:21 -07:00
Koushik Dutta
7f2bf0b542 webrtc: fix ffmpeg leak 2023-04-13 12:51:52 -07:00
Koushik Dutta
9e3990400c zwave: publish 2023-04-13 12:51:41 -07:00
Koushik Dutta
95eed80735 webrtc: fix ffmpeg leak 2023-04-13 12:51:29 -07:00
Koushik Dutta
be43d0c017 zwave: publish 2023-04-12 09:47:05 -07:00
mikeburgh
386ea9a98a Fixing sensor mapping to position (#719) 2023-04-11 20:11:46 -07:00
Koushik Dutta
9b40978f61 client/server: fix various async generator bugs in remote client 2023-04-11 13:53:38 -07:00
Koushik Dutta
f0ee435cd0 videoanalysis: fix detection calculation/throttling in snapshot mode 2023-04-10 17:49:33 -07:00
Koushik Dutta
30748784ef videoanalysis: fix logging 2023-04-10 14:30:14 -07:00
Koushik Dutta
8310e33719 videoanalysis: profile system performance and use snapshot mode when necessary 2023-04-10 12:48:32 -07:00
Koushik Dutta
1d18697161 videoanalysis: watch for pipeline hangs. fix race conditions around pipeline startup/termination. 2023-04-10 10:17:26 -07:00
Koushik Dutta
d500b3fd6c h264 packetizer: update codec information with stapa packets 2023-04-10 08:06:41 -07:00
Koushik Dutta
95ae916b6c Merge branch 'main' of github.com:koush/scrypted 2023-04-09 21:02:47 -07:00
Koushik Dutta
ec3e16f20f onvif (reolink): implement two way audio 2023-04-09 21:02:42 -07:00
Brett Jia
30d28f543c arlo: boolean settings + publish (#713) 2023-04-09 14:49:43 -07:00
Koushik Dutta
e0cce24999 python-codecs: publish 2023-04-09 12:04:40 -07:00
Koushik Dutta
409b25f8b0 python-codecs: fix windows process cleanup 2023-04-09 12:04:16 -07:00
Koushik Dutta
8f278abec8 videoanalysis: fix bug where stream failure may cause motion detector to never restart 2023-04-08 11:10:49 -07:00
Koushik Dutta
d6179dab82 prebeta 2023-04-08 10:19:11 -07:00
Koushik Dutta
ed186e2142 server/rpc: improve typings on rpc message type 2023-04-08 10:19:06 -07:00
Koushik Dutta
3c021bb2c8 prebeta 2023-04-08 10:17:24 -07:00
Koushik Dutta
c522edc622 server/rpc: improve typings on rpc message type 2023-04-08 10:17:18 -07:00
Koushik Dutta
022a103bcb prebeta 2023-04-08 10:05:07 -07:00
Koushik Dutta
efd125b6e4 server/rpc: add Uint8Array to node transport safe arguments 2023-04-08 10:05:02 -07:00
Koushik Dutta
19f7688a65 python-codecs: publish 2023-04-08 09:18:54 -07:00
Koushik Dutta
7f18e4629c prebeta 2023-04-08 09:18:38 -07:00
Koushik Dutta
dfe2c937a1 server: add hook for cluster peer creation 2023-04-08 09:18:31 -07:00
Koushik Dutta
47d7a23a3d postrelease 2023-04-07 21:15:04 -07:00
Koushik Dutta
0ea609c80c server: update dependencies 2023-04-07 21:14:58 -07:00
Koushik Dutta
71ee5727f1 sdk: update 2023-04-07 15:11:24 -07:00
Koushik Dutta
2383f16112 videoanalysis: improve logging for debugging 2023-04-07 14:53:27 -07:00
Koushik Dutta
7d5defd736 homekit: publish 2023-04-07 14:06:38 -07:00
Koushik Dutta
cbf4cf0579 server: fix typo in storage of undefined 2023-04-07 14:06:17 -07:00
Koushik Dutta
422dd94e5c plugins: update problematic setStorage(key, undefined) 2023-04-07 14:05:49 -07:00
Koushik Dutta
076f5e27f1 postrelease 2023-04-07 13:50:09 -07:00
Koushik Dutta
645de2e5fd predict: improve input aspect ratio match fast path 2023-04-07 10:17:05 -07:00
Koushik Dutta
dcf24a77d7 postrelease 2023-04-07 08:30:27 -07:00
Koushik Dutta
7065365a47 postrelease 2023-04-07 08:30:21 -07:00
Koushik Dutta
b82520776e sdk/server: search for TypedDict in typing and typing_extensions 2023-04-07 08:29:47 -07:00
Koushik Dutta
638c1f77fd ring: fix login issues 2023-04-07 08:06:16 -07:00
Koushik Dutta
73a489ea37 rtc: null check double offer error 2023-04-06 22:35:52 -07:00
Koushik Dutta
77d69f025a server: fix release build scripts 2023-04-06 08:32:44 -07:00
Koushik Dutta
3bc14ad248 prebeta 2023-04-06 08:32:33 -07:00
Koushik Dutta
03e5a9dec1 Merge branch 'main' of github.com:koush/scrypted 2023-04-06 08:22:11 -07:00
Koushik Dutta
57b790c332 server: publish beta 2023-04-06 08:22:05 -07:00
Koushik Dutta
ce2ea63be7 server: add hook for npm exec in non-node environment (electron) 2023-04-06 08:21:37 -07:00
Alex Leeds
2dd4721b7f ring: fix login 406 error (#698) 2023-04-06 07:45:33 -07:00
Justin Angevaare
667075dfad Add tip about digest authentication (#697) 2023-04-06 07:45:24 -07:00
Koushik Dutta
7abdb06b66 postrelease 2023-04-05 14:39:28 -07:00
Koushik Dutta
43e5822c93 server: fix first run account creation bug 2023-04-05 14:39:20 -07:00
Koushik Dutta
bc579514e7 python-codecs: add numpy to requirements.txt 2023-04-05 11:55:04 -07:00
Koushik Dutta
825100f94e webrtc: add answer only option 2023-04-05 10:17:17 -07:00
Koushik Dutta
803bfc1560 pam-diff: tweak default motion percent 2023-04-05 10:16:46 -07:00
Koushik Dutta
b2013a54ed pam-diff: tweak default motion percent 2023-04-05 10:15:40 -07:00
Koushik Dutta
f252407935 rebroadcast: fix settings clear issue 2023-04-04 11:37:37 -07:00
Koushik Dutta
516f2a2a7b server: fetch version from package registry 2023-04-04 10:14:31 -07:00
Koushik Dutta
c1677ce691 postrelease 2023-04-04 09:59:39 -07:00
Koushik Dutta
5028fb812d server: storage polyfill should serialize keys and values as strings 2023-04-04 09:58:51 -07:00
Koushik Dutta
2db4e2579f server: add more files to .npmignore 2023-04-04 08:24:23 -07:00
Koushik Dutta
b339ca6cd2 fix bug where deleted users have continued/escalated permissions 2023-04-04 08:17:44 -07:00
Koushik Dutta
f100999cb1 postrelease 2023-04-04 08:17:13 -07:00
Koushik Dutta
2863756bd6 Revert "webrtc: startRtpForwarderProcess remove werift dependency"
This reverts commit 143a0b2c41.
2023-04-03 14:26:56 -07:00
Koushik Dutta
cc408850a0 videoanalysis: changing pipeline should restart video analysis 2023-04-03 12:26:02 -07:00
Koushik Dutta
ed1ceeda51 core: return correct acls for admins 2023-04-03 11:16:14 -07:00
Koushik Dutta
df09d8e92a Merge branch 'main' of github.com:koush/scrypted 2023-04-03 08:34:35 -07:00
Koushik Dutta
298ac960d1 core: fix checkbox ui 2023-04-03 08:34:30 -07:00
Nick Berardi
62d4d55aae unifi: added native zoom capability (#684) 2023-04-03 08:24:56 -07:00
Nick Berardi
a2121c0dc5 alexa: add setting to publish debug events to console (#685) 2023-04-03 08:24:31 -07:00
Koushik Dutta
9b5ea27c0b core: fix checkbox ui 2023-04-03 08:23:20 -07:00
Koushik Dutta
0b0e90fc04 server: fix version being off by 1 in release notes/tag 2023-04-02 10:13:52 -07:00
Koushik Dutta
d8aff609bf core: publish 2023-04-02 09:37:54 -07:00
Koushik Dutta
d8283c261a homekit: publish beta 2023-04-02 09:37:49 -07:00
Koushik Dutta
e3aca964be Merge branch 'main' of github.com:koush/scrypted 2023-04-02 09:34:07 -07:00
Brett Jia
f97669949d sdk, core: add Charger interface (#680)
* add Charger interface

* add charger icon to web ui

* import correct path

* get charge state displayed correctly
2023-04-01 21:03:54 -07:00
172 changed files with 3875 additions and 4877 deletions

View File

@@ -69,12 +69,18 @@ jobs:
tags: |
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:thin-s6' || '' }}
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:thin-s6' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

3
.gitmodules vendored
View File

@@ -32,9 +32,6 @@
[submodule "plugins/sample-cameraprovider"]
path = plugins/sample-cameraprovider
url = ../../koush/scrypted-sample-cameraprovider
[submodule "plugins/tensorflow-lite/sort_oh"]
path = plugins/sort-tracker/sort_oh
url = ../../koush/sort_oh.git
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/node-nat-upnp
url = ../../koush/node-nat-upnp.git

View File

@@ -1,15 +1,17 @@
export class Deferred<T> {
finished = false;
resolve!: (value: T|PromiseLike<T>) => void;
reject!: (error: Error) => void;
resolve!: (value: T|PromiseLike<T>) => this;
reject!: (error: Error) => this;
promise: Promise<T> = new Promise((resolve, reject) => {
this.resolve = v => {
this.finished = true;
resolve(v);
return this;
};
this.reject = e => {
this.finished = true;
reject(e);
return this;
};
});
}

View File

@@ -298,7 +298,7 @@ export async function connectRTCSignalingClients(
if (offerOptions?.offer && answerOptions?.offer)
throw new Error('Both RTC clients have offers and can not negotiate. Consider implementing this in @scrypted/webrtc.');
if (offerOptions?.requiresOffer && answerOptions.requiresOffer)
if (offerOptions?.requiresOffer && answerOptions?.requiresOffer)
throw new Error('Both RTC clients require offers and can not negotiate.');
offerSetup.type = 'offer';

View File

@@ -681,7 +681,7 @@ export class RtspClient extends RtspBase {
});
}
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions) {
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions, headers?: Headers) {
const protocol = options.type === 'udp' ? '' : '/TCP';
const client = options.type === 'udp' ? 'client_port' : 'interleaved';
let port: number;
@@ -697,9 +697,9 @@ export class RtspClient extends RtspBase {
port = options.dgram.address().port;
options.dgram.on('message', data => options.onRtp(undefined, data));
}
const headers: any = {
headers = Object.assign({
Transport: `RTP/AVP${protocol};unicast;${client}=${port}-${port + 1}`,
};
}, headers);
const response = await this.request('SETUP', headers, options.path);
let interleaved: {
begin: number;

View File

@@ -27,13 +27,6 @@ echo "sdk > npm run build"
npm run build
popd
pushd external/HAP-NodeJS
echo "external/HAP-NodeJS > npm install"
npm install
echo "external/HAP-NodeJS > npm run build"
npm run build
popd
pushd external/werift
echo "external/werift > npm install"
npm install

View File

@@ -1,15 +1,15 @@
{
"name": "@scrypted/client",
"version": "1.1.43",
"version": "1.1.51",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.43",
"version": "1.1.51",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.78",
"@scrypted/types": "^0.2.80",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
@@ -21,9 +21,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.2.78",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.78.tgz",
"integrity": "sha512-SiIUh9ph96aZPjt/oO+W/mlJobrP02ADwFDI9jnvw8/UegUti2x/7JE8Pi3kGXOIkN+cX74Qg4xJEMIpdpO1zw=="
"version": "0.2.80",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.80.tgz",
"integrity": "sha512-YVu7jcD5sYgjJLP7kH1K2FJzqrlcjdpDxzZoLXudZCKiujldbmLYcwglSgnN9bRqkKZcGOfru/WssvQj+0JioQ=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.43",
"version": "1.1.51",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -17,7 +17,7 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@scrypted/types": "^0.2.78",
"@scrypted/types": "^0.2.80",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"

View File

@@ -7,6 +7,7 @@ import { timeoutPromise } from "../../../common/src/promise-utils";
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
import type { IOSocket } from '../../../server/src/io';
import { MediaObject } from '../../../server/src/plugin/mediaobject';
import type { MediaObjectRemote } from '../../../server/src/plugin/plugin-api';
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
import { RpcPeer } from '../../../server/src/rpc';
@@ -505,22 +506,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
const mo: MediaObjectRemote & {
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
} = {
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: {
mimeType,
sourceId: options?.sourceId,
},
mimeType,
sourceId: options?.sourceId,
async getData() {
return data;
},
};
return mo as any;
return new MediaObject(mimeType, data, options) as any;
}
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.2.3",
"version": "0.2.4",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -15,6 +15,11 @@ const includeToken = 4;
export let DEBUG = false;
function debug(...args: any[]) {
if (DEBUG)
console.debug(...args);
}
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
@@ -34,6 +39,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
type: 'string',
readonly: true
},
debug: {
title: 'Debug Events',
description: 'Log all events to the console. This will be very noisy and should not be left enabled.',
type: 'boolean',
onPut(oldValue: boolean, newValue: boolean) {
DEBUG = newValue;
}
}
});
@@ -44,6 +57,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
constructor(nativeId?: string) {
super(nativeId);
DEBUG = this.storageSettings.values.debug ?? false;
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
@@ -141,12 +156,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
if (!supportedType)
return;
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
let report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
if (!report && eventDetails.eventInterface === ScryptedInterface.Online) {
report = {};
}
if (!report && eventDetails.eventInterface === ScryptedInterface.Battery) {
report = {};
}
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
debug("event", eventDetails.eventInterface, eventDetails.property, eventSource.type);
let data = {
"event": {
"header": {
@@ -234,7 +260,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const endpoint = await this.getAlexaEndpoint();
const self = this;
this.console.assert(!DEBUG, `event:`, data);
debug("send event to alexa", data);
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
@@ -570,6 +596,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const { authorization } = request.headers;
if (!this.validAuths.has(authorization)) {
try {
debug("making authorization request to Scrypted");
await axios.get('https://home.scrypted.app/_punch/getcookie', {
headers: {
'Authorization': authorization,
@@ -590,11 +618,11 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const { directive } = body;
const { namespace, name } = directive.header;
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
const mapName = `${namespace}/${name}`;
const handler = alexaHandlers.get(mapName);
debug("received directive from alexa", mapName, body);
const handler = alexaHandlers.get(mapName);
if (handler)
return handler.apply(this, [request, response, directive]);
@@ -641,7 +669,7 @@ class HttpResponseLoggingImpl implements AlexaHttpResponse {
if (options.code !== 200)
this.console.error(`response error ${options.code}:`, body);
else
this.console.assert(!DEBUG, `response ${options.code}:`, body);
debug("response to alexa directive", options.code, body);
if (typeof body === 'object')
body = JSON.stringify(body);

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.120",
"version": "0.0.121",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.120",
"version": "0.0.121",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
@@ -16,7 +16,7 @@
"multiparty": "^4.2.2"
},
"devDependencies": {
"@types/node": "^16.11.0"
"@types/node": "^18.15.11"
}
},
"../../common": {
@@ -36,7 +36,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.87",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -100,9 +100,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"node_modules/auth-header": {
"version": "1.0.0",
@@ -291,9 +291,9 @@
}
},
"@types/node": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"auth-header": {
"version": "1.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.120",
"version": "0.0.121",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -36,12 +36,12 @@
},
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/multiparty": "^0.0.33",
"multiparty": "^4.2.2"
},
"devDependencies": {
"@types/node": "^16.11.0"
"@types/node": "^18.15.11"
}
}

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/arlo",
"version": "0.7.12",
"version": "0.7.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.7.12",
"version": "0.7.13",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.85",
"version": "0.2.87",
"dev": true,
"license": "ISC",
"dependencies": {

View File

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

View File

@@ -273,9 +273,10 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
])
return result
@async_print_exception_guard
async def putSetting(self, key, value) -> None:
if key in ["webrtc_emulation", "two_way_audio", "wired_to_power"]:
self.storage.setItem(key, value == "true")
self.storage.setItem(key, value == "true" or value == True)
await self.provider.discover_devices()
async def getPictureOptions(self) -> List[ResponsePictureOptions]:

View File

@@ -480,6 +480,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
return results
@async_print_exception_guard
async def putSetting(self, key: str, value: SettingValue) -> None:
if not self.validate_setting(key, value):
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
@@ -492,7 +493,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
# force arlo client to be invalidated and reloaded
self.invalidate_arlo_client()
elif key == "plugin_verbosity":
self.storage.setItem(key, "Verbose" if value == "true" else "Normal")
self.storage.setItem(key, "Verbose" if value == "true" or value == True else "Normal")
self.propagate_verbosity()
skip_arlo_client = True
else:

View File

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

View File

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

View File

@@ -27,25 +27,8 @@ export class Scheduler {
];
const date = new Date();
if (schedule.clockType === 'AM' || schedule.clockType === 'PM') {
let hour = schedule.hour;
if (schedule.clockType === 'AM') {
if (hour === 12)
hour -= 12;
}
else {
if (hour != 12)
hour += 12;
}
date.setHours(hour);
date.setMinutes(schedule.minute, 0, 0);
}
else if (schedule.clockType === '24HourClock') {
date.setHours(schedule.hour, schedule.minute, 0, 0);
}
else {
throw new Error('sunrise/sunset clock not supported');
}
date.setHours(schedule.hour);
date.setMinutes(schedule.minute);
const ret: ScryptedDevice = {
async setName() { },
@@ -65,7 +48,7 @@ export class Scheduler {
if (!days[day])
continue;
source.log.i(`event will fire at ${future}`);
source.log.i(`event will fire at ${future.toLocaleString()}`);
return future;
}
source.log.w('event will never fire');
@@ -80,6 +63,7 @@ export class Scheduler {
}
const delay = when.getTime() - Date.now();
source.log.i(`event will fire in ${Math.round(delay / 60 / 1000)} minutes.`);
let timeout = setTimeout(() => {
reschedule();

View File

@@ -23,6 +23,15 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
})
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
const usersService = await sdk.systemManager.getComponent('users');
const users: DBUser[] = await usersService.getAllUsers();
const user = users.find(user => user.username === this.username);
if (!user)
throw new Error("user not found");
if (user.admin)
return;
const self = sdk.deviceManager.getDeviceState(this.nativeId);
const ret: ScryptedUserAccessControl = {

View File

@@ -13,7 +13,6 @@
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/vue-fontawesome": "^2.0.8",
"@radial-color-picker/vue-color-picker": "^2.3.0",
"@scrypted/client": "file:../../../packages/client",
"@scrypted/common": "file:../../../common",
"@scrypted/sdk": "file:../../../sdk",
"@scrypted/types": "file:../../../sdk/types",
@@ -32,6 +31,7 @@
"register-service-worker": "^1.7.2",
"router": "^1.3.6",
"semver": "^6.3.0",
"v-calendar": "^2.4.1",
"vue": "^2.7.14",
"vue-apexcharts": "^1.6.2",
"vue-async-computed": "^3.9.0",
@@ -118,27 +118,24 @@
},
"../../../packages/client": {
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.48",
"extraneous": true,
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"adm-zip": "^0.5.9",
"@scrypted/types": "^0.2.78",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
}
},
"../../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.87",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -175,7 +172,7 @@
},
"../../../sdk/types": {
"name": "@scrypted/types",
"version": "0.2.63",
"version": "0.2.79",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",
@@ -2265,6 +2262,16 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"node_modules/@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@radial-color-picker/color-wheel": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@radial-color-picker/color-wheel/-/color-wheel-2.2.0.tgz",
@@ -2287,10 +2294,6 @@
"vue": "^2.5.21"
}
},
"node_modules/@scrypted/client": {
"resolved": "../../../packages/client",
"link": true
},
"node_modules/@scrypted/common": {
"resolved": "../../../common",
"link": true
@@ -7819,7 +7822,6 @@
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
"dev": true,
"engines": {
"node": ">=0.11"
},
@@ -7828,6 +7830,14 @@
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/date-fns-tz": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
"peerDependencies": {
"date-fns": ">=2.0.0"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -18977,6 +18987,31 @@
"uuid": "bin/uuid"
}
},
"node_modules/v-calendar": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-2.4.1.tgz",
"integrity": "sha512-nhzOlHM2cinv+8jIcnAx+nTo63U40szv3Ig41uLMpGK1U5sApgCP6ggigprsnlMOM5VRq1G/1B8rNHkRrLbGjw==",
"dependencies": {
"core-js": "^3.15.2",
"date-fns": "^2.22.1",
"date-fns-tz": "^1.1.4",
"lodash": "^4.17.21"
},
"peerDependencies": {
"@popperjs/core": "^2.4.0",
"vue": "^2.5.18"
}
},
"node_modules/v-calendar/node_modules/core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
@@ -22773,6 +22808,12 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"peer": true
},
"@radial-color-picker/color-wheel": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@radial-color-picker/color-wheel/-/color-wheel-2.2.0.tgz",
@@ -22792,22 +22833,6 @@
"@radial-color-picker/rotator": "2.1.0"
}
},
"@scrypted/client": {
"version": "file:../../../packages/client",
"requires": {
"@scrypted/types": "^0.2.64",
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"adm-zip": "^0.5.9",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"rimraf": "^3.0.2",
"typescript": "^4.7.4"
}
},
"@scrypted/common": {
"version": "file:../../../common",
"requires": {
@@ -27308,8 +27333,13 @@
"date-fns": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
"dev": true
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw=="
},
"date-fns-tz": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
"requires": {}
},
"de-indent": {
"version": "1.0.2",
@@ -36063,6 +36093,24 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
},
"v-calendar": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-2.4.1.tgz",
"integrity": "sha512-nhzOlHM2cinv+8jIcnAx+nTo63U40szv3Ig41uLMpGK1U5sApgCP6ggigprsnlMOM5VRq1G/1B8rNHkRrLbGjw==",
"requires": {
"core-js": "^3.15.2",
"date-fns": "^2.22.1",
"date-fns-tz": "^1.1.4",
"lodash": "^4.17.21"
},
"dependencies": {
"core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
}
}
},
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",

View File

@@ -34,6 +34,7 @@
"register-service-worker": "^1.7.2",
"router": "^1.3.6",
"semver": "^6.3.0",
"v-calendar": "^2.4.1",
"vue": "^2.7.14",
"vue-apexcharts": "^1.6.2",
"vue-async-computed": "^3.9.0",

View File

@@ -215,6 +215,7 @@ import Notifier from "../interfaces/Notifier.vue";
import OnOff from "../interfaces/OnOff.vue";
import Brightness from "../interfaces/Brightness.vue";
import Battery from "../interfaces/Battery.vue";
import Charger from "../interfaces/Charger.vue";
import Lock from "../interfaces/Lock.vue";
import ColorSettingHsv from "../interfaces/ColorSettingHsv.vue";
import ColorSettingRgb from "../interfaces/ColorSettingRgb.vue";
@@ -263,6 +264,7 @@ const cardHeaderInterfaces = [
ScryptedInterface.AudioSensor,
ScryptedInterface.HumiditySensor,
ScryptedInterface.Thermometer,
ScryptedInterface.Charger,
ScryptedInterface.Battery,
ScryptedInterface.Lock,
ScryptedInterface.OnOff,
@@ -362,6 +364,7 @@ export default {
Lock,
OnOff,
Charger,
Battery,
Thermometer,
HumiditySensor,

View File

@@ -40,7 +40,7 @@
<v-btn :dark="!isLive" v-on="on" small :color="isLive ? 'white' : 'blue'" :outlined="isLive">
<v-icon small color="white" :outlined="isLive">fa fa-calendar-alt</v-icon>&nbsp;{{ monthDay }}</v-btn>
</template>
<v-date-picker @input="datePicked"></v-date-picker>
<vc-date-picker mode="date" :value="startTime" @input="datePicked"></vc-date-picker>
</v-dialog>
<v-btn v-if="showNvr" :dark="!isLive" small :color="isLive ? 'white' : adjustingTime ? 'green' : 'blue'"
@@ -181,8 +181,8 @@ export default {
methods: {
datePicked(value) {
this.dateDialog = false;
const dt = datePickerLocalTimeToUTC(value);
this.streamRecorder(dt);
if (value && value.getTime)
this.streamRecorder(value.getTime());
},
doTimeScroll(e) {
if (!this.device.interfaces.includes(ScryptedInterface.VideoRecorder))

View File

@@ -0,0 +1,52 @@
<template>
<v-tooltip left>
<template v-slot:activator="{ on }">
<v-icon
v-on="on"
v-if="lazyValue.chargeState === Charging"
class="mr-1 mr-1"
small
>fa-plug</v-icon>
<v-icon
v-on="on"
v-else-if="lazyValue.chargeState == Trickle"
class="mr-1 mr-1"
small
>fa-plug-circle-minus</v-icon>
<v-icon
v-on="on"
v-else
class="mr-1 mr-1"
small
>fa-plug-circle-xmark</v-icon>
</template>
<span>{{ chargeText }}</span>
</v-tooltip>
</template>
<script>
import { ChargeState } from '@scrypted/types';
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
data() {
return {
Charging: ChargeState.Charging,
Trickle: ChargeState.Trickle,
NotCharging: ChargeState.NotCharging,
};
},
computed: {
chargeText() {
if (this.lazyValue.chargeState === "trickle") {
return "Trickle Charging";
}
if (this.lazyValue.chargeState === "charging") {
return "Charging";
}
return "Not Charging";
},
},
};
</script>

View File

@@ -22,6 +22,7 @@ export default {
watch: {
device() {
this.watchDevice();
this.refresh();
},
},
methods: {

View File

@@ -17,10 +17,21 @@ export default {
VueMarkdown,
CardTitle,
},
data() {
return {
token: 0,
}
},
methods: {
refresh() {
this.token++;
}
},
asyncComputed: {
readme: {
async get() {
return this.device.getReadmeMarkdown();;
await this.token;
return this.device.getReadmeMarkdown();
},
default: undefined,
}

View File

@@ -1,6 +1,10 @@
<template>
<div>
<v-checkbox v-if="lazyValue.type === 'boolean'" dense :readonly="lazyValue.readonly" v-model="booleanValue"
<vc-date-picker v-if="lazyValue.type === 'date'" mode="date" v-model="dateValue" :is-range="lazyValue.combobox"></vc-date-picker>
<vc-date-picker v-else-if="lazyValue.type === 'time'" mode="time" v-model="dateValue"
class="hide-header" :is-range="lazyValue.combobox"></vc-date-picker>
<vc-date-picker v-else-if="lazyValue.type === 'datetime'" mode="datetime" v-model="dateValue" :is-range="lazyValue.combobox"></vc-date-picker>
<v-checkbox v-else-if="lazyValue.type === 'boolean'" dense :readonly="lazyValue.readonly" v-model="booleanValue"
:label="lazyValue.title" :hint="lazyValue.description" :placeholder="lazyValue.placeholder" persistent-hint
@change="save" :class="lazyValue.description ? 'mb-2' : ''"></v-checkbox>
<div v-else-if="lazyValue.type === 'qrcode'">
@@ -41,8 +45,7 @@
</template>
</DevicePicker>
<DevicePicker v-else-if="lazyValue.type === 'interface'" v-model="lazyValue.value" :multiple="lazyValue.multiple"
:readonly="lazyValue.readonly" :devices="interfaces" :title="lazyValue.title"
:description="lazyValue.description">
:readonly="lazyValue.readonly" :devices="interfaces" :title="lazyValue.title" :description="lazyValue.description">
<template v-slot:append-outer>
<v-btn v-if="dirty && device" color="success" @click="save" class="shift-up">
<v-icon>send</v-icon>
@@ -52,7 +55,7 @@
<div v-else-if="lazyValue.type === 'clippath'" class="mb-2">
<v-btn small block @click="editZone">{{ lazyValue.title }} </v-btn>
<Camera :value="device" :device="device" :clipPathValue="sanitizedClipPathValue" :showDialog="editingZone"
:hidePreview="true" @dialog="editingZoneChanged" @clipPath="lazyValue.value = $event"></Camera>
:hidePreview="true" @dialog="editingZoneChanged" @clipPath="updateClipPath"></Camera>
</div>
<v-textarea v-else-if="lazyValue.type === 'textarea'" v-model="lazyValue.value" outlined persistent-hint
:hint="lazyValue.description" :label="lazyValue.title">
@@ -88,6 +91,7 @@ export default {
data() {
return {
editingZone: false,
clipPathThrottle: null,
};
},
watch: {
@@ -134,6 +138,25 @@ export default {
return [];
}
},
dateValue: {
get() {
if (this.lazyValue.combobox) {
return {
start: new Date(parseInt(this.lazyValue.value?.[0]) || Date.now()),
end: new Date(parseInt(this.lazyValue.value?.[1]) || Date.now()),
};
}
return new Date(parseInt(this.lazyValue.value) || Date.now());
},
set(val) {
if (this.lazyValue.combobox) {
this.lazyValue.value = [val.start.getTime(), val.end.getTime()];
}
else {
this.lazyValue.value = val.getTime();
}
}
},
booleanValue: {
get() {
return (
@@ -142,7 +165,7 @@ export default {
);
},
set(val) {
this.lazyValue.value = val.toString();
this.lazyValue.value = !!val;
},
},
dirty() {
@@ -228,6 +251,17 @@ export default {
},
methods: {
onChange() { },
updateClipPath(e) {
clearTimeout(this.clipPathThrottle);
this.clipPathThrottle = setTimeout(() => {
this.lazyValue.value = e;
this.rpc().putSetting(
this.lazyValue.key,
this.createInputValue().value
);
this.onInput();
}, 500)
},
editingZoneChanged(value) {
this.editingZone = value;
if (!value) {
@@ -240,6 +274,7 @@ export default {
},
createLazyValue() {
var type = this.value.type || "";
if (type.indexOf("[]") == -1 && type !== "clippath") {
return cloneDeep(this.value);
}
@@ -254,6 +289,7 @@ export default {
},
createInputValue() {
var type = this.lazyValue.type || "";
if (type.indexOf("[]") == -1 && type !== "clippath") {
return this.lazyValue;
}
@@ -276,4 +312,8 @@ export default {
.shift-up {
margin-top: -8px;
}
</style>
.hide-header .vc-date {
display: none !important;
}
</style>

View File

@@ -40,11 +40,11 @@
<v-btn v-on="on" small>
<v-icon x-small>fa fa-calendar-alt</v-icon>
&nbsp;
{{ year }}-{{ month }}-{{ date }}
{{ new Date(date).getFullYear() }}-{{ new Date(date).getMonth() }}-{{ new Date(date).getDate() }}
</v-btn>
</template>
<v-card>
<v-date-picker @input="onDate"> </v-date-picker>
<vc-date-picker mode="date" @input="onDate" v-model="date"> </vc-date-picker>
</v-card>
</v-dialog>
<v-btn text small disabled v-if="pages">{{ pageRange }}</v-btn>
@@ -70,7 +70,6 @@
</div>
</template>
<script>
import { datePickerLocalTimeToUTC } from "../common/date";
import { fetchClipThumbnail, fetchClipUrl } from "../common/videoclip";
import RPCInterface from "./RPCInterface.vue";
import Vue from "vue";
@@ -129,14 +128,11 @@ export default {
clips: {
async get() {
await this.refreshNonce;
const date = new Date();
const date = new Date(this.date);
date.setMilliseconds(0);
date.setSeconds(0);
date.setMinutes(0);
date.setHours(0);
date.setFullYear(this.year);
date.setMonth(this.month - 1);
date.setDate(this.date);
console.log(date);
const dt = date.getTime();
const ret = await this.device.getVideoClips({
@@ -165,9 +161,7 @@ export default {
fetchingImages: [],
page: 1,
dialog: false,
date: new Date().getDate(),
month: new Date().getMonth() + 1,
year: new Date().getFullYear(),
date: Date.now(),
};
},
methods: {
@@ -202,11 +196,8 @@ export default {
onDate(value) {
this.page = 1;
this.dialog = false;
const dt = datePickerLocalTimeToUTC(value);
const d = new Date(dt);
this.month = d.getMonth() + 1;
this.date = d.getDate();
this.year = d.getFullYear();
console.log(value);
this.date = value;
this.refresh();
},
},

View File

@@ -1,33 +1,12 @@
<template>
<v-layout row wrap justify-center align-center>
<v-flex xs3 md2 lg2 xl1 v-for="day of days" :key="day">
<v-btn
block
class="white--text"
@click="toggleDay(day)"
color="info"
small
:text="!lazyValue[day]"
>{{ day.substring(0, 3) }}</v-btn>
<v-btn block class="white--text" @click="toggleDay(day)" color="info" small :text="!lazyValue[day]">{{
day.substring(0, 3) }}</v-btn>
</v-flex>
<v-flex xs12>
<v-layout justify-center align-center>
<v-time-picker v-model="time" format="24hr" @input="onChange"></v-time-picker>
</v-layout>
</v-flex>
<v-flex xs12>
<v-layout justify-center align-center>
<v-flex xs12 md8 lg6 xl4>
<v-select
xs3
reverse
:items="clockTypes"
solo
item-value="id"
v-model="lazyValue.clockType"
@input="onChange"
></v-select>
</v-flex>
<vc-date-picker v-model="time" class="hide-header" @input="onChange" mode="time"></vc-date-picker>
</v-layout>
</v-flex>
</v-layout>
@@ -52,62 +31,37 @@ function zeroPrefix(arr, len) {
arr.push(i >= 10 ? i.toString() : "0" + i);
}
}
const clockTypes = [
{
id: "AM",
text: "AM"
},
{
id: "PM",
text: "PM"
},
{
text: "24 Hour Clock",
id: "TwentyFourHourClock"
},
{
text: "Before Sunrise",
id: "BeforeSunrise"
},
{
text: "After Sunrise",
id: "AfterSunrise"
},
{
text: "Before Sunset",
id: "BeforeSunset"
},
{
text: "After Sunset",
id: "AfterSunset"
}
];
zeroPrefix(hours, 24);
zeroPrefix(minutes, 59);
export default {
mixins: [RPCInterface],
data: function() {
data: function () {
return {
clockTypes,
days,
};
},
computed: {
time: {
get() {
return `${this.lazyValue.hour}:${this.lazyValue.minute}`;
const date = new Date();
date.setMilliseconds(0);
date.setSeconds(0);
date.setMinutes(this.lazyValue.minute);
date.setHours(this.lazyValue.hour);
return date;
},
set(value) {
this.lazyValue.hour = value.split(":")[0];
this.lazyValue.minute = value.split(":")[1];
this.lazyValue.hour = value.getHours();
this.lazyValue.minute = value.getMinutes();
this.onChange();
}
}
},
methods: {
toggleDay: function(day) {
toggleDay: function (day) {
this.lazyValue[day] = !this.lazyValue[day];
this.onChange();
},
@@ -117,11 +71,10 @@ export default {
ret.minute = ret.minute || 0;
return ret;
},
onChange: function() {
onChange: function () {
const schedule = {
hour: parseInt(this.lazyValue.hour) || 0,
minute: parseInt(this.lazyValue.minute) || 0,
clockType: this.lazyValue.clockType || "AM",
};
days.forEach(day => {
schedule[day] = this.lazyValue[day] || false;
@@ -139,9 +92,15 @@ export default {
-webkit-appearance: none;
appearance: none;
}
.semicolon-pad {
margin-left: 2px;
margin-right: 2px;
margin-top: 4px;
}
.hide-header .vc-date {
display: none !important;
}
</style>

View File

@@ -10,6 +10,13 @@ import './plugins/is-mobile';
import Launcher from './Launcher.vue'
import './registerServiceWorker'
import VCalendar from 'v-calendar';
// Use v-calendar & v-date-picker components
Vue.use(VCalendar, {
componentPrefix: 'vc', // Use <vc-calendar /> instead of <v-calendar />
});
// STYLES
// Main Theme SCSS
// import './assets/scss/theme.scss'

View File

@@ -58,6 +58,8 @@ import {
faLightbulb,
faToggleOn,
faPlug,
faPlugCircleMinus,
faPlugCircleXmark,
faExclamationTriangle,
faSun,
faCode,
@@ -150,6 +152,8 @@ const icons: IconDefinition[] =[
faLightbulb,
faToggleOn,
faPlug,
faPlugCircleMinus,
faPlugCircleXmark,
faExclamationTriangle,
faSun,
faCode,

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/coreml",
"version": "0.1.8",
"version": "0.1.12",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.8",
"version": "0.1.12",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

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

View File

@@ -42,6 +42,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
labels_contents = open(labelsFile, 'r').read()
self.labels = parse_label_contents(labels_contents)
self.loop = asyncio.get_event_loop()
self.minThreshold = .2
# width, height, channels
def get_input_details(self) -> Tuple[int, int, int]:
@@ -53,9 +54,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
# run in executor if this is the plugin loop
if asyncio.get_event_loop() is self.loop:
out_dict = await asyncio.get_event_loop().run_in_executor(predictExecutor, lambda: self.model.predict({'image': input, 'confidenceThreshold': .2 }))
out_dict = await asyncio.get_event_loop().run_in_executor(predictExecutor, lambda: self.model.predict({'image': input, 'confidenceThreshold': self.minThreshold }))
else:
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
out_dict = self.model.predict({'image': input, 'confidenceThreshold': self.minThreshold })
coordinatesList = out_dict['coordinates']
@@ -65,7 +66,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
values = confidenceList
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
maxConfidence = confidenceList[maxConfidenceIndex]
if maxConfidence < .2:
if maxConfidence < self.minThreshold:
continue
coordinates = coordinatesList[index]
@@ -90,6 +91,5 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
))
objs.append(obj)
allowList = settings.get('allowList', None) if settings else None
ret = self.create_detection_result(objs, src_size, allowList, cvss)
ret = self.create_detection_result(objs, src_size, cvss)
return ret

View File

@@ -2,5 +2,3 @@
out/
node_modules/
dist/
.venv
all_models*

View File

@@ -7,9 +7,5 @@ src
.vscode
dist/*.js
dist/*.txt
__pycache__
all_models
sort_oh
download_models.sh
tsconfig.json
.venv
HAP-NodeJS
.gitmodules

23
plugins/doorbird/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Scrypted Debugger",
"address": "${config:scrypted.debugHost}",
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",
"sourceMaps": true,
"localRoot": "${workspaceFolder}/out",
"remoteRoot": "/plugin/",
"type": "node"
}
]
}

View File

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

View File

@@ -0,0 +1,9 @@
# Doorbird Plugin for Scrypted
The Doorbird Plugin bridges compatible Doorbird video doorbell cameras to Scrypted.
# Notes
* Make sure that the user you want to use for the Doorbird plugin login has the API access rights.
* Doorbrid cameras are quite limited in terms of maximum number of concurrent streams. Keep this in mind if you are also using other software with the Doorbird station. You have the possibility to override the internally used RTSP URL and provide another RTSP server which provides the video stream.
* The doorbird mobile apps always have precedence over the public LAN API. So when somebody uses the Doorbird app to talk to the Doorbird station, the streams will be interrupted.
* The doorbird camera just provides JPEG snapshots with VGA resolution. You can use the scrypted snapshot plugin to get a snapshot from the higher resolution video stream. Just set the option in the snapshot plugin to "enabled".

264
plugins/doorbird/package-lock.json generated Normal file
View File

@@ -0,0 +1,264 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/doorbird",
"version": "0.0.1",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"doorbird": "^2.1.2"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.15.11"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"dev": true,
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.97",
"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",
"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"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"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"
}
},
"node_modules/@koush/axios-digest-auth": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
"dependencies": {
"auth-header": "^1.0.0",
"axios": "^0.21.4"
}
},
"node_modules/@koush/axios-digest-auth/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
"link": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"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
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/auth-header": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
},
"node_modules/axios": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/chacha-js": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/chacha-js/-/chacha-js-2.1.1.tgz",
"integrity": "sha512-0ySdjUv/oUkr2cjCo00CNil8Y9f39nm5/3pCgc6hO3X7LvMLBnmugQ5WZ+3Z2SwP9jX7oMIjU3m6p23thtMnHA==",
"dependencies": {
"inherits": "^2.0.1",
"readable-stream": "^1.0.33"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/doorbird": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/doorbird/-/doorbird-2.1.2.tgz",
"integrity": "sha512-ivwwsS/nOslDnuLg3UB60Axo76w5LQuZ67mCPEeWFr5+HbGYRL7PCY3iLjWYaIakh5+IvZyFPHKR4yHAvAc1WQ==",
"dependencies": {
"axios": "^1.2.1",
"chacha-js": "^2.1.1",
"libsodium-wrappers-sumo": "^0.7.11"
}
},
"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==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"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=="
},
"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==",
"dependencies": {
"libsodium-sumo": "^0.7.11"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
}
}
}

View File

@@ -0,0 +1,45 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.1",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
"build": "scrypted-webpack",
"prepublishOnly": "cross-env NODE_ENV=production scrypted-webpack",
"prescrypted-vscode-launch": "scrypted-webpack",
"scrypted-vscode-launch": "scrypted-deploy-debug",
"scrypted-deploy-debug": "scrypted-deploy-debug",
"scrypted-debug": "scrypted-debug",
"scrypted-deploy": "scrypted-deploy",
"scrypted-readme": "scrypted-readme",
"scrypted-package-json": "scrypted-package-json"
},
"keywords": [
"scrypted",
"plugin",
"doorbird"
],
"scrypted": {
"name": "Doorbird Plugin",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"DeviceCreator",
"Settings"
],
"pluginDependencies": [
"@scrypted/prebuffer-mixin",
"@scrypted/pam-diff",
"@scrypted/snapshot"
]
},
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"doorbird": "^2.1.2"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.15.11"
}
}

View File

@@ -0,0 +1,92 @@
import Doorbird, { DoorbirdUdpSocket, Scheme, Response, DoorbirdInfoBHA } from 'doorbird';
export interface ApiRingEvent {
event: string;
timestamp: Date;
}
export interface ApiMotionEvent {
timestamp: Date;
}
export type ApiRingCallback = (event: ApiRingEvent) => void;
export type ApiMotionCallback = (event: ApiMotionEvent) => void;
export class DoorbirdAPI {
private console?: Console
private doorbird: Doorbird;
private doorbirdUdpSocket: DoorbirdUdpSocket;
private ringCallback: ApiRingCallback;
private motionCallback: ApiMotionCallback;
private intercomId: String;
constructor(host: string, username: string, password: string, console?: Console) {
this.console = console;
this.doorbird = new Doorbird({
scheme: Scheme.http,
host: host,
username: username,
password: password
});
this.intercomId = username.substring(0, 6);
this.console?.log("Doorbird: Our intercomId is: ", this.intercomId);
}
startEventSocket() {
this.console?.log("Doorbird: starting event socket listening...");
// initialize dgram UDP socket where Doorbird stations broadcast their event info
this.doorbirdUdpSocket = this.doorbird.startUdpSocket(6524); // 6524 or 35344 - both shall contain the same payload
// register a listener for ring events
this.doorbirdUdpSocket.registerRingListener(ringEvent => {
this.console?.log("Doorbird: Event from IntercomId:", ringEvent.intercomId);
// Make sure that we only call this if the intercom ID matches our desired one
if (ringEvent.intercomId === this.intercomId) {
this.ringCallback({
event: ringEvent.event,
timestamp: ringEvent.timestamp
});
}
});
// register a listener for motion events
this.doorbirdUdpSocket.registerMotionListener(motionEvent => {
this.console?.log("Doorbird: Event from IntercomId:", motionEvent.intercomId);
// Make sure that we only call this if the intercom ID matches our desired one
if (motionEvent.intercomId === this.intercomId) {
this.motionCallback({
timestamp: motionEvent.timestamp
});
}
});
}
stopEventSocket() {
this.console?.log("Doorbird: stopping event socket listening...");
this.doorbirdUdpSocket.close();
}
registerRingCallback(ringCallback: ApiRingCallback) {
this.ringCallback = ringCallback;
}
registerMotionCallback(motionCallback: ApiMotionCallback) {
this.motionCallback = motionCallback;
}
async getImage(): Promise<Buffer> {
this.console?.log("Doorbird: getting JPEG image...");
return this.doorbird.getImage();
}
async getInfo(): Promise<any> {
const dbInfo = await this.doorbird.getInfo();
return {
deviceType: dbInfo.BHA.VERSION[0]['DEVICE-TYPE'],
firmwareVersion: dbInfo.BHA.VERSION[0].FIRMWARE,
buildNumber: dbInfo.BHA.VERSION[0].BUILD_NUMBER,
serialNumber: dbInfo.BHA.VERSION[0].WIFI_MAC_ADDR,
}
}
}

View File

@@ -0,0 +1,565 @@
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 AxiosDigestAuth from '@koush/axios-digest-auth';
import { readLength } from "@scrypted/common/src/read-stream";
import { ApiRingEvent, ApiMotionEvent, DoorbirdAPI } from "./doorbird-api";
const { deviceManager, mediaManager } = sdk;
class DoorbirdCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCamera, Settings, BinarySensor, MotionSensor {
doorbirdApi: DoorbirdAPI | undefined;
binarySensorTimeout: NodeJS.Timeout;
motionSensorTimeout: NodeJS.Timeout;
doorbellAudioActive: boolean;
audioTXProcess: ChildProcess;
audioRXProcess: ChildProcess;
audioSilenceProcess: ChildProcess;
audioRXClientSocket: net.Socket;
pendingPicture: Promise<MediaObject>;
constructor(nativeId: string, public provider: DoorbirdCamProvider) {
super(nativeId);
this.binaryState = false;
this.doorbellAudioActive = false;
this.updateDeviceInfo();
}
getDoorbirdApi() {
const ip = this.storage.getItem('ip');
if (!ip)
return undefined;
if (!this.doorbirdApi) {
this.doorbirdApi = new DoorbirdAPI(this.getIPAddress(), this.getUsername(), this.getPassword(), this.console);
this.getDoorbirdApi()?.registerRingCallback((event: ApiRingEvent) => {
this.console?.log("Ring event");
this.console?.log("Event:", event.event);
this.console?.log("Time:", event.timestamp);
this.triggerBinarySensor();
});
this.getDoorbirdApi()?.registerMotionCallback((event: ApiMotionEvent) => {
this.console?.log("Motion event");
this.console?.log("Time:", event.timestamp);
this.triggerMotionSensor();
});
this.getDoorbirdApi()?.startEventSocket();
}
return this.doorbirdApi;
}
async updateDeviceInfo(): Promise<void> {
const ip = this.storage.getItem('ip');
if (!ip)
return;
const deviceInfo: DeviceInformation = {
...this.info,
ip
};
const response = await this.getDoorbirdApi()?.getInfo();
deviceInfo.firmware = response.firmwareVersion + '-' + response.buildNumber;
this.info = deviceInfo;
}
async takePicture(option?: PictureOptions): Promise<MediaObject> {
if (!this.pendingPicture) {
this.pendingPicture = this.takePictureThrottled(option);
this.pendingPicture.finally(() => this.pendingPicture = undefined);
}
return this.pendingPicture;
}
async takePictureThrottled(option?: PictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getDoorbirdApi().getImage(), 'image/jpeg');
}
// Unfortunately, the Doorbird public API only offers JPEG snapshots with VGA resolution.
// Recommendation: use the snapshot plugin to get snapshots with maximum resolution.
public async getPictureOptions(): Promise<PictureOptions[]> {
return [{
id: 'VGA',
picture: { width: 640, height: 480 }
}];
}
public async putSetting(key: string, value: string | number | boolean) {
this.doorbirdApi?.stopEventSocket();
this.doorbirdApi = undefined;
this.storage.setItem(key, value.toString());
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
this.provider.updateDevice(this.nativeId, this.name);
}
async getSettings(): Promise<Setting[]> {
return [
{
key: 'username',
title: 'Username',
value: this.storage.getItem('username'),
description: 'Required: Username for Doorbird HTTP API.',
},
{
key: 'password',
title: 'Password',
value: this.storage.getItem('password'),
type: 'password',
description: 'Required: Password for Doorbird HTTP API.',
},
{
key: 'ip',
title: 'IP Address',
placeholder: '192.168.1.100',
value: this.storage.getItem('ip'),
description: 'Required: IP address of the Doorbird station.',
},
{
key: 'httpPort',
subgroup: 'Advanced',
title: 'HTTP Port Override',
placeholder: '80',
value: this.storage.getItem('httpPort'),
description: 'Use this if you have some network firewall rules which change the HTTP port of the camera HTTP port.',
},
{
key: 'rtspUrl',
subgroup: 'Advanced',
title: 'RTSP URL Override',
placeholder: 'rtsp://192.168.2.100/my_doorbird_video_stream',
value: this.storage.getItem('rtspUrl'),
description: 'Use this in case you are already using another RTSP server/proxy (e.g. mediamtx, go2rtc, etc.) to limit the number of streams from the camera.',
}
];
}
// When the intercom is started, we also start the audio receiver which receives audio fro the doorbird microphone.
// This audio is then fed into ffmpeg instead of the silent audio from the silence generator.
// We also start another process(audioTXProcess) which sends audio to the doorbird speaker.
async startIntercom(media: MediaObject): Promise<void> {
await this.startAudioReceiver();
await this.startAudioTransmitter(media);
}
async stopIntercom(): Promise<void> {
this.stopAudioTransmitter();
this.stopAudioReceiver();
}
async startAudioTransmitter(media: MediaObject): Promise<void> {
const ffmpegInput: FFmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput)).toString());
const ffmpegArgs = ffmpegInput.inputArguments.slice();
ffmpegArgs.push(
'-vn', '-dn', '-sn',
'-acodec', 'pcm_mulaw',
'-flags', '+global_header',
'-ac', '1',
'-ar', '8k',
'-f', 'mulaw',
'pipe:3'
);
safePrintFFmpegArguments(console, ffmpegArgs);
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), ffmpegArgs, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
this.audioTXProcess = cp;
ffmpegLogInitialOutput(console, cp);
cp.on('exit', () => this.console.log('Doorbird: Audio transmitter ended.'));
cp.stdout.on('data', data => this.console.log(data.toString()));
cp.stderr.on('data', data => this.console.log(data.toString()));
const socket = cp.stdio[3] as Readable;
const username: string = this.getUsername();
const password: string = this.getPassword();
const audioTxUrl: string = `${this.getHttpBaseAddress()}/bha-api/audio-transmit.cgi`;
this.console.log('Doorbird: Starting audio transmitter...');
(async () => {
this.console.log('Doorbird: audio transmitter started.');
const passthrough = new PassThrough();
const digestAuth = new AxiosDigestAuth({
username,
password
});
digestAuth.request({
method: 'POST',
url: audioTxUrl,
headers: {
'Content-Type': 'audio/basic',
'Content-Length': '9999999'
},
data: passthrough,
});
try {
while (true) {
const data = await readLength(socket, 1024);
passthrough.push(data);
}
}
catch (e) {
}
finally {
this.console.log('Doorbird: audio transmitter finished.');
passthrough.end();
}
this.stopAudioTransmitter();
})();
}
stopAudioTransmitter() {
this.audioTXProcess?.kill('SIGKILL');
this.audioTXProcess = undefined;
}
async startAudioReceiver(): Promise<void> {
const audioRxUrl = `${this.getHttpBaseAddress()}/bha-api/audio-receive.cgi`;
this.console.log('Doorbird: Starting audio receiver...');
const ffmpegPath = await mediaManager.getFFmpegPath();
const ffmpegArgs = [
'-hide_banner',
'-nostats',
'-analyzeduration', '0',
'-probesize', '32',
'-re',
'-ar', '8000',
'-ac', '1',
'-f', 'mulaw',
'-i', `${audioRxUrl}`,
'-acodec', 'copy',
'-f', 'mulaw',
'pipe:3'
];
safePrintFFmpegArguments(console, ffmpegArgs);
const cp = child_process.spawn(ffmpegPath, ffmpegArgs, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
this.audioRXProcess = cp;
ffmpegLogInitialOutput(console, cp);
cp.on('exit', () => {
this.console.log('Doorbird: audio receiver ended.')
this.audioRXProcess = undefined;
});
cp.stdout.on('data', data => this.console.log(data.toString()));
cp.stderr.on('data', data => this.console.log(data.toString()));
this.doorbellAudioActive = true;
cp.stdio[3].on('data', data => {
if (this.doorbellAudioActive && this.audioRXClientSocket) {
this.audioRXClientSocket.write(data);
}
});
}
stopAudioReceiver() {
this.doorbellAudioActive = false;
this.audioRXProcess?.kill('SIGKILL');
this.audioRXProcess = undefined;
}
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
return [{
id: 'default',
name: 'default',
container: '', // must be empty to support prebuffering
video: {
codec: 'h264'
},
audio: { /*this.isAudioDisabled() ? null : {}, */
// this is a hint to let homekit, et al, know that it's OPUS audio and does not need transcoding.
codec: 'pcm_mulaw',
}
}]; }
async getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
const port = await this.startAudioRXServer();
const ffmpegInput: FFmpegInput = {
url: undefined,
inputArguments: [
'-analyzeduration', '0',
'-probesize', '32',
'-fflags', 'nobuffer',
'-flags', 'low_delay',
'-f', 'rtsp',
'-rtsp_transport', 'tcp',
'-i', `${this.getRtspAddress()}`,
'-f', 'mulaw',
'-ac', '1',
'-ar', '8000',
'-channel_layout', 'mono',
'-use_wallclock_as_timestamps', 'true',
'-i', `tcp://127.0.0.1:${port}?tcp_nodelay=1`,
],
mediaStreamOptions: options,
};
return mediaManager.createFFmpegMediaObject(ffmpegInput);
}
async startSilenceGenerator() {
if (this.audioSilenceProcess)
return;
this.console.log('Doorbird: starting audio silence generator...')
const ffmpegPath = await mediaManager.getFFmpegPath();
const ffmpegArgs = [
'-hide_banner',
'-nostats',
'-re',
'-f', 'lavfi',
'-i', 'anullsrc=r=8000:cl=mono',
'-f', 'mulaw',
'pipe:3'
];
safePrintFFmpegArguments(console, ffmpegArgs);
const cp = child_process.spawn(ffmpegPath, ffmpegArgs, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
this.audioSilenceProcess = cp;
ffmpegLogInitialOutput(console, cp);
cp.on('exit', () => {
this.console.log('Doorbird: audio silence generator ended.')
this.audioSilenceProcess = undefined;
});
cp.stdout.on('data', data => this.console.log(data.toString()));
cp.stderr.on('data', data => this.console.log(data.toString()));
cp.stdio[3].on('data', data => {
if (!this.doorbellAudioActive && this.audioRXClientSocket) {
this.audioRXClientSocket.write(data);
}
});
}
stopSilenceGenerator() {
this.audioSilenceProcess?.kill();
this.audioSilenceProcess = null;
}
async startAudioRXServer(): Promise<number> {
const server = net.createServer(async (clientSocket) => {
clearTimeout(serverTimeout);
this.audioRXClientSocket = clientSocket;
this.startSilenceGenerator();
this.audioRXClientSocket.on('close', () => {
this.stopSilenceGenerator();
this.audioRXClientSocket = null;
});
});
const serverTimeout = setTimeout(() => {
this.console.log('Doorbird: timed out waiting for tcp client from ffmpeg');
server.close();
}, 30000);
const port = await listenZero(server);
return port;
}
triggerBinarySensor() {
this.binaryState = true;
clearTimeout(this.binarySensorTimeout);
this.binarySensorTimeout = setTimeout(() => this.binaryState = false, 3000);
}
triggerMotionSensor() {
this.motionDetected = true;
clearTimeout(this.motionSensorTimeout);
this.motionSensorTimeout = setTimeout(() => this.motionDetected = false, 3000);
}
setHttpPortOverride(port: string) {
this.storage.setItem('httpPort', port || '');
}
getHttpBaseAddress() {
return `http://${this.getUsername()}:${this.getPassword()}@${this.getIPAddress()}:${this.storage.getItem('httpPort') || 80}`;
}
getRtspAddress() {
if (this.storage.getItem('rtspUrl') !== undefined) {
return this.storage.getItem('rtspUrl');
}
else {
return this.getRtspDefaultAddress();
}
}
getRtspDefaultAddress() {
return `rtsp://${this.getUsername()}:${this.getPassword()}@${this.getIPAddress()}/mpeg/media.amp`;
}
getIPAddress() {
return this.storage.getItem('ip');
}
setIPAddress(ip: string) {
return this.storage.setItem('ip', ip);
}
getUsername() {
return this.storage.getItem('username');
}
getPassword() {
return this.storage.getItem('password');
}
}
export class DoorbirdCamProvider extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
devices = new Map<string, any>();
constructor(nativeId?: string) {
super(nativeId);
for (const camId of deviceManager.getNativeIds()) {
if (camId)
this.getDevice(camId);
}
}
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
let info: DeviceInformation = {};
const host = settings.ip?.toString();
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
if (!skipValidate) {
const api = new DoorbirdAPI(host, username, password, this.console);
try {
const deviceInfo = await api.getInfo();
settings.newCamera = deviceInfo.deviceType;
info.model = deviceInfo.deviceType;
info.serialNumber = deviceInfo.serialNumber;
info.mac = deviceInfo.serialNumber;
info.manufacturer = 'Bird Home Automation GmbH';
info.managementUrl = 'https://webadmin.doorbird.com';
}
catch (e) {
this.console.error('Error adding Doorbird camera', e);
throw e;
}
}
settings.newCamera ||= 'Doorbird Camera';
nativeId ||= randomBytes(4).toString('hex');
const name = settings.newCamera?.toString();
await this.updateDevice(nativeId, name);
const device = await this.getDevice(nativeId) as DoorbirdCamera;
device.info = info;
device.putSetting('username', username);
device.putSetting('password', password);
device.setIPAddress(settings.ip.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
return nativeId;
}
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
key: 'username',
title: 'Username',
},
{
key: 'password',
title: 'Password',
type: 'password',
},
{
key: 'ip',
title: 'IP Address',
placeholder: '192.168.2.222',
},
{
key: 'httpPort',
title: 'HTTP Port',
description: 'Optional: Override the HTTP Port from the default value of 80',
placeholder: '80',
},
{
key: 'skipValidate',
title: 'Skip Validation',
description: 'Add the device without verifying the credentials and network settings.',
type: 'boolean',
}
]
}
updateDevice(nativeId: string, name: string) {
return deviceManager.onDeviceDiscovered({
nativeId,
name,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.VideoCamera,
ScryptedInterface.Settings,
ScryptedInterface.Intercom,
ScryptedInterface.BinarySensor,
ScryptedInterface.MotionSensor
],
type: ScryptedDeviceType.Doorbell,
info: deviceManager.getNativeIds().includes(nativeId) ? deviceManager.getDeviceState(nativeId)?.info : undefined,
});
}
getDevice(nativeId: string) {
let ret = this.devices.get(nativeId);
if (!ret) {
ret = this.createCamera(nativeId);
if (ret)
this.devices.set(nativeId, ret);
}
return ret;
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
if( this.devices.delete( nativeId ) ) {
this.console.log("Doorbird: Removed device from list: " + id + " / " + nativeId )
}
}
createCamera(nativeId: string): DoorbirdCamera {
return new DoorbirdCamera(nativeId, this);
}
}
export default new DoorbirdCamProvider();

View File

@@ -1,8 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2021",
"resolveJsonModule": true,
"target": "esnext",
"moduleResolution": "Node16",
"esModuleInterop": true,
"sourceMap": true

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/ffmpeg-camera",
"version": "0.0.20",
"version": "0.0.21",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ffmpeg-camera",
"version": "0.0.20",
"version": "0.0.21",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
@@ -36,7 +36,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.86",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/ffmpeg-camera",
"version": "0.0.20",
"version": "0.0.21",
"description": "FFmpeg Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -144,7 +144,7 @@ export abstract class CameraBase<T extends ResponseMediaStreamOptions> extends S
if (key === 'defaultStream') {
const vsos = await this.getVideoStreamOptions();
const stream = vsos.find(vso => vso.name === value);
this.storage.setItem('defaultStream', stream?.id);
this.storage.setItem('defaultStream', stream?.id || '');
}
else {
this.storage.setItem(key, value.toString());

View File

@@ -1,13 +1,12 @@
{
"name": "@scrypted/gstreamer-camera",
"version": "0.0.3",
"version": "0.0.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/gstreamer-camera",
"version": "0.0.3",
"hasInstallScript": true,
"version": "0.0.5",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
@@ -37,39 +36,40 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.199",
"version": "0.2.86",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
"typedoc": "^0.23.21"
}
},
"../sdk": {
@@ -141,9 +141,9 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"node_modules/url-parse": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
@@ -174,23 +174,24 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@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": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"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.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
@@ -229,9 +230,9 @@
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"url-parse": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"requires": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/gstreamer-camera",
"version": "0.0.3",
"version": "0.0.5",
"description": "GStreamer Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,4 +1,4 @@
import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, ScryptedDeviceType, VideoCamera, MediaObject, MediaStreamOptions, ScryptedInterface, FFmpegInput, Camera, PictureOptions, SettingValue, DeviceCreator, DeviceCreatorSettings } from "@scrypted/sdk";
import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, ScryptedDeviceType, VideoCamera, MediaObject, MediaStreamOptions, ScryptedInterface, FFmpegInput, Camera, PictureOptions, SettingValue, DeviceCreator, DeviceCreatorSettings, ResponseMediaStreamOptions } from "@scrypted/sdk";
import { recommendRebroadcast } from "./recommend";
import AxiosDigestAuth from '@koush/axios-digest-auth';
import https from 'https';
@@ -14,7 +14,7 @@ export interface UrlMediaStreamOptions extends MediaStreamOptions {
url: string;
}
export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedDeviceBase implements Camera, VideoCamera, Settings {
export abstract class CameraBase<T extends ResponseMediaStreamOptions> extends ScryptedDeviceBase implements Camera, VideoCamera, Settings {
snapshotAuth: AxiosDigestAuth;
pendingPicture: Promise<MediaObject>;
@@ -194,7 +194,7 @@ export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedD
if (key === 'defaultStream') {
const vsos = await this.getVideoStreamOptions();
const stream = vsos.find(vso => vso.name === value);
this.storage.setItem('defaultStream', stream?.id);
this.storage.setItem('defaultStream', stream?.id || '');
}
else {
this.storage.setItem(key, value.toString());
@@ -220,7 +220,7 @@ export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedD
}
}
export abstract class CameraProviderBase<T extends MediaStreamOptions> extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
devices = new Map<string, any>();
constructor(nativeId?: string) {
@@ -234,6 +234,9 @@ export abstract class CameraProviderBase<T extends MediaStreamOptions> extends S
recommendRebroadcast();
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const nativeId = randomBytes(4).toString('hex');
const name = settings.newCamera.toString();

View File

@@ -1,4 +1,4 @@
import sdk, { FFmpegInput, MediaObject, MediaStreamOptions, Setting, SettingValue } from "@scrypted/sdk";
import sdk, { FFmpegInput, MediaObject, MediaStreamOptions, ResponseMediaStreamOptions, Setting, SettingValue } from "@scrypted/sdk";
import child_process, { ChildProcess } from "child_process";
import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "./common";
// import {} from "../../../common/src/stream-parser"
@@ -8,10 +8,10 @@ import { listenZero } from "../../../common/src/listen-cluster"
const { log, deviceManager, mediaManager } = sdk;
class GStreamerCamera extends CameraBase<MediaStreamOptions> {
class GStreamerCamera extends CameraBase<ResponseMediaStreamOptions> {
currentProcess: ChildProcess;
createGStreamerMediaStreamOptions(gstreamerInput: string, index: number): MediaStreamOptions {
createGStreamerMediaStreamOptions(gstreamerInput: string, index: number): ResponseMediaStreamOptions {
return {
id: `channel${index}`,
name: `Stream ${index + 1}`,
@@ -32,7 +32,7 @@ class GStreamerCamera extends CameraBase<MediaStreamOptions> {
return gstreamerInputs;
}
getRawVideoStreamOptions(): MediaStreamOptions[] {
getRawVideoStreamOptions(): ResponseMediaStreamOptions[] {
const gstreamerInputs = this.getGStreamerInputs();
// filter out empty strings.
@@ -86,7 +86,7 @@ class GStreamerCamera extends CameraBase<MediaStreamOptions> {
];
}
async createVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
async createVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
const index = this.getRawVideoStreamOptions()?.findIndex(vso => vso.id === options.id);
const gstreamerInputs = this.getGStreamerInputs();
const gstreamerInput = gstreamerInputs[index];
@@ -147,7 +147,7 @@ class GStreamerCamera extends CameraBase<MediaStreamOptions> {
}
class GStreamerProvider extends CameraProviderBase<MediaStreamOptions> {
class GStreamerProvider extends CameraProviderBase<ResponseMediaStreamOptions> {
createCamera(nativeId: string): GStreamerCamera {
return new GStreamerCamera(nativeId, this);
}

View File

@@ -41,7 +41,7 @@ The Channel number is the hundreds digit and (sub-)stream is ones digit:
# Troubleshooting
## General
* Not receiving motion alerts in the device's Scrypted event log? Check all of the following: **(1)** device has a motion detection grid drawn and enabled, **(2)** user or group access permissions of account used for device **(3)** do not use self-signed certs for HTTPS on the device, and **(4)** `CGI` and `ISAPI` integration protocol/service on device is enabled.
* Not receiving motion alerts in the device's Scrypted event log? Check all of the following: **(1)** device has a motion detection grid drawn and enabled, **(2)** user or group access permissions of account used for device **(3)** do not use self-signed certs for HTTPS on the device, **(4)** `CGI` and `ISAPI` integration protocol/service on device is enabled, and **(5)** that the authentication method on the device is set to "digest".
* If device has HTTPS enabled, try disabling HTTPS on the device to see if that resolves issue (do not use self-signed certs).
* If device has enabled user lockout, max connections, concurrent requests, etc., try disabling and/or increasing to max allowed for troubleshooting.
* Does your account (`Username`) have proper user and/or group permissions? Try granting all permissions for testing.

View File

@@ -1,28 +1,24 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.124",
"version": "0.0.126",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/hikvision",
"version": "0.0.124",
"version": "0.0.126",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/highland": "^2.12.14",
"@types/lodash": "^4.14.172",
"@types/multiparty": "^0.0.33",
"@types/node": "^16.9.1",
"@types/xml2js": "^0.4.9",
"axios": "^0.23.0",
"highland": "^2.13.5",
"lodash": "^4.17.21",
"multiparty": "^4.2.2",
"net-keepalive": "^3.0.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@types/node": "^18.15.11"
}
},
"../../common": {
@@ -42,7 +38,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.87",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -105,31 +101,10 @@
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/highland": {
"version": "2.12.14",
"resolved": "https://registry.npmjs.org/@types/highland/-/highland-2.12.14.tgz",
"integrity": "sha512-afgFIPeRlysJjWAVmtxqt1nfRo29fjXwooX/MEc+GVlXKMiSsFOryY8hma1PNnjNjOI01Qe37/z5n3WGBk5WCg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.172",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz",
"integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw=="
},
"node_modules/@types/multiparty": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz",
"integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"node_modules/@types/xml2js": {
"version": "0.4.9",
@@ -152,60 +127,6 @@
"follow-redirects": "^1.14.4"
}
},
"node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ffi-napi": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"hasInstallScript": true,
"dependencies": {
"debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1",
"ref-napi": "^2.0.1 || ^3.0.2",
"ref-struct-di": "^1.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ffi-napi/node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/ffi-napi/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
@@ -225,210 +146,16 @@
}
}
},
"node_modules/get-symbol-from-current-process-h": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
},
"node_modules/get-uv-event-loop-napi-h": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"dependencies": {
"get-symbol-from-current-process-h": "^1.0.1"
}
},
"node_modules/highland": {
"version": "2.13.5",
"resolved": "https://registry.npmjs.org/highland/-/highland-2.13.5.tgz",
"integrity": "sha512-dn2flPapIIAa4BtkB2ahjshg8iSJtrJtdhEb9/oiOrS5HMQTR/GuhFpqJ+11YBdtnl3AwWKvbZd1Uxr8uAmA7A==",
"dependencies": {
"util-deprecate": "^1.0.2"
}
},
"node_modules/http-errors": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/multiparty": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
"dependencies": {
"http-errors": "~1.8.0",
"safe-buffer": "5.2.1",
"uid-safe": "2.1.5"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/net-keepalive": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-3.0.0.tgz",
"integrity": "sha512-wfDa7VPeSltY5aIQcujS7AiWnO2JHJCpO3is4nwQ7kFYs4YMpzDNMwiuILPkWwgMbPMSHzO7O1tuL8rC0SP3ag==",
"dependencies": {
"ffi-napi": "^4.0.1",
"ref-napi": "^3.0.0"
},
"engines": {
"node": ">=10.20.0"
}
},
"node_modules/node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node_modules/node-gyp-build": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ref-napi": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"hasInstallScript": true,
"dependencies": {
"debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1"
},
"engines": {
"node": ">= 10.0"
}
},
"node_modules/ref-napi/node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/ref-napi/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/ref-struct-di": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"dependencies": {
"debug": "^3.1.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
@@ -505,31 +232,10 @@
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@types/highland": {
"version": "2.12.14",
"resolved": "https://registry.npmjs.org/@types/highland/-/highland-2.12.14.tgz",
"integrity": "sha512-afgFIPeRlysJjWAVmtxqt1nfRo29fjXwooX/MEc+GVlXKMiSsFOryY8hma1PNnjNjOI01Qe37/z5n3WGBk5WCg==",
"requires": {
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.172",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz",
"integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw=="
},
"@types/multiparty": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz",
"integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"@types/xml2js": {
"version": "0.4.9",
@@ -552,206 +258,21 @@
"follow-redirects": "^1.14.4"
}
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"ffi-napi": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/ffi-napi/-/ffi-napi-4.0.3.tgz",
"integrity": "sha512-PMdLCIvDY9mS32RxZ0XGb95sonPRal8aqRhLbeEtWKZTe2A87qRFG9HjOhvG8EX2UmQw5XNRMIOT+1MYlWmdeg==",
"requires": {
"debug": "^4.1.1",
"get-uv-event-loop-napi-h": "^1.0.5",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1",
"ref-napi": "^2.0.1 || ^3.0.2",
"ref-struct-di": "^1.1.0"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"get-symbol-from-current-process-h": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-from-current-process-h/-/get-symbol-from-current-process-h-1.0.2.tgz",
"integrity": "sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw=="
},
"get-uv-event-loop-napi-h": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/get-uv-event-loop-napi-h/-/get-uv-event-loop-napi-h-1.0.6.tgz",
"integrity": "sha512-t5c9VNR84nRoF+eLiz6wFrEp1SE2Acg0wS+Ysa2zF0eROes+LzOfuTaVHxGy8AbS8rq7FHEJzjnCZo1BupwdJg==",
"requires": {
"get-symbol-from-current-process-h": "^1.0.1"
}
},
"highland": {
"version": "2.13.5",
"resolved": "https://registry.npmjs.org/highland/-/highland-2.13.5.tgz",
"integrity": "sha512-dn2flPapIIAa4BtkB2ahjshg8iSJtrJtdhEb9/oiOrS5HMQTR/GuhFpqJ+11YBdtnl3AwWKvbZd1Uxr8uAmA7A==",
"requires": {
"util-deprecate": "^1.0.2"
}
},
"http-errors": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multiparty": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
"requires": {
"http-errors": "~1.8.0",
"safe-buffer": "5.2.1",
"uid-safe": "2.1.5"
}
},
"net-keepalive": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/net-keepalive/-/net-keepalive-3.0.0.tgz",
"integrity": "sha512-wfDa7VPeSltY5aIQcujS7AiWnO2JHJCpO3is4nwQ7kFYs4YMpzDNMwiuILPkWwgMbPMSHzO7O1tuL8rC0SP3ag==",
"requires": {
"ffi-napi": "^4.0.1",
"ref-napi": "^3.0.0"
}
},
"node-addon-api": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
"integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A=="
},
"node-gyp-build": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz",
"integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"ref-napi": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/ref-napi/-/ref-napi-3.0.3.tgz",
"integrity": "sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==",
"requires": {
"debug": "^4.1.1",
"get-symbol-from-current-process-h": "^1.0.2",
"node-addon-api": "^3.0.0",
"node-gyp-build": "^4.2.1"
},
"dependencies": {
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"ref-struct-di": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ref-struct-di/-/ref-struct-di-1.1.1.tgz",
"integrity": "sha512-2Xyn/0Qgz89VT+++WP0sTosdm9oeowLP23wRJYhG4BFdMUrLj3jhwHZNEytYNYgtPKLNTP3KJX4HEgBvM1/Y2g==",
"requires": {
"debug": "^3.1.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.124",
"version": "0.0.126",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -38,10 +38,12 @@
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.9.1",
"@types/xml2js": "^0.4.9",
"axios": "^0.23.0",
"lodash": "^4.17.21",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@types/node": "^18.15.11"
}
}

View File

@@ -8,6 +8,8 @@ import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
import { hikvisionHttpsAgent } from './probe';
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
const { mediaManager } = sdk;
@@ -21,8 +23,8 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
detectedChannels: Promise<Map<string, MediaStreamOptions>>;
client: HikvisionCameraAPI;
onvifIntercom = new OnvifIntercom(this);
cp: ChildProcess;
activeIntercom: Awaited<ReturnType<typeof startRtpForwarderProcess>>;
constructor(nativeId: string, provider: RtspProvider) {
super(nativeId, provider);
@@ -360,13 +362,11 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
async startIntercom(media: MediaObject): Promise<void> {
if (this.storage.getItem('twoWayAudio') === 'ONVIF') {
this.activeIntercom?.kill();
this.activeIntercom = undefined;
const options = await this.getConstructedVideoStreamOptions();
const stream = options[0];
const url = new URL(stream.url);
// amcrest onvif requires this proto query parameter, or onvif two way
// will not activate.
url.searchParams.set('proto', 'Onvif');
this.onvifIntercom.url = url.toString();
this.onvifIntercom.url = stream.url;
return this.onvifIntercom.startIntercom(media);
}
@@ -390,7 +390,7 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
}
}
catch (e) {
this.console.error('Fialure while determining two way audio codec', e);
this.console.error('Failure while determining two way audio codec', e);
}
if (codec === 'G.711ulaw') {
@@ -415,76 +415,64 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
const args = ffmpegInput.inputArguments.slice();
args.unshift('-hide_banner');
args.push(
"-vn",
'-ar', '8000',
'-ac', '1',
'-acodec', codec,
'-f', format,
'pipe:3',
);
this.console.log('ffmpeg intercom', args);
const ffmpeg = await mediaManager.getFFmpegPath();
this.cp = child_process.spawn(ffmpeg, args, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
const passthrough = new PassThrough();
const open = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/open`;
const { data } = await this.getClient().digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: 'PUT',
url: open,
});
this.cp.on('exit', () => this.cp = undefined);
ffmpegLogInitialOutput(this.console, this.cp);
const socket = this.cp.stdio[3] as Readable;
this.console.log('two way audio opened', data);
(async () => {
const passthrough = new PassThrough();
const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/audioData`;
this.console.log('posting audio data to', url);
try {
const open = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/open`;
const { data } = await this.getClient().digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: 'PUT',
url: open,
});
this.console.log('two way audio opened', data);
const put = this.getClient().digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: 'PUT',
url,
headers: {
'Content-Type': 'application/octet-stream',
// 'Connection': 'close',
'Content-Length': '0'
},
data: passthrough,
});
const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/audioData`;
this.console.log('posting audio data to', url);
// seems the dahua doorbells preferred 1024 chunks. should investigate adts
// parsing and sending multipart chunks instead.
this.getClient().digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: 'PUT',
url,
headers: {
'Content-Type': 'application/octet-stream',
// 'Connection': 'close',
'Content-Length': '0'
},
data: passthrough,
});
while (true) {
const data = await readLength(socket, 1024);
passthrough.push(data);
}
}
catch (e) {
}
finally {
this.console.log('audio finished');
passthrough.end();
let available = Buffer.alloc(0);
this.activeIntercom?.kill();
const forwarder = this.activeIntercom = await startRtpForwarderProcess(this.console, ffmpegInput, {
audio: {
onRtp: rtp => {
const parsed = RtpPacket.deSerialize(rtp);
available = Buffer.concat([available, parsed.payload]);
if (available.length > 1024) {
passthrough.push(available.subarray(0, 1024));
available = available.subarray(1024);
}
},
codecCopy: codec,
encoderArguments: [
'-ar', '8000',
'-ac', '1',
'-acodec', codec,
]
}
});
forwarder.killPromise.finally(() => {
this.console.log('audio finished');
passthrough.end();
this.stopIntercom();
})();
});
put.finally(() => forwarder.kill());
}
async stopIntercom(): Promise<void> {
this.activeIntercom?.kill();
this.activeIntercom = undefined;
if (this.storage.getItem('twoWayAudio') === 'ONVIF') {
return this.onvifIntercom.stopIntercom();
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/homekit",
"version": "1.2.22",
"version": "1.2.23",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/homekit",
"version": "1.2.22",
"version": "1.2.23",
"dependencies": {
"@koush/werift-src": "file:../../external/werift",
"check-disk-space": "^3.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/homekit",
"version": "1.2.22",
"version": "1.2.23",
"description": "HomeKit Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",

View File

@@ -192,7 +192,7 @@ The latest troubleshooting guide for all known streaming or recording issues can
this.storage.setItem(key, JSON.stringify(value));
}
else {
this.storage.setItem(key, value?.toString());
this.storage.setItem(key, value?.toString() || '');
}
if (key === 'detectAudio' || key === 'linkedMotionSensor' || key === 'objectDetectionContactSensors') {

View File

@@ -459,6 +459,10 @@ export class H264Repacketizer {
if (this.shouldFilter(nalType)) {
return false;
}
if (nalType === NAL_TYPE_SPS)
this.updateSps(payload);
if (nalType === NAL_TYPE_PPS)
this.updatePps(payload);
return true;
});
if (depacketized.length === 0) {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.121",
"version": "0.0.132",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",
@@ -35,18 +35,16 @@
"name": "Video Analysis Plugin",
"type": "API",
"interfaces": [
"DeviceProvider",
"Settings",
"MixinProvider",
"DeviceProvider"
"MixinProvider"
],
"realfs": true,
"pluginDependencies": [
"@scrypted/python-codecs"
]
},
"optionalDependencies": {
"sharp": "^0.31.3"
},
"optionalDependencies": {},
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
@@ -58,7 +56,6 @@
"devDependencies": {
"@types/lodash": "^4.14.175",
"@types/node": "^14.17.11",
"@types/semver": "^7.3.13",
"@types/sharp": "^0.31.1"
"@types/semver": "^7.3.13"
}
}

View File

@@ -1,92 +0,0 @@
export class DenoisedDetectionEntry<T> {
id?: string;
boundingBox?: [number, number, number, number];
name: string;
score: number;
detection: T;
firstSeen?: number;
firstBox?: [number, number, number, number];
lastSeen?: number;
lastBox?: [number, number, number, number];
durationGone?: number;
}
export interface DenoisedDetectionOptions<T> {
added?: (detection: DenoisedDetectionEntry<T>) => void;
removed?: (detection: DenoisedDetectionEntry<T>) => void;
retained?: (detection: DenoisedDetectionEntry<T>, previous: DenoisedDetectionEntry<T>) => void;
untracked?: (detection: DenoisedDetectionOptions<T>) => void,
expiring?: (previous: DenoisedDetectionEntry<T>) => void;
timeout?: number;
now?: number;
}
export interface DenoisedDetectionState<T> {
previousDetections?: DenoisedDetectionEntry<T>[];
frameCount?: number;
lastDetection?: number;
// id to time
externallyTracked?: Map<string, DenoisedDetectionEntry<T>>;
}
export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
currentDetections: DenoisedDetectionEntry<T>[],
options?: DenoisedDetectionOptions<T>
) {
if (!state.previousDetections)
state.previousDetections = [];
const now = options.now || Date.now();
const lastDetection = state.lastDetection || now;
const sinceLastDetection = now - lastDetection;
if (!state.externallyTracked)
state.externallyTracked = new Map();
for (const tracked of currentDetections) {
tracked.durationGone = 0;
tracked.lastSeen = now;
tracked.lastBox = tracked.boundingBox;
if (!tracked.id) {
const id = tracked.id = `untracked-${tracked.name}`;
if (!state.externallyTracked.get(id)) {
// crappy track untracked objects for 1 minute.
setTimeout(() => state.externallyTracked.delete(id), 60000);
}
}
let previous = state.externallyTracked.get(tracked.id);
if (previous) {
state.externallyTracked.delete(tracked.id);
tracked.firstSeen = previous.firstSeen;
tracked.firstBox = previous.firstBox;
previous.durationGone = 0;
previous.lastSeen = now;
previous.lastBox = tracked.boundingBox;
options?.retained(tracked, previous);
}
else {
tracked.firstSeen = now;
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
options?.added(tracked);
}
}
for (const previous of state.externallyTracked.values()) {
if (now - previous.lastSeen) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
options?.expiring(previous);
}
}
}
for (const tracked of currentDetections) {
state.externallyTracked.set(tracked.id, tracked);
}
}

View File

@@ -0,0 +1,169 @@
import { Deferred } from "@scrypted/common/src/deferred";
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
import { readLength, readLine } from "@scrypted/common/src/read-stream";
import sdk, { FFmpegInput, Image, ImageFormat, ImageOptions, MediaObject, ScryptedDeviceBase, ScryptedMimeTypes, VideoFrame, VideoFrameGenerator, VideoFrameGeneratorOptions } from "@scrypted/sdk";
import child_process from 'child_process';
import { Readable } from 'stream';
interface RawFrame {
width: number;
height: number;
data: Buffer;
}
async function createRawImageMediaObject(image: RawImage): Promise<VideoFrame & MediaObject> {
const ret = await sdk.mediaManager.createMediaObject(image, ScryptedMimeTypes.Image, {
format: null,
timestamp: 0,
width: image.width,
height: image.height,
queued: 0,
toBuffer: (options: ImageOptions) => image.toBuffer(options),
toImage: (options: ImageOptions) => image.toImage(options),
flush: async () => { },
});
return ret;
}
class RawImage implements Image, RawFrame {
constructor(public data: Buffer, public width: number, public height: number, public format: ImageFormat) {
}
checkOptions(options: ImageOptions) {
if (options?.resize || options?.crop)
throw new Error('resize and crop are not supported');
if (options?.format && options?.format !== this.format)
throw new Error('format not supported');
}
async toBuffer(options: ImageOptions) {
this.checkOptions(options);
return this.data;
}
async toImage(options: ImageOptions) {
this.checkOptions(options);
return createRawImageMediaObject(this);
}
}
export class FFmpegVideoFrameGenerator extends ScryptedDeviceBase implements VideoFrameGenerator {
async *generateVideoFramesInternal(mediaObject: MediaObject, options?: VideoFrameGeneratorOptions, filter?: (videoFrame: VideoFrame & MediaObject) => Promise<boolean>): AsyncGenerator<VideoFrame & MediaObject, any, unknown> {
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(mediaObject, ScryptedMimeTypes.FFmpegInput);
const gray = options?.format === 'gray';
const channels = gray ? 1 : 3;
const format: ImageFormat = gray ? 'gray' : 'rgb';
const vf: string[] = [];
if (options?.fps)
vf.push(`fps=${options.fps}`);
if (options.resize)
vf.push(`scale=${options.resize.width}:${options.resize.height}`);
const args = [
'-hide_banner',
//'-hwaccel', 'auto',
...ffmpegInput.inputArguments,
'-vcodec', 'pam',
'-pix_fmt', gray ? 'gray' : 'rgb24',
...vf.length ? [
'-vf',
vf.join(','),
] : [],
'-f', 'image2pipe',
'pipe:3',
];
// this seems to reduce latency.
// addVideoFilterArguments(args, 'fps=10', 'fps');
const cp = child_process.spawn(await sdk.mediaManager.getFFmpegPath(), args, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
const console = mediaObject?.sourceId ? sdk.deviceManager.getMixinConsole(mediaObject.sourceId) : this.console;
safePrintFFmpegArguments(console, args);
ffmpegLogInitialOutput(console, cp);
let finished = false;
let frameDeferred: Deferred<RawFrame>;
const reader = async () => {
try {
const readable = cp.stdio[3] as Readable;
const headers = new Map<string, string>();
while (!finished) {
const line = await readLine(readable);
if (line !== 'ENDHDR') {
const [key, value] = line.split(' ');
headers[key] = value;
continue;
}
if (headers['TUPLTYPE'] !== 'RGB' && headers['TUPLTYPE'] !== 'GRAYSCALE')
throw new Error(`Unexpected TUPLTYPE in PAM stream: ${headers['TUPLTYPE']}`);
const width = parseInt(headers['WIDTH']);
const height = parseInt(headers['HEIGHT']);
if (!width || !height)
throw new Error('Invalid dimensions in PAM stream');
const length = width * height * channels;
headers.clear();
const data = await readLength(readable, length);
if (frameDeferred) {
const f = frameDeferred;
frameDeferred = undefined;
f.resolve({
width,
height,
data,
});
}
else {
// this.console.warn('skipped frame');
}
}
}
catch (e) {
}
finally {
console.log('finished reader');
finished = true;
frameDeferred?.reject(new Error('frame generator finished'));
}
}
try {
reader();
while (!finished) {
frameDeferred = new Deferred();
const raw = await frameDeferred.promise;
const { width, height, data } = raw;
const rawImage = new RawImage(data, width, height, format);
try {
const mo = await createRawImageMediaObject(rawImage);
yield mo;
}
finally {
rawImage.data = undefined;
}
}
}
catch (e) {
}
finally {
console.log('finished generator');
finished = true;
safeKillFFmpeg(cp);
}
}
async generateVideoFrames(mediaObject: MediaObject, options?: VideoFrameGeneratorOptions, filter?: (videoFrame: VideoFrame & MediaObject) => Promise<boolean>): Promise<AsyncGenerator<VideoFrame & MediaObject, any, unknown>> {
return this.generateVideoFramesInternal(mediaObject, options, filter);
}
}

View File

@@ -33,11 +33,13 @@ async function createVipsMediaObject(image: VipsImage): Promise<VideoFrame & Med
timestamp: 0,
width: image.width,
height: image.height,
queued: 0,
toBuffer: (options: ImageOptions) => image.toBuffer(options),
toImage: async (options: ImageOptions) => {
const newImage = await image.toVipsImage(options);
return createVipsMediaObject(newImage);
}
},
flush: async () => {},
});
return ret;

View File

@@ -1,13 +1,13 @@
import sdk, { Camera, DeviceProvider, DeviceState, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoFrame, VideoFrameGenerator } from '@scrypted/sdk';
import { Deferred } from '@scrypted/common/src/deferred';
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { Camera, DeviceProvider, DeviceState, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionGeneratorResult, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoFrame, VideoFrameGenerator } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
import { DenoisedDetectionState } from './denoise';
import { FFmpegVideoFrameGenerator, sharpLib } from './ffmpeg-videoframes';
import { serverSupportsMixinEventMasking } from './server-version';
import { sleep } from './sleep';
import { getAllDevices, safeParseJson } from './util';
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes-no-sharp';
const polygonOverlap = require('polygon-overlap');
const insidePolygon = require('point-inside-polygon');
@@ -35,12 +35,6 @@ interface ZoneInfo {
}
type ZoneInfos = { [zone: string]: ZoneInfo };
type TrackedDetection = ObjectDetectionResult & {
newOrBetterDetection?: boolean;
bestScore?: number;
bestSecondPassScore?: number;
};
class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera & MotionSensor & ObjectDetector> implements ObjectDetector, Settings {
motionListener: EventListenerRegister;
motionMixinListener: EventListenerRegister;
@@ -61,6 +55,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
choices,
}
},
onPut: () => {
this.endObjectDetection();
this.maybeStartMotionDetection();
},
defaultValue: 'Default',
},
motionSensorSupplementation: {
@@ -112,11 +110,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
zones = this.getZones();
zoneInfos = this.getZoneInfos();
detectionIntervalTimeout: NodeJS.Timeout;
detectionState: DenoisedDetectionState<TrackedDetection> = {};
detectionId: string;
detectorRunning = false;
analyzeStop = 0;
lastDetectionInput = 0;
detectorSignal = new Deferred<void>().resolve();
get detectorRunning() {
return !this.detectorSignal.finished;
}
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, public model: ObjectDetectionModel, group: string, public hasMotionType: boolean, public settings: Setting[]) {
super({
@@ -129,7 +127,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
});
this.cameraDevice = systemManager.getDeviceById<Camera & VideoCamera & MotionSensor & ObjectDetector>(this.id);
this.detectionId = model.name + '-' + this.cameraDevice.id;
this.bindObjectDetection();
this.register();
@@ -147,7 +144,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (this.hasMotionType) {
// force a motion detection restart if it quit
if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_REPLACE)
await this.startPipelineAnalysis();
this.startPipelineAnalysis();
return;
}
}, this.storageSettings.values.detectionInterval * 1000);
@@ -190,11 +187,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
if (this.motionSensorSupplementation !== BUILTIN_MOTION_SENSOR_REPLACE)
return;
await this.startPipelineAnalysis();
this.startPipelineAnalysis();
}
endObjectDetection() {
this.detectorRunning = false;
this.detectorSignal.resolve();
}
bindObjectDetection() {
@@ -223,7 +220,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
}
await this.startPipelineAnalysis();
this.startPipelineAnalysis();
});
return;
@@ -241,7 +238,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
if (!this.detectorRunning)
this.console.log('built in motion sensor started motion, starting video detection.');
await this.startPipelineAnalysis();
this.startPipelineAnalysis();
return;
}
@@ -256,20 +253,68 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
}
async startPipelineAnalysis() {
if (this.detectorRunning)
startPipelineAnalysis() {
if (!this.detectorSignal.finished)
return;
this.detectorRunning = true;
this.analyzeStop = Date.now() + this.getDetectionDuration();
const signal = this.detectorSignal = new Deferred();
if (!this.hasMotionType)
this.plugin.objectDetectionStarted(this.console);
const options = {
snapshotPipeline: this.plugin.shouldUseSnapshotPipeline(),
};
this.runPipelineAnalysis(signal, options)
.catch(e => {
this.console.error('Video Analysis ended with error', e);
}).finally(() => {
if (!this.hasMotionType)
this.plugin.objectDetectionEnded(this.console, options.snapshotPipeline);
else
this.console.log('Video Analysis motion detection ended.');
signal.resolve();
});
}
async runPipelineAnalysis(signal: Deferred<void>, options: {
snapshotPipeline: boolean,
}) {
const start = Date.now();
this.analyzeStop = start + this.getDetectionDuration();
let lastStatusTime = Date.now();
let lastStatus = 'starting';
const updatePipelineStatus = (status: string) => {
lastStatus = status;
lastStatusTime = Date.now();
}
let frameGenerator: AsyncGenerator<VideoFrame & MediaObject, void>;
let detectionGenerator: AsyncGenerator<ObjectDetectionGeneratorResult, void>;
const interval = setInterval(() => {
if (Date.now() - lastStatusTime > 30000) {
signal.resolve();
this.console.error('VideoAnalysis is hung and will terminate:', lastStatus);
}
}, 30000);
signal.promise.finally(() => clearInterval(interval));
let newPipeline: string = this.newPipeline;
if (!this.hasMotionType && (!newPipeline || newPipeline === 'Default')) {
if (options.snapshotPipeline) {
newPipeline = 'Snapshot';
this.console.warn(`Due to limited performance, Snapshot mode is being used with ${this.plugin.statsSnapshotConcurrent} actively detecting cameras.`);
}
}
const newPipeline = this.newPipeline;
let generator: () => Promise<AsyncGenerator<VideoFrame & MediaObject>>;
if (newPipeline === 'Snapshot' && !this.hasMotionType) {
options.snapshotPipeline = true;
this.console.log('decoder:', 'Snapshot +', this.objectDetection.name);
const self = this;
generator = async () => (async function* gen() {
frameGenerator = (async function* gen() {
try {
while (self.detectorRunning) {
while (!signal.finished) {
const now = Date.now();
const sleeper = async () => {
const diff = now + 1100 - Date.now();
@@ -278,9 +323,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
};
let image: MediaObject & VideoFrame;
try {
updatePipelineStatus('takePicture');
const mo = await self.cameraDevice.takePicture({
reason: 'event',
});
updatePipelineStatus('converting image');
image = await sdk.mediaManager.convertMediaObject(mo, ScryptedMimeTypes.Image);
}
catch (e) {
@@ -290,6 +337,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
// self.console.log('yield')
updatePipelineStatus('processing image');
yield image;
// self.console.log('done yield')
await sleeper();
@@ -303,16 +351,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
else {
const destination: MediaStreamDestination = this.hasMotionType ? 'low-resolution' : 'local-recorder';
const videoFrameGenerator = systemManager.getDeviceById<VideoFrameGenerator>(newPipeline);
this.console.log('decoder:', videoFrameGenerator.name);
if (!videoFrameGenerator)
throw new Error('invalid VideoFrameGenerator');
this.console.log(videoFrameGenerator.name, '+', this.objectDetection.name);
updatePipelineStatus('getVideoStream');
const stream = await this.cameraDevice.getVideoStream({
prebuffer: this.model.prebuffer,
destination,
// ask rebroadcast to mute audio, not needed.
audio: null,
});
generator = async () => videoFrameGenerator.generateVideoFrames(stream, {
frameGenerator = await videoFrameGenerator.generateVideoFrames(stream, {
queue: 0,
resize: this.model?.inputSize ? {
width: this.model.inputSize[0],
height: this.model.inputSize[1],
@@ -321,51 +372,71 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
});
}
const start = Date.now();
let detections = 0;
try {
for await (const detected
of await this.objectDetection.generateObjectDetections(await generator(), {
settings: this.getCurrentSettings(),
sourceId: this.id,
})) {
if (!this.detectorRunning) {
break;
}
if (!this.hasMotionType && Date.now() > this.analyzeStop) {
break;
}
const currentDetections = new Set<string>();
let lastReport = 0;
detectionGenerator = await sdk.connectRPCObject(await this.objectDetection.generateObjectDetections(frameGenerator, {
settings: this.getCurrentSettings(),
sourceId: this.id,
}));
// apply the zones to the detections and get a shallow copy list of detections after
// exclusion zones have applied
const zonedDetections = this.applyZones(detected.detected);
detected.detected.detections = zonedDetections;
updatePipelineStatus('waiting result');
detections++;
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
if (detected.detected.detectionId) {
const jpeg = await detected.videoFrame.toBuffer({
format: 'jpg',
});
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
this.setDetection(detected.detected, mo);
// this.console.log('image saved', detected.detected.detections);
}
this.reportObjectDetections(detected.detected);
if (this.hasMotionType) {
await sleep(250);
}
// this.handleDetectionEvent(detected.detected);
for await (const detected of detectionGenerator) {
if (signal.finished) {
break;
}
if (!this.hasMotionType && Date.now() > this.analyzeStop) {
break;
}
// apply the zones to the detections and get a shallow copy list of detections after
// exclusion zones have applied
const zonedDetections = this.applyZones(detected.detected);
detected.detected.detections = zonedDetections;
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
if (!this.hasMotionType) {
this.plugin.trackDetection();
for (const d of detected.detected.detections) {
currentDetections.add(d.className);
}
const now = Date.now();
if (now > lastReport + 10000) {
const found = [...currentDetections.values()];
if (!found.length)
found.push('[no detections]');
this.console.log(`[${Math.round((now - start) / 100) / 10}s] Detected:`, ...found);
currentDetections.clear();
lastReport = now;
}
}
if (detected.detected.detectionId) {
updatePipelineStatus('creating jpeg');
// const start = Date.now();
const vf = await sdk.connectRPCObject(detected.videoFrame);
const jpeg = await vf.toBuffer({
format: 'jpg',
});
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
// this.console.log('retain took', Date.now() -start);
this.setDetection(detected.detected, mo);
// this.console.log('image saved', detected.detected.detections);
}
this.reportObjectDetections(detected.detected);
if (this.hasMotionType) {
// const diff = Date.now() - when;
// when = Date.now();
// this.console.log('sleper', diff);
await sleep(250);
}
updatePipelineStatus('waiting result');
// this.handleDetectionEvent(detected.detected);
}
catch (e) {
this.console.error('video pipeline ended with error', e);
}
finally {
this.console.log('video pipeline analysis ended, dps:', detections / (Date.now() - start) * 1000);
this.endObjectDetection();
}
}
normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]) {
@@ -451,7 +522,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
copy = copy.filter(c => c !== o);
}
return copy as TrackedDetection[];
return copy;
}
reportObjectDetections(detection: ObjectsDetected) {
@@ -490,7 +561,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (!detection.detectionId)
detection.detectionId = crypto.randomBytes(4).toString('hex');
this.console.log('retaining detection image');
this.console.log('retaining detection image', ...detection.detections);
const { detectionId } = detection;
this.detections.set(detectionId, detectionInput);
@@ -699,9 +770,9 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
if (key === 'analyzeButton') {
this.analyzeStop = Date.now() + 60000;
// await this.snapshotDetection();
await this.startPipelineAnalysis();
this.startPipelineAnalysis();
this.analyzeStop = Date.now() + 60000;
}
else {
const settings = this.getCurrentSettings();
@@ -785,9 +856,17 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
}
}
interface ObjectDetectionStatistics {
dps: number;
sampleTime: number;
}
class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings, DeviceProvider {
currentMixins = new Set<ObjectDetectorMixin>();
objectDetectionStatistics = new Map<number, ObjectDetectionStatistics>();
statsSnapshotTime: number;
statsSnapshotDetections: number;
statsSnapshotConcurrent = 0;
storageSettings = new StorageSettings(this, {
activeMotionDetections: {
title: 'Active Motion Detection Sessions',
@@ -802,12 +881,77 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
title: 'Active Object Detection Sessions',
readonly: true,
mapGet: () => {
// could use the stats variable...
return [...this.currentMixins.values()]
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
.reduce((c2, v2) => c2 + (!v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
}
},
}
})
});
shouldUseSnapshotPipeline() {
this.pruneOldStatistics();
for (const [k, v] of this.objectDetectionStatistics.entries()) {
// check the stats history to see if any sessions
// with same or lower number of cameras were on the struggle bus.
if (v.dps < 2 && k <= this.statsSnapshotConcurrent)
return true;
}
return false;
}
pruneOldStatistics() {
const now = Date.now();
for (const [k, v] of this.objectDetectionStatistics.entries()) {
// purge the stats every hour
if (Date.now() - v.sampleTime > 60 * 60 * 1000)
this.objectDetectionStatistics.delete(k);
}
}
trackDetection() {
this.statsSnapshotDetections++;
}
objectDetectionStarted(console: Console) {
this.resetStats(console);
this.statsSnapshotConcurrent++;
}
objectDetectionEnded(console: Console, snapshotPipeline: boolean) {
this.resetStats(console, snapshotPipeline);
this.statsSnapshotConcurrent--;
}
resetStats(console: Console, snapshotPipeline?: boolean) {
const now = Date.now();
const concurrentSessions = this.statsSnapshotConcurrent;
if (concurrentSessions) {
const duration = now - this.statsSnapshotTime;
const stats: ObjectDetectionStatistics = {
sampleTime: now,
dps: this.statsSnapshotDetections / (duration / 1000),
};
// ignore short sessions and sessions with no detections (busted?).
// also ignore snapshot sessions because that will skew/throttle the stats used
// to determine system dps capabilities.
if (duration > 10000 && this.statsSnapshotDetections && !snapshotPipeline)
this.objectDetectionStatistics.set(concurrentSessions, stats);
this.pruneOldStatistics();
const str = `video analysis, ${concurrentSessions} camera(s), dps: ${Math.round(stats.dps * 10) / 10} (${this.statsSnapshotDetections}/${Math.round(duration / 1000)})`;
this.console.log(str);
console?.log(str);
}
this.statsSnapshotDetections = 0;
this.statsSnapshotTime = now;
}
constructor(nativeId?: ScryptedNativeId) {
super(nativeId);
@@ -818,9 +962,9 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
{
name: 'FFmpeg Frame Generator',
type: ScryptedDeviceType.Builtin,
interfaces: sharpLib ? [
interfaces: [
ScryptedInterface.VideoFrameGenerator,
] : [],
],
nativeId: 'ffmpeg',
}
]

View File

@@ -1,3 +0,0 @@
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -1,18 +1,17 @@
{
"name": "@scrypted/onvif",
"version": "0.0.118",
"version": "0.0.120",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/onvif",
"version": "0.0.118",
"version": "0.0.120",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.9.1",
"base-64": "^1.0.0",
"http-auth-utils": "^3.0.2",
"md5": "^2.3.0",
@@ -21,6 +20,7 @@
},
"devDependencies": {
"@types/md5": "^2.3.1",
"@types/node": "^18.15.11",
"@types/xml2js": "^0.4.9"
}
},
@@ -65,7 +65,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.87",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -130,9 +130,10 @@
}
},
"node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
"dev": true
},
"node_modules/@types/xml2js": {
"version": "0.4.9",
@@ -328,9 +329,10 @@
}
},
"@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
"dev": true
},
"@types/xml2js": {
"version": "0.4.9",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/onvif",
"version": "0.0.118",
"version": "0.0.120",
"description": "ONVIF Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -39,7 +39,6 @@
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.9.1",
"base-64": "^1.0.0",
"http-auth-utils": "^3.0.2",
"md5": "^2.3.0",
@@ -48,6 +47,7 @@
},
"devDependencies": {
"@types/md5": "^2.3.1",
"@types/node": "^18.15.11",
"@types/xml2js": "^0.4.9"
}
}

View File

@@ -35,7 +35,7 @@ function stripNamespaces(topic: string) {
let parts = topic.split('/')
for (let index = 0; index < parts.length; index++) {
let stringNoNamespace = parts[index].split(':').pop() // split on :, then return the last item in the array
if (output.length == 0) {
if (output.length === 0) {
output += stringNoNamespace
} else {
output += '/' + stringNoNamespace
@@ -92,9 +92,18 @@ export class OnvifCameraAPI {
else
ret.emit('event', OnvifEvent.AudioStop)
}
// Reolink
else if (eventTopic.includes('Visitor') && (dataValue === true || dataValue === false)) {
if (dataValue) {
ret.emit('event', OnvifEvent.BinaryStart)
}
else {
ret.emit('event', OnvifEvent.BinaryStop)
}
}
// Mobotix T26
else if (eventTopic.includes('VideoSource/Alarm')) {
if (dataValue == "Ring" || dataValue == "CameraBellButton") {
if (dataValue === "Ring" || dataValue === "CameraBellButton") {
ret.emit('event', OnvifEvent.BinaryRingEvent);
}
}
@@ -155,7 +164,7 @@ export class OnvifCameraAPI {
this.console.log('supportsEvents error', err);
return reject(err);
}
if (!err && data.events && data.events.WSPullPointSupport && data.events.WSPullPointSupport == true) {
if (!err && data.events && data.events.WSPullPointSupport && data.events.WSPullPointSupport === true) {
this.console.log('Camera supports WSPullPoint', xml);
} else {
this.console.log('Camera does not show WSPullPoint support, but trying anyway', xml);

View File

@@ -1,11 +1,13 @@
import sdk, { MediaObject, Intercom, FFmpegInput, ScryptedMimeTypes } from "@scrypted/sdk";
import { RtspSmartCamera } from "../../rtsp/src/rtsp";
import { parseSemicolonDelimited, RtspClient } from "@scrypted/common/src/rtsp-server";
import { createBindZero } from "@scrypted/common/src/listen-cluster";
import { RtspClient, parseSemicolonDelimited } from "@scrypted/common/src/rtsp-server";
import { parseSdp } from "@scrypted/common/src/sdp-utils";
import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "@scrypted/common/src/media-helpers";
import child_process from 'child_process';
import { createBindZero, reserveUdpPort } from "@scrypted/common/src/listen-cluster";
import sdk, { FFmpegInput, Intercom, MediaObject, ScryptedMimeTypes } from "@scrypted/sdk";
import crypto from 'crypto';
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
import { nextSequenceNumber } from "../../homekit/src/types/camera/jitter-buffer";
import { RtspSmartCamera } from "../../rtsp/src/rtsp";
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
const { mediaManager } = sdk;
@@ -80,11 +82,11 @@ export class OnvifIntercom implements Intercom {
const url = new URL(this.url);
url.username = username;
url.password = password;
this.intercomClient = new RtspClient(url.toString());
this.intercomClient.console = this.camera.console;
await this.intercomClient.options();
const intercomClient = this.intercomClient = new RtspClient(url.toString());
intercomClient.console = this.camera.console;
await intercomClient.options();
const describe = await this.intercomClient.describe({
const describe = await intercomClient.describe({
Require,
});
this.camera.console.log('ONVIF Backchannel SDP:');
@@ -94,31 +96,35 @@ export class OnvifIntercom implements Intercom {
if (!audioBackchannel)
throw new Error('ONVIF audio backchannel not found');
return audioBackchannel;
return { audioBackchannel, intercomClient };
}
async startIntercom(media: MediaObject) {
const ffmpegInput = await mediaManager.convertMediaObjectToJSON<FFmpegInput>(media, ScryptedMimeTypes.FFmpegInput);
await this.stopIntercom();
const audioBackchannel = await this.checkIntercom();
const { audioBackchannel, intercomClient } = await this.checkIntercom();
if (!audioBackchannel)
throw new Error('ONVIF audio backchannel not found');
const rtp = await reserveUdpPort();
const rtpServer = await createBindZero('udp4');
const rtp = rtpServer.port;
const rtcp = rtp + 1;
let ip: string;
let serverRtp: number;
let transportDict: ReturnType<typeof parseSemicolonDelimited>;
let tcp = false;
try {
const headers: any = {
Require,
Transport: `RTP/AVP;unicast;client_port=${rtp}-${rtcp}`,
};
const response = await this.intercomClient.request('SETUP', headers, audioBackchannel.control);
const response = await intercomClient.request('SETUP', headers, audioBackchannel.control);
transportDict = parseSemicolonDelimited(response.headers.transport);
this.intercomClient.session = response.headers.session.split(';')[0];
intercomClient.session = response.headers.session.split(';')[0];
ip = this.camera.getIPAddress();
const { server_port } = transportDict;
@@ -126,6 +132,7 @@ export class OnvifIntercom implements Intercom {
serverRtp = parseInt(serverPorts[0]);
}
catch (e) {
tcp = true;
this.camera.console.error('onvif udp backchannel failed, falling back to tcp', e);
const headers: any = {
@@ -133,21 +140,19 @@ export class OnvifIntercom implements Intercom {
Transport: `RTP/AVP/TCP;unicast;interleaved=0-1`,
};
const response = await this.intercomClient.request('SETUP', headers, audioBackchannel.control);
const response = await intercomClient.request('SETUP', headers, audioBackchannel.control);
transportDict = parseSemicolonDelimited(response.headers.transport);
this.intercomClient.session = response.headers.session.split(';')[0];
intercomClient.session = response.headers.session.split(';')[0];
ip = '127.0.0.1';
const server = await createBindZero('udp4');
this.intercomClient.client.on('close', () => server.server.close());
intercomClient.client.on('close', () => server.server.close());
serverRtp = server.port;
server.server.on('message', data => {
this.intercomClient.send(data, 0);
intercomClient.send(data, 0);
});
}
this.camera.console.log('backchannel transport', transportDict);
const ffmpegInput = await mediaManager.convertMediaObjectToJSON<FFmpegInput>(media, ScryptedMimeTypes.FFmpegInput);
const availableCodecs = [...parseCodecs(audioBackchannel.contents)];
let match: CodecMatch;
let codec: SupportedCodec;
@@ -171,27 +176,69 @@ export class OnvifIntercom implements Intercom {
}
// ffmpeg expects ssrc as signed int32.
const ssrc = ssrcBuffer.readInt32BE(0);
const ssrcUnsigned = ssrcBuffer.readUint32BE(0);
const args = [
'-hide_banner',
...ffmpegInput.inputArguments,
'-vn',
'-acodec', codec.ffmpegCodec,
'-ar', match.sampleRate,
'-ac', match.channels || '1',
"-payload_type", match.payloadType,
"-ssrc", ssrc.toString(),
'-f', 'rtp',
`rtp://${ip}:${serverRtp}?localrtpport=${rtp}&localrtcpport=${rtcp}`,
];
safePrintFFmpegArguments(this.camera.console, args);
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args);
const payloadType = parseInt(match.payloadType);
ffmpegLogInitialOutput(this.camera.console, cp);
await this.intercomClient.play({
await intercomClient.play({
Require,
});
let pending: RtpPacket;
let seqNumber = 0;
const forwarder = await startRtpForwarderProcess(console, ffmpegInput, {
audio: {
onRtp: (rtp) => {
// if (true) {
// const p = RtpPacket.deSerialize(rtp);
// p.header.payloadType = payloadType;
// p.header.ssrc = ssrcUnsigned;
// p.header.marker = true;
// rtpServer.server.send(p.serialize(), serverRtp, ip);
// return;
// }
const p = RtpPacket.deSerialize(rtp);
if (!pending) {
pending = p;
return;
}
if (pending.payload.length + p.payload.length < 1024) {
pending.payload = Buffer.concat([pending.payload, p.payload]);
return;
}
pending.header.payloadType = payloadType;
pending.header.ssrc = ssrcUnsigned;
pending.header.sequenceNumber = seqNumber;
seqNumber = nextSequenceNumber(seqNumber);
pending.header.marker = true;
if (!tcp)
rtpServer.server.send(pending.serialize(), serverRtp, ip);
else
intercomClient.send(pending.serialize(), 0);
pending = p;
},
codecCopy: codec.ffmpegCodec,
payloadType,
ssrc,
packetSize: 1024,
encoderArguments: [
'-acodec', codec.ffmpegCodec,
'-ar', match.sampleRate,
'-ac', match.channels || '1',
],
}
});
intercomClient.client.on('close', () => forwarder.kill());
forwarder.killPromise.finally(() => intercomClient?.client.destroy());
this.camera.console.log('intercom playing');
}

View File

@@ -9,7 +9,7 @@ import imutils
import numpy as np
import scrypted_sdk
from PIL import Image
from scrypted_sdk.types import (ObjectDetectionGeneratorSession,
from scrypted_sdk.types import (ObjectDetectionGeneratorSession,ObjectDetectionSession,
ObjectDetectionResult, ObjectsDetected,
Setting, VideoFrame)
@@ -116,35 +116,37 @@ class OpenCVPlugin(DetectPlugin):
blur = int(settings.get('blur', blur))
return area, threshold, interval, blur
def detect(self, frame, settings: Any, detection_session: OpenCVDetectionSession, src_size, convert_to_src_size) -> ObjectsDetected:
def detect(self, frame, detection_session: ObjectDetectionSession, src_size, convert_to_src_size) -> ObjectsDetected:
session: OpenCVDetectionSession = detection_session['settings']['session']
settings = detection_session and detection_session.get('settings', None)
area, threshold, interval, blur = self.parse_settings(settings)
gray = frame
detection_session.curFrame = cv2.GaussianBlur(
gray, (blur, blur), 0, dst=detection_session.curFrame)
session.curFrame = cv2.GaussianBlur(
gray, (blur, blur), 0, dst=session.curFrame)
detections: List[ObjectDetectionResult] = []
detection_result: ObjectsDetected = {}
detection_result['detections'] = detections
detection_result['inputDimensions'] = src_size
if detection_session.previous_frame is None:
detection_session.previous_frame = detection_session.curFrame
detection_session.curFrame = None
if session.previous_frame is None:
session.previous_frame = session.curFrame
session.curFrame = None
return detection_result
detection_session.frameDelta = cv2.absdiff(
detection_session.previous_frame, detection_session.curFrame, dst=detection_session.frameDelta)
tmp = detection_session.curFrame
detection_session.curFrame = detection_session.previous_frame
detection_session.previous_frame = tmp
session.frameDelta = cv2.absdiff(
session.previous_frame, session.curFrame, dst=session.frameDelta)
tmp = session.curFrame
session.curFrame = session.previous_frame
session.previous_frame = tmp
_, detection_session.thresh = cv2.threshold(
detection_session.frameDelta, threshold, 255, cv2.THRESH_BINARY, dst=detection_session.thresh)
detection_session.dilated = cv2.dilate(
detection_session.thresh, None, iterations=2, dst=detection_session.dilated)
_, session.thresh = cv2.threshold(
session.frameDelta, threshold, 255, cv2.THRESH_BINARY, dst=session.thresh)
session.dilated = cv2.dilate(
session.thresh, None, iterations=2, dst=session.dilated)
fcontours = cv2.findContours(
detection_session.dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
session.dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(fcontours)
@@ -205,24 +207,16 @@ class OpenCVPlugin(DetectPlugin):
detection_session.cap = None
return super().end_session(detection_session)
async def generateObjectDetections(self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None) -> Any:
try:
ds = OpenCVDetectionSession()
videoFrames = await scrypted_sdk.sdk.connectRPCObject(videoFrames)
async for videoFrame in videoFrames:
detected = await self.run_detection_videoframe(videoFrame, session and session.get('settings'), ds)
yield {
'__json_copy_serialize_children': True,
'detected': detected,
'videoFrame': videoFrame,
}
finally:
try:
await videoFrames.aclose()
except:
pass
async def generateObjectDetections(self, videoFrames: Any, detection_session: ObjectDetectionGeneratorSession = None) -> Any:
if not detection_session:
detection_session = {}
if not detection_session.get('settings'):
detection_session['settings'] = {}
settings = detection_session['settings']
settings['session'] = OpenCVDetectionSession()
return super().generateObjectDetections(videoFrames, detection_session)
async def run_detection_videoframe(self, videoFrame: VideoFrame, settings: Any, detection_session: OpenCVDetectionSession) -> ObjectsDetected:
async def run_detection_videoframe(self, videoFrame: VideoFrame, detection_session: ObjectDetectionSession) -> ObjectsDetected:
width = videoFrame.width
height = videoFrame.height
@@ -267,5 +261,5 @@ class OpenCVPlugin(DetectPlugin):
def convert_to_src_size(point):
return point[0] * scale, point[1] * scale
mat = np.ndarray((height, width, 1), buffer=buffer, dtype=np.uint8)
detections = self.detect(mat, settings, detection_session, (videoFrame.width, videoFrame.height), convert_to_src_size)
detections = self.detect(mat, detection_session, (videoFrame.width, videoFrame.height), convert_to_src_size)
return detections

View File

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

View File

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

View File

@@ -1,54 +1,15 @@
import sdk, { FFmpegInput, MediaObject, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionGeneratorResult, ObjectDetectionGeneratorSession, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionSession, ObjectsDetected, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, VideoFrame } from '@scrypted/sdk';
import child_process, { ChildProcess } from 'child_process';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "../../../common/src/media-helpers";
import sdk, { MediaObject, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionGeneratorResult, ObjectDetectionGeneratorSession, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionSession, ObjectsDetected, ScryptedDeviceBase, VideoFrame } from '@scrypted/sdk';
import PD from 'pam-diff';
import P2P from 'pipe2pam';
import { PassThrough, Writable } from 'stream';
const { mediaManager } = sdk;
const defaultDifference = 9;
const defaultPercentage = 15;
const defaultPercentage = 2;
interface PamDiffSession {
id: string;
timeout?: NodeJS.Timeout;
cp?: ChildProcess;
pamDiff?: any;
callbacks: ObjectDetectionCallbacks;
}
class PamDiff extends ScryptedDeviceBase implements ObjectDetection {
sessions = new Map<string, PamDiffSession>();
endSession(id: string) {
const pds = this.sessions.get(id);
if (!pds)
return;
this.sessions.delete(pds.id);
const event: ObjectsDetected = {
timestamp: Date.now(),
running: false,
detectionId: pds.id,
}
clearTimeout(pds.timeout);
safeKillFFmpeg(pds.cp);
if (pds.callbacks) {
pds.callbacks.onDetectionEnded(event);
}
else {
this.onDeviceEvent(ScryptedInterface.ObjectDetection, event);
}
}
reschedule(id: string, duration: number,) {
const pds = this.sessions.get(id);
if (!pds)
return;
clearTimeout(pds.timeout);
pds.timeout = setTimeout(() => this.endSession(id), duration);
}
async * generateObjectDetectionsInternal(videoFrames: AsyncGenerator<VideoFrame, any, unknown>, session: ObjectDetectionGeneratorSession): AsyncGenerator<ObjectDetectionGeneratorResult, any, unknown> {
videoFrames = await sdk.connectRPCObject(videoFrames);
@@ -92,7 +53,6 @@ class PamDiff extends ScryptedDeviceBase implements ObjectDetection {
}
const event: ObjectsDetected = {
timestamp: Date.now(),
running: true,
inputDimensions: [width, height],
detections,
}
@@ -111,10 +71,10 @@ ENDHDR
`;
const buffer = await videoFrame.toBuffer({
resize: {
resize: (videoFrame.width !== width || videoFrame.height !== height) ? {
width,
height,
},
} : undefined,
format: 'rgb',
});
pt.write(Buffer.from(header));
@@ -146,157 +106,7 @@ ENDHDR
}
async detectObjects(mediaObject: MediaObject, session?: ObjectDetectionSession, callbacks?: ObjectDetectionCallbacks): Promise<ObjectsDetected> {
if (mediaObject && mediaObject.mimeType?.startsWith('image/'))
throw new Error('can not run motion detection on image')
let { detectionId } = session;
let pds = this.sessions.get(detectionId);
if (pds)
pds.callbacks = callbacks;
if (!session?.duration) {
this.endSession(detectionId);
return {
detectionId,
running: false,
timestamp: Date.now(),
}
}
if (pds) {
this.reschedule(detectionId, session.duration);
pds.pamDiff.setDifference(session.settings?.difference || defaultDifference).setPercent(session.settings?.percent || defaultPercentage);
return {
detectionId,
running: true,
timestamp: Date.now(),
};
}
// unable to start/extend this session.
if (!mediaObject) {
this.endSession(detectionId);
return {
detectionId,
running: false,
timestamp: Date.now(),
}
}
const ffmpeg = await mediaManager.getFFmpegPath();
const ffmpegInput: FFmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(
mediaObject,
ScryptedMimeTypes.FFmpegInput
)).toString());
pds = {
id: detectionId,
callbacks,
}
this.reschedule(detectionId, session.duration);
const args = ffmpegInput.inputArguments.slice();
args.unshift(
'-hide_banner',
...ffmpegInput.videoDecoderArguments || [],
)
args.push(
'-an', '-dn',
'-c:v',
'pam',
'-pix_fmt',
'rgb24',
'-f',
'image2pipe',
'-vf',
`fps=2,scale=640:360`,
'pipe:3',
);
const p2p = new P2P();
const pamDiff = new PD({
difference: session.settings?.difference || defaultDifference,
percent: session.settings?.percent || defaultPercentage,
response: session?.settings?.motionAsObjects ? 'blobs' : 'percent',
});
pamDiff.on('diff', async (data: any) => {
const trigger = data.trigger[0];
// console.log(trigger.blobs.length);
const { blobs } = trigger;
const detections: ObjectDetectionResult[] = [];
if (blobs?.length) {
for (const blob of blobs) {
detections.push(
{
className: 'motion',
score: 1,
boundingBox: [blob.minX, blob.minY, blob.maxX - blob.minX, blob.maxY - blob.minY],
}
)
}
}
else {
detections.push(
{
className: 'motion',
score: 1,
}
)
}
const event: ObjectsDetected = {
timestamp: Date.now(),
running: true,
detectionId: pds.id,
inputDimensions: [640, 360],
detections,
}
if (pds.callbacks) {
pds.callbacks.onDetection(event);
}
else {
this.onDeviceEvent(ScryptedInterface.ObjectDetection, event);
}
});
const console = sdk.deviceManager.getMixinConsole(mediaObject.sourceId, this.nativeId);
pds.pamDiff = pamDiff;
pds.pamDiff
.setDifference(session.settings?.difference || defaultDifference)
.setPercent(session.settings?.percent || defaultPercentage)
.setResponse(session?.settings?.motionAsObjects ? 'blobs' : 'percent');;
safePrintFFmpegArguments(console, args);
pds.cp = child_process.spawn(ffmpeg, args, {
stdio: ['inherit', 'pipe', 'pipe', 'pipe']
});
let pamTimeout: NodeJS.Timeout;
const resetTimeout = () => {
clearTimeout(pamTimeout);
pamTimeout = setTimeout(() => {
const check = this.sessions.get(detectionId);
if (check !== pds)
return;
console.error('PAM image stream timed out. Ending session.');
this.endSession(detectionId);
}, 60000);
}
p2p.on('data', () => {
resetTimeout();
})
resetTimeout();
pds.cp.stdio[3].pipe(p2p as any).pipe(pamDiff as any);
pds.cp.on('exit', () => this.endSession(detectionId));
ffmpegLogInitialOutput(console, pds.cp);
this.sessions.set(detectionId, pds);
return {
detectionId,
running: true,
timestamp: Date.now(),
}
throw new Error('can not run motion detection on image')
}
async getDetectionModel(): Promise<ObjectDetectionModel> {

View File

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

View File

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

View File

@@ -1618,7 +1618,7 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
if (this.streamSettings.storageSettings.settings[key])
await this.streamSettings.storageSettings.putSetting(key, value);
else
this.storage.setItem(key, value?.toString());
this.storage.setItem(key, value?.toString() || '');
// no prebuffer change necessary if the setting is a transcoding hint.
if (this.streamSettings.storageSettings.settings[key]?.group === 'Transcoding')

View File

@@ -111,7 +111,7 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
placeholder: '-hwaccel auto',
choices: Object.keys(getH264DecoderArgs()),
combobox: true,
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue,
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue || '',
hide: true,
},
videoFilterArguments: {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.27",
"version": "0.1.35",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/python-codecs",
"version": "0.1.27",
"version": "0.1.35",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.27",
"version": "0.1.35",
"description": "Python Codecs for Scrypted",
"keywords": [
"scrypted",

View File

@@ -1,3 +1,4 @@
import traceback
import asyncio
import scrypted_sdk
from scrypted_sdk import Setting, SettingValue
@@ -6,6 +7,7 @@ import gstreamer
import libav
import vipsimage
import pilimage
import time
Gst = None
try:
@@ -128,22 +130,35 @@ def create_scrypted_plugin():
class CodecFork:
async def generateVideoFramesGstreamer(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None, h264Decoder: str = None) -> scrypted_sdk.VideoFrame:
start = time.time()
try:
async for data in gstreamer.generateVideoFramesGstreamer(mediaObject, options, filter, h264Decoder):
yield data
except Exception as e:
traceback.print_exc()
raise
finally:
print('gstreamer finished after %s' % (time.time() - start))
import os
os._exit(os.EX_OK)
pass
async def generateVideoFramesLibav(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
start = time.time()
try:
async for data in libav.generateVideoFramesLibav(mediaObject, options, filter):
yield data
except Exception as e:
traceback.print_exc()
raise
finally:
import os
os._exit(os.EX_OK)
pass
print('libav finished after %s' % (time.time() - start))
import sys
if sys.platform == 'win32':
sys.exit()
else:
import os
os._exit(os.EX_OK)
async def fork():

View File

@@ -34,6 +34,7 @@ class PILImage(scrypted_sdk.VideoFrame):
finally:
rgb.close()
return await to_thread(format)
# TODO: gray...
def save():
bytesArray = io.BytesIO()

View File

@@ -1,3 +1,6 @@
# needed by libav to_ndarray
numpy>=1.16.2
# gobject instrospection for gstreamer.
PyGObject>=3.30.4; sys_platform != 'win32'

View File

@@ -22,18 +22,30 @@ class VipsImage(scrypted_sdk.VideoFrame):
def format():
return memoryview(vipsImage.vipsImage.write_to_memory())
return await to_thread(format)
elif options['format'] == 'rgba':
def format():
if not vipsImage.vipsImage.hasalpha():
rgba = vipsImage.vipsImage.addalpha()
else:
rgba = vipsImage.vipsImage
return memoryview(rgba.write_to_memory())
return await to_thread(format)
elif options['format'] == 'rgb':
def format():
if vipsImage.vipsImage.hasalpha():
rgb = vipsImage.vipsImage.extract_band(0, n=vipsImage.vipsImage.bands - 1)
else:
rgb = vipsImage.vipsImage
mem = memoryview(rgb.write_to_memory())
return mem
return memoryview(rgb.write_to_memory())
return await to_thread(format)
elif options['format'] == 'gray':
def format():
return memoryview(vipsImage.vipsImage.write_to_memory())
if vipsImage.vipsImage.bands == 1:
def format():
return memoryview(vipsImage.vipsImage.write_to_memory())
else:
def format():
gray = vipsImage.vipsImage.colourspace("b-w")
return memoryview(gray.write_to_memory())
return await to_thread(format)
return await to_thread(lambda: vipsImage.vipsImage.write_to_buffer('.' + options['format']))

View File

@@ -1,5 +1,9 @@
# Reolink Plugin for Scrypted
<span style="color:red">Reolink cameras should use the ONVIF plugin. This plugin is for older Reolink cameras that do not have ONVIF support or the ONVIF implementation is buggy.</span>
<span style="color:red">Reolink doorbells MUST use the ONVIF plugin. This plugin does not support two way audio or the doorbell button event.</span>
Reolink Cameras offer both RTMP and RTSP streams. RTMP streams are more reliable than RTSP on Reolink Cameras, but Scrypted highly recommends using RTSP streams if they are stable on your Reolink hardware. RTMP streams will be preferred by default. The defaults can be changed in the camera's Rebroadcast `Stream Management` settings.
Reolink Two Way Audio is not supported. It is a proprietary and undocumented protocol.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/reolink",
"version": "0.0.19",
"version": "0.0.22",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/reolink",
"version": "0.0.19",
"version": "0.0.22",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
@@ -36,38 +36,39 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.206",
"version": "0.2.86",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
"typedoc": "^0.23.21"
}
},
"../sdk": {
@@ -260,23 +261,24 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@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": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"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.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/reolink",
"version": "0.0.19",
"version": "0.0.22",
"description": "Reolink Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -51,9 +51,11 @@ class ReolinkCamera extends RtspSmartCamera implements Camera {
(async () => {
while (!killed) {
try {
// const ai = await client.getAiState();
// ret.emit('data', JSON.stringify(ai));
const { value, data } = await client.getMotionState();
this.motionDetected = value;
ret.emit('data', data);
ret.emit('data', JSON.stringify(data));
}
catch (e) {
ret.emit('error', e);
@@ -116,14 +118,17 @@ class ReolinkCamera extends RtspSmartCamera implements Camera {
}
// rough guesses for rebroadcast stream selection.
ret[0].container = 'rtmp';
ret[0].video = {
width: 2560,
height: 1920,
}
ret[1].container = 'rtmp';
ret[1].video = {
width: 896,
height: 672,
}
ret[2].container = 'rtmp';
ret[2].video = {
width: 640,
height: 480,
@@ -147,6 +152,28 @@ class ReolinkCamera extends RtspSmartCamera implements Camera {
});
}
// rough guesses for h264
ret[3].container = 'rtsp';
ret[3].video = {
codec: 'h264',
width: 2560,
height: 1920,
}
ret[4].container = 'rtsp';
ret[4].video = {
codec: 'h264',
width: 896,
height: 672,
}
ret[5].container = 'rtsp';
ret[5].video = {
codec: 'h265',
width: 896,
height: 672,
}
return ret;
}

View File

@@ -6,7 +6,7 @@ export const reolinkHttpsAgent = new https.Agent({
});
export async function getMotionState(digestAuth: AxiosDigestAuth, username: string, password: string, address: string, channelId: number) {
const url = new URL(`http://${address}/cgi-bin/api.cgi`);
const url = new URL(`http://${address}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetMdState');
params.set('channel', channelId.toString());

View File

@@ -26,6 +26,23 @@ export class ReolinkCameraClient {
return getMotionState(this.digestAuth, this.username, this.password, this.host, this.channelId);
}
async getAiState() {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetAiState');
params.set('channel', this.channelId.toString());
params.set('user', this.username);
params.set('password', this.password);
const response = await this.digestAuth.request({
url: url.toString(),
httpsAgent: reolinkHttpsAgent,
});
return {
value: !!response.data?.[0]?.value?.state,
data: response.data,
};
}
async jpegSnapshot() {
const url = new URL(`http://${this.host}/cgi-bin/api.cgi`);
const params = url.searchParams;

View File

@@ -1,18 +1,18 @@
{
"name": "@scrypted/ring",
"version": "0.0.107",
"version": "0.0.109",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ring",
"version": "0.0.107",
"version": "0.0.109",
"dependencies": {
"@koush/ring-client-api": "file:../../external/ring-client-api",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.15.3",
"axios": "^1.3.4",
"@types/node": "^18.15.11",
"axios": "^1.3.5",
"rxjs": "^7.8.0"
},
"optionalDependencies": {
@@ -42,14 +42,14 @@
],
"devDependencies": {
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.0",
"@changesets/cli": "^2.26.1",
"@swc-node/register": "^1.6.2",
"turbo": "^1.8.2"
"turbo": "^1.8.5"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.85",
"version": "0.2.86",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -148,9 +148,9 @@
}
},
"node_modules/@types/node": {
"version": "18.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz",
"integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"node_modules/@types/responselike": {
"version": "1.0.0",
@@ -179,9 +179,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.5.tgz",
"integrity": "sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",

View File

@@ -36,13 +36,13 @@
"@koush/ring-client-api": "file:../../external/ring-client-api",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^18.15.3",
"axios": "^1.3.4",
"@types/node": "^18.15.11",
"axios": "^1.3.5",
"rxjs": "^7.8.0"
},
"optionalDependencies": {
"got": "11.8.6",
"socket.io-client": "^2.5.0"
},
"version": "0.0.107"
"version": "0.0.109"
}

View File

@@ -2,6 +2,7 @@ import sdk, { Device, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, Sc
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { RingLocationDevice } from './location';
import { generateUuid, Location, RingBaseApi, RingRestClient } from './ring-client-api';
import { sleep } from '@scrypted/common/src/sleep';
const { deviceManager, mediaManager } = sdk;
@@ -19,12 +20,18 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
},
email: {
title: 'Email',
onPut: async () => this.clearTryDiscoverDevices(),
onPut: async () => {
if (await this.loginNextTick())
this.clearTryDiscoverDevices();
},
},
password: {
title: 'Password',
type: 'password',
onPut: async () => this.clearTryDiscoverDevices(),
onPut: async () => {
if (await this.loginNextTick())
this.clearTryDiscoverDevices();
},
},
loginCode: {
title: 'Two Factor Code',
@@ -72,8 +79,18 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
this.settingsStorage.values.systemId = generateUuid();
}
waiting = false;
async loginNextTick() {
if (this.waiting)
return false;
this.waiting = true;
await sleep(500);
this.waiting = false;
return true;
}
async clearTryDiscoverDevices() {
this.settingsStorage.values.refreshToken = undefined;
this.settingsStorage.values.refreshToken = '';
await this.discoverDevices();
this.console.log('discovery completed successfully');
}
@@ -192,7 +209,7 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
return this.devices.get(nativeId);
}
async releaseDevice(id: string, nativeId: string): Promise<void> {}
async releaseDevice(id: string, nativeId: string): Promise<void> { }
}
export default RingPlugin;

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/rtsp",
"version": "0.0.52",
"version": "0.0.53",
"description": "RTSP Cameras and Streams Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -327,7 +327,7 @@ export abstract class RtspSmartCamera extends RtspCamera {
}
setHttpPortOverride(port: string) {
this.storage.setItem('httpPort', port);
this.storage.setItem('httpPort', port || '');
}
getRtspUrlOverride() {

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