mirror of
https://github.com/koush/scrypted.git
synced 2026-05-04 21:30:30 +01:00
Plugin for Synology Surveillance Station
This commit is contained in:
4
plugins/synology-ss/.gitignore
vendored
Normal file
4
plugins/synology-ss/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
6
plugins/synology-ss/.npmignore
Normal file
6
plugins/synology-ss/.npmignore
Normal file
@@ -0,0 +1,6 @@
|
||||
out/
|
||||
*.map
|
||||
fs
|
||||
src
|
||||
.vscode
|
||||
dist/*.js
|
||||
22
plugins/synology-ss/.vscode/launch.json
vendored
Normal file
22
plugins/synology-ss/.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/synology-ss/.vscode/settings.json
vendored
Normal file
4
plugins/synology-ss/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1"
|
||||
}
|
||||
20
plugins/synology-ss/.vscode/tasks.json
vendored
Normal file
20
plugins/synology-ss/.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}",
|
||||
},
|
||||
]
|
||||
}
|
||||
15
plugins/synology-ss/README.md
Normal file
15
plugins/synology-ss/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# @scrypted/synology-ss
|
||||
|
||||
## npm commands
|
||||
* npm run scrypted-webpack
|
||||
* npm run scrypted-deploy <ipaddress>
|
||||
* npm run scrypted-debug <ipaddress>
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
|
||||
## Visual Studio Code configuration
|
||||
|
||||
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
|
||||
* Launch Scrypted Debugger from the launch menu.
|
||||
135
plugins/synology-ss/package-lock.json
generated
Normal file
135
plugins/synology-ss/package-lock.json
generated
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"name": "@scrypted/synology-ss",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/synology-ss",
|
||||
"version": "0.0.1",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.6.1"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.0.93",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
|
||||
"integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
|
||||
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
|
||||
"integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.5",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
|
||||
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
39
plugins/synology-ss/package.json
Normal file
39
plugins/synology-ss/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@scrypted/synology-ss",
|
||||
"version": "0.0.1",
|
||||
"description": "A Synology Surveillance Station plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json",
|
||||
"scrypted-webpack": "scrypted-webpack"
|
||||
},
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
"synology",
|
||||
"surveillance station"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Synology Surveillance Station",
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.6.1"
|
||||
}
|
||||
}
|
||||
188
plugins/synology-ss/src/api/synology-api-client.ts
Normal file
188
plugins/synology-ss/src/api/synology-api-client.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
const errorCodeDescriptions = {
|
||||
'100': 'Unknown error',
|
||||
'101': 'Invalid parameters',
|
||||
'102': 'API does not exist',
|
||||
'103': 'Method does not exist',
|
||||
'104': 'This API version is not supported',
|
||||
'105': 'Insufficient user privilege',
|
||||
'106': 'Connection time out',
|
||||
'107': 'Multiple login detected'
|
||||
}
|
||||
|
||||
export class SynologyApiClient {
|
||||
private apiInfo: Promise<Record<string, SynologyApiInfo>>;
|
||||
private readonly client: AxiosInstance;
|
||||
|
||||
public readonly url: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
this.client = axios.create({
|
||||
baseURL: `${url}/webapi/`,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Fetch info about API method paths and versions in the background
|
||||
this.apiInfo = this.queryApiInfo();
|
||||
}
|
||||
|
||||
public async getCameraLiveViewPath(cameraIds: string[]): Promise<SynologyCameraLiveViewPath[]> {
|
||||
const params = {
|
||||
api: 'SYNO.SurveillanceStation.Camera',
|
||||
version: 9,
|
||||
method: 'GetLiveViewPath',
|
||||
idList: cameraIds.join(',')
|
||||
};
|
||||
|
||||
return await this.sendRequest<SynologyCameraLiveViewPath[]>(params);
|
||||
}
|
||||
|
||||
public async getCameraSnapshot(cameraId: number | string) {
|
||||
const params = {
|
||||
api: 'SYNO.SurveillanceStation.Camera',
|
||||
version: 9,
|
||||
method: 'GetSnapshot',
|
||||
id: cameraId
|
||||
};
|
||||
|
||||
const response = await this.client.get<ArrayBuffer>(await this.getApiPath(params.api), { params, responseType: 'arraybuffer' });
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async listCameras(): Promise<SynologyCamera[]> {
|
||||
const params = {
|
||||
api: 'SYNO.SurveillanceStation.Camera',
|
||||
version: 9,
|
||||
method: 'List',
|
||||
privCamType: 1,
|
||||
streamInfo: true,
|
||||
basic: true
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<SynologyApiListCamerasResponse>(params);
|
||||
|
||||
return response.cameras;
|
||||
}
|
||||
|
||||
public async login(account: string, password: string): Promise<void> {
|
||||
const params = {
|
||||
api: 'SYNO.API.Auth',
|
||||
version: 6,
|
||||
method: 'login',
|
||||
session: 'SurveillanceStation',
|
||||
account: account,
|
||||
passwd: password
|
||||
};
|
||||
|
||||
const errorCodeDescs = {
|
||||
'400': 'Invalid password',
|
||||
'401': 'Guest or disabled account',
|
||||
'402': 'Permission denied',
|
||||
'403': 'One time password not specified',
|
||||
'404': 'One time password authenticate failed',
|
||||
'405': 'App portal incorrect',
|
||||
'406': 'OTP code enforced',
|
||||
'407': 'Max Tries (if auto blocking is set to true)',
|
||||
'408': 'Password Expired Can not Change',
|
||||
'409': 'Password Expired',
|
||||
'410': 'Password must change (when first time use or after reset password by admin)',
|
||||
'411': 'Account Locked (when account max try exceed)'
|
||||
};
|
||||
|
||||
await this.sendRequest(params, null, true, errorCodeDescs);
|
||||
}
|
||||
|
||||
private async queryApiInfo(): Promise<Record<string, SynologyApiInfo>> {
|
||||
const params = {
|
||||
api: 'SYNO.API.Info',
|
||||
version: 1,
|
||||
method: 'Query',
|
||||
query: 'SYNO.API.Auth,SYNO.SurveillanceStation.'
|
||||
};
|
||||
|
||||
return await this.sendRequest<Record<string, SynologyApiInfo>>(params, 'query.cgi');
|
||||
}
|
||||
|
||||
private async getApiPath(api: string): Promise<string> {
|
||||
return (await this.apiInfo)[api].path;
|
||||
}
|
||||
|
||||
private async sendRequest<T>(params: SynologyApiRequestParams, url?: string, storeCookies: boolean = false,
|
||||
extraErrorCodes?: Record<string, string>): Promise<T> {
|
||||
const response = await this.client.get<SynologyApiResponse<T>>(url ?? await this.getApiPath(params.api), { params });
|
||||
|
||||
if (!response.data?.success) {
|
||||
if (response.data?.error?.code) {
|
||||
const errorCodeLookup = { ...errorCodeDescriptions, ...extraErrorCodes}
|
||||
throw new Error(`${errorCodeLookup[response.data.error.code]} (error code ${response.data.error.code})`)
|
||||
} else {
|
||||
throw new Error(`Synology API call failed with status code ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (storeCookies) {
|
||||
this.client.defaults.headers.common['Cookie'] = response.headers["set-cookie"].join('; ');
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SynologyApiInfo {
|
||||
path: string;
|
||||
minVersion: number;
|
||||
maxVersion: number;
|
||||
}
|
||||
|
||||
export interface SynologyApiError {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface SynologyApiRequestParams {
|
||||
api: string,
|
||||
version: number,
|
||||
method: string,
|
||||
}
|
||||
|
||||
interface SynologyApiResponse<T> {
|
||||
data?: T;
|
||||
error?: SynologyApiError;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface SynologyApiListCamerasResponse {
|
||||
total: number;
|
||||
cameras: SynologyCamera[];
|
||||
}
|
||||
|
||||
export interface SynologyCamera {
|
||||
firmware: string;
|
||||
id: number;
|
||||
model: string;
|
||||
newName: string;
|
||||
stream1?: SynologyCameraStream;
|
||||
stream2?: SynologyCameraStream;
|
||||
stream3?: SynologyCameraStream;
|
||||
vendor: string;
|
||||
}
|
||||
|
||||
export interface SynologyCameraLiveViewPath {
|
||||
id: string;
|
||||
mjpegHttpPath: string;
|
||||
multicstPath: string;
|
||||
mxpegHttpPath: string;
|
||||
rtspOverHttpPath: string;
|
||||
rtspPath: string;
|
||||
}
|
||||
|
||||
export interface SynologyCameraStream {
|
||||
id: string;
|
||||
fps?: number;
|
||||
resolution?: string;
|
||||
bitrateCtrl?: number;
|
||||
quality?: string;
|
||||
constantBitrate?: string;
|
||||
}
|
||||
283
plugins/synology-ss/src/main.ts
Normal file
283
plugins/synology-ss/src/main.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, ScryptedDeviceType, VideoCamera, MediaObject, Device, ScryptedInterface, Camera, MediaStreamOptions, PictureOptions } from "@scrypted/sdk";
|
||||
import { recommendRebroadcast } from "../../rtsp/src/recommend";
|
||||
import {SynologyApiClient, SynologyCameraStream, SynologyCamera} from "./api/synology-api-client";
|
||||
|
||||
const { log, deviceManager, mediaManager } = sdk;
|
||||
|
||||
class SynologyCameraDevice extends ScryptedDeviceBase implements Camera, Settings, VideoCamera {
|
||||
private provider: SynologySurveillanceStation;
|
||||
private streams: SynologyCameraStream[];
|
||||
|
||||
constructor(provider: SynologySurveillanceStation, nativeId: string, camera: SynologyCamera) {
|
||||
super(nativeId);
|
||||
this.provider = provider;
|
||||
|
||||
this.streams = [
|
||||
{ ...camera.stream1, id: '1' },
|
||||
{ ...camera.stream2, id: '2' },
|
||||
{ ...camera.stream3, id: '3' },
|
||||
].filter(s => !!s.resolution);
|
||||
|
||||
this.motionDetected = false;
|
||||
if (this.interfaces.includes(ScryptedInterface.BinarySensor)) {
|
||||
this.binaryState = false;
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultOrderedVideoStreamOptions(vsos: MediaStreamOptions[]) {
|
||||
if (!vsos || !vsos.length)
|
||||
return vsos;
|
||||
const defaultStream = this.getDefaultStream(vsos);
|
||||
if (!defaultStream)
|
||||
return vsos;
|
||||
vsos = vsos.filter(vso => vso.id !== defaultStream?.id);
|
||||
vsos.unshift(defaultStream);
|
||||
return vsos;
|
||||
}
|
||||
|
||||
private getDefaultStream(vsos: MediaStreamOptions[]) {
|
||||
let defaultStreamIndex = vsos.findIndex(vso => vso.id === this.storage.getItem('defaultStream'));
|
||||
if (defaultStreamIndex === -1)
|
||||
defaultStreamIndex = 0;
|
||||
|
||||
return vsos[defaultStreamIndex];
|
||||
}
|
||||
|
||||
public async getSettings(): Promise<Setting[]> {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
const defaultStream = this.getDefaultStream(vsos);
|
||||
return [
|
||||
{
|
||||
title: 'Default Stream',
|
||||
key: 'defaultStream',
|
||||
value: defaultStream?.name,
|
||||
choices: vsos.map(vso => vso.name),
|
||||
description: 'The default stream to use when not specified',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public async putSetting(key: string, value: string | number | boolean) {
|
||||
if (key === 'defaultStream') {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
const stream = vsos.find(vso => vso.name === value);
|
||||
this.storage.setItem('defaultStream', stream?.id);
|
||||
}
|
||||
else {
|
||||
this.storage.setItem(key, value?.toString());
|
||||
}
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
|
||||
private async getSnapshot(options?: PictureOptions): Promise<Buffer> {
|
||||
const data = await this.provider.api.getCameraSnapshot(this.nativeId);
|
||||
|
||||
return Buffer.from(data);
|
||||
}
|
||||
|
||||
public async takePicture(options?: PictureOptions): Promise<MediaObject> {
|
||||
const buffer = await this.getSnapshot(options);
|
||||
return mediaManager.createMediaObject(buffer, 'image/jpeg');
|
||||
}
|
||||
|
||||
public async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
const vso = vsos.find(check => check.id === options?.id) || this.getDefaultStream(vsos);
|
||||
|
||||
const rtspChannel = this.streams.find(check => check.id === vso.id);
|
||||
|
||||
const liveViewPaths = await this.provider.api.getCameraLiveViewPath([this.nativeId]);
|
||||
if (!liveViewPaths?.length)
|
||||
throw new Error(`Unable to locate RTSP stream for camera ${this.nativeId}`);
|
||||
|
||||
return mediaManager.createFFmpegMediaObject({
|
||||
inputArguments: [
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
'-analyzeduration', '15000000',
|
||||
'-probesize', '100000000',
|
||||
"-reorder_queue_size",
|
||||
"1024",
|
||||
"-max_delay",
|
||||
"20000000",
|
||||
"-i",
|
||||
liveViewPaths[0].rtspPath,
|
||||
],
|
||||
mediaStreamOptions: this.createMediaStreamOptions(rtspChannel),
|
||||
});
|
||||
}
|
||||
|
||||
private createMediaStreamOptions(stream: SynologyCameraStream) {
|
||||
const ret: MediaStreamOptions = {
|
||||
id: stream.id,
|
||||
name: stream.id,
|
||||
video: {
|
||||
codec: 'h264',
|
||||
width: parseInt(stream.resolution.substring(0, stream.resolution.indexOf('x'))),
|
||||
height: parseInt(stream.resolution.substring(stream.resolution.indexOf('x') + 1)),
|
||||
bitrate: parseInt(stream.constantBitrate, 10),
|
||||
fps: stream.fps
|
||||
},
|
||||
audio: {
|
||||
codec: 'aac',
|
||||
},
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
public async getVideoStreamOptions(): Promise<MediaStreamOptions[]> {
|
||||
const video = this.streams.map(channel => this.createMediaStreamOptions(channel));
|
||||
|
||||
return this.getDefaultOrderedVideoStreamOptions(video);
|
||||
}
|
||||
|
||||
public async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings, DeviceProvider {
|
||||
private cameras: SynologyCamera[];
|
||||
private cameraDevices: Map<string, SynologyCameraDevice> = new Map();
|
||||
api: SynologyApiClient;
|
||||
private startup: Promise<void>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.startup = this.discoverDevices(0);
|
||||
recommendRebroadcast();
|
||||
}
|
||||
|
||||
public async discoverDevices(duration: number): Promise<void> {
|
||||
const url = this.getSetting('url');
|
||||
const username = this.getSetting('username');
|
||||
const password = this.getSetting('password');
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
if (!url) {
|
||||
this.log.a('Must provide URL.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!username) {
|
||||
this.log.a('Must provide username.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
this.log.a('Must provide password.');
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.api || url !== this.api.url) {
|
||||
this.api = new SynologyApiClient(url);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.login(username, password);
|
||||
|
||||
this.cameras = await this.api.listCameras();
|
||||
|
||||
if (!this.cameras) {
|
||||
this.console.error('Cameras failed to load. Retrying in 10 seconds.');
|
||||
setTimeout(() => {
|
||||
this.discoverDevices(0);
|
||||
}, 100000);
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.info(`Discovered ${this.cameras.length} camera(s)`);
|
||||
|
||||
const devices: Device[] = [];
|
||||
for (let camera of this.cameras) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: camera.newName,
|
||||
nativeId: '' + camera.id,
|
||||
info: {
|
||||
manufacturer: camera.vendor,
|
||||
model: camera.model,
|
||||
firmware: camera.firmware,
|
||||
serialNumber: '' + camera.id
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.VideoCamera,
|
||||
],
|
||||
type: ScryptedDeviceType.Camera
|
||||
};
|
||||
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const d of devices) {
|
||||
await deviceManager.onDeviceDiscovered(d);
|
||||
}
|
||||
|
||||
// todo: this was done, october 31st. remove sometime later.
|
||||
// todo: uncomment after implementing per providerNativeId onDevicesChanged.
|
||||
// await deviceManager.onDevicesChanged({
|
||||
// providerNativeId: this.nativeId,
|
||||
// devices
|
||||
// });
|
||||
|
||||
for (const device of devices) {
|
||||
this.getDevice(device.nativeId);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.log.a(`login error: ${e}`);
|
||||
this.console.error('login error', e);
|
||||
}
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
await this.startup;
|
||||
if (this.cameraDevices.has(nativeId))
|
||||
return this.cameraDevices.get(nativeId);
|
||||
const camera = this.cameras.find(camera => ('' + camera.id) === nativeId);
|
||||
if (!camera)
|
||||
throw new Error('camera not found?');
|
||||
const ret = new SynologyCameraDevice(this, nativeId, camera);
|
||||
this.cameraDevices.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
getSetting(key: string): string {
|
||||
return this.storage.getItem(key);
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const ret: Setting[] = [
|
||||
{
|
||||
key: 'username',
|
||||
title: 'Username',
|
||||
value: this.getSetting('username'),
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
title: 'Password',
|
||||
type: 'password',
|
||||
value: this.getSetting('password'),
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
title: 'Synology Surveillance Station URL',
|
||||
placeholder: 'http://192.168.1.100:5000',
|
||||
value: this.getSetting('url'),
|
||||
},
|
||||
];
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string | number) {
|
||||
this.storage.setItem(key, value.toString());
|
||||
this.discoverDevices(0);
|
||||
}
|
||||
}
|
||||
|
||||
export default new SynologySurveillanceStation();
|
||||
Reference in New Issue
Block a user