diff --git a/plugins/prebuffer-mixin/src/stream-settings.ts b/plugins/prebuffer-mixin/src/stream-settings.ts index 7e7010a32..2cc8030ac 100644 --- a/plugins/prebuffer-mixin/src/stream-settings.ts +++ b/plugins/prebuffer-mixin/src/stream-settings.ts @@ -203,8 +203,8 @@ export function createStreamSettings(device: MixinDeviceBase) { const msos = await device.mixinDevice.getVideoStreamOptions(); enabledStreams = { - defaultValue: getDefaultPrebufferedStreams(msos)?.map(mso => mso.name), - choices: msos.map((mso, index) => mso.name || `Stream ${index}`), + defaultValue: getDefaultPrebufferedStreams(msos)?.map(mso => mso.name || mso.id), + choices: msos.map((mso, index) => mso.name || mso.id), hide: false, }; diff --git a/plugins/reolink/.gitignore b/plugins/reolink/.gitignore new file mode 100644 index 000000000..9cdb546bf --- /dev/null +++ b/plugins/reolink/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +out/ +node_modules/ +dist/ diff --git a/plugins/reolink/.npmignore b/plugins/reolink/.npmignore new file mode 100644 index 000000000..295d8caf5 --- /dev/null +++ b/plugins/reolink/.npmignore @@ -0,0 +1,4 @@ +.DS_Store +out/ +node_modules/ +dist/*.map \ No newline at end of file diff --git a/plugins/reolink/.vscode/launch.json b/plugins/reolink/.vscode/launch.json new file mode 100644 index 000000000..0c868d0cf --- /dev/null +++ b/plugins/reolink/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // 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": [ + "**/plugin-remote-worker.*", + "/**" + ], + "preLaunchTask": "scrypted: deploy+debug", + "sourceMaps": true, + "localRoot": "${workspaceFolder}/out", + "remoteRoot": "/plugin/", + "type": "node" + } + ] +} \ No newline at end of file diff --git a/plugins/reolink/.vscode/settings.json b/plugins/reolink/.vscode/settings.json new file mode 100644 index 000000000..77ccdbd6d --- /dev/null +++ b/plugins/reolink/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "scrypted.debugHost": "127.0.0.1", +} \ No newline at end of file diff --git a/plugins/reolink/.vscode/tasks.json b/plugins/reolink/.vscode/tasks.json new file mode 100644 index 000000000..4d922a539 --- /dev/null +++ b/plugins/reolink/.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/reolink/README.md b/plugins/reolink/README.md new file mode 100644 index 000000000..3e9e3feb2 --- /dev/null +++ b/plugins/reolink/README.md @@ -0,0 +1,3 @@ +# Reolink Plugin for Scrypted + +Reolink Cameras offer both RTMP and RTSP streams. RTMP streams are more reliable than RTSP on Reolink Cameras, but Scrypted highly recommends using RTSP streams if they are stable on your hardware. RTMP streams will be preferred by default. The defaults can be changed in a camera's Rebroadcast `Stream Management` settings. diff --git a/plugins/reolink/package-lock.json b/plugins/reolink/package-lock.json new file mode 100644 index 000000000..20cacca7d --- /dev/null +++ b/plugins/reolink/package-lock.json @@ -0,0 +1,381 @@ +{ + "name": "@scrypted/amcrest", + "version": "0.0.3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@scrypted/amcrest", + "version": "0.0.3", + "hasInstallScript": true, + "license": "Apache", + "dependencies": { + "@koush/axios-digest-auth": "^0.8.5", + "@scrypted/common": "file:../../common", + "@scrypted/sdk": "file:../../sdk", + "@types/multiparty": "^0.0.33", + "multiparty": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^16.11.0" + } + }, + "../../common": { + "name": "@scrypted/common", + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "@scrypted/sdk": "file:../sdk", + "@scrypted/server": "file:../server", + "http-auth-utils": "^3.0.2", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, + "../../sdk": { + "name": "@scrypted/sdk", + "version": "0.0.199", + "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-setup-project": "bin/scrypted-setup-project.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" + } + }, + "../sdk": { + "extraneous": true + }, + "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/@scrypted/common": { + "resolved": "../../common", + "link": true + }, + "node_modules/@scrypted/sdk": { + "resolved": "../../sdk", + "link": true + }, + "node_modules/@types/multiparty": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz", + "integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz", + "integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg==" + }, + "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.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/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "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 + } + } + }, + "node_modules/http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/multiparty": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz", + "integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==", + "dependencies": { + "http-errors": "~1.8.0", + "safe-buffer": "5.2.1", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + } + }, + "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" + } + }, + "@scrypted/common": { + "version": "file:../../common", + "requires": { + "@scrypted/sdk": "file:../sdk", + "@scrypted/server": "file:../server", + "@types/node": "^16.9.0", + "http-auth-utils": "^3.0.2", + "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/multiparty": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz", + "integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz", + "integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg==" + }, + "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.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + }, + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "multiparty": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz", + "integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==", + "requires": { + "http-errors": "~1.8.0", + "safe-buffer": "5.2.1", + "uid-safe": "2.1.5" + } + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + } + } +} diff --git a/plugins/reolink/package.json b/plugins/reolink/package.json new file mode 100644 index 000000000..edf4f8443 --- /dev/null +++ b/plugins/reolink/package.json @@ -0,0 +1,47 @@ +{ + "name": "@scrypted/reolink", + "version": "0.0.3", + "description": "Reolink Plugin for Scrypted", + "author": "Scrypted", + "license": "Apache", + "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", + "reolink", + "camera" + ], + "scrypted": { + "name": "Reolink Camera Plugin", + "type": "DeviceProvider", + "interfaces": [ + "DeviceProvider", + "DeviceCreator" + ], + "pluginDependencies": [ + "@scrypted/prebuffer-mixin" + ] + }, + "dependencies": { + "@koush/axios-digest-auth": "^0.8.5", + "@scrypted/sdk": "file:../../sdk", + "@scrypted/common": "file:../../common", + "@types/multiparty": "^0.0.33", + "multiparty": "^4.2.2" + }, + "devDependencies": { + "@types/node": "^16.11.0" + } +} diff --git a/plugins/reolink/src/main.ts b/plugins/reolink/src/main.ts new file mode 100644 index 000000000..a1bee68c0 --- /dev/null +++ b/plugins/reolink/src/main.ts @@ -0,0 +1,184 @@ +import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers'; +import { readLength } from "@scrypted/common/src/read-stream"; +import { sleep } from '@scrypted/common/src/sleep'; +import sdk, { Camera, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk"; +import child_process, { ChildProcess } from 'child_process'; +import { EventEmitter, PassThrough, Readable, Stream } from "stream"; +import { OnvifIntercom } from "../../onvif/src/onvif-intercom"; +import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp"; +import { ReolinkCameraClient } from './reolink.api'; + +const { mediaManager } = sdk; + +class ReolinkCamera extends RtspSmartCamera implements Camera { + client: ReolinkCameraClient; + + constructor(nativeId: string, provider: RtspProvider) { + super(nativeId, provider); + + 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; + } + } + + getClient() { + if (!this.client) + this.client = new ReolinkCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.getRtspChannel(), this.console); + return this.client; + } + + async listenEvents() { + const client = this.getClient(); + let killed = false; + const events = new EventEmitter(); + const ret: Destroyable = { + on: function (eventName: string | symbol, listener: (...args: any[]) => void): void { + events.on(eventName, listener); + }, + destroy: function (): void { + killed = true; + }, + emit: function (eventName: string | symbol, ...args: any[]): boolean { + return events.emit(eventName, ...args); + } + }; + + (async () => { + while (!killed) { + try { + this.motionDetected = await client.getMotionState(); + } + catch (e) { + this.console.error('polling error', e); + } + await sleep(1000); + } + })(); + return ret; + } + + async takeSmartCameraPicture(option?: PictureOptions): Promise { + return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg'); + } + + async getUrlSettings(): Promise { + return [ + { + key: 'rtspChannel', + title: 'Channel Number Override', + group: 'Advanced', + description: "The channel number to use for snapshots and video. E.g., 0, 1, 2, etc.", + placeholder: '0', + type: 'number', + value: this.getRtspChannel(), + }, + ...await super.getUrlSettings(), + ] + } + + getRtspChannel() { + return parseInt(this.storage.getItem('rtspChannel')) || 0; + } + + createRtspMediaStreamOptions(url: string, index: number) { + const ret = super.createRtspMediaStreamOptions(url, index); + ret.tool = 'scrypted'; + return ret; + } + + async getConstructedVideoStreamOptions(): Promise { + const ret: UrlMediaStreamOptions[] = []; + + const rtmpPreviews = [ + `main.bcs`, + `ext.bcs`, + `sub.bcs`, + ]; + for (const preview of rtmpPreviews) { + const url = new URL(`rtmp://${this.getRtmpAddress()}/bcs/channel${this.getRtspChannel()}_${preview}`); + const params = url.searchParams; + params.set('channel', this.getRtspChannel().toString()); + params.set('stream', '0'); + params.set('user', this.getUsername()); + params.set('password', this.getPassword()); + ret.push({ + name: `RTMP ${preview}`, + id: preview, + url: url.toString(), + }); + } + + const channel = (this.getRtspChannel() + 1).toString().padStart(2, '0'); + const rtspPreviews = [ + `h264Preview_${channel}_main`, + `h264Preview_${channel}_sub`, + `h265Preview_${channel}_main`, + ]; + for (const preview of rtspPreviews) { + ret.push({ + name: `RTSP ${preview}`, + id: preview, + url: `rtsp://${this.getRtspAddress()}/${preview}` + }); + } + + return ret; + } + + async putSetting(key: string, value: string) { + this.client = undefined; + super.putSetting(key, value); + this.updateManagementUrl(); + } + + showRtspUrlOverride() { + return false; + } + + getRtspPortOverrideSettings(): Setting[] { + if (!this.showRtspPortOverride()) { + return []; + } + return [ + ...super.getRtspPortOverrideSettings(), + { + key: 'rtmpPort', + group: 'Advanced', + title: 'RTMP Port Override', + placeholder: '1935', + value: this.storage.getItem('rtmpPort'), + }, + ]; + } + + getRtmpAddress() { + return `${this.getIPAddress()}:${this.storage.getItem('rtmpPort') || 1935}`; + } +} + +class ReolinkProider extends RtspProvider { + getAdditionalInterfaces() { + return [ + ScryptedInterface.VideoCameraConfiguration, + ScryptedInterface.Camera, + ScryptedInterface.AudioSensor, + ScryptedInterface.MotionSensor, + ]; + } + + createCamera(nativeId: string) { + return new ReolinkCamera(nativeId, this); + } +} + +export default ReolinkProider; diff --git a/plugins/reolink/src/reolink.api.ts b/plugins/reolink/src/reolink.api.ts new file mode 100644 index 000000000..ad2834939 --- /dev/null +++ b/plugins/reolink/src/reolink.api.ts @@ -0,0 +1,53 @@ +import { sleep } from "@scrypted/common/src/sleep"; +import AxiosDigestAuth from "@koush/axios-digest-auth/dist"; +import crypto from 'crypto'; + +export class ReolinkCameraClient { + digestAuth: AxiosDigestAuth; + + constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console) { + this.digestAuth = new AxiosDigestAuth({ + password, + username, + }); + } + + // [ + // { + // "cmd" : "GetMdState", + // "code" : 0, + // "value" : { + // "state" : 0 + // } + // } + // ] + async getMotionState() { + const url = new URL(`http://${this.host}/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 !!response.data[0].value.state; + } + + async jpegSnapshot() { + const url = new URL(`http://${this.host}/api.cgi`); + const params = url.searchParams; + params.set('cmd', 'Snap'); + params.set('channel', this.channelId.toString()); + params.set('rs', Date.now().toString()); + params.set('user', this.username); + params.set('password', this.password); + + const response = await this.digestAuth.request({ + url: url.toString(), + responseType: 'arraybuffer' + }); + + return Buffer.from(response.data); + } +} diff --git a/plugins/reolink/tsconfig.json b/plugins/reolink/tsconfig.json new file mode 100644 index 000000000..22d317309 --- /dev/null +++ b/plugins/reolink/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "resolveJsonModule": true, + "moduleResolution": "node", + "target": "esnext", + "esModuleInterop": true, + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/plugins/rtsp/src/rtsp.ts b/plugins/rtsp/src/rtsp.ts index 07b6492ab..435d6569f 100644 --- a/plugins/rtsp/src/rtsp.ts +++ b/plugins/rtsp/src/rtsp.ts @@ -58,7 +58,7 @@ export class RtspCamera extends CameraBase { // Note the trailing colon. // issue: https://github.com/koush/scrypted/issues/134 const parsedUrl = url.parse(rtspUrl); - this.console.log('rtsp stream url', rtspUrl); + this.console.log('stream url', rtspUrl); const username = this.storage.getItem("username"); const password = this.storage.getItem("password"); if (username) {