diff --git a/plugins/ffmpeg-camera/README.md b/plugins/ffmpeg-camera/README.md index 688d48314..d25a0f051 100644 --- a/plugins/ffmpeg-camera/README.md +++ b/plugins/ffmpeg-camera/README.md @@ -1,15 +1,3 @@ -# RTSP Cameras and Streams Plugin +# FFmpeg Camera Plugin for Scrypted -## npm commands - * npm run scrypted-webpack - * npm run scrypted-deploy - * npm run scrypted-debug - -## 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. +Add FFmpeg Camera video streams to Scrypted. Cameras often have still image snapshot URLs. To use these snapshots you will also need the [Snapshot Plugin](#/component/plugin/install/@scrypted/snapshot). diff --git a/plugins/ffmpeg-camera/package-lock.json b/plugins/ffmpeg-camera/package-lock.json index 3c554c6d4..3556bfc60 100644 --- a/plugins/ffmpeg-camera/package-lock.json +++ b/plugins/ffmpeg-camera/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/ffmpeg-camera", - "version": "0.0.8", + "version": "0.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/ffmpeg-camera", - "version": "0.0.8", + "version": "0.0.9", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/ffmpeg-camera/package.json b/plugins/ffmpeg-camera/package.json index 7f12add6c..0472ed8ea 100644 --- a/plugins/ffmpeg-camera/package.json +++ b/plugins/ffmpeg-camera/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/ffmpeg-camera", - "version": "0.0.8", + "version": "0.0.9", "description": "FFmpeg Camera Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/ffmpeg-camera/src/common.ts b/plugins/ffmpeg-camera/src/common.ts index 802e1e368..d8c06103e 100644 --- a/plugins/ffmpeg-camera/src/common.ts +++ b/plugins/ffmpeg-camera/src/common.ts @@ -4,7 +4,7 @@ import AxiosDigestAuth from '@koush/axios-digest-auth'; import https from 'https'; import { randomBytes } from "crypto"; -const { log, deviceManager, mediaManager } = sdk; +const { deviceManager, mediaManager } = sdk; const httpsAgent = new https.Agent({ rejectUnauthorized: false @@ -22,8 +22,21 @@ export abstract class CameraBase extends ScryptedD super(nativeId); } - getSnapshotUrl() { - return this.storage.getItem('snapshotUrl'); + protected async takePictureUrl(snapshotUrl: string) { + if (!this.snapshotAuth) { + this.snapshotAuth = new AxiosDigestAuth({ + username: this.getUsername(), + password: this.getPassword(), + }); + } + const response = await this.snapshotAuth.request({ + httpsAgent, + method: "GET", + responseType: 'arraybuffer', + url: snapshotUrl, + }); + + return mediaManager.createMediaObject(Buffer.from(response.data), response.headers['Content-Type'] || 'image/jpeg'); } async takePicture(option?: PictureOptions): Promise { @@ -35,28 +48,7 @@ export abstract class CameraBase extends ScryptedD return this.pendingPicture; } - async takePictureThrottled(option?: PictureOptions): Promise { - const snapshotUrl = this.getSnapshotUrl(); - if (!snapshotUrl) { - throw new Error('Camera has no snapshot URL'); - } - - if (!this.snapshotAuth) { - this.snapshotAuth = new AxiosDigestAuth({ - username: this.getUsername(), - password: this.getPassword(), - }); - } - - const response = await this.snapshotAuth.request({ - httpsAgent, - method: "GET", - responseType: 'arraybuffer', - url: snapshotUrl, - }); - - return mediaManager.createMediaObject(Buffer.from(response.data), response.headers['Content-Type'] || 'image/jpeg'); - } + abstract takePictureThrottled(option?: PictureOptions): Promise; async getPictureOptions(): Promise { return; @@ -92,21 +84,8 @@ export abstract class CameraBase extends ScryptedD abstract createVideoStream(options?: T): Promise; - async getSnapshotUrlSettings(): Promise { - return [ - { - key: 'snapshotUrl', - title: 'Snapshot URL', - placeholder: 'http://192.168.1.100[:80]/snapshot.jpg', - value: this.getSnapshotUrl(), - description: 'Optional: The snapshot URL that will returns the current JPEG image.' - }, - ]; - } - async getUrlSettings(): Promise { return [ - ...await this.getSnapshotUrlSettings(), ]; } @@ -130,7 +109,7 @@ export abstract class CameraBase extends ScryptedD defaultStreamIndex = defaultStreamIndex || 0; return vsos?.[defaultStreamIndex]; } - + async getStreamSettings(): Promise { try { const vsos = await this.getVideoStreamOptions(); @@ -208,16 +187,6 @@ export abstract class CameraBase extends ScryptedD async putSetting(key: string, value: SettingValue) { this.putSettingBase(key, value); - - if (key === 'snapshotUrl') { - let interfaces = this.providedInterfaces; - if (!value) - interfaces = interfaces.filter(iface => iface !== ScryptedInterface.Camera) - else - interfaces.push(ScryptedInterface.Camera); - - this.provider.updateDevice(this.nativeId, this.providedName, interfaces); - } } } @@ -271,7 +240,7 @@ export abstract class CameraProviderBase extends S }); } - async putSetting(key: string, value: string | number) { + async putSetting(value: string | number) { // generate a random id const nativeId = randomBytes(4).toString('hex'); const name = value.toString(); diff --git a/plugins/ffmpeg-camera/src/main.ts b/plugins/ffmpeg-camera/src/main.ts index c502e0801..922627012 100644 --- a/plugins/ffmpeg-camera/src/main.ts +++ b/plugins/ffmpeg-camera/src/main.ts @@ -1,12 +1,17 @@ -import sdk, { FFMpegInput, Intercom, MediaObject, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue } from "@scrypted/sdk"; +import sdk, { FFMpegInput, Intercom, MediaObject, PictureOptions, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue } from "@scrypted/sdk"; import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "./common"; import { StorageSettings } from "../../../common/src/settings"; import { ffmpegLogInitialOutput, safePrintFFmpegArguments } from "../../../common/src/media-helpers"; import child_process, { ChildProcess } from "child_process"; +import { recommendDumbPlugins } from "./recommend"; -const { log, deviceManager, mediaManager } = sdk; +const { mediaManager } = sdk; class FFmpegCamera extends CameraBase implements Intercom { + takePictureThrottled(option?: PictureOptions): Promise { + throw new Error("The RTSP Camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL."); + } + storageSettings = new StorageSettings(this, { ffmpegInputs: { title: 'FFmpeg Input Stream Arguments', @@ -52,7 +57,6 @@ class FFmpegCamera extends CameraBase implements Intercom // this might be usable as a url so check that. let url: string; try { - const parsedUrl = new URL(ffmpegInput); } catch (e) { } @@ -96,7 +100,6 @@ class FFmpegCamera extends CameraBase implements Intercom async getUrlSettings(): Promise { return [ - ...await this.getSnapshotUrlSettings(), ...await this.getFFmpegInputSettings(), ]; } @@ -121,6 +124,11 @@ class FFmpegCamera extends CameraBase implements Intercom } class FFmpegProvider extends CameraProviderBase { + constructor(nativeId?: string) { + super(nativeId); + recommendDumbPlugins(); + } + createCamera(nativeId: string): FFmpegCamera { return new FFmpegCamera(nativeId, this); } diff --git a/plugins/ffmpeg-camera/src/recommend.ts b/plugins/ffmpeg-camera/src/recommend.ts index 07110d5c8..26f1216f1 100644 --- a/plugins/ffmpeg-camera/src/recommend.ts +++ b/plugins/ffmpeg-camera/src/recommend.ts @@ -5,3 +5,10 @@ export async function recommendRebroadcast() { '@scrypted/prebuffer-mixin': 'Rebroadcast', }); } + +export async function recommendDumbPlugins() { + alertRecommendedPlugins({ + '@scrypted/snapshot': 'Snapshot Plugin', + '@scrypted/opencv': 'OpenCV Motion Detection', + }); +} diff --git a/plugins/rtsp/README.md b/plugins/rtsp/README.md index b4ffc63ed..dd58ad94a 100644 --- a/plugins/rtsp/README.md +++ b/plugins/rtsp/README.md @@ -1,34 +1,3 @@ -# RTSP Cameras and Streams Plugin +# RTSP Camera Plugin for Scrypted -## npm commands - * npm run scrypted-webpack - * npm run scrypted-deploy - * npm run scrypted-debug - -## 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. - -# Setup and Configuration - - 1. Once the plugin is installed, click "Add a device" and give the RTSP Camera a name. - - 2. Then under "Settings" and then "General", type in the username and password for your RTSP Stream. Click the green arrow to save the changes. - - 3. Then add the RTSP Stream Link. - - ie: rtsp://:// - - *Please note that RTSP Streams differ between each camera make and model.* - - If your camera has support for a substream, click the "Add" button to add another RTSP Stream URL. - - 4. Once you are done, Click "Save RTSP Stream URL" - - Enable "No Audio" if the camera does not have audio or if you want to mute audio. - +Add RTSP Camera video streams to Scrypted. RTSP Cameras often have still image snapshot URLs. To use these snapshots you will also need the [Snapshot Plugin](#/component/plugin/install/@scrypted/snapshot). diff --git a/plugins/rtsp/package-lock.json b/plugins/rtsp/package-lock.json index dc79b4fea..00e92cbea 100644 --- a/plugins/rtsp/package-lock.json +++ b/plugins/rtsp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/rtsp", - "version": "0.0.46", + "version": "0.0.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/rtsp", - "version": "0.0.46", + "version": "0.0.47", "license": "Apache", "dependencies": { "@koush/axios-digest-auth": "^0.8.5", diff --git a/plugins/rtsp/package.json b/plugins/rtsp/package.json index afa95769e..5b52d9a0f 100644 --- a/plugins/rtsp/package.json +++ b/plugins/rtsp/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/rtsp", - "version": "0.0.46", + "version": "0.0.47", "description": "RTSP Cameras and Streams Plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/rtsp/src/recommend.ts b/plugins/rtsp/src/recommend.ts deleted file mode 100644 index 07110d5c8..000000000 --- a/plugins/rtsp/src/recommend.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { alertRecommendedPlugins } from "@scrypted/common/src/alert-recommended-plugins"; - -export async function recommendRebroadcast() { - alertRecommendedPlugins({ - '@scrypted/prebuffer-mixin': 'Rebroadcast', - }); -} diff --git a/plugins/rtsp/src/rtsp.ts b/plugins/rtsp/src/rtsp.ts index 25b475fe2..d388913d4 100644 --- a/plugins/rtsp/src/rtsp.ts +++ b/plugins/rtsp/src/rtsp.ts @@ -1,18 +1,18 @@ -import sdk, { Setting, MediaObject, MediaStreamOptions, ScryptedInterface, FFMpegInput, PictureOptions, SettingValue } from "@scrypted/sdk"; +import sdk, { Setting, MediaObject, ScryptedInterface, FFMpegInput, PictureOptions, SettingValue } from "@scrypted/sdk"; import { EventEmitter } from "stream"; -import https from 'https'; import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "../../ffmpeg-camera/src/common"; import url from 'url'; +import { recommendDumbPlugins } from "./../../ffmpeg-camera/src/recommend"; export { UrlMediaStreamOptions } from "../../ffmpeg-camera/src/common"; -const { log, deviceManager, mediaManager } = sdk; - -const httpsAgent = new https.Agent({ - rejectUnauthorized: false -}); +const { mediaManager } = sdk; export class RtspCamera extends CameraBase { + takePictureThrottled(option?: PictureOptions): Promise { + throw new Error("The RTSP Camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL."); + } + createRtspMediaStreamOptions(url: string, index: number) { return { id: `channel${index}`, @@ -133,7 +133,6 @@ export class RtspCamera extends CameraBase { async getUrlSettings(): Promise { return [ - ...await this.getSnapshotUrlSettings(), ...await this.getRtspUrlSettings(), ]; } @@ -194,28 +193,11 @@ export abstract class RtspSmartCamera extends RtspCamera { } async takePictureThrottled(option?: PictureOptions) { - if (this.showSnapshotUrlOverride() && this.getSnapshotUrl()) { - return super.takePictureThrottled(option); - } - return this.takeSmartCameraPicture(option);; } abstract takeSmartCameraPicture(options?: PictureOptions): Promise; - async getSnapshotUrlSettings(): Promise { - return [ - { - key: 'snapshotUrl', - group: 'Advanced', - title: 'Snapshot URL Override', - placeholder: 'http://192.168.1.100[:80]/snapshot.jpg', - value: this.storage.getItem('snapshotUrl'), - description: 'Override the snapshot URL that will returns the current JPEG image.' - }, - ]; - } - async getRtspUrlSettings(): Promise { return [ { @@ -253,12 +235,6 @@ export abstract class RtspSmartCamera extends RtspCamera { ); } - if (this.showSnapshotUrlOverride()) { - ret.push( - ... await this.getSnapshotUrlSettings(), - ); - } - return ret; } @@ -304,10 +280,6 @@ export abstract class RtspSmartCamera extends RtspCamera { return true; } - showSnapshotUrlOverride() { - return true; - } - getHttpAddress() { return `${this.getIPAddress()}:${this.storage.getItem('httpPort') || 80}`; } @@ -316,7 +288,7 @@ export abstract class RtspSmartCamera extends RtspCamera { this.storage.setItem('httpPort', port); } - getRtspUrlOverride(options?: MediaStreamOptions) { + getRtspUrlOverride() { if (!this.showRtspUrlOverride()) return; return this.storage.getItem('rtspUrlOverride'); @@ -350,6 +322,11 @@ export abstract class RtspSmartCamera extends RtspCamera { } export class RtspProvider extends CameraProviderBase { + constructor(nativeId?: string) { + super(nativeId); + recommendDumbPlugins(); + } + createCamera(nativeId: string): RtspCamera { return new RtspCamera(nativeId, this); } diff --git a/plugins/snapshot/.gitignore b/plugins/snapshot/.gitignore new file mode 100644 index 000000000..c3f88796f --- /dev/null +++ b/plugins/snapshot/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +out/ +node_modules/ +dist/ +.venv diff --git a/plugins/snapshot/.npmignore b/plugins/snapshot/.npmignore new file mode 100644 index 000000000..72f2e2832 --- /dev/null +++ b/plugins/snapshot/.npmignore @@ -0,0 +1,9 @@ +.DS_Store +out/ +node_modules/ +*.map +fs +src +.vscode +dist/*.js +.venv diff --git a/plugins/snapshot/.vscode/launch.json b/plugins/snapshot/.vscode/launch.json new file mode 100644 index 000000000..0669f79b4 --- /dev/null +++ b/plugins/snapshot/.vscode/launch.json @@ -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": [ + "/**" + ], + "preLaunchTask": "scrypted: deploy+debug", + "sourceMaps": true, + "localRoot": "${workspaceFolder}/out", + "remoteRoot": "/plugin/", + "type": "pwa-node" + } + ] +} \ No newline at end of file diff --git a/plugins/snapshot/.vscode/settings.json b/plugins/snapshot/.vscode/settings.json new file mode 100644 index 000000000..77ccdbd6d --- /dev/null +++ b/plugins/snapshot/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "scrypted.debugHost": "127.0.0.1", +} \ No newline at end of file diff --git a/plugins/snapshot/.vscode/tasks.json b/plugins/snapshot/.vscode/tasks.json new file mode 100644 index 000000000..4d922a539 --- /dev/null +++ b/plugins/snapshot/.vscode/tasks.json @@ -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}", + }, + ] +} diff --git a/plugins/snapshot/README.md b/plugins/snapshot/README.md new file mode 100644 index 000000000..864393b93 --- /dev/null +++ b/plugins/snapshot/README.md @@ -0,0 +1,3 @@ +# Snapshot Plugin for Scrypted + +Add custom Snapshot URLs to any camera. diff --git a/plugins/snapshot/package-lock.json b/plugins/snapshot/package-lock.json new file mode 100644 index 000000000..70d6b62b9 --- /dev/null +++ b/plugins/snapshot/package-lock.json @@ -0,0 +1,211 @@ +{ + "name": "@scrypted/snapshot", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@scrypted/snapshot", + "dependencies": { + "@koush/axios-digest-auth": "^0.8.5", + "@types/node": "^16.6.1", + "axios": "^0.24.0" + }, + "devDependencies": { + "@scrypted/common": "file:../../common", + "@scrypted/sdk": "file:../../sdk" + }, + "version": "0.0.2" + }, + "../../common": { + "name": "@scrypted/common", + "version": "1.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, + "../../sdk": { + "name": "@scrypted/sdk", + "version": "0.0.173", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/preset-typescript": "^7.16.7", + "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", + "ncp": "^2.0.0", + "raw-loader": "^4.0.2", + "rimraf": "^3.0.2", + "tmp": "^0.2.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" + }, + "devDependencies": { + "@types/node": "^16.11.1", + "@types/stringify-object": "^4.0.0", + "stringify-object": "^3.3.0", + "ts-node": "^10.4.0", + "typedoc": "^0.22.8", + "typescript-json-schema": "^0.50.1", + "webpack-bundle-analyzer": "^4.5.0" + } + }, + "node_modules/@koush/axios-digest-auth": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz", + "integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==", + "dependencies": { + "auth-header": "^1.0.0", + "axios": "^0.21.4" + } + }, + "node_modules/@koush/axios-digest-auth/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/@scrypted/common": { + "resolved": "../../common", + "link": true + }, + "node_modules/@scrypted/sdk": { + "resolved": "../../sdk", + "link": true + }, + "node_modules/@types/node": { + "version": "16.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", + "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==" + }, + "node_modules/auth-header": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz", + "integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA==" + }, + "node_modules/axios": { + "version": "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.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + } + }, + "dependencies": { + "@koush/axios-digest-auth": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz", + "integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==", + "requires": { + "auth-header": "^1.0.0", + "axios": "^0.21.4" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + } + } + }, + "@scrypted/common": { + "version": "file:../../common", + "requires": { + "@koush/werift": "file:../external/werift/packages/webrtc", + "@scrypted/sdk": "file:../sdk", + "@types/node": "^16.9.0", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + } + }, + "@scrypted/sdk": { + "version": "file:../../sdk", + "requires": { + "@babel/preset-typescript": "^7.16.7", + "@types/node": "^16.11.1", + "@types/stringify-object": "^4.0.0", + "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", + "ncp": "^2.0.0", + "raw-loader": "^4.0.2", + "rimraf": "^3.0.2", + "stringify-object": "^3.3.0", + "tmp": "^0.2.1", + "ts-node": "^10.4.0", + "typedoc": "^0.22.8", + "typescript-json-schema": "^0.50.1", + "webpack": "^5.59.0", + "webpack-bundle-analyzer": "^4.5.0" + } + }, + "@types/node": { + "version": "16.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", + "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==" + }, + "auth-header": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz", + "integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA==" + }, + "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.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + } + }, + "version": "0.0.2" +} diff --git a/plugins/snapshot/package.json b/plugins/snapshot/package.json new file mode 100644 index 000000000..5daca9dea --- /dev/null +++ b/plugins/snapshot/package.json @@ -0,0 +1,36 @@ +{ + "name": "@scrypted/snapshot", + "version": "0.0.2", + "description": "Snapshot Plugin for Scrypted", + "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-webpack": "scrypted-webpack" + }, + "keywords": [ + "scrypted", + "plugin", + "snapshot", + "camera" + ], + "scrypted": { + "name": "Snapshot Plugin", + "type": "API", + "interfaces": [ + "MixinProvider" + ] + }, + "dependencies": { + "@koush/axios-digest-auth": "^0.8.5", + "@types/node": "^16.6.1", + "axios": "^0.24.0" + }, + "devDependencies": { + "@scrypted/common": "file:../../common", + "@scrypted/sdk": "file:../../sdk" + } +} diff --git a/plugins/snapshot/src/main.ts b/plugins/snapshot/src/main.ts new file mode 100644 index 000000000..3dbae0d01 --- /dev/null +++ b/plugins/snapshot/src/main.ts @@ -0,0 +1,90 @@ +import sdk, { Camera, MediaObject, MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue } from "@scrypted/sdk"; +import { SettingsMixinDeviceBase } from "@scrypted/common/src/settings-mixin" +import { StorageSettings } from "@scrypted/common/src/settings" +import AxiosDigestAuth from '@koush/axios-digest-auth'; +import https from 'https'; +import axios, { Axios } from "axios"; + +const { mediaManager } = sdk; +const httpsAgent = new https.Agent({ + rejectUnauthorized: false +}); + +class SnapshotMixin extends SettingsMixinDeviceBase implements Camera { + storageSettings = new StorageSettings(this, { + snapshotUrl: { + title: 'Snapshot URL', + } + }); + axiosClient: Axios | AxiosDigestAuth; + + constructor(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }, providerNativeId: string) { + super(mixinDevice, mixinDeviceState, { + providerNativeId, + mixinDeviceInterfaces, + group: 'Snapshot', + groupKey: 'snapshot', + }); + } + + async takePicture(): Promise { + if (!this.axiosClient) { + let username: string; + let password: string; + + if (this.mixinDeviceInterfaces.includes(ScryptedInterface.Settings)) { + const settings = await this.mixinDevice.getSettings(); + username = settings?.find(setting => setting.key === 'username')?.value?.toString(); + password = settings?.find(setting => setting.key === 'userpasswordname')?.value?.toString(); + } + + if (username && password) { + this.axiosClient = new AxiosDigestAuth({ + username, + password, + }); + } + else { + this.axiosClient = axios; + } + } + + const response = await this.axiosClient.request({ + httpsAgent, + method: "GET", + responseType: 'arraybuffer', + url: this.storageSettings.values.snapshotUrl, + }); + + return mediaManager.createMediaObject(Buffer.from(response.data), 'image/jpeg'); + } + + async getPictureOptions() { + return undefined; + } + + getMixinSettings(): Promise { + return this.storageSettings.getSettings(); + } + + putMixinSetting(key: string, value: SettingValue) { + return this.storageSettings.putSetting(key, value); + } +} + +class SnapshotPlugin extends ScryptedDeviceBase implements MixinProvider { + async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise { + if (type === ScryptedDeviceType.Camera && interfaces.includes(ScryptedInterface.VideoCamera)) + return [ScryptedInterface.Camera, ScryptedInterface.Settings]; + return undefined; + + } + async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise { + return new SnapshotMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId); + } + + async releaseMixin(id: string, mixinDevice: any): Promise { + } +} + +export default new SnapshotPlugin();