rtsp/ffmpeg/snapshot: strip out the snapshot urls from ffmpeg and rtsp and direct the user to the snapshot plugin

This commit is contained in:
Koushik Dutta
2022-02-22 14:34:36 -08:00
parent 8371d51104
commit 93f8ef2dba
20 changed files with 461 additions and 150 deletions

View File

@@ -1,15 +1,3 @@
# RTSP Cameras and Streams Plugin
# FFmpeg Camera Plugin for Scrypted
## 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.
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).

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<T extends MediaStreamOptions> 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<MediaObject> {
@@ -35,28 +48,7 @@ export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedD
return this.pendingPicture;
}
async takePictureThrottled(option?: PictureOptions): Promise<MediaObject> {
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<MediaObject>;
async getPictureOptions(): Promise<PictureOptions[]> {
return;
@@ -92,21 +84,8 @@ export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedD
abstract createVideoStream(options?: T): Promise<MediaObject>;
async getSnapshotUrlSettings(): Promise<Setting[]> {
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<Setting[]> {
return [
...await this.getSnapshotUrlSettings(),
];
}
@@ -130,7 +109,7 @@ export abstract class CameraBase<T extends MediaStreamOptions> extends ScryptedD
defaultStreamIndex = defaultStreamIndex || 0;
return vsos?.[defaultStreamIndex];
}
async getStreamSettings(): Promise<Setting[]> {
try {
const vsos = await this.getVideoStreamOptions();
@@ -208,16 +187,6 @@ export abstract class CameraBase<T extends MediaStreamOptions> 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<T extends MediaStreamOptions> 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();

View File

@@ -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<UrlMediaStreamOptions> implements Intercom {
takePictureThrottled(option?: PictureOptions): Promise<MediaObject> {
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<UrlMediaStreamOptions> 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<UrlMediaStreamOptions> implements Intercom
async getUrlSettings(): Promise<Setting[]> {
return [
...await this.getSnapshotUrlSettings(),
...await this.getFFmpegInputSettings(),
];
}
@@ -121,6 +124,11 @@ class FFmpegCamera extends CameraBase<UrlMediaStreamOptions> implements Intercom
}
class FFmpegProvider extends CameraProviderBase<UrlMediaStreamOptions> {
constructor(nativeId?: string) {
super(nativeId);
recommendDumbPlugins();
}
createCamera(nativeId: string): FFmpegCamera {
return new FFmpegCamera(nativeId, this);
}

View File

@@ -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',
});
}

View File

@@ -1,34 +1,3 @@
# RTSP Cameras and Streams Plugin
# RTSP Camera Plugin for Scrypted
## 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.
# 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://<ip-address>:<port>/<channel>/<mode>
*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).

View File

@@ -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",

View File

@@ -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",

View File

@@ -1,7 +0,0 @@
import { alertRecommendedPlugins } from "@scrypted/common/src/alert-recommended-plugins";
export async function recommendRebroadcast() {
alertRecommendedPlugins({
'@scrypted/prebuffer-mixin': 'Rebroadcast',
});
}

View File

@@ -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<UrlMediaStreamOptions> {
takePictureThrottled(option?: PictureOptions): Promise<MediaObject> {
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<UrlMediaStreamOptions> {
async getUrlSettings(): Promise<Setting[]> {
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<MediaObject>;
async getSnapshotUrlSettings(): Promise<Setting[]> {
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<Setting[]> {
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<UrlMediaStreamOptions> {
constructor(nativeId?: string) {
super(nativeId);
recommendDumbPlugins();
}
createCamera(nativeId: string): RtspCamera {
return new RtspCamera(nativeId, this);
}

5
plugins/snapshot/.gitignore vendored Normal file
View File

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

View File

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

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

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

View File

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

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

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

View File

@@ -0,0 +1,3 @@
# Snapshot Plugin for Scrypted
Add custom Snapshot URLs to any camera.

211
plugins/snapshot/package-lock.json generated Normal file
View File

@@ -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"
}

View File

@@ -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"
}
}

View File

@@ -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<Camera> 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<MediaObject> {
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<Setting[]> {
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<string[]> {
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<any> {
return new SnapshotMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId);
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
}
}
export default new SnapshotPlugin();