mirror of
https://github.com/koush/scrypted.git
synced 2026-02-08 08:19:56 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6336407f15 | ||
|
|
38e3492137 | ||
|
|
255e426e2d | ||
|
|
fed1cf2a0d | ||
|
|
a5cb8c3fdc | ||
|
|
514d86144f | ||
|
|
21db7934c9 | ||
|
|
14e4b5c0e3 | ||
|
|
6478ad0411 | ||
|
|
81b7d432e9 | ||
|
|
dcbf5094f9 | ||
|
|
69ba181dfa | ||
|
|
88654275c1 | ||
|
|
62f28271ed | ||
|
|
0538130e78 | ||
|
|
95a3a16227 | ||
|
|
bfbf89ff69 | ||
|
|
e630589489 | ||
|
|
99d1e51f36 | ||
|
|
ab42ccd889 | ||
|
|
767af25aa0 | ||
|
|
7575dd82ce | ||
|
|
9307bbd09e | ||
|
|
68a9ec09e6 | ||
|
|
f8a548401f | ||
|
|
26d1f8e58c | ||
|
|
8772e25c8e | ||
|
|
378ac82c8c | ||
|
|
fcb1292ffd | ||
|
|
18112ee40f | ||
|
|
fa8b9dfe99 | ||
|
|
e7dff4edc9 |
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
7
packages/cli/.vscode/launch.json
vendored
7
packages/cli/.vscode/launch.json
vendored
@@ -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": [
|
||||
|
||||
4
packages/cli/package-lock.json
generated
4
packages/cli/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.0.57",
|
||||
"version": "1.0.58",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
|
||||
@@ -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();
|
||||
|
||||
18
packages/client/package-lock.json
generated
18
packages/client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
4
plugins/amcrest/package-lock.json
generated
4
plugins/amcrest/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.115",
|
||||
"version": "0.0.119",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
42
plugins/amcrest/src/probe.ts
Normal file
42
plugins/amcrest/src/probe.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
762
plugins/google-device-access/package-lock.json
generated
762
plugins/google-device-access/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
|
||||
2942
plugins/google-device-access/pubsub-server/package-lock.json
generated
2942
plugins/google-device-access/pubsub-server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
4
plugins/hikvision/package-lock.json
generated
4
plugins/hikvision/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
34
plugins/hikvision/src/probe.ts
Normal file
34
plugins/hikvision/src/probe.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
4
plugins/onvif/package-lock.json
generated
4
plugins/onvif/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
4
plugins/remote/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
10
plugins/remote/.npmignore
Normal file
10
plugins/remote/.npmignore
Normal 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
22
plugins/remote/.vscode/launch.json
vendored
Normal 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
4
plugins/remote/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
20
plugins/remote/.vscode/tasks.json
vendored
Normal file
20
plugins/remote/.vscode/tasks.json
vendored
Normal 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
3
plugins/remote/README.md
Normal 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
95
plugins/remote/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
plugins/remote/package.json
Normal file
39
plugins/remote/package.json
Normal 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
332
plugins/remote/src/main.ts
Normal 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();
|
||||
13
plugins/remote/tsconfig.json
Normal file
13
plugins/remote/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
4
plugins/reolink/package-lock.json
generated
4
plugins/reolink/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.13",
|
||||
"version": "0.0.16",
|
||||
"description": "Reolink Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -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() {
|
||||
|
||||
23
plugins/reolink/src/probe.ts
Normal file
23
plugins/reolink/src/probe.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
96
plugins/ring/package-lock.json
generated
96
plugins/ring/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
Submodule sdk/developer.scrypted.app updated: c4724e2e90...62a1d3d9e7
4
sdk/package-lock.json
generated
4
sdk/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.69",
|
||||
"version": "0.2.71",
|
||||
"description": "",
|
||||
"main": "dist/src/index.js",
|
||||
"exports": {
|
||||
|
||||
4
sdk/types/package-lock.json
generated
4
sdk/types/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.6.24",
|
||||
"version": "0.6.26",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user