Compare commits

..

32 Commits

Author SHA1 Message Date
Koushik Dutta
6336407f15 prepublish 2023-02-28 11:17:26 -08:00
Koushik Dutta
38e3492137 rebroadcast: use new rtsp code with auth and extenral access 2023-02-28 11:15:32 -08:00
Koushik Dutta
255e426e2d client: update 2023-02-28 11:10:47 -08:00
Koushik Dutta
fed1cf2a0d cli: update 2023-02-28 11:10:22 -08:00
Koushik Dutta
a5cb8c3fdc sdk: update 2023-02-28 11:09:07 -08:00
Koushik Dutta
514d86144f sdk: update 2023-02-28 11:07:47 -08:00
Koushik Dutta
21db7934c9 Merge branch 'main' of github.com:koush/scrypted 2023-02-28 08:16:00 -08:00
Koushik Dutta
14e4b5c0e3 rebroadcast/rtsp: initial support for clustering 2023-02-28 08:15:56 -08:00
Koushik Duta
6478ad0411 Merge branch 'main' of github.com:koush/scrypted 2023-02-27 22:55:45 -08:00
Koushik Duta
81b7d432e9 docker: add gstreamer vaapi decoder 2023-02-27 22:55:30 -08:00
Koushik Dutta
dcbf5094f9 Merge branch 'main' of github.com:koush/scrypted 2023-02-27 22:38:42 -08:00
Koushik Dutta
69ba181dfa docker: add intel vaapi compose example 2023-02-27 22:38:35 -08:00
Koushik Duta
88654275c1 hikvision: trim deps 2023-02-27 22:27:23 -08:00
Koushik Duta
62f28271ed sdk: disable link on build 2023-02-27 22:25:27 -08:00
Koushik Dutta
0538130e78 amcrest: publish support for multiple call buttons on dahua models 2023-02-27 13:28:09 -08:00
KoljaV
95a3a16227 amcrest: New DahuaEventType and introduction of CallerId for Dahua Intercoms with multiple Bell Buttons. (#592)
* Update main.ts

Some Dahua Intercom support multiple Call Buttons. Therefore a CallerID is introduced. Plus a new event (DahuaCallDeny) that resets the ring status.

* Update amcrest-api.ts

New Dahua Event Type for Call Deny.

* tested multiple times and sorted out two bugs

---------

Co-authored-by: Kolja Vornholt <kvornholt@MBP-von-Kolja.lan>
2023-02-27 13:27:34 -08:00
Koushik Dutta
bfbf89ff69 Merge branch 'main' of github.com:koush/scrypted 2023-02-27 13:20:36 -08:00
Koushik Dutta
e630589489 cameras: auto detect two way audio 2023-02-27 13:19:32 -08:00
Alex Leeds
99d1e51f36 ring: add support for various sensor types (#591)
* ring: add support for various sensor types

* ring: bump version
2023-02-27 10:52:50 -08:00
Koushik Dutta
ab42ccd889 cameras: publish 2023-02-26 23:21:47 -08:00
Koushik Dutta
767af25aa0 cameras: create probe utils 2023-02-26 22:09:30 -08:00
Koushik Dutta
7575dd82ce onvif: unescape xml strings 2023-02-26 16:52:32 -08:00
Koushik Dutta
9307bbd09e Merge branch 'main' of github.com:koush/scrypted 2023-02-26 14:21:59 -08:00
Koushik Dutta
68a9ec09e6 docker: readd google gpg keys that were failing 2023-02-26 14:21:40 -08:00
Brett Jia
f8a548401f remote: allow doorbells and filter out irrelevant plugins (#588) 2023-02-26 11:44:47 -08:00
Koushik Dutta
26d1f8e58c cameras: include ip in device info 2023-02-26 11:42:52 -08:00
Koushik Dutta
8772e25c8e sdk/client: update 2023-02-26 11:16:28 -08:00
Koushik Dutta
378ac82c8c cameras: refresh device info on startup 2023-02-26 09:54:25 -08:00
Koushik Dutta
fcb1292ffd Update Dockerfile.full.header 2023-02-25 19:17:24 -08:00
Brett Jia
18112ee40f remote: allow API type and use ObjectDetection (#586)
* include implementation from standalone repo

* simplify monkeypatching

* allow API types and use ObjectDetection interface
2023-02-25 19:00:57 -08:00
Brett Jia
fa8b9dfe99 remote: Scrypted remote plugin (#585)
* include implementation from standalone repo

* simplify monkeypatching
2023-02-25 18:46:33 -08:00
Alex Leeds
e7dff4edc9 add fan support to nest thermostats (#583)
* add fan support to nest thermostats

* fix fan refresh
2023-02-25 17:58:23 -08:00
63 changed files with 3423 additions and 1763 deletions

View File

@@ -7,7 +7,7 @@ import net from 'net';
import { Duplex, Readable } from 'stream';
import tls from 'tls';
import { Deferred } from './deferred';
import { closeQuiet, createBindUdp, createBindZero } from './listen-cluster';
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
import { timeoutPromise } from './promise-utils';
import { readLength, readLine } from './read-stream';
import { MSection, parseSdp } from './sdp-utils';
@@ -1053,3 +1053,33 @@ export class RtspServer {
}
}
}
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
hostname?: string,
pathToken?: string,
createServer?(duplex: Duplex): T,
}) {
const pathToken = options?.pathToken || crypto.randomBytes(8).toString('hex');
let { url, clientPromise, server } = await listenZeroSingleClient(options?.hostname);
const rtspServerPath = '/' + pathToken;
url = url.replace('tcp:', 'rtsp:') + rtspServerPath;
const rtspServerPromise = clientPromise.then(client => {
const createServer = options?.createServer || (duplex => new RtspServer(duplex));
const rtspServer = createServer(client);
rtspServer.checkRequest = async (method, url, headers, message) => {
rtspServer.checkRequest = undefined;
const u = new URL(url);
return u.pathname === rtspServerPath;
};
return rtspServer as T;
});
return {
url,
rtspServerPromise,
server,
}
}

View File

@@ -31,21 +31,16 @@ RUN apt-get -y install \
build-essential \
cmake \
gcc \
gir1.2-gtk-3.0 \
libcairo2-dev \
libgirepository1.0-dev \
libglib2.0-dev \
libjpeg-dev \
libgif-dev \
libopenjp2-7 \
libpango1.0-dev \
librsvg2-dev \
pkg-config
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
RUN bash -c 'if [ $(uname -m) == "x86_64" ]; then apt-get -y install gstreamer1.0-vaapi; fi'
# python native
RUN apt-get -y install \
python3 \
@@ -64,7 +59,6 @@ RUN apt-get -y install \
# python pip
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
RUN python3 -m pip install dlib
################################################################
# End section generated from template/Dockerfile.full.header

View File

@@ -38,6 +38,8 @@ services:
# - /dev/ttyACM0:/dev/ttyACM0
# all usb devices, such as coral tpu
# - /dev/bus/usb:/dev/bus/usb
# intel hardware accelerated video decoding
# - /dev/dri:/dev/dri
volumes:
- ~/.scrypted/volume:/server/volume

View File

@@ -26,22 +26,18 @@ RUN apt-get -y upgrade
# base development stuff
RUN apt-get -y install \
build-essential \
cmake \
gcc \
gir1.2-gtk-3.0 \
libcairo2-dev \
libgirepository1.0-dev \
libglib2.0-dev \
libjpeg-dev \
libgif-dev \
libopenjp2-7 \
libpango1.0-dev \
librsvg2-dev \
pkg-config
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
RUN bash -c 'if [ $(uname -m) == "x86_64" ]; then apt-get -y install gstreamer1.0-vaapi; fi'
# python native
RUN apt-get -y install \
python3 \

View File

@@ -6,7 +6,7 @@
"configurations": [
{
"console": "integratedTerminal",
"type": "pwa-node",
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
@@ -20,8 +20,9 @@
"ts-node/register"
],
"args": [
"install",
"@scrypted/google-device-access"
"ffplay",
"Kitchen",
"getVideoStream"
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,12 +1,12 @@
{
"name": "scrypted",
"version": "1.0.57",
"version": "1.0.58",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.0.57",
"version": "1.0.58",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",

View File

@@ -1,6 +1,6 @@
{
"name": "scrypted",
"version": "1.0.57",
"version": "1.0.58",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"bin": {

View File

@@ -8,10 +8,14 @@ import https from 'https';
import mkdirp from 'mkdirp';
import { installServe, serveMain } from './service';
import { connectScryptedClient } from '../../client/src/index';
import { ScryptedMimeTypes, FFMpegInput } from '@scrypted/types';
import { ScryptedMimeTypes, FFmpegInput } from '../../../sdk/types/src/types.input';
import semver from 'semver';
import child_process from 'child_process';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
if (!semver.gte(process.version, '16.0.0')) {
throw new Error('"node" version out of date. Please update node to v16 or higher.')
}
@@ -57,6 +61,7 @@ async function doLogin(host: string) {
password,
},
url,
httpsAgent,
}, axiosConfig));
mkdirp.sync(scryptedHome);
@@ -112,6 +117,9 @@ async function runCommand() {
pluginId: '@scrypted/core',
username: login.username,
password: login.token,
axiosConfig: {
httpsAgent,
}
});
const device: any = sdk.systemManager.getDeviceById(idOrName) || sdk.systemManager.getDeviceByName(idOrName);
@@ -157,9 +165,15 @@ async function main() {
}
else if (process.argv[2] === 'ffplay') {
const { sdk, pendingResult } = await runCommand();
const ffinput = await sdk.mediaManager.convertMediaObjectToJSON<FFMpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
console.log(ffinput);
child_process.spawn('ffplay', ffinput.inputArguments, {
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
if (ffmpegInput.url && ffmpegInput.urls?.[0]) {
const url = new URL(ffmpegInput.url);
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0]) {
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
}
}
console.log('ffplay', ...ffmpegInput.inputArguments);
child_process.spawn('ffplay', ffmpegInput.inputArguments, {
stdio: 'inherit',
});
sdk.disconnect();

View File

@@ -1,15 +1,15 @@
{
"name": "@scrypted/client",
"version": "1.1.38",
"version": "1.1.40",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.38",
"version": "1.1.40",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"@scrypted/types": "^0.2.65",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"rimraf": "^3.0.2"
@@ -50,9 +50,9 @@
"extraneous": true
},
"node_modules/@scrypted/types": {
"version": "0.2.64",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.65.tgz",
"integrity": "sha512-V/gfPy+xeRds6WMHwU6trt2YBkH9qcC/3Bx9q5hOxpE+rZSL4ru+nvlaumCRM3mSNWXBav4nbd23JCoGJ0F2eA=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -289,9 +289,9 @@
},
"dependencies": {
"@scrypted/types": {
"version": "0.2.64",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.65.tgz",
"integrity": "sha512-V/gfPy+xeRds6WMHwU6trt2YBkH9qcC/3Bx9q5hOxpE+rZSL4ru+nvlaumCRM3mSNWXBav4nbd23JCoGJ0F2eA=="
},
"@socket.io/component-emitter": {
"version": "3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.38",
"version": "1.1.40",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -17,7 +17,7 @@
"typescript": "^4.7.4"
},
"dependencies": {
"@scrypted/types": "^0.2.64",
"@scrypted/types": "^0.2.65",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"rimraf": "^3.0.2"

View File

@@ -49,6 +49,7 @@ export interface ScryptedClientStatic extends ScryptedStatic {
connectionType: ScryptedClientConnectionType;
authorization?: string;
queryToken?: { [parameter: string]: string };
rpcPeer: RpcPeer,
}
export interface ScryptedConnectionOptions {
@@ -587,6 +588,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
browserSignalingSession,
authorization,
queryToken,
rpcPeer,
}
socket.on('close', () => {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.115",
"version": "0.0.119",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -2,6 +2,7 @@ import AxiosDigestAuth from '@koush/axios-digest-auth';
import { Readable } from 'stream';
import https from 'https';
import { IncomingMessage } from 'http';
import { amcrestHttpsAgent, getDeviceInfo } from './probe';
export enum AmcrestEvent {
MotionStart = "Code=VideoMotion;action=Start",
@@ -17,12 +18,10 @@ export enum AmcrestEvent {
PhoneCallDetectStop = "Code=PhoneCallDetect;action=Stop",
DahuaTalkInvite = "Code=CallNoAnswered;action=Start",
DahuaTalkHangup = "Code=PassiveHungup;action=Start",
DahuaCallDeny = "Code=HungupPhone;action=Pulse",
DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse",
}
export const amcrestHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
export class AmcrestCameraClient {
digestAuth: AxiosDigestAuth;
@@ -34,6 +33,16 @@ export class AmcrestCameraClient {
});
}
async checkTwoWayAudio() {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`,
});
return (response.data as string).includes('result=1');
}
// appAutoStart=true
// deviceType=IP4M-1041B
// hardwareVersion=1.00
@@ -42,30 +51,7 @@ export class AmcrestCameraClient {
// updateSerial=IPC-AW46WN-S2
// updateSerialCloudUpgrade=IPC-AW46WN-.....
async getDeviceInfo() {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
});
const lines = (response.data as string).split('\n');
const vals: {
[key: string]: string,
} = {};
for (const line of lines) {
let index = line.indexOf('=');
if (index === -1)
index = line.length;
const k = line.substring(0, index);
const v = line.substring(index + 1);
vals[k] = v.trim();
}
return {
deviceType: vals.deviceType,
hardwareVersion: vals.hardwareVersion,
serialNumber: vals.serialNumber,
}
return getDeviceInfo(this.digestAuth, this.ip);
}
async jpegSnapshot(): Promise<Buffer> {

View File

@@ -5,7 +5,8 @@ import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable, Stream } from "stream";
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent, amcrestHttpsAgent } from "./amcrest-api";
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
import { amcrestHttpsAgent } from './probe';
const { mediaManager } = sdk;
@@ -37,19 +38,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
this.updateDeviceInfo();
this.updateManagementUrl();
}
updateManagementUrl() {
const ip = this.storage.getItem('ip');
if (!ip)
return;
const info = this.info || {};
const managementUrl = `http://${ip}`;
if (info.managementUrl !== managementUrl) {
info.managementUrl = managementUrl;
this.info = info;
}
}
getRecordingStreamCurrentTime(recordingStream: MediaObject): Promise<number> {
@@ -80,9 +68,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async updateDeviceInfo(): Promise<void> {
if (this.info)
const ip = this.storage.getItem('ip');
if (!ip)
return;
const deviceInfo = {};
const managementUrl = `http://${ip}`;
const deviceInfo: DeviceInformation = {
...this.info,
ip,
managementUrl,
};
const deviceParameters = [
{ action: "getVendor", replace: "vendor=", parameter: "manufacturer" },
@@ -161,6 +156,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const client = new AmcrestCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console);
const events = await client.listenEvents();
const doorbellType = this.storage.getItem('doorbellType');
const callerId = this.storage.getItem('callerID');
const multipleCallIds = this.storage.getItem('multipleCallIds') === 'true';
let pulseTimeout: NodeJS.Timeout;
@@ -187,11 +184,21 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|| event === AmcrestEvent.PhoneCallDetectStart
|| event === AmcrestEvent.AlarmIPCStart
|| event === AmcrestEvent.DahuaTalkInvite) {
this.binaryState = true;
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds)
{
if (payload.includes(callerId))
{
this.binaryState = true;
}
} else
{
this.binaryState = true;
}
}
else if (event === AmcrestEvent.TalkHangup
|| event === AmcrestEvent.PhoneCallDetectStop
|| event === AmcrestEvent.AlarmIPCStop
|| event === AmcrestEvent.DahuaCallDeny
|| event === AmcrestEvent.DahuaTalkHangup) {
this.binaryState = false;
}
@@ -246,6 +253,36 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (!twoWayAudio)
twoWayAudio = isDoorbell ? 'Amcrest' : 'None';
if (doorbellType == DAHUA_DOORBELL_TYPE)
{
ret.push(
{
title: 'Multiple Call Buttons',
key: 'multipleCallIds',
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
type: 'boolean',
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
}
);
}
const multipleCallIds = this.storage.getItem('multipleCallIds');
if (multipleCallIds)
{
ret.push(
{
title: 'Caller ID',
key: 'callerID',
description: 'Caller ID',
type: 'number',
value: this.storage.getItem('callerID'),
}
)
}
ret.push(
{
@@ -266,7 +303,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
);
return ret;
}
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
@@ -441,7 +482,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
interfaces.push(ScryptedInterface.VideoRecorder);
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
this.updateManagementUrl();
this.updateDeviceInfo();
}
async startIntercom(media: MediaObject): Promise<void> {
@@ -550,9 +591,10 @@ class AmcrestProvider extends RtspProvider {
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
let twoWayAudio: string;
if (!skipValidate) {
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
try {
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
const deviceInfo = await api.getDeviceInfo();
settings.newCamera = deviceInfo.deviceType;
@@ -563,6 +605,16 @@ class AmcrestProvider extends RtspProvider {
this.console.error('Error adding Amcrest camera', e);
throw e;
}
try {
if (await api.checkTwoWayAudio()) {
// onvif seems to work better than Amcrest, except for AD110.
twoWayAudio = 'ONVIF';
}
}
catch (e) {
this.console.warn('Error probing two way audio', e);
}
}
settings.newCamera ||= 'Hikvision Camera';
@@ -574,6 +626,8 @@ class AmcrestProvider extends RtspProvider {
device.putSetting('password', password);
device.setIPAddress(settings.ip?.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
return nativeId;
}

View File

@@ -0,0 +1,42 @@
import https from 'https';
export const amcrestHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
// appAutoStart=true
// deviceType=IP4M-1041B
// hardwareVersion=1.00
// processor=SSC327DE
// serialNumber=12345
// updateSerial=IPC-AW46WN-S2
import AxiosDigestAuth from "@koush/axios-digest-auth";
// updateSerialCloudUpgrade=IPC-AW46WN-.....
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
const response = await digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${address}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
});
const lines = (response.data as string).split('\n');
const vals: {
[key: string]: string,
} = {};
for (const line of lines) {
let index = line.indexOf('=');
if (index === -1)
index = line.length;
const k = line.substring(0, index);
const v = line.substring(index + 1);
vals[k] = v.trim();
}
return {
deviceType: vals.deviceType,
hardwareVersion: vals.hardwareVersion,
serialNumber: vals.serialNumber,
}
}

View File

@@ -208,6 +208,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
name,
interfaces,
type: type || ScryptedDeviceType.Camera,
info: deviceManager.getNativeIds().includes(nativeId) ? deviceManager.getDeviceState(nativeId)?.info : undefined,
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -39,18 +39,15 @@
"dependencies": {
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@googleapis/smartdevicemanagement": "^0.2.0",
"axios": "^0.21.1",
"@googleapis/smartdevicemanagement": "^1.0.0",
"axios": "^1.3.4",
"client-oauth2": "^4.3.3",
"lodash": "^4.17.21",
"query-string": "^7.0.0",
"url-parse": "^1.5.1"
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/lodash": "^4.14.168",
"@types/node": "^14.17.11",
"@types/url-parse": "^1.4.3"
"@types/debug": "^4.1.7",
"@types/lodash": "^4.14.191",
"@types/node": "^18.14.1"
},
"version": "0.0.95"
"version": "0.0.96"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "appengine-hello-world",
"description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.",
"version": "0.0.3",
"version": "0.0.4",
"private": true,
"license": "Apache-2.0",
"author": "Google Inc.",
@@ -10,24 +10,24 @@
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
},
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
},
"scripts": {
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"@google-cloud/datastore": "^6.5.0",
"axios": "^0.21.1",
"@google-cloud/datastore": "^7.3.2",
"axios": "^1.3.4",
"client-oauth2": "^4.3.3",
"express": "^4.17.1",
"typescript": "^4.4.2"
"express": "^4.18.2",
"typescript": "^4.9.5"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^16.7.10",
"mocha": "^9.0.0",
"supertest": "^6.0.0",
"ts-node": "^10.2.1"
"@types/express": "^4.17.17",
"@types/node": "^18.14.1",
"mocha": "^10.2.0",
"supertest": "^6.3.3",
"ts-node": "^10.9.1"
}
}

View File

@@ -1,12 +1,12 @@
import sdk, { ScryptedDeviceBase, DeviceManifest, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MotionSensor, OauthClient, Refresh, ScryptedDeviceType, ScryptedInterface, Setting, Settings, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera, BinarySensor, DeviceInformation, RTCAVSignalingSetup, Camera, PictureOptions, ObjectsDetected, ObjectDetector, ObjectDetectionTypes, FFmpegInput, RequestMediaStreamOptions, Readme, RTCSignalingChannel, RTCSessionControl, RTCSignalingSession, ResponseMediaStreamOptions, RTCSignalingSendIceCandidate, ScryptedMimeTypes, MediaStreamUrl, TemperatureCommand, OnOff } from '@scrypted/sdk';
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { BinarySensor, Camera, DeviceInformation, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MediaStreamUrl, MotionSensor, OauthClient, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, Readme, Refresh, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, TemperatureCommand, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera } from '@scrypted/sdk';
import axios from 'axios';
import ClientOAuth2 from 'client-oauth2';
import { randomBytes } from 'crypto';
import fs from 'fs';
import throttle from 'lodash/throttle';
import qs from 'query-string';
import querystring from "querystring";
import { URL } from 'url';
const { deviceManager, mediaManager, endpointManager, systemManager } = sdk;
@@ -334,11 +334,12 @@ for (const [k, v] of setpointMap.entries()) {
setpointReverseMap.set(v, k);
}
class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Settings, Refresh {
class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Settings, Refresh, OnOff {
device: any;
provider: GoogleSmartDeviceAccess;
executeCommandSetMode: any = undefined;
executeCommandSetCelsius: any = undefined;
executeCommandSetTimer: any = undefined;
executeThrottle = throttle(async () => {
if (this.executeCommandSetCelsius) {
@@ -365,6 +366,12 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
this.console.log('executeCommandSetCelsius', command);
return this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, command);
}
if (this.executeCommandSetTimer) {
const command = this.executeCommandSetTimer;
this.executeCommandSetTimer = undefined;
this.console.log('executeCommandSetTimer', command);
return this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, command);
}
}, 12000)
constructor(provider: GoogleSmartDeviceAccess, device: any) {
@@ -425,6 +432,33 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
// not supported by API. throw?
}
async turnOff(): Promise<void> {
// You can't turn the fan off when the HVAC unit is currently running.
if (this.thermostatActiveMode !== ThermostatMode.Off) {
this.on = false;
await this.refresh(null, true); // Refresh the state to turn the fan switch back to active.
return;
}
this.executeCommandSetTimer = {
command: 'sdm.devices.commands.Fan.SetTimer',
params: {
timerMode: 'OFF',
},
}
await this.executeThrottle();
await this.refresh(null, true);
}
async turnOn(): Promise<void> {
this.executeCommandSetTimer = {
command: 'sdm.devices.commands.Fan.SetTimer',
params: {
timerMode: 'ON',
},
}
await this.executeThrottle();
await this.refresh(null, true);
}
reload() {
const device = this.device;
@@ -478,6 +512,9 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
setpoint,
availableModes: modes,
}
// Set Fan Status
this.on = this.thermostatActiveMode !== ThermostatMode.Off || device.traits?.['sdm.devices.traits.Fan']?.timerMode === "ON";
}
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
@@ -764,7 +801,7 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
response_type: 'code',
scope: 'https://www.googleapis.com/auth/sdm.service',
}
return `${this.authorizationUri}?${qs.stringify(params)}`;
return `${this.authorizationUri}?${querystring.stringify(params)}`;
}
async onOauthCallback(callbackUrl: string) {
const cb = new URL(callbackUrl);
@@ -833,6 +870,7 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
ScryptedInterface.HumiditySensor,
ScryptedInterface.Thermometer,
ScryptedInterface.Settings,
ScryptedInterface.OnOff
],
info,
})

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.120",
"version": "0.0.124",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -38,16 +38,10 @@
"@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"
}
}

View File

@@ -1,10 +1,6 @@
import AxiosDigestAuth from '@koush/axios-digest-auth';
import { IncomingMessage } from 'http';
import https from 'https';
export const hikvisionHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
import { getDeviceInfo, hikvisionHttpsAgent } from './probe';
export function getChannel(channel: string) {
return channel || '101';
@@ -44,31 +40,18 @@ export class HikvisionCameraAPI {
}
async getDeviceInfo() {
try {
const response = await this.digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/ISAPI/System/deviceInfo`,
});
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
return {
deviceModel,
deviceName,
serialNumber,
macAddress,
firmwareVersion,
};
}
catch (e) {
if (e?.response?.data?.includes('notActivated'))
throw new Error(`Camera must be first be activated at http://${this.ip}.`)
throw e;
}
return getDeviceInfo(this.digestAuth, this.ip);
}
async checkTwoWayAudio() {
const response = await this.digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/ISAPI/System/TwoWayAudio/channels`,
});
return (response.data as string).includes('Speaker');
}
async checkDeviceModel(): Promise<string> {

View File

@@ -1,13 +1,13 @@
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
import { readLength } from '@scrypted/common/src/read-stream';
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoStreamOptions } from "@scrypted/sdk";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable } from "stream";
import { sleep } from "../../../common/src/sleep";
import xml2js from 'xml2js';
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { getChannel, HikvisionCameraAPI, HikvisionCameraEvent, hikvisionHttpsAgent } from "./hikvision-camera-api";
import xml2js from 'xml2js';
import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
import { hikvisionHttpsAgent } from './probe';
const { mediaManager } = sdk;
@@ -26,19 +26,29 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
constructor(nativeId: string, provider: RtspProvider) {
super(nativeId, provider);
this.updateManagementUrl();
this.updateDeviceInfo();
}
updateManagementUrl() {
async updateDeviceInfo() {
const ip = this.storage.getItem('ip');
if (!ip)
return;
const info = this.info || {};
const managementUrl = `http://${ip}`;
if (info.managementUrl !== managementUrl) {
info.managementUrl = managementUrl;
this.info = info;
const info: DeviceInformation = {
...this.info,
managementUrl,
ip,
manufacturer: 'Hikvision',
};
const client = this.getClient();
const deviceInfo = await client.getDeviceInfo().catch(() => { });
if (deviceInfo) {
info.model = deviceInfo.deviceModel;
info.mac = deviceInfo.macAddress;
info.firmware = deviceInfo.firmwareVersion;
info.serialNumber = deviceInfo.serialNumber;
}
this.info = info;
}
async listenEvents() {
@@ -284,7 +294,7 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
this.updateManagementUrl();
this.updateDeviceInfo();
}
async getOtherSettings(): Promise<Setting[]> {
@@ -526,9 +536,10 @@ class HikvisionProvider extends RtspProvider {
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
let twoWayAudio: string;
if (!skipValidate) {
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
try {
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
const deviceInfo = await api.getDeviceInfo();
settings.newCamera = deviceInfo.deviceName;
@@ -542,6 +553,15 @@ class HikvisionProvider extends RtspProvider {
this.console.error('Error adding Hikvision camera', e);
throw e;
}
try {
if (await api.checkTwoWayAudio()) {
twoWayAudio = 'Hikvision';
}
}
catch (e) {
this.console.warn('Error probing two way audio', e);
}
}
settings.newCamera ||= 'Hikvision Camera';
@@ -553,6 +573,8 @@ class HikvisionProvider extends RtspProvider {
device.putSetting('password', password);
device.setIPAddress(settings.ip?.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
return nativeId;
}

View File

@@ -0,0 +1,34 @@
import https from 'https';
import AxiosDigestAuth from '@koush/axios-digest-auth';
export const hikvisionHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
try {
const response = await digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${address}/ISAPI/System/deviceInfo`,
});
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
return {
deviceModel,
deviceName,
serialNumber,
macAddress,
firmwareVersion,
};
}
catch (e) {
if (e?.response?.data?.includes('notActivated'))
throw new Error(`Camera must be first be activated at http://${address}.`)
throw e;
}
}

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import { AddressInfo } from "net";
import onvif from 'onvif';
import { Stream } from "stream";
import xml2js from 'xml2js';
@@ -37,7 +38,7 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
constructor(nativeId: string, provider: RtspProvider) {
super(nativeId, provider);
this.updateManagementUrl();
this.updateDeviceInfo();
this.updateDevice();
}
@@ -94,16 +95,30 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
});
}
updateManagementUrl() {
async updateDeviceInfo() {
const ip = this.storage.getItem('ip');
if (!ip)
return;
const info = this.info || {};
const client = await this.getClient();
const onvifInfo = await client.getDeviceInformation().catch(() => { });
const managementUrl = `http://${ip}`;
if (info.managementUrl !== managementUrl) {
info.managementUrl = managementUrl;
this.info = info;
let info = {
...this.info,
managementUrl,
ip,
};
if (onvifInfo) {
info = {
...info,
serialNumber: onvifInfo.serialNumber,
manufacturer: onvifInfo.manufacturer,
firmware: onvifInfo.firmwareVersion,
model: onvifInfo.model,
}
}
this.info = info;
}
getDetectionInput(detectionId: any, eventId?: any): Promise<MediaObject> {
@@ -382,7 +397,7 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
this.client = undefined;
this.rtspMediaStreamOptions = undefined;
this.updateManagementUrl();
this.updateDeviceInfo();
if (key !== 'onvifDoorbell' && key !== 'onvifTwoWay')
return super.putSetting(key, value);
@@ -414,7 +429,7 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
constructor(nativeId?: string) {
super(nativeId);
onvif.Discovery.on('device', (cam: any, rinfo: any, xml: any) => {
onvif.Discovery.on('device', (cam: any, rinfo: AddressInfo, xml: any) => {
// Function will be called as soon as the NVT responses
// Parsing of Discovery responses taken from my ONVIF-Audit project, part of the 2018 ONVIF Open Source Challenge
@@ -435,19 +450,29 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
}
const urn = result['Envelope']['Body'][0]['ProbeMatches'][0]['ProbeMatch'][0]['EndpointReference'][0]['Address'][0].payload;
const xaddrs = result['Envelope']['Body'][0]['ProbeMatches'][0]['ProbeMatch'][0]['XAddrs'][0].payload;
let name: string;
const knownScopes = {
'onvif://www.onvif.org/name/': '',
'onvif://www.onvif.org/MAC/': '',
'onvif://www.onvif.org/hardware/': '',
};
this.console.log('discovered device payload', xml);
try {
let scopes = result['Envelope']['Body'][0]['ProbeMatches'][0]['ProbeMatch'][0]['Scopes'][0].payload;
scopes = scopes.split(" ");
const splitScopes = scopes.split(" ") as string[];
for (let i = 0; i < scopes.length; i++) {
if (scopes[i].includes('onvif://www.onvif.org/name')) { name = decodeURI(scopes[i].substring(27)); }
for (const scope of splitScopes) {
for (const known of Object.keys(knownScopes)) {
if (scope.startsWith(known)) {
knownScopes[known] = decodeURIComponent(scope.substring(known.length));
}
}
}
}
catch (e) {
}
const name = knownScopes["onvif://www.onvif.org/name/"] || 'ONVIF Camera';
this.console.log('Discovery Reply from ' + rinfo.address + ' (' + name + ') (' + xaddrs + ') (' + urn + ')');
if (deviceManager.getNativeIds().includes(urn) || this.discoveredDevices.has(urn))
@@ -455,6 +480,11 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
const device: Device = {
name,
info: {
ip: rinfo.address,
mac: knownScopes["onvif://www.onvif.org/MAC/"] || undefined,
model: knownScopes['onvif://www.onvif.org/hardware/'] || undefined,
},
nativeId: urn,
type: ScryptedDeviceType.Camera,
interfaces: this.getInterfaces(),
@@ -531,6 +561,21 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
device.putSetting('password', password);
device.setIPAddress(settings.ip?.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
const intercom = new OnvifIntercom(device);
try {
intercom.url = (await device.getConstructedVideoStreamOptions())[0].url;
if (await intercom.checkIntercom()) {
device.putSetting('onvifTwoWay', 'true');
}
}
catch (e) {
this.console.warn("error while probing intercom", e);
}
finally {
intercom.intercomClient?.client.destroy();
}
return nativeId;
}

View File

@@ -65,6 +65,8 @@ function* parseCodecs(audioSection: string): Generator<CodecMatch> {
}
}
const Require = 'www.onvif.org/ver20/backchannel';
export class OnvifIntercom implements Intercom {
intercomClient: RtspClient;
url: string;
@@ -72,9 +74,7 @@ export class OnvifIntercom implements Intercom {
constructor(public camera: RtspSmartCamera) {
}
async startIntercom(media: MediaObject) {
await this.stopIntercom();
async checkIntercom() {
const username = this.camera.storage.getItem("username");
const password = this.camera.storage.getItem("password");
const url = new URL(this.url);
@@ -83,7 +83,6 @@ export class OnvifIntercom implements Intercom {
this.intercomClient = new RtspClient(url.toString());
this.intercomClient.console = this.camera.console;
await this.intercomClient.options();
const Require = 'www.onvif.org/ver20/backchannel';
const describe = await this.intercomClient.describe({
Require,
@@ -95,6 +94,16 @@ export class OnvifIntercom implements Intercom {
if (!audioBackchannel)
throw new Error('ONVIF audio backchannel not found');
return audioBackchannel;
}
async startIntercom(media: MediaObject) {
await this.stopIntercom();
const audioBackchannel = await this.checkIntercom();
if (!audioBackchannel)
throw new Error('ONVIF audio backchannel not found');
const rtp = await reserveUdpPort();
const rtcp = rtp + 1;

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Headers, RtspServer } from "@scrypted/common/src/rtsp-server";
import fs from 'fs';
import net from 'net';
import { Duplex } from "stream";
// non standard extension that dumps the rtp payload to a file.
export class FileRtspServer extends RtspServer {
@@ -8,7 +8,7 @@ export class FileRtspServer extends RtspServer {
segmentBytesWritten = 0;
writeConsole: Console;
constructor(client: net.Socket, sdp?: string, checkRequest?: (method: string, url: string, headers: Headers, rawMessage: string[]) => Promise<boolean>) {
constructor(client: Duplex, sdp?: string, checkRequest?: (method: string, url: string, headers: Headers, rawMessage: string[]) => Promise<boolean>) {
super(client, sdp, undefined, checkRequest);
this.client.on('close', () => {

View File

@@ -5,7 +5,7 @@ import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
import { handleRebroadcasterClient, ParserOptions, ParserSession, startParserSession } from '@scrypted/common/src/ffmpeg-rebroadcast';
import { closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { readLength } from '@scrypted/common/src/read-stream';
import { createRtspParser, findH264NaluType, getNaluTypes, H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_STAP_B, RtspServer, RtspTrack } from '@scrypted/common/src/rtsp-server';
import { createRtspParser, findH264NaluType, getNaluTypes, H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_STAP_B, listenSingleRtspClient, RtspServer, RtspTrack } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
import { sleep } from '@scrypted/common/src/sleep';
@@ -1063,6 +1063,7 @@ class PrebufferSession {
let socketPromise: Promise<Duplex>;
let url: string;
let urls: string[];
let filter: (chunk: StreamChunk, prebuffer: boolean) => StreamChunk;
let interleavePassthrough = false;
const interleavedMap = new Map<string, number>();
@@ -1114,17 +1115,19 @@ class PrebufferSession {
return chunk;
}
const hostname = options?.route === 'external' ? '0.0.0.0' : undefined;
const client = await listenZeroSingleClient(hostname);
const rtspServerPath = '/' + crypto.randomBytes(8).toString('hex');
socketPromise = client.clientPromise.then(async (socket) => {
sdp = addTrackControls(sdp);
server = new FileRtspServer(socket, sdp, async (method, url, headers, rawMessage) => {
server.checkRequest = undefined;
const u = new URL(url);
return u.pathname === rtspServerPath;
});
server.writeConsole = this.console;
const hostname = options?.route === 'internal' ? undefined : '0.0.0.0';
const clientPromise = await listenSingleRtspClient({
hostname,
createServer: duplex => {
sdp = addTrackControls(sdp);
server = new FileRtspServer(duplex, sdp);
server.writeConsole = this.console;
return server;
}
});
socketPromise = clientPromise.rtspServerPromise.then(async server => {
if (session.parserSpecific) {
const parserSpecific = session.parserSpecific as RtspSessionParserSpecific;
server.resolveInterleaved = msection => {
@@ -1134,7 +1137,7 @@ class PrebufferSession {
}
// server.console = this.console;
await server.handlePlayback();
server.handleTeardown().finally(() => socket.destroy());
server.handleTeardown().finally(() => server.client.destroy());
for (const track of Object.values(server.setupTracks)) {
if (track.protocol === 'udp') {
serverPortMap.set(track.codec, track);
@@ -1146,20 +1149,27 @@ class PrebufferSession {
}
interleavePassthrough = session.parserSpecific && serverPortMap.size === 0;
return socket;
return server.client;
})
url = client.url.replace('tcp://', 'rtsp://') + rtspServerPath;
url = clientPromise.url;
if (hostname) {
try {
const addresses = await sdk.endpointManager.getLocalAddresses();
const [address] = addresses;
if (address) {
if (address && options?.route === 'external') {
const u = new URL(url);
u.hostname = address;
url = u.toString();
}
urls = addresses.map(address => {
const u = new URL(url);
u.hostname = address;
return u.toString();
})
}
catch (e) {
this.console.warn('Error determining external addresses. Is Scrypted Server Address configured?');
}
}
}
@@ -1226,6 +1236,7 @@ class PrebufferSession {
const ffmpegInput: FFmpegInput = {
url,
urls,
container,
inputArguments: [
...inputArguments,

4
plugins/remote/.gitignore vendored Normal file
View File

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

10
plugins/remote/.npmignore Normal file
View File

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

22
plugins/remote/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,22 @@
{
// 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": [
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",
"sourceMaps": true,
"localRoot": "${workspaceFolder}/out",
"remoteRoot": "/plugin/",
"type": "pwa-node"
}
]
}

4
plugins/remote/.vscode/settings.json vendored Normal file
View File

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

20
plugins/remote/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "scrypted: deploy+debug",
"type": "shell",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
},
]
}

3
plugins/remote/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Scrypted Remote Plugin
The Scrypted Remote Plugin allows connecting this Scrypted instance to a remote Scrypted server, using the Scrypted Client API. Devices from the remote Scrypted server will be imported to this current Scrypted instance.

95
plugins/remote/package-lock.json generated Normal file
View File

@@ -0,0 +1,95 @@
{
"name": "@scrypted/scrypted-remote",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/scrypted-remote",
"version": "0.0.1",
"dependencies": {
"@scrypted/client": "file:../../packages/client",
"@scrypted/sdk": "file:../../sdk",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/node": "^17.0.24"
}
},
"../../packages/client": {
"name": "@scrypted/client",
"version": "1.1.38",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.69",
"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/@scrypted/client": {
"resolved": "../../packages/client",
"link": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/node": {
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"dev": true
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
}
}
}

View File

@@ -0,0 +1,39 @@
{
"name": "@scrypted/scrypted-remote",
"version": "0.0.1",
"description": "Scrypted Remote Plugin",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
"build": "scrypted-webpack",
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
"prescrypted-vscode-launch": "scrypted-webpack",
"scrypted-vscode-launch": "scrypted-deploy-debug",
"scrypted-deploy-debug": "scrypted-deploy-debug",
"scrypted-debug": "scrypted-debug",
"scrypted-deploy": "scrypted-deploy",
"scrypted-readme": "scrypted-readme",
"scrypted-package-json": "scrypted-package-json"
},
"keywords": [
"scrypted",
"plugin",
"remote"
],
"scrypted": {
"name": "Scrypted Remote Plugin",
"type": "DeviceCreator",
"interfaces": [
"DeviceProvider",
"DeviceCreator"
]
},
"dependencies": {
"@scrypted/client": "file:../../packages/client",
"@scrypted/sdk": "file:../../sdk",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/node": "^17.0.24"
}
}

332
plugins/remote/src/main.ts Normal file
View File

@@ -0,0 +1,332 @@
import { Device, DeviceProvider, DeviceCreator, DeviceCreatorSettings, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, Battery, VideoCamera, SettingValue, RequestMediaStreamOptions, MediaObject, DeviceManifest} from '@scrypted/sdk';
import sdk from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { connectScryptedClient, ScryptedClientStatic } from '@scrypted/client';
import https from 'https';
import { v4 as uuidv4 } from 'uuid';
const { deviceManager } = sdk;
class ScryptedRemoteInstance extends ScryptedDeviceBase implements DeviceProvider, Settings {
client: ScryptedClientStatic = null;
devices = new Map<string, ScryptedDevice>();
settingsStorage = new StorageSettings(this, {
baseUrl: {
title: 'Base URL',
placeholder: 'https://localhost:10443',
onPut: async () => await this.clearTryDiscoverDevices(),
},
username: {
title: 'Username',
onPut: async () => await this.clearTryDiscoverDevices(),
},
password: {
title: 'Password',
type: 'password',
onPut: async () => await this.clearTryDiscoverDevices(),
},
});
constructor(nativeId: string) {
super(nativeId);
this.clearTryDiscoverDevices();
}
/**
* Checks the given remote device to see if it can be correctly imported by this plugin.
* Returns the (potentially modified) device that is allowed, or null if the device cannot
* be imported.
*
* @param device
* The local device representation. Will be modified in-place and returned.
*/
filtered(device: Device): Device {
// only permit the following device types through
const allowedTypes = [
ScryptedDeviceType.Camera,
ScryptedDeviceType.Doorbell,
ScryptedDeviceType.DeviceProvider,
ScryptedDeviceType.API,
]
if (!allowedTypes.includes(device.type)) {
return null;
}
// only permit the following functional interfaces through
const allowedInterfaces = [
ScryptedInterface.VideoCamera,
ScryptedInterface.Camera,
ScryptedInterface.RTCSignalingChannel,
ScryptedInterface.Battery,
ScryptedInterface.MotionSensor,
ScryptedInterface.AudioSensor,
ScryptedInterface.DeviceProvider,
ScryptedInterface.ObjectDetection,
];
const intersection = allowedInterfaces.filter(i => device.interfaces.includes(i));
if (intersection.length == 0) {
return null;
}
// explicitly drop plugins if all they do is provide devices
if (device.interfaces.includes(ScryptedInterface.ScryptedPlugin) && intersection.length == 1 && intersection[0] == ScryptedInterface.DeviceProvider) {
return null;
}
// some extra interfaces that are nice to expose, but not needed
const nonessentialInterfaces = [
ScryptedInterface.Readme,
];
const nonessentialIntersection = nonessentialInterfaces.filter(i => device.interfaces.includes(i));
device.interfaces = intersection.concat(nonessentialIntersection);
return device;
}
/**
* Configures relevant proxies for the local device representation and the remote device.
* Listeners are added for interface property updates, and select remote function calls are
* intercepted to tweak arguments for better remote integration.
*
* @param device
* The local device representation.
*
* @param remoteDevice
* The RPC reference to the remote device.
*/
setupProxies(device: Device, remoteDevice: ScryptedDevice) {
// set up event listeners for all the relevant interfaces
device.interfaces.map(iface => remoteDevice.listen(iface, (source, details, data) => {
if (!details.property) {
deviceManager.onDeviceEvent(device.nativeId, details.eventInterface, data);
} else {
deviceManager.getDeviceState(device.nativeId)[details.property] = data;
}
}));
// for certain interfaces with fixed state, transfer the initial values over
if (device.interfaces.includes(ScryptedInterface.Battery)) {
deviceManager.getDeviceState(device.nativeId).batteryLevel = (<Battery>remoteDevice).batteryLevel;
}
// since the remote may be using rebroadcast, explicitly request the external
// address for video streams
if (device.interfaces.includes(ScryptedInterface.VideoCamera)) {
const remoteGetVideoStream = (<VideoCamera><any>remoteDevice).getVideoStream;
(<VideoCamera><any>remoteDevice).getVideoStream = async (options?: RequestMediaStreamOptions): Promise<MediaObject> => {
if (!options) {
options = {};
}
(<any>options).route = "external";
return await remoteGetVideoStream(options);
}
}
// for device providers, we need to translate the nativeId
if (device.interfaces.includes(ScryptedInterface.DeviceProvider)) {
const plugin = this;
(<DeviceProvider><any>remoteDevice).getDevice = async (nativeId: string): Promise<Device> => {
return <Device>plugin.devices.get(nativeId);
}
(<DeviceProvider><any>remoteDevice).releaseDevice = async (id: string, nativeId: string): Promise<any> => {
// don't delete the device from the remote
plugin.releaseDevice(id, nativeId);
}
}
}
/**
* Resets the connection to the remote Scrypted server and attempts to reconnect
* and rediscover remoted devices.
*/
async clearTryDiscoverDevices(): Promise<void> {
await this.tryLogin();
// bjia56:
// there's some race condition with multi-tier device discovery that I haven't
// sorted out, but it appears to work fine if we run discovery twice
await this.discoverDevices(0);
await this.discoverDevices(0);
}
async tryLogin(): Promise<void> {
this.client = null;
if (!this.settingsStorage.values.baseUrl || !this.settingsStorage.values.username || !this.settingsStorage.values.password) {
this.console.log("Initializing remote Scrypted login requires the base URL, username, and password");
return;
}
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
this.client = await connectScryptedClient({
baseUrl: this.settingsStorage.values.baseUrl,
pluginId: '@scrypted/core',
username: this.settingsStorage.values.username,
password: this.settingsStorage.values.password,
axiosConfig: {
httpsAgent,
},
})
this.console.log(`Connected to remote Scrypted server. Remote server version: ${this.client.serverVersion}`)
}
getSettings(): Promise<Setting[]> {
return this.settingsStorage.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.settingsStorage.putSetting(key, value);
}
async discoverDevices(duration: number): Promise<void> {
if (!this.client) {
return
}
// construct initial (flat) list of devices from the remote server
const state = this.client.systemManager.getSystemState();
const devices = <Device[]>[];
for (const id in state) {
const remoteDevice = this.client.systemManager.getDeviceById(id);
const remoteProviderDevice = this.client.systemManager.getDeviceById(remoteDevice.providerId);
const remoteProviderNativeId = remoteProviderDevice?.id == remoteDevice.id ? undefined : remoteProviderDevice?.id;
const nativeId = `${this.nativeId}:${remoteDevice.id}`;
const device = this.filtered(<Device>{
name: remoteDevice.name,
type: remoteDevice.type,
interfaces: remoteDevice.interfaces,
info: remoteDevice.info,
nativeId: nativeId,
providerNativeId: remoteProviderNativeId ? `${this.nativeId}:${remoteProviderNativeId}` : this.nativeId,
});
if (!device) {
this.console.log(`Device ${remoteDevice.name} is not supported, ignoring`)
continue;
}
this.console.log(`Found ${remoteDevice.name}\n${JSON.stringify(device, null, 2)}`);
this.devices.set(device.nativeId, remoteDevice);
devices.push(device)
}
// it may be that a parent device was filtered out, so reparent these child devices to
// the top level
devices.map(device => {
if (!this.devices.has(device.providerNativeId)) {
device.providerNativeId = this.nativeId;
}
});
// group devices by parent provider id
const providerDeviceMap = new Map<string, Device[]>();
devices.map(device => {
// group devices by parent provider id
if (!providerDeviceMap.has(device.providerNativeId)) {
providerDeviceMap.set(device.providerNativeId, [device]);
} else {
providerDeviceMap.get(device.providerNativeId).push(device);
}
})
// first register the top level devices, then register the remaining
// devices by provider id
await deviceManager.onDevicesChanged(<DeviceManifest>{
devices: providerDeviceMap.get(this.nativeId),
providerNativeId: this.nativeId,
});
for (let [providerNativeId, devices] of providerDeviceMap) {
await deviceManager.onDevicesChanged(<DeviceManifest>{
devices,
providerNativeId,
});
}
// setup relevant proxies and monkeypatches for all devices
devices.map(device => this.setupProxies(device, this.devices.get(device.nativeId)));
this.console.log(`Discovered ${devices.length} devices`);
}
async getDevice(nativeId: string): Promise<Device> {
if (!this.devices.has(nativeId)) {
throw new Error(`${nativeId} does not exist`);
}
return <Device>this.devices.get(nativeId);
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
this.devices.delete(nativeId)
}
}
class ScryptedRemotePlugin extends ScryptedDeviceBase implements DeviceCreator, DeviceProvider {
remotes = new Map<string, ScryptedRemoteInstance>();
constructor() {
super();
}
async getDevice(nativeId: string): Promise<Device> {
if (!this.remotes.has(nativeId)) {
this.remotes.set(nativeId, new ScryptedRemoteInstance(nativeId));
}
return this.remotes.get(nativeId) as Device;
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
return;
}
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
key: 'name',
title: 'Name',
},
{
key: 'baseUrl',
title: 'Base URL',
placeholder: 'https://localhost:10443',
},
{
key: 'username',
title: 'Username',
},
{
key: 'password',
title: 'Password',
type: 'password',
},
];
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const name = settings.name?.toString();
const url = settings.baseUrl?.toString();
const username = settings.username?.toString();
const password = settings.password?.toString();
const nativeId = uuidv4();
await deviceManager.onDeviceDiscovered(<Device>{
nativeId,
name,
interfaces: [
ScryptedInterface.Settings,
ScryptedInterface.DeviceProvider
],
type: ScryptedDeviceType.DeviceProvider,
});
const remote = await this.getDevice(nativeId) as ScryptedRemoteInstance;
remote.storage.setItem("baseUrl", url);
remote.storage.setItem("username", username);
remote.storage.setItem("password", password);
await remote.clearTryDiscoverDevices();
return nativeId;
}
}
export default new ScryptedRemotePlugin();

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",
"esModuleInterop": true,
"sourceMap": true
},
"include": [
"src/**/*"
]
}

View File

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

View File

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

View File

@@ -20,11 +20,10 @@ class ReolinkCamera extends RtspSmartCamera implements Camera {
if (!ip)
return;
const info = this.info || {};
const managementUrl = `http://${ip}`;
if (info.managementUrl !== managementUrl) {
info.managementUrl = managementUrl;
this.info = info;
}
info.ip = ip;
info.manufacturer = 'Reolink';
info.managementUrl = `http://${ip}`;
this.info = info;
}
getClient() {

View File

@@ -0,0 +1,23 @@
import AxiosDigestAuth from "@koush/axios-digest-auth";
import https from 'https';
export const reolinkHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
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 params = url.searchParams;
params.set('cmd', 'GetMdState');
params.set('channel', channelId.toString());
params.set('user', username);
params.set('password', password);
const response = await digestAuth.request({
url: url.toString(),
httpsAgent: reolinkHttpsAgent,
});
return {
value: !!response.data?.[0]?.value?.state,
data: response.data,
};
}

View File

@@ -1,18 +1,13 @@
import axios from 'axios';
import AxiosDigestAuth from "@koush/axios-digest-auth/dist";
import AxiosDigestAuth from "@koush/axios-digest-auth";
import https from 'https';
import { getMotionState, reolinkHttpsAgent } from './probe';
export class ReolinkCameraClient {
digestAuth: AxiosDigestAuth;
axios = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,
})
});
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console) {
this.digestAuth = new AxiosDigestAuth({
axios: this.axios,
password,
username,
});
@@ -28,19 +23,7 @@ export class ReolinkCameraClient {
// }
// ]
async getMotionState() {
const url = new URL(`http://${this.host}/cgi-bin/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetMdState');
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(),
});
return {
value: !!response.data?.[0]?.value?.state,
data: response.data,
};
return getMotionState(this.digestAuth, this.username, this.password, this.host, this.channelId);
}
async jpegSnapshot() {
@@ -54,7 +37,8 @@ export class ReolinkCameraClient {
const response = await this.digestAuth.request({
url: url.toString(),
responseType: 'arraybuffer'
responseType: 'arraybuffer',
httpsAgent: reolinkHttpsAgent,
});
return Buffer.from(response.data);

View File

@@ -1,24 +1,23 @@
{
"name": "@scrypted/ring",
"version": "0.0.94",
"version": "0.0.96",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ring",
"version": "0.0.94",
"hasInstallScript": true,
"version": "0.0.96",
"dependencies": {
"@koush/ring-client-api": "file:../../external/ring-client-api",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.6.1",
"axios": "^0.24.0",
"rxjs": "^7.5.5"
"@types/node": "^16.9.0",
"axios": "^1.3.4",
"rxjs": "^7.8.0"
},
"optionalDependencies": {
"got": "11.8.2",
"socket.io-client": "^2.4.0"
"got": "11.8.6",
"socket.io-client": "^2.5.0"
}
},
"../../common": {
@@ -107,7 +106,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.69",
"version": "0.2.70",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -231,12 +230,19 @@
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
"optional": 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/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.14.4"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/backo2": {
@@ -299,6 +305,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"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/component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
@@ -362,6 +379,14 @@
"node": ">=10"
}
},
"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/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -422,6 +447,19 @@
}
}
},
"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/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -438,9 +476,9 @@
}
},
"node_modules/got": {
"version": "11.8.2",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
"integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==",
"version": "11.8.6",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"optional": true,
"dependencies": {
"@sindresorhus/is": "^4.0.0",
@@ -448,7 +486,7 @@
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
@@ -532,6 +570,25 @@
"node": ">=8"
}
},
"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/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@@ -589,6 +646,11 @@
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==",
"optional": true
},
"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/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",

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": "^16.6.1",
"axios": "^0.24.0",
"rxjs": "^7.5.5"
"@types/node": "^16.9.0",
"axios": "^1.3.4",
"rxjs": "^7.8.0"
},
"optionalDependencies": {
"got": "11.8.2",
"socket.io-client": "^2.4.0"
"got": "11.8.6",
"socket.io-client": "^2.5.0"
},
"version": "0.0.95"
"version": "0.0.96"
}

View File

@@ -4,7 +4,7 @@ import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
import { RtspServer } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp, replacePorts } from '@scrypted/common/src/sdp-utils';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import sdk, { BinarySensor, Camera, Device, DeviceProvider, EntrySensor, FFmpegInput, MediaObject, MediaStreamUrl, MotionSensor, OnOff, PictureOptions, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SecuritySystem, SecuritySystemMode, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import sdk, { Battery, BinarySensor, Camera, Device, DeviceProvider, EntrySensor, FFmpegInput, FloodSensor, MediaObject, MediaStreamUrl, MotionSensor, OnOff, PictureOptions, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, SecuritySystem, SecuritySystemMode, Setting, Settings, SettingValue, TamperSensor, VideoCamera } from '@scrypted/sdk';
import child_process, { ChildProcess } from 'child_process';
import dgram from 'dgram';
import { RtcpReceiverInfo, RtcpRrPacket } from '../../../external/werift/packages/rtp/src/rtcp/rr';
@@ -661,9 +661,13 @@ class RingCameraDevice extends ScryptedDeviceBase implements DeviceProvider, Cam
}
}
class RingContactSensor extends ScryptedDeviceBase implements EntrySensor {
class RingSensor extends ScryptedDeviceBase implements TamperSensor, Battery, EntrySensor, MotionSensor, FloodSensor {
updateState(data: RingDeviceData) {
this.entryOpen = data.faulted
this.tampered = data.tamperStatus === 'tamper';
this.batteryLevel = data.batteryLevel;
this.entryOpen = data.faulted;
this.motionDetected = data.faulted;
this.flooded = data.flood?.faulted || data.faulted;
}
}
@@ -761,7 +765,7 @@ export class RingLocationDevice extends ScryptedDeviceBase implements DeviceProv
async getDevice(nativeId: string) {
if (!this.devices.has(nativeId)) {
if (nativeId.endsWith('-sensor')) {
const sensor = new RingContactSensor(nativeId);
const sensor = new RingSensor(nativeId);
this.devices.set(nativeId, sensor);
} else {
const camera = new RingCameraDevice(this.plugin, this, nativeId);
@@ -1025,31 +1029,61 @@ class RingPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings
}
const sensors = (await location.getDevices()).filter(x => {
return x.data.status !== 'disabled' && (x.data.deviceType === RingDeviceType.ContactSensor || x.data.deviceType === RingDeviceType.RetrofitZone)
const supportedSensors = [
RingDeviceType.ContactSensor,
RingDeviceType.RetrofitZone,
RingDeviceType.TiltSensor,
RingDeviceType.MotionSensor,
RingDeviceType.FloodFreezeSensor,
RingDeviceType.WaterSensor,
]
return x.data.status !== 'disabled' && (supportedSensors.includes(x.data.deviceType))
});
for (const sensor of sensors) {
const nativeId = sensor.id.toString() + '-sensor';
const data: RingDeviceData = sensor.data;
const interfaces = [ScryptedInterface.TamperSensor];
switch (data.deviceType){
case RingDeviceType.ContactSensor:
case RingDeviceType.RetrofitZone:
case RingDeviceType.TiltSensor:
interfaces.push(ScryptedInterface.EntrySensor);
break;
case RingDeviceType.MotionSensor:
interfaces.push(ScryptedInterface.MotionSensor);
break;
case RingDeviceType.FloodFreezeSensor:
case RingDeviceType.WaterSensor:
interfaces.push(ScryptedInterface.FloodSensor);
break;
default: break;
}
if (data.batteryStatus !== 'none')
interfaces.push(ScryptedInterface.Battery);
const device: Device = {
info: {
model: sensor.data.deviceType,
model: data.deviceType,
manufacturer: 'Ring',
serialNumber: sensor.data.serialNumber ?? 'Unknown'
serialNumber: data.serialNumber ?? 'Unknown'
},
providerNativeId: location.id,
nativeId: nativeId,
name: sensor.name,
type: ScryptedDeviceType.Sensor,
interfaces: [ScryptedInterface.EntrySensor],
interfaces,
};
devices.push(device);
const getScryptedDevice = async () => {
const locationDevice = await this.getDevice(location.id);
const scryptedDevice = await locationDevice?.getDevice(nativeId);
return scryptedDevice as RingContactSensor;
return scryptedDevice as RingSensor;
}
sensor.onData.subscribe(async data => {
sensor.onData.subscribe(async (data: RingDeviceData) => {
const scryptedDevice = await getScryptedDevice();
scryptedDevice?.updateState(data)
});

4
sdk/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/sdk",
"version": "0.2.69",
"version": "0.2.71",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/sdk",
"version": "0.2.69",
"version": "0.2.71",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.2.69",
"version": "0.2.71",
"description": "",
"main": "dist/src/index.js",
"exports": {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/types",
"version": "0.2.64",
"version": "0.2.66",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/types",
"version": "0.2.64",
"version": "0.2.66",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",

View File

@@ -1,13 +1,13 @@
{
"name": "@scrypted/types",
"version": "0.2.64",
"version": "0.2.66",
"description": "",
"main": "dist/index.js",
"author": "",
"license": "ISC",
"scripts": {
"prepublishOnly": "npm run build",
"build": "rimraf dist gen && typedoc && ts-node ./src/build.ts && tsc && npm link"
"build": "rimraf dist gen && typedoc && ts-node ./src/build.ts && tsc"
},
"devDependencies": {
"@types/rimraf": "^3.0.2",

View File

@@ -316,6 +316,7 @@ class DeviceCreatorSettings(TypedDict):
class DeviceInformation(TypedDict):
firmware: str
ip: str
mac: str
managementUrl: str
manufacturer: str
@@ -368,6 +369,7 @@ class FFmpegInput(TypedDict):
inputArguments: list[str]
mediaStreamOptions: ResponseMediaStreamOptions
url: str
urls: list[str]
videoDecoderArguments: list[str]
pass
@@ -536,7 +538,7 @@ class RequestMediaStreamOptions(TypedDict):
prebuffer: float
prebufferBytes: float
refresh: bool
route: Any | Any
route: Any | Any | Any
tool: MediaStreamTool
video: VideoStreamOptions
pass
@@ -564,7 +566,7 @@ class RequestRecordingStreamOptions(TypedDict):
prebuffer: float
prebufferBytes: float
refresh: bool
route: Any | Any
route: Any | Any | Any
startTime: float
tool: MediaStreamTool
video: VideoStreamOptions

View File

@@ -574,7 +574,7 @@ export interface RequestMediaStreamOptions extends MediaStreamOptions {
* such as an NVR or restreamers.
* An external route will request that that provided route is exposed to the local network.
*/
route?: 'external' | 'direct';
route?: 'external' | 'direct' | 'internal';
/**
* Specify the stream refresh behavior when this stream is requested.
@@ -1416,12 +1416,22 @@ export interface MediaManager {
*/
getFilesPath(): Promise<string>;
}
export interface MediaStreamUrl {
url: string;
export interface MediaContainer {
container?: string;
mediaStreamOptions?: ResponseMediaStreamOptions;
}
export interface FFmpegInput extends MediaStreamUrl {
export interface MediaStreamUrl extends MediaContainer {
url: string;
}
export interface FFmpegInput extends MediaContainer {
/**
* The media url for this FFmpegInput.
*/
url?: string;
/**
* Alternate media urls for this FFmpegInput.
*/
urls?: string[];
inputArguments?: string[];
destinationVideoBitrate?: number;
h264EncoderArguments?: string[];
@@ -1434,6 +1444,7 @@ export interface DeviceInformation {
version?: string;
firmware?: string;
serialNumber?: string;
ip?: string;
mac?: string;
metadata?: any;
managementUrl?: string;

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.6.23",
"version": "0.6.24",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.6.23",
"version": "0.6.24",
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.6.24",
"version": "0.6.26",
"description": "",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",

View File

@@ -26,10 +26,14 @@ export async function listenZeroSingleClient(hostname?: string) {
clientPromise.catch(() => { });
let host = hostname;
if (!host || host === '0.0.0.0')
host = '127.0.0.1';
return {
server,
url: `tcp://127.0.0.1:${port}`,
host: '127.0.0.1',
url: `tcp://${host}:${port}`,
host,
port,
clientPromise,
}