mirror of
https://github.com/koush/scrypted.git
synced 2026-02-07 16:02:13 +00:00
Compare commits
219 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef218c653 | ||
|
|
aea24a84f0 | ||
|
|
bab3bef0d1 | ||
|
|
56bda46ae9 | ||
|
|
368b0fc26a | ||
|
|
fa50d6faab | ||
|
|
115d168cd3 | ||
|
|
7ab93e8883 | ||
|
|
e25cf860f0 | ||
|
|
b5b56d81a8 | ||
|
|
e7b6cb021c | ||
|
|
e96f374432 | ||
|
|
c4126d7569 | ||
|
|
074ba733a3 | ||
|
|
622f494703 | ||
|
|
28c8b97c26 | ||
|
|
c0af17b38d | ||
|
|
44a82a1afa | ||
|
|
c9b4c14e35 | ||
|
|
76534f1368 | ||
|
|
3f9a863961 | ||
|
|
83b0ebd5f0 | ||
|
|
b923a4ea27 | ||
|
|
941d213087 | ||
|
|
0e11b8b4c5 | ||
|
|
1be14878d1 | ||
|
|
3e323911c7 | ||
|
|
ca36ab2d2d | ||
|
|
a80e95912e | ||
|
|
4432d8dd67 | ||
|
|
2d83d3ba97 | ||
|
|
1078faef62 | ||
|
|
47a981e15a | ||
|
|
e253cab555 | ||
|
|
80124ca83b | ||
|
|
f883d8738c | ||
|
|
bdca3b545c | ||
|
|
3ba02c44ab | ||
|
|
fe4733bb97 | ||
|
|
7ff893fbd3 | ||
|
|
e79c544690 | ||
|
|
13bf44ce50 | ||
|
|
544531122d | ||
|
|
778f0b7ad1 | ||
|
|
35e8a86593 | ||
|
|
c370773af4 | ||
|
|
184f293b92 | ||
|
|
6e10172f7e | ||
|
|
c5ae2cd539 | ||
|
|
e40566e89c | ||
|
|
59ccd4e4d8 | ||
|
|
ae80eb7727 | ||
|
|
f054172dcf | ||
|
|
0d7fb9e13c | ||
|
|
a526816b07 | ||
|
|
563e16b08f | ||
|
|
fd56990d64 | ||
|
|
d7aaf57e8f | ||
|
|
a2d50d54d5 | ||
|
|
1f86745252 | ||
|
|
1f4343ba2e | ||
|
|
3ad311898f | ||
|
|
799e5b53c7 | ||
|
|
833e5b34ab | ||
|
|
c99ac28e89 | ||
|
|
841475cb97 | ||
|
|
4b03a3a458 | ||
|
|
d686dd815c | ||
|
|
e0386a8922 | ||
|
|
9ef3478c88 | ||
|
|
690d160f33 | ||
|
|
59ff987bca | ||
|
|
1669f17c96 | ||
|
|
b0bfd4e05e | ||
|
|
7152671913 | ||
|
|
537c178699 | ||
|
|
77ecee110b | ||
|
|
29b163a7d8 | ||
|
|
5d74e80e90 | ||
|
|
764b6441d5 | ||
|
|
e2c43cb4ff | ||
|
|
7b6d094e8c | ||
|
|
3dfb2db02a | ||
|
|
e5a549db6a | ||
|
|
d500c815fe | ||
|
|
5f71c59b5a | ||
|
|
27407942a5 | ||
|
|
11b6963744 | ||
|
|
b9ee8866f0 | ||
|
|
bc80d31eaa | ||
|
|
327688232c | ||
|
|
2883a4ce46 | ||
|
|
1ad2fb915d | ||
|
|
fb701a32b7 | ||
|
|
7a8c661bb3 | ||
|
|
54d72fb371 | ||
|
|
e48812cec7 | ||
|
|
6c2db072c4 | ||
|
|
4bf2c0b614 | ||
|
|
a93cdb0ae4 | ||
|
|
ff85b7abc6 | ||
|
|
46dfb8d98e | ||
|
|
5240200f0f | ||
|
|
3bcb94fc6b | ||
|
|
a596bc712c | ||
|
|
f6d2dc456e | ||
|
|
441cce239e | ||
|
|
3016df32d1 | ||
|
|
5bd8ed0b1a | ||
|
|
79286a5138 | ||
|
|
8874e01072 | ||
|
|
0223a9f0f6 | ||
|
|
890c2667d0 | ||
|
|
ca14764e17 | ||
|
|
1030d7d03c | ||
|
|
2d40320868 | ||
|
|
3e32c3d019 | ||
|
|
1f9fa3966f | ||
|
|
c2d86237d6 | ||
|
|
5cfcfafc00 | ||
|
|
35b4028a47 | ||
|
|
bf6038a5d3 | ||
|
|
e1b2216543 | ||
|
|
89c1682421 | ||
|
|
5a4c527c59 | ||
|
|
9d9c10aa1e | ||
|
|
ccf20a5fca | ||
|
|
692e7964a7 | ||
|
|
57e38072b1 | ||
|
|
4e8e862482 | ||
|
|
eddcef8e54 | ||
|
|
09edc6d75e | ||
|
|
72c7c43d79 | ||
|
|
805f471ff9 | ||
|
|
6f797d53ec | ||
|
|
4903a0efcd | ||
|
|
36e3fcf429 | ||
|
|
78a126fe0a | ||
|
|
5029baf2d4 | ||
|
|
769bc014a8 | ||
|
|
096700486a | ||
|
|
b3a7d6be9c | ||
|
|
05751bce44 | ||
|
|
dced62a527 | ||
|
|
359f1cfc2f | ||
|
|
d4cae8abbb | ||
|
|
0e6b3346ed | ||
|
|
2409cc457c | ||
|
|
0b794aa381 | ||
|
|
98017a5aa6 | ||
|
|
f2e7cc4017 | ||
|
|
d7201a16a7 | ||
|
|
99d1dc7282 | ||
|
|
18ae09e41c | ||
|
|
2ebe774e59 | ||
|
|
b887b8a47c | ||
|
|
8e391dee2f | ||
|
|
469f693d58 | ||
|
|
1c96a7d492 | ||
|
|
3f1b45c435 | ||
|
|
4b715e55d2 | ||
|
|
75dc63acc3 | ||
|
|
6c79f42bb7 | ||
|
|
9d4f006caa | ||
|
|
05b206f897 | ||
|
|
1f22218b23 | ||
|
|
c9568df165 | ||
|
|
c98e91cd39 | ||
|
|
e3ecff04ce | ||
|
|
f9f50f34c3 | ||
|
|
cd298f7d76 | ||
|
|
c95248fce0 | ||
|
|
e50f3fa793 | ||
|
|
c74be7e90f | ||
|
|
4d288727ce | ||
|
|
1f19dc191d | ||
|
|
37d4e5be73 | ||
|
|
e64ec98211 | ||
|
|
8b6c0c4f7b | ||
|
|
3b16c68c75 | ||
|
|
67be05880c | ||
|
|
414a9403c2 | ||
|
|
053106415c | ||
|
|
f3690af92a | ||
|
|
c4cc12fdff | ||
|
|
58e8772f7c | ||
|
|
4ae9b72471 | ||
|
|
a8c64aa2d4 | ||
|
|
8ccbba485a | ||
|
|
2ec192e0fd | ||
|
|
e257953338 | ||
|
|
9e80eca8e1 | ||
|
|
172b32fd47 | ||
|
|
a6bf055b85 | ||
|
|
dab5be1103 | ||
|
|
126c489934 | ||
|
|
7f714b3d6a | ||
|
|
fde3c47d8c | ||
|
|
4b1623dfce | ||
|
|
1e62f7a418 | ||
|
|
83c9d9a4a6 | ||
|
|
b42afe0ca0 | ||
|
|
e8e5f9b33e | ||
|
|
15916d83b8 | ||
|
|
c1327974b2 | ||
|
|
33e2291912 | ||
|
|
2d2c5c436f | ||
|
|
8088ae20b1 | ||
|
|
4c658b8d99 | ||
|
|
aab78ec797 | ||
|
|
11ecff985d | ||
|
|
80a1a78a79 | ||
|
|
7875c51d62 | ||
|
|
b04aa75117 | ||
|
|
fc7d1eaf32 | ||
|
|
e5a7a55be8 | ||
|
|
fa9a2eb947 | ||
|
|
30891e0769 | ||
|
|
fb8256709a |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,6 +7,17 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# Github Issues is not a Forum
|
||||
|
||||
**This issue tracker is not for hardware support or feature requests**. If you are troubleshooting adding a device for the first time, use Discord, Reddit, or Github Discussions. However, if something **was working**, and is now **no longer working**, you may create a Github issue.
|
||||
Created issues that do not meet these requirements or are improperly filled out will be immediately closed.
|
||||
|
||||
|
||||
# New Issue Instructions
|
||||
|
||||
1. Delete this section and everything above it.
|
||||
2. Fill out the sections below.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. The issue tracker is only for reporting bugs in Scrypted, for general support check Discord. Hardrware support requests or assistance requests will be immediately closed.
|
||||
|
||||
|
||||
18
.github/workflows/docker-common.yml
vendored
18
.github/workflows/docker-common.yml
vendored
@@ -10,7 +10,10 @@ jobs:
|
||||
# runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: ["18", "20"]
|
||||
NODE_VERSION: [
|
||||
"18",
|
||||
# "20"
|
||||
]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite", "thin"]
|
||||
steps:
|
||||
@@ -26,21 +29,13 @@ jobs:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/arm64,linux/armhf
|
||||
platforms: linux/arm64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -63,9 +58,10 @@ jobs:
|
||||
BASE=${{ matrix.BASE }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
|
||||
platforms: linux/amd64,linux/armhf,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
ghcr.io/koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
12
.github/workflows/docker.yml
vendored
12
.github/workflows/docker.yml
vendored
@@ -52,21 +52,13 @@ jobs:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/arm64,linux/armhf
|
||||
platforms: linux/arm64
|
||||
append: |
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM7 }}
|
||||
platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
@@ -89,7 +81,7 @@ jobs:
|
||||
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
|
||||
context: install/docker/
|
||||
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
|
||||
platforms: linux/amd64,linux/arm64,linux/armhf
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -35,3 +35,9 @@
|
||||
[submodule "plugins/cloud/node-nat-upnp"]
|
||||
path = plugins/cloud/external/node-nat-upnp
|
||||
url = ../../koush/node-nat-upnp.git
|
||||
[submodule "plugins/wyze/docker-wyze-bridge"]
|
||||
path = plugins/wyze/docker-wyze-bridge
|
||||
url = ../../koush/docker-wyze-bridge.git
|
||||
[submodule "plugins/onvif/onvif"]
|
||||
path = plugins/onvif/onvif
|
||||
url = ../../koush/onvif.git
|
||||
|
||||
22
common/.vscode/launch.json
vendored
22
common/.vscode/launch.json
vendored
@@ -5,17 +5,19 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"name": "ts-node",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
"args": [
|
||||
"${workspaceFolder}/src/test.ts",
|
||||
],
|
||||
"program": "${workspaceFolder}/dist/common/src/test.js",
|
||||
"preLaunchTask": "npm: build",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
}
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
]
|
||||
}
|
||||
510
common/package-lock.json
generated
510
common/package-lock.json
generated
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.2",
|
||||
"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"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.10.8",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../external/werift/packages/webrtc": {
|
||||
@@ -74,7 +75,7 @@
|
||||
},
|
||||
"../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.68",
|
||||
"version": "0.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -111,68 +112,103 @@
|
||||
},
|
||||
"../server": {
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.6.19",
|
||||
"version": "0.81.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
"@scrypted/types": "^0.2.63",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "^0.21.4",
|
||||
"body-parser": "^1.19.0",
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/types": "^0.3.4",
|
||||
"adm-zip": "^0.5.10",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
"engine.io": "^6.2.0",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.18.2",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"level": "^6.0.1",
|
||||
"level": "^8.0.0",
|
||||
"linkfs": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^3.4.7",
|
||||
"memfs": "^4.6.0",
|
||||
"mime": "^3.0.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"nan": "^2.17.0",
|
||||
"nan": "^2.18.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^8.4.1",
|
||||
"router": "^1.3.7",
|
||||
"semver": "^7.3.8",
|
||||
"node-gyp": "^10.0.1",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^6.1.11",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.8.4",
|
||||
"whatwg-mimetype": "^2.3.0",
|
||||
"ws": "^8.9.0"
|
||||
"tar": "^6.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-serve": "bin/scrypted-serve"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/http-auth": "^4.1.1",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/mime": "^3.0.1",
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node-dijkstra": "^2.5.3",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@types/pem": "^1.9.6",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/source-map-support": "^0.5.6",
|
||||
"@types/tar": "^4.0.5",
|
||||
"@types/whatwg-mimetype": "^2.1.1",
|
||||
"@types/ws": "^7.4.7"
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.10",
|
||||
"@types/pem": "^1.14.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/tar": "^6.1.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../sdk",
|
||||
"link": true
|
||||
@@ -181,12 +217,81 @@
|
||||
"resolved": "../server",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz",
|
||||
"integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==",
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
|
||||
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
|
||||
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz",
|
||||
@@ -231,6 +336,12 @@
|
||||
"node": ">=12.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -265,18 +376,73 @@
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
|
||||
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==",
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
|
||||
@@ -286,15 +452,55 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yerror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.0.1.tgz",
|
||||
"integrity": "sha512-0Bxo+NyeucjxhmPB5z3lmI/N/cOu8L1Q8JVta6/I5G6J/JhCSSPwk8qt9N4yOFSjwkvhDwzUSQglfBIAllvi1Q==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.2.1.tgz",
|
||||
"integrity": "sha512-WPZgybhCBzsMSSqGYBnj20NZo4FiFKQG0+i/21cYGVd4B7eYtvYDOpjk/0e8UM1eVHJ+4Ja6bZ7TAjHH6mk+ew==",
|
||||
"engines": {
|
||||
"node": ">=12.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../sdk",
|
||||
"requires": {
|
||||
@@ -322,62 +528,117 @@
|
||||
"@scrypted/server": {
|
||||
"version": "file:../server",
|
||||
"requires": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
||||
"@scrypted/types": "^0.2.63",
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/http-auth": "^4.1.1",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/mime": "^3.0.1",
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
"@types/node-dijkstra": "^2.5.3",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@types/pem": "^1.9.6",
|
||||
"@types/rimraf": "^3.0.2",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/source-map-support": "^0.5.6",
|
||||
"@types/tar": "^4.0.5",
|
||||
"@types/whatwg-mimetype": "^2.1.1",
|
||||
"@types/ws": "^7.4.7",
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "^0.21.4",
|
||||
"body-parser": "^1.19.0",
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"@scrypted/types": "^0.3.4",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/http-auth": "^4.1.4",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/mime": "^3.0.4",
|
||||
"@types/node-dijkstra": "^2.5.6",
|
||||
"@types/node-forge": "^1.3.10",
|
||||
"@types/pem": "^1.14.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/tar": "^6.1.10",
|
||||
"@types/whatwg-mimetype": "^3.0.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"adm-zip": "^0.5.10",
|
||||
"body-parser": "^1.20.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
"engine.io": "^6.2.0",
|
||||
"engine.io": "^6.5.4",
|
||||
"express": "^4.18.2",
|
||||
"ffmpeg-static": "^5.1.0",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"http-auth": "^4.2.0",
|
||||
"ip": "^1.1.8",
|
||||
"level": "^6.0.1",
|
||||
"level": "^8.0.0",
|
||||
"linkfs": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"memfs": "^3.4.7",
|
||||
"memfs": "^4.6.0",
|
||||
"mime": "^3.0.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"nan": "^2.17.0",
|
||||
"nan": "^2.18.0",
|
||||
"node-dijkstra": "^2.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"node-gyp": "^8.4.1",
|
||||
"node-gyp": "^10.0.1",
|
||||
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
|
||||
"router": "^1.3.7",
|
||||
"semver": "^7.3.8",
|
||||
"router": "^1.3.8",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tar": "^6.1.11",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.8.4",
|
||||
"whatwg-mimetype": "^2.3.0",
|
||||
"ws": "^8.9.0"
|
||||
"tar": "^6.2.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
|
||||
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true
|
||||
},
|
||||
"@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz",
|
||||
"integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==",
|
||||
"version": "20.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
|
||||
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
|
||||
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
|
||||
"dev": true
|
||||
},
|
||||
"arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true
|
||||
},
|
||||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true
|
||||
},
|
||||
"fetch-blob": {
|
||||
@@ -405,6 +666,12 @@
|
||||
"yerror": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -419,10 +686,43 @@
|
||||
"web-streams-polyfill": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
|
||||
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA=="
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"web-streams-polyfill": {
|
||||
"version": "3.2.0",
|
||||
@@ -430,9 +730,15 @@
|
||||
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
|
||||
},
|
||||
"yerror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.0.1.tgz",
|
||||
"integrity": "sha512-0Bxo+NyeucjxhmPB5z3lmI/N/cOu8L1Q8JVta6/I5G6J/JhCSSPwk8qt9N4yOFSjwkvhDwzUSQglfBIAllvi1Q=="
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.2.1.tgz",
|
||||
"integrity": "sha512-WPZgybhCBzsMSSqGYBnj20NZo4FiFKQG0+i/21cYGVd4B7eYtvYDOpjk/0e8UM1eVHJ+4Ja6bZ7TAjHH6mk+ew=="
|
||||
},
|
||||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/server": "file:../server",
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.10.8",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import sdk from "@scrypted/sdk";
|
||||
|
||||
const { systemManager, log } = sdk;
|
||||
|
||||
export async function alertRecommendedPlugins(plugins: { [pkg: string]: string }) {
|
||||
const pluginsComponent = await systemManager.getComponent('plugins');
|
||||
let recommended: any;
|
||||
try {
|
||||
recommended = JSON.parse(localStorage.getItem('alert-recommended'));
|
||||
}
|
||||
catch (e) {
|
||||
recommended = {};
|
||||
}
|
||||
for (const plugin of Object.keys(plugins)) {
|
||||
try {
|
||||
if (recommended[plugin])
|
||||
continue;
|
||||
|
||||
recommended[plugin] = true;
|
||||
localStorage.setItem('alert-recommended', JSON.stringify(recommended));
|
||||
const id = await pluginsComponent.getIdForPluginId(plugin);
|
||||
if (id)
|
||||
continue;
|
||||
const name = plugins[plugin];
|
||||
log.a(`Installation of the ${name} plugin is also recommended. origin:/#/component/plugin/install/${plugin}`)
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ class EndError extends Error {
|
||||
|
||||
export function createAsyncQueue<T>() {
|
||||
let ended: Error | undefined;
|
||||
const endDeferred = new Deferred<void>();
|
||||
const waiting: Deferred<T>[] = [];
|
||||
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
|
||||
|
||||
@@ -75,7 +76,8 @@ export function createAsyncQueue<T>() {
|
||||
if (ended)
|
||||
return false;
|
||||
// catch to prevent unhandled rejection.
|
||||
ended = e || new EndError()
|
||||
ended = e || new EndError();
|
||||
endDeferred.resolve();
|
||||
while (waiting.length) {
|
||||
waiting.shift().reject(ended);
|
||||
}
|
||||
@@ -124,6 +126,7 @@ export function createAsyncQueue<T>() {
|
||||
get ended() {
|
||||
return ended;
|
||||
},
|
||||
endPromise: endDeferred.promise,
|
||||
take,
|
||||
clear() {
|
||||
return clear();
|
||||
|
||||
@@ -58,18 +58,13 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
worker.worker.terminate();
|
||||
}
|
||||
|
||||
const smProxy = new SystemManagerImpl();
|
||||
smProxy.state = systemManager.getSystemState();
|
||||
const apiProxy = new PluginAPIProxy(sdk.pluginHostAPI);
|
||||
smProxy.api = apiProxy;
|
||||
|
||||
const allParams = Object.assign({}, params, {
|
||||
sdk,
|
||||
fs: require('realfs'),
|
||||
fetch,
|
||||
ScryptedDeviceBase,
|
||||
MixinDeviceBase,
|
||||
systemManager: smProxy,
|
||||
systemManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager,
|
||||
@@ -104,7 +99,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
return {
|
||||
value,
|
||||
defaultExport,
|
||||
apiProxy,
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
148
common/src/http-auth-fetch.ts
Normal file
148
common/src/http-auth-fetch.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, getHttpFetchParser, httpFetch } from '@scrypted/server/src/http-fetch-helpers';
|
||||
import crypto from 'crypto';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { BASIC, DIGEST, buildAuthorizationHeader, parseWWWAuthenticateHeader } from 'http-auth-utils/src/index';
|
||||
|
||||
export interface AuthFetchCredentialState {
|
||||
username: string;
|
||||
password: string;
|
||||
count?: number;
|
||||
digest?: ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
|
||||
basic?: ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
|
||||
}
|
||||
|
||||
export interface AuthFetchOptions<T extends HttpFetchResponseType> extends HttpFetchOptions<T> {
|
||||
credential: AuthFetchCredentialState;
|
||||
}
|
||||
|
||||
function getAuth(options: AuthFetchOptions<any>, method: string) {
|
||||
if (!options.credential)
|
||||
return;
|
||||
const { digest, basic } = options.credential;
|
||||
if (digest) {
|
||||
options.credential.count ||= 0;
|
||||
++options.credential.count;
|
||||
const nc = ('00000000' + options.credential.count).slice(-8);
|
||||
const cnonce = crypto.randomBytes(24).toString('hex');
|
||||
const uri = new URL(options.url).pathname;
|
||||
|
||||
const response = DIGEST.computeHash({
|
||||
username: options.credential.username,
|
||||
password: options.credential.password,
|
||||
method,
|
||||
uri,
|
||||
nc,
|
||||
cnonce,
|
||||
algorithm: 'MD5',
|
||||
qop: digest.data.qop!,
|
||||
...digest.data,
|
||||
});
|
||||
|
||||
const header = buildAuthorizationHeader(DIGEST, {
|
||||
username: options.credential.username,
|
||||
uri,
|
||||
nc,
|
||||
cnonce,
|
||||
algorithm: digest.data.algorithm!,
|
||||
qop: digest.data.qop!,
|
||||
response,
|
||||
...digest.data,
|
||||
});
|
||||
|
||||
return header;
|
||||
}
|
||||
else if (basic) {
|
||||
const header = buildAuthorizationHeader(BASIC, {
|
||||
username: options.credential.username,
|
||||
password: options.credential.password,
|
||||
});
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
|
||||
export async function authHttpFetch<T extends HttpFetchResponseType>(options: AuthFetchOptions<T>): ReturnType<typeof httpFetch<AuthFetchOptions<T>>> {
|
||||
const method = options.method || 'GET';
|
||||
const headers = new Headers(options.headers);
|
||||
options.headers = headers;
|
||||
|
||||
const initialHeader = getAuth(options, method);
|
||||
if (initialHeader)
|
||||
headers.set('Authorization', initialHeader);
|
||||
|
||||
const initialResponse = await httpFetch({
|
||||
...options,
|
||||
ignoreStatusCode: true,
|
||||
responseType: 'readable',
|
||||
});
|
||||
|
||||
const parser = getHttpFetchParser(options.responseType);
|
||||
|
||||
if (initialResponse.statusCode !== 401 || !options.credential) {
|
||||
if (!options?.ignoreStatusCode)
|
||||
checkStatus(initialResponse.statusCode);
|
||||
return {
|
||||
...initialResponse,
|
||||
body: await parser.parse(initialResponse.body),
|
||||
};
|
||||
}
|
||||
|
||||
let authenticateHeaders: string | string[] = initialResponse.headers['www-authenticate'];
|
||||
if (!authenticateHeaders)
|
||||
throw new Error('Did not find WWW-Authenticate header.');
|
||||
|
||||
|
||||
if (typeof authenticateHeaders === 'string')
|
||||
authenticateHeaders = [authenticateHeaders];
|
||||
|
||||
const parsedHeaders = authenticateHeaders.map(h => parseWWWAuthenticateHeader(h));
|
||||
|
||||
const digest = parsedHeaders.find(p => p.type === 'Digest') as ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
|
||||
const basic = parsedHeaders.find(p => p.type === 'Basic') as ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
|
||||
|
||||
options.credential.digest = digest;
|
||||
options.credential.basic = basic;
|
||||
|
||||
if (!digest && !basic)
|
||||
throw new Error(`Unknown WWW-Authenticate type: ${parsedHeaders[0]?.type}`);
|
||||
|
||||
const header = getAuth(options, method);
|
||||
if (header)
|
||||
headers.set('Authorization', header);
|
||||
|
||||
return httpFetch(options);
|
||||
}
|
||||
|
||||
function ensureType<T>(v: T) {
|
||||
}
|
||||
|
||||
async function test() {
|
||||
const a = await authHttpFetch({
|
||||
credential: undefined,
|
||||
url: 'http://example.com',
|
||||
});
|
||||
|
||||
ensureType<Buffer>(a.body);
|
||||
|
||||
const b = await authHttpFetch({
|
||||
credential: undefined,
|
||||
url: 'http://example.com',
|
||||
responseType: 'json',
|
||||
});
|
||||
ensureType<any>(b.body);
|
||||
|
||||
const c = await authHttpFetch({
|
||||
credential: undefined,
|
||||
url: 'http://example.com',
|
||||
responseType: 'readable',
|
||||
});
|
||||
ensureType<IncomingMessage>(c.body);
|
||||
|
||||
const d = await authHttpFetch({
|
||||
credential: undefined,
|
||||
url: 'http://example.com',
|
||||
responseType: 'buffer',
|
||||
});
|
||||
ensureType<Buffer>(d.body);
|
||||
}
|
||||
|
||||
@@ -78,11 +78,17 @@ export function createPromiseDebouncer<T>() {
|
||||
export function createMapPromiseDebouncer<T>() {
|
||||
const map = new Map<string, Promise<T>>();
|
||||
|
||||
return (key: any, func: () => Promise<T>): Promise<T> => {
|
||||
return (key: any, debounce: number, func: () => Promise<T>): Promise<T> => {
|
||||
const keyStr = JSON.stringify(key);
|
||||
let value = map.get(keyStr);
|
||||
if (!value) {
|
||||
value = func().finally(() => map.delete(keyStr));
|
||||
value = func().finally(() => {
|
||||
if (!debounce) {
|
||||
map.delete(keyStr);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => map.delete(keyStr), debounce);
|
||||
});
|
||||
map.set(keyStr, value);
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -59,13 +59,13 @@ export async function read16BELengthLoop(readable: Readable, options: {
|
||||
|
||||
export class StreamEndError extends Error {
|
||||
constructor() {
|
||||
super()
|
||||
super('stream ended');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
|
||||
if (readable.readableEnded || readable.destroyed)
|
||||
throw new Error("stream ended");
|
||||
throw new StreamEndError();
|
||||
|
||||
if (!length) {
|
||||
return Buffer.alloc(0);
|
||||
|
||||
@@ -73,6 +73,14 @@ function createOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
// can be called on anything with getStats, ie for receiver specific reports or connection reports.
|
||||
export async function getPacketsLost(t: { getStats(): Promise<RTCStatsReport> }) {
|
||||
const stats = await t.getStats();
|
||||
const packetsLost = ([...stats.values()] as { packetsLost: number }[]).filter(stat => 'packetsLost' in stat).map(stat => stat.packetsLost);
|
||||
const total = packetsLost.reduce((p, c) => p + c, 0);
|
||||
return total;
|
||||
}
|
||||
|
||||
export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
private pc: RTCPeerConnection;
|
||||
pcDeferred = new Deferred<RTCPeerConnection>();
|
||||
@@ -90,6 +98,10 @@ export class BrowserSignalingSession implements RTCSignalingSession {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
async getPacketsLost() {
|
||||
return getPacketsLost(this.pc);
|
||||
}
|
||||
|
||||
async setMicrophone(enabled: boolean) {
|
||||
if (this.microphone && enabled && !this.micEnabled) {
|
||||
this.micEnabled = true;
|
||||
|
||||
@@ -207,6 +207,10 @@ export function parseRtpMap(mlineType: string, rtpmap: string) {
|
||||
codec = 'pcm_s16be';
|
||||
ffmpegEncoder = 'pcm_s16be';
|
||||
}
|
||||
else if (rtpmap?.includes('speex')) {
|
||||
codec = 'speex';
|
||||
ffmpegEncoder = 'libspeex';
|
||||
}
|
||||
else if (rtpmap?.includes('h264')) {
|
||||
codec = 'h264';
|
||||
}
|
||||
|
||||
2
external/axios-digest-auth
vendored
2
external/axios-digest-auth
vendored
Submodule external/axios-digest-auth updated: d0872934e6...1180ec6d53
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: 3db4127b58...3797e311ed
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 25be131232...a0070297a4
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-jammy-full.s6-v0.66.0"
|
||||
version: "18-jammy-full.s6-v0.80.0"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
@@ -29,9 +29,9 @@ environment:
|
||||
SCRYPTED_ADMIN_USERNAME: "homeassistant"
|
||||
SCRYPTED_INSTALL_ENVIRONMENT: "ha"
|
||||
backup_exclude:
|
||||
- '/server/**'
|
||||
- '/data/scrypted_nvr/**'
|
||||
- '/data/scrypted_data/plugins/**'
|
||||
- '*/server/**'
|
||||
- '*/scrypted_nvr/**'
|
||||
- '*/scrypted_data/plugins/**'
|
||||
map:
|
||||
- config:rw
|
||||
- media:rw
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BASE="18-jammy-full"
|
||||
FROM koush/scrypted-common:${BASE}
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
WORKDIR /
|
||||
# cache bust
|
||||
@@ -14,4 +14,8 @@ WORKDIR /server
|
||||
# https://github.com/nodejs/node/issues/41145#issuecomment-992948130
|
||||
ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240103"
|
||||
|
||||
CMD npm --prefix /server exec scrypted-serve
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BASE="16-jammy"
|
||||
FROM koush/scrypted-common:${BASE}
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
WORKDIR /
|
||||
RUN git clone --depth=1 https://github.com/koush/scrypted
|
||||
|
||||
@@ -25,10 +25,14 @@ RUN apt-get update && apt-get -y install \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get install -y ca-certificates curl gnupg
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN echo "Installing python."
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
@@ -38,44 +42,25 @@ RUN apt-get -y install \
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN echo "Installing pillow-simd dependencies."
|
||||
RUN apt-get -y install \
|
||||
libjpeg-dev zlib1g-dev
|
||||
|
||||
# plugins support fallback to pillow, but vips is faster.
|
||||
RUN apt-get -y install \
|
||||
libvips
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
RUN echo "Installing gstreamer bindings."
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
# which causes weird behavior in python which looks at the arch version
|
||||
# which still reports 64bit, even if running in 32bit docker.
|
||||
# this scenario is not supported and will be reported at runtime.
|
||||
# this bit is not necessary on amd64, but leaving it for consistency.
|
||||
RUN apt-get -y install \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
@@ -87,20 +72,11 @@ RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
then \
|
||||
apt-get update && apt-get install -y gpg-agent && \
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg && \
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
@@ -110,7 +86,6 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
@@ -127,9 +102,6 @@ ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -17,7 +17,10 @@ RUN apt-get update && apt-get -y install \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get install -y ca-certificates curl gnupg
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
@@ -41,7 +44,4 @@ ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM koush/scrypted-common
|
||||
FROM ghcr.io/koush/scrypted-common
|
||||
|
||||
WORKDIR /
|
||||
COPY . .
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM koush/18-jammy-full.s6
|
||||
FROM ghcr.io/koush/scrypted:18-jammy-full.s6
|
||||
|
||||
WORKDIR /
|
||||
|
||||
@@ -12,11 +12,3 @@ ENV PATH=$CONDA_DIR/bin:$PATH
|
||||
RUN conda install -c conda-forge cudatoolkit=11.2.2 cudnn=8.1.0
|
||||
ENV CONDA_PREFIX=/opt/conda
|
||||
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/
|
||||
|
||||
# this is a copy pasta, seems to need a reinstall.
|
||||
# python pip
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BASE="18-jammy-full"
|
||||
FROM koush/scrypted-common:${BASE}
|
||||
FROM ghcr.io/koush/scrypted-common:${BASE}
|
||||
|
||||
# avahi advertiser support
|
||||
RUN apt-get update && apt-get -y install \
|
||||
@@ -44,4 +44,8 @@ WORKDIR /server
|
||||
# https://github.com/nodejs/node/issues/41145#issuecomment-992948130
|
||||
ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20240103"
|
||||
|
||||
CMD npm --prefix /server exec scrypted-serve
|
||||
|
||||
@@ -9,7 +9,7 @@ RUN apt-get -y update && \
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && apt-get update && apt-get install -y nodejs
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
@@ -19,7 +19,4 @@ ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="thin"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
./template/generate-dockerfile.sh
|
||||
|
||||
docker build -t koush/scrypted-common -f Dockerfile.common . && \
|
||||
docker build -t koush/scrypted -f Dockerfile.local .
|
||||
docker build -t ghcr.io/koush/scrypted-common -f Dockerfile.full . && \
|
||||
docker build -t ghcr.io/koush/scrypted -f Dockerfile.local .
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
./docker-build.sh
|
||||
|
||||
docker build -t koush/scrypted:18-jammy-full.nvidia -f Dockerfile.nvidia
|
||||
docker build -t ghcr.io/koush/scrypted:18-jammy-full.nvidia -f Dockerfile.nvidia .
|
||||
@@ -11,8 +11,8 @@ echo $BASE
|
||||
SUPERVISOR=.s6
|
||||
SUPERVISOR_BASE=$BASE$SUPERVISOR
|
||||
|
||||
docker build -t koush/scrypted-common:$BASE -f Dockerfile.$FLAVOR \
|
||||
docker build -t ghcr.io/koush/scrypted-common:$BASE -f Dockerfile.$FLAVOR \
|
||||
--build-arg NODE_VERSION=$NODE_VERSION --build-arg BASE=$IMAGE_BASE . && \
|
||||
\
|
||||
docker build -t koush/scrypted:$SUPERVISOR_BASE -f Dockerfile$SUPERVISOR \
|
||||
docker build -t ghcr.io/koush/scrypted:$SUPERVISOR_BASE -f Dockerfile$SUPERVISOR \
|
||||
--build-arg BASE=$BASE --build-arg SCRYPTED_INSTALL_VERSION=$SCRYPTED_INSTALL_VERSION .
|
||||
|
||||
@@ -34,14 +34,14 @@ services:
|
||||
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
|
||||
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
|
||||
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
# Uncomment next line to run avahi-daemon inside the container
|
||||
# Don't use if dbus and avahi run on the host and are bind-mounted
|
||||
# (see below under "volumes")
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
# runtime: nvidia
|
||||
|
||||
volumes:
|
||||
@@ -90,7 +90,7 @@ services:
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
image: koush/scrypted
|
||||
image: ghcr.io/koush/scrypted
|
||||
|
||||
# logging is noisy and will unnecessarily wear on flash storage.
|
||||
# scrypted has per device in memory logging that is preferred.
|
||||
|
||||
16
install/docker/install-intel-graphics.sh
Normal file
16
install/docker/install-intel-graphics.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
if [ "$(uname -m)" = "x86_64" ]
|
||||
then
|
||||
echo "Installing Intel graphics packages."
|
||||
apt-get update && apt-get install -y gpg-agent &&
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg &&
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
|
||||
apt-get -y update &&
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
exit $?
|
||||
else
|
||||
echo "Intel graphics will not be installed on this architecture."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -6,19 +6,6 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$SERVICE_USER" == "root" ]
|
||||
then
|
||||
echo "Scrypted SERVICE_USER root is not allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
mkdir -p $SCRYPTED_HOME
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
@@ -30,6 +17,22 @@ function readyn() {
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$SERVICE_USER" == "root" ]
|
||||
then
|
||||
readyn "Scrypted will store its files in the root user home directory. Running as a non-root user is recommended. Are you sure?"
|
||||
if [ "$yn" == "n" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
SCRYPTED_HOME=$USER_HOME/.scrypted
|
||||
mkdir -p $SCRYPTED_HOME
|
||||
|
||||
set -e
|
||||
cd $SCRYPTED_HOME
|
||||
|
||||
readyn "Install Docker?"
|
||||
|
||||
if [ "$yn" == "y" ]
|
||||
|
||||
@@ -4,20 +4,11 @@
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu for openvino
|
||||
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
then \
|
||||
apt-get update && apt-get install -y gpg-agent && \
|
||||
rm -f /usr/share/keyrings/intel-graphics.gpg && \
|
||||
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
|
||||
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
|
||||
apt-get -y update && \
|
||||
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
@@ -27,7 +18,6 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
@@ -44,9 +34,6 @@ ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20230727"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -22,10 +22,14 @@ RUN apt-get update && apt-get -y install \
|
||||
apt-get -y upgrade
|
||||
|
||||
ARG NODE_VERSION=18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
|
||||
RUN apt-get install -y ca-certificates curl gnupg
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
# python native
|
||||
RUN echo "Installing python."
|
||||
RUN apt-get -y install \
|
||||
python3 \
|
||||
python3-dev \
|
||||
@@ -35,44 +39,25 @@ RUN apt-get -y install \
|
||||
|
||||
# these are necessary for pillow-simd, additional on disk size is small
|
||||
# but could consider removing this.
|
||||
RUN echo "Installing pillow-simd dependencies."
|
||||
RUN apt-get -y install \
|
||||
libjpeg-dev zlib1g-dev
|
||||
|
||||
# plugins support fallback to pillow, but vips is faster.
|
||||
RUN apt-get -y install \
|
||||
libvips
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
RUN apt-get -y install \
|
||||
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
RUN echo "Installing gstreamer bindings."
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
# which causes weird behavior in python which looks at the arch version
|
||||
# which still reports 64bit, even if running in 32bit docker.
|
||||
# this scenario is not supported and will be reported at runtime.
|
||||
# this bit is not necessary on amd64, but leaving it for consistency.
|
||||
RUN apt-get -y install \
|
||||
python3-matplotlib \
|
||||
python3-numpy \
|
||||
python3-opencv \
|
||||
python3-pil \
|
||||
python3-skimage
|
||||
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# pyvips is broken on x86 due to mismatch ffi
|
||||
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
|
||||
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
|
||||
@@ -12,6 +12,26 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
case $yn in
|
||||
[Yy]* ) break;;
|
||||
[Nn]* ) break;;
|
||||
* ) echo "Please answer yes or no. (y/n)";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$SERVICE_USER" = "root" ] && [ -z "$SERVICE_USER_ROOT" ]
|
||||
then
|
||||
readyn "Scrypted will store its files in the root user home directory. Running as a non-root user is recommended. Are you sure?"
|
||||
if [ "$yn" == "n" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Stopping existing service if it is running..."
|
||||
systemctl stop scrypted.service
|
||||
|
||||
@@ -49,6 +69,14 @@ ENV() {
|
||||
}
|
||||
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.header)
|
||||
if [ -z "$SCRYPTED_INSTALL_ENVIRONMENT" ]
|
||||
then
|
||||
SCRYPTED_INSTALL_ENVIRONMENT=local
|
||||
fi
|
||||
if [ "$SCRYPTED_INSTALL_ENVIRONMENT" = "lxc" ]
|
||||
then
|
||||
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.footer)
|
||||
fi
|
||||
|
||||
if [ -z "$SERVICE_USER" ]
|
||||
then
|
||||
@@ -56,12 +84,6 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$SERVICE_USER" == "root" ]
|
||||
then
|
||||
echo "Scrypted SERVICE_USER root is not allowed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# this is not RUN as we do not care about the result
|
||||
USER_HOME=$(eval echo ~$SERVICE_USER)
|
||||
echo "Setting permissions on $USER_HOME/.scrypted"
|
||||
@@ -84,6 +106,7 @@ ExecStart=/usr/bin/npx -y scrypted serve
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
Environment="NODE_OPTIONS=$NODE_OPTIONS"
|
||||
Environment="SCRYPTED_INSTALL_ENVIRONMENT=$SCRYPTED_INSTALL_ENVIRONMENT"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
75
install/local/install-scrypted-proxmox.sh
Normal file
75
install/local/install-scrypted-proxmox.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
function readyn() {
|
||||
while true; do
|
||||
read -p "$1 (y/n) " yn
|
||||
case $yn in
|
||||
[Yy]* ) break;;
|
||||
[Nn]* ) break;;
|
||||
* ) echo "Please answer yes or no. (y/n)";;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.80.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
if [ ! -f "$SCRYPTED_TAR_ZST" ]
|
||||
then
|
||||
curl -O -L https://github.com/koush/scrypted/releases/download/$SCRYPTED_VERSION/scrypted.tar.zst
|
||||
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
|
||||
fi
|
||||
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "Existing container $VMID found. Run this script with --force to overwrite the existing container."
|
||||
echo "This will wipe all existing data. Clone the existing container to retain the data, then reassign the owner of the scrypted volume after installation is complete."
|
||||
echo ""
|
||||
echo "bash $0 --force"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct restore failed"
|
||||
echo ""
|
||||
echo "This may be caused by the server's 'local' storage not supporting containers."
|
||||
echo "Try running this script again with a different storage device (local-lvm, local-zfs). For example:"
|
||||
echo ""
|
||||
echo "bash $0 --storage local-lvm"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONF=/etc/pve/lxc/$VMID.conf
|
||||
if [ -f "$CONF" ]
|
||||
then
|
||||
echo "onboot: 1" >> $CONF
|
||||
else
|
||||
echo "$CONF not found? Start on boot must be enabled manually."
|
||||
fi
|
||||
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
udevadm control --reload-rules && udevadm trigger
|
||||
fi
|
||||
|
||||
echo "Scrypted setup is complete and the container resources can be started."
|
||||
echo "Scrypted NVR users should provide at least 4 cores and 16GB RAM prior to starting."
|
||||
4
packages/cli/package-lock.json
generated
4
packages/cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "scrypted",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/client": "^1.3.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "scrypted",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.5",
|
||||
"description": "",
|
||||
"main": "./dist/packages/cli/src/main.js",
|
||||
"bin": {
|
||||
|
||||
@@ -18,7 +18,12 @@ async function runCommand(command: string, ...args: string[]) {
|
||||
command += '.cmd';
|
||||
console.log('running', command, ...args);
|
||||
const cp = child_process.spawn(command, args, {
|
||||
stdio: 'inherit'
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
// https://github.com/lovell/sharp/blob/eefaa998725cf345227d94b40615e090495c6d09/lib/libvips.js#L115C19-L115C46
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS: 'true',
|
||||
},
|
||||
});
|
||||
await once(cp, 'exit');
|
||||
if (cp.exitCode)
|
||||
|
||||
35
plugins/alexa/package-lock.json
generated
35
plugins/alexa/package-lock.json
generated
@@ -1,24 +1,40 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.10",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "../../common",
|
||||
"@scrypted/sdk": "../../sdk",
|
||||
"@types/node": "^18.4.2"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"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.2.104",
|
||||
"version": "0.2.108",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -54,6 +70,13 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../common": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
@@ -70,9 +93,9 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.10",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.4.2",
|
||||
"@scrypted/sdk": "../../sdk"
|
||||
"@scrypted/sdk": "../../sdk",
|
||||
"@scrypted/common": "../../common"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (status === DeviceMixinStatus.Setup)
|
||||
await this.syncEndpoints();
|
||||
|
||||
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
|
||||
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
|
||||
|
||||
if (!this.devices.has(eventSource.id)) {
|
||||
this.devices.set(eventSource.id, eventSource);
|
||||
@@ -142,7 +142,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
await this.syncEndpoints();
|
||||
}
|
||||
|
||||
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
|
||||
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any): Promise<void> {
|
||||
if (!eventSource)
|
||||
return;
|
||||
|
||||
@@ -194,14 +194,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
|
||||
// nothing to report
|
||||
if (data.context === undefined && data.event.payload === undefined)
|
||||
return;
|
||||
|
||||
return;
|
||||
|
||||
data = await this.addAccessToken(data);
|
||||
|
||||
await this.postEvent(data);
|
||||
}
|
||||
|
||||
private async addAccessToken(data: any) : Promise<any> {
|
||||
private async addAccessToken(data: any): Promise<any> {
|
||||
const accessToken = await this.getAccessToken();
|
||||
|
||||
if (data.event === undefined)
|
||||
@@ -232,7 +232,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
'api.fe.amazonalexa.com'
|
||||
];
|
||||
|
||||
async getAlexaEndpoint() : Promise<string> {
|
||||
async getAlexaEndpoint(): Promise<string> {
|
||||
if (this.storageSettings.values.apiEndpoint)
|
||||
return this.storageSettings.values.apiEndpoint;
|
||||
|
||||
@@ -276,7 +276,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
});
|
||||
}
|
||||
|
||||
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
|
||||
async getEndpoints(): Promise<DiscoveryEndpoint[]> {
|
||||
const endpoints: DiscoveryEndpoint[] = [];
|
||||
|
||||
for (const id of Object.keys(systemManager.getSystemState())) {
|
||||
@@ -284,7 +284,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
|
||||
if (!device.mixins?.includes(this.id))
|
||||
continue;
|
||||
|
||||
|
||||
const endpoint = await this.getEndpointForDevice(device);
|
||||
if (endpoint)
|
||||
endpoints.push(endpoint);
|
||||
@@ -319,7 +319,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
const endpoints = await this.getEndpoints();
|
||||
|
||||
if (!endpoints.length)
|
||||
return [];
|
||||
return [];
|
||||
|
||||
const accessToken = await this.getAccessToken();
|
||||
const data = {
|
||||
@@ -448,7 +448,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
self.console.warn(error?.response?.data);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
break;
|
||||
|
||||
|
||||
case 'expired_token':
|
||||
self.console.warn(error?.response?.data);
|
||||
self.log.a(error?.response?.data?.error_description);
|
||||
@@ -480,9 +480,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
this.storageSettings.values.tokenInfo = grant;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
|
||||
|
||||
const self = this;
|
||||
let accessToken = await this.getAccessToken().catch(reason => {
|
||||
let accessToken: any;
|
||||
|
||||
try {
|
||||
accessToken = await this.getAccessToken();
|
||||
}
|
||||
catch (reason) {
|
||||
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
@@ -491,36 +496,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
|
||||
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (accessToken !== undefined) {
|
||||
this.log.clearAlerts();
|
||||
|
||||
try {
|
||||
response.send({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Authorization",
|
||||
"name": "AcceptGrant.Response",
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.console.error(`AcceptGrant.Response failed because ${error}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
throw error;
|
||||
return;
|
||||
};
|
||||
this.log.clearAlerts();
|
||||
response.send({
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Authorization",
|
||||
"name": "AcceptGrant.Response",
|
||||
"messageId": createMessageId(),
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"payload": {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
|
||||
async getEndpointForDevice(device: ScryptedDevice): Promise<DiscoveryEndpoint> {
|
||||
if (!device)
|
||||
return;
|
||||
|
||||
@@ -545,7 +537,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
};
|
||||
|
||||
let supportedEndpointHealths: any[] = [];
|
||||
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.Online)) {
|
||||
supportedEndpointHealths.push({
|
||||
"name": "connectivity"
|
||||
@@ -632,8 +624,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
debug("received directive from alexa", mapName, body);
|
||||
|
||||
const handler = alexaHandlers.get(mapName);
|
||||
if (handler)
|
||||
return handler.apply(this, [request, response, directive]);
|
||||
if (handler) {
|
||||
await handler.apply(this, [request, response, directive]);
|
||||
return;
|
||||
}
|
||||
|
||||
const deviceHandler = alexaDeviceHandlers.get(mapName);
|
||||
|
||||
@@ -644,7 +638,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
return;
|
||||
}
|
||||
|
||||
return deviceHandler.apply(this, [request, response, directive, device]);
|
||||
await deviceHandler.apply(this, [request, response, directive, device]);
|
||||
return;
|
||||
} else {
|
||||
this.console.error(`no handler for: ${mapName}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { v4 as createMessageId } from 'uuid';
|
||||
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
|
||||
import { alexaDeviceHandlers } from "../../handlers";
|
||||
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
|
||||
export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
constructor(public response: AlexaHttpResponse, public directive: any) {
|
||||
@@ -13,7 +14,8 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
|
||||
__proxy_props: { options: RTCSignalingOptions; };
|
||||
options: RTCSignalingOptions;
|
||||
|
||||
remoteDescription = new Deferred<void>();
|
||||
|
||||
async getOptions(): Promise<RTCSignalingOptions> {
|
||||
return this.options;
|
||||
}
|
||||
@@ -39,11 +41,17 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
}
|
||||
|
||||
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
|
||||
if (type !== 'offer')
|
||||
throw new Error('Alexa only supports RTC offer');
|
||||
if (type !== 'offer') {
|
||||
const e = new Error('Alexa only supports RTC offer');
|
||||
this.remoteDescription.reject(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (sendIceCandidate)
|
||||
throw new Error("Alexa does not support trickle ICE");
|
||||
if (sendIceCandidate) {
|
||||
const e = new Error("Alexa does not support trickle ICE");
|
||||
this.remoteDescription.reject(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return {
|
||||
type: type,
|
||||
@@ -67,15 +75,16 @@ export class AlexaSignalingSession implements RTCSignalingSession {
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
|
||||
data.event.header.name = "AnswerGeneratedForSession";
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
|
||||
data.event.payload.answer = {
|
||||
format: 'SDP',
|
||||
value: description.sdp,
|
||||
};
|
||||
|
||||
this.remoteDescription.resolve();
|
||||
this.response.send(data);
|
||||
}
|
||||
}
|
||||
@@ -85,13 +94,14 @@ const sessionCache = new Map<string, RTCSessionControl>();
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/InitiateSessionWithOffer', async (request, response, directive: any, device: ScryptedDevice & RTCSignalingChannel) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const { sessionId } = payload;
|
||||
|
||||
|
||||
const session = new AlexaSignalingSession(response, directive);
|
||||
const control = await device.startRTCSignalingSession(session);
|
||||
control.setPlayback({
|
||||
audio: true,
|
||||
video: false,
|
||||
})
|
||||
});
|
||||
await session.remoteDescription.promise;
|
||||
|
||||
sessionCache.set(sessionId, control);
|
||||
});
|
||||
@@ -115,13 +125,13 @@ alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (re
|
||||
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async (request, response, directive: any, device: ScryptedDevice) => {
|
||||
const { header, endpoint, payload } = directive;
|
||||
const { sessionId } = payload;
|
||||
|
||||
|
||||
const session = sessionCache.get(sessionId);
|
||||
if (session) {
|
||||
sessionCache.delete(sessionId);
|
||||
await session.endSession();
|
||||
}
|
||||
|
||||
|
||||
const data: WebRTCSessionDisconnectedEvent = {
|
||||
"event": {
|
||||
header,
|
||||
@@ -130,9 +140,9 @@ alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async
|
||||
},
|
||||
context: undefined
|
||||
};
|
||||
|
||||
|
||||
data.event.header.messageId = createMessageId();
|
||||
|
||||
|
||||
response.send(data);
|
||||
});
|
||||
|
||||
@@ -152,14 +162,14 @@ alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetect
|
||||
},
|
||||
"context": {
|
||||
"properties": [{
|
||||
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"name": "objectDetectionClasses",
|
||||
"value": detectionTypes.classes.map(type => ({
|
||||
"imageNetClass": type
|
||||
})),
|
||||
timeOfSample: new Date().toISOString(),
|
||||
uncertaintyInMilliseconds: 0
|
||||
}]
|
||||
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
|
||||
"name": "objectDetectionClasses",
|
||||
"value": detectionTypes.classes.map(type => ({
|
||||
"imageNetClass": type
|
||||
})),
|
||||
timeOfSample: new Date().toISOString(),
|
||||
uncertaintyInMilliseconds: 0
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
179
plugins/amcrest/package-lock.json
generated
179
plugins/amcrest/package-lock.json
generated
@@ -1,25 +1,23 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.130",
|
||||
"version": "0.0.131",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.130",
|
||||
"version": "0.0.131",
|
||||
"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.3"
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.18"
|
||||
"@types/node": "^20.10.8"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -27,14 +25,16 @@
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.10.8",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"version": "0.2.103",
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -69,15 +69,6 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"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
|
||||
@@ -86,150 +77,20 @@
|
||||
"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": "18.16.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
|
||||
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw=="
|
||||
},
|
||||
"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==",
|
||||
"version": "20.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
|
||||
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"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.3",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz",
|
||||
"integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==",
|
||||
"dependencies": {
|
||||
"http-errors": "~1.8.1",
|
||||
"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": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"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": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"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"
|
||||
}
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.130",
|
||||
"version": "0.0.131",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -35,13 +35,10 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.3"
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.18"
|
||||
"@types/node": "^20.10.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { Readable } from 'stream';
|
||||
import https from 'https';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { amcrestHttpsAgent, getDeviceInfo } from './probe';
|
||||
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
|
||||
import { getDeviceInfo } from './probe';
|
||||
|
||||
export enum AmcrestEvent {
|
||||
MotionStart = "Code=VideoMotion;action=Start",
|
||||
@@ -22,35 +21,44 @@ export enum AmcrestEvent {
|
||||
DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse",
|
||||
}
|
||||
|
||||
|
||||
export class AmcrestCameraClient {
|
||||
digestAuth: AxiosDigestAuth;
|
||||
credential: AuthFetchCredentialState;
|
||||
|
||||
constructor(public ip: string, username: string, password: string, public console?: Console) {
|
||||
this.digestAuth = new AxiosDigestAuth({
|
||||
this.credential = {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
|
||||
const options: AuthFetchOptions<T> = {
|
||||
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
|
||||
url: urlOrOptions,
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
credential: this.credential,
|
||||
body,
|
||||
};
|
||||
|
||||
const response = await authHttpFetch(options);
|
||||
return response;
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=reboot`,
|
||||
responseType: 'text',
|
||||
});
|
||||
return response.data as string;
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async checkTwoWayAudio() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`,
|
||||
responseType: 'text',
|
||||
});
|
||||
return (response.data as string).includes('result=1');
|
||||
return response.body.includes('result=1');
|
||||
}
|
||||
|
||||
// appAutoStart=true
|
||||
@@ -61,33 +69,27 @@ export class AmcrestCameraClient {
|
||||
// updateSerial=IPC-AW46WN-S2
|
||||
// updateSerialCloudUpgrade=IPC-AW46WN-.....
|
||||
async getDeviceInfo() {
|
||||
return getDeviceInfo(this.digestAuth, this.ip);
|
||||
return getDeviceInfo(this.credential, this.ip);
|
||||
}
|
||||
|
||||
async jpegSnapshot(): Promise<Buffer> {
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async listenEvents() {
|
||||
const url = `http://${this.ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]`;
|
||||
console.log('preparing event listener', url);
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'stream',
|
||||
const response = await this.request({
|
||||
url,
|
||||
responseType: 'readable',
|
||||
});
|
||||
const stream = response.data as IncomingMessage;
|
||||
const stream = response.body;
|
||||
stream.socket.setKeepAlive(true);
|
||||
|
||||
stream.on('data', (buffer: Buffer) => {
|
||||
@@ -118,12 +120,12 @@ export class AmcrestCameraClient {
|
||||
async enableContinousRecording(channel: number) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const url = `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&Record[${channel - 1}].TimeSection[${i}][0]=1 00:00:00-23:59:59`;
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "POST",
|
||||
const response = await this.request({
|
||||
url,
|
||||
});
|
||||
this.console.log(response.data);
|
||||
method: 'POST',
|
||||
responseType: 'text',
|
||||
},);
|
||||
this.console.log(response.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { PassThrough, Readable, Stream } from "stream";
|
||||
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
|
||||
import { amcrestHttpsAgent } from './probe';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
@@ -94,12 +93,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
for (const element of deviceParameters) {
|
||||
try {
|
||||
const response = await this.getClient().digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`
|
||||
const response = await this.getClient().request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
const result = String(response.data).replace(element.replace, "").trim();
|
||||
const result = String(response.body).replace(element.replace, "").trim();
|
||||
deviceInfo[element.parameter] = result;
|
||||
}
|
||||
catch (e) {
|
||||
@@ -147,11 +145,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
if (![...params.keys()].length)
|
||||
return;
|
||||
|
||||
const response = await this.getClient().digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`
|
||||
const response = await this.getClient().request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
|
||||
responseType: 'text',
|
||||
});
|
||||
this.console.log('reconfigure result', response.data);
|
||||
this.console.log('reconfigure result', response.body);
|
||||
}
|
||||
|
||||
getClient() {
|
||||
@@ -309,9 +307,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
|
||||
}
|
||||
@@ -334,7 +329,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
return this.storage.getItem('rtspChannel');
|
||||
}
|
||||
|
||||
|
||||
createRtspMediaStreamOptions(url: string, index: number) {
|
||||
const ret = super.createRtspMediaStreamOptions(url, index);
|
||||
ret.tool = 'scrypted';
|
||||
@@ -348,12 +342,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
this.videoStreamOptions = (async () => {
|
||||
let mas: string;
|
||||
try {
|
||||
const response = await client.digestAuth.request({
|
||||
const response = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
})
|
||||
mas = response.data.split('=')[1].trim();
|
||||
mas = response.body.split('=')[1].trim();
|
||||
this.storage.setItem('maxExtraStreams', mas.toString());
|
||||
}
|
||||
catch (e) {
|
||||
@@ -366,18 +359,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
|
||||
|
||||
try {
|
||||
const capResponse = await client.digestAuth.request({
|
||||
const capResponse = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
});
|
||||
this.console.log(capResponse.data);
|
||||
const encodeResponse = await client.digestAuth.request({
|
||||
this.console.log(capResponse.body);
|
||||
const encodeResponse = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
|
||||
responseType: 'text',
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
});
|
||||
this.console.log(encodeResponse.data);
|
||||
this.console.log(encodeResponse.body);
|
||||
|
||||
for (let i = 0; i < vsos.length; i++) {
|
||||
const vso = vsos[i];
|
||||
@@ -392,9 +383,9 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
encName = `table.Encode[${channel - 1}].ExtraFormat[${i - 1}]`;
|
||||
}
|
||||
|
||||
const videoCodec = findValue(encodeResponse.data, encName, 'Video.Compression')
|
||||
const videoCodec = findValue(encodeResponse.body, encName, 'Video.Compression')
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
let audioCodec = findValue(encodeResponse.data, encName, 'Audio.Compression')
|
||||
let audioCodec = findValue(encodeResponse.body, encName, 'Audio.Compression')
|
||||
?.replace('.', '')?.toLowerCase()?.trim();
|
||||
if (audioCodec?.includes('aac'))
|
||||
audioCodec = 'aac';
|
||||
@@ -409,18 +400,18 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
vso.audio.codec = audioCodec;
|
||||
vso.video.codec = videoCodec;
|
||||
|
||||
const width = findValue(encodeResponse.data, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.data, encName, 'Video.Height');
|
||||
const width = findValue(encodeResponse.body, encName, 'Video.Width');
|
||||
const height = findValue(encodeResponse.body, encName, 'Video.Height');
|
||||
if (width && height) {
|
||||
vso.video.width = parseInt(width);
|
||||
vso.video.height = parseInt(height);
|
||||
}
|
||||
|
||||
const bitrateOptions = findValue(capResponse.data, capName, 'Video.BitRateOptions');
|
||||
const bitrateOptions = findValue(capResponse.body, capName, 'Video.BitRateOptions');
|
||||
if (!bitrateOptions)
|
||||
continue;
|
||||
|
||||
const encodeOptions = findValue(encodeResponse.data, encName, 'Video.BitRate');
|
||||
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
|
||||
if (!encodeOptions)
|
||||
continue;
|
||||
|
||||
@@ -538,16 +529,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|
||||
// seems the dahua doorbells preferred 1024 chunks. should investigate adts
|
||||
// parsing and sending multipart chunks instead.
|
||||
const passthrough = new PassThrough();
|
||||
this.getClient().digestAuth.request({
|
||||
method: 'POST',
|
||||
this.getClient().request({
|
||||
url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'Audio/AAC',
|
||||
'Content-Length': '9999999'
|
||||
},
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
data: passthrough,
|
||||
});
|
||||
responseType: 'readable',
|
||||
}, passthrough);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import https from 'https';
|
||||
|
||||
export const amcrestHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
|
||||
// appAutoStart=true
|
||||
// deviceType=IP4M-1041B
|
||||
@@ -11,17 +7,16 @@ export const amcrestHttpsAgent = new https.Agent({
|
||||
// serialNumber=12345
|
||||
// updateSerial=IPC-AW46WN-S2
|
||||
|
||||
import AxiosDigestAuth from "@koush/axios-digest-auth";
|
||||
|
||||
// updateSerialCloudUpgrade=IPC-AW46WN-.....
|
||||
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
|
||||
const response = await digestAuth.request({
|
||||
httpsAgent: amcrestHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
export async function getDeviceInfo(credential: AuthFetchCredentialState, address: string) {
|
||||
const response = await authHttpFetch({
|
||||
credential,
|
||||
url: `http://${address}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
|
||||
rejectUnauthorized: false,
|
||||
responseType: 'text',
|
||||
});
|
||||
const lines = (response.data as string).split('\n');
|
||||
const lines = response.body.split('\n');
|
||||
const vals: {
|
||||
[key: string]: string,
|
||||
} = {};
|
||||
|
||||
6
plugins/bticino/package-lock.json
generated
6
plugins/bticino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"dependencies": {
|
||||
"@slyoldfox/sip": "^0.0.6-1",
|
||||
"sdp": "^3.0.3",
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.105",
|
||||
"version": "0.3.2",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/bticino",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { closeQuiet, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import { RtspServer } from '@scrypted/common/src/rtsp-server';
|
||||
import { addTrackControls } from '@scrypted/common/src/sdp-utils';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
|
||||
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, MotionSensor, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
|
||||
import { SipCallSession } from '../../sip/src/sip-call-session';
|
||||
import { RtpDescription } from '../../sip/src/rtp-utils';
|
||||
import { RtpDescription, getPayloadType, getSequenceNumber, isRtpMessagePayloadType, isStunMessage } from '../../sip/src/rtp-utils';
|
||||
import { VoicemailHandler } from './bticino-voicemailHandler';
|
||||
import { CompositeSipMessageHandler } from '../../sip/src/compositeSipMessageHandler';
|
||||
import { SipHelper } from './sip-helper';
|
||||
@@ -16,7 +16,7 @@ import { BticinoSipLock } from './bticino-lock';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
|
||||
import { PersistentSipManager } from './persistent-sip-manager';
|
||||
import { InviteHandler } from './bticino-inviteHandler';
|
||||
import { SipRequest } from '../../sip/src/sip-manager';
|
||||
import { SipOptions, SipRequest } from '../../sip/src/sip-manager';
|
||||
|
||||
import { get } from 'http'
|
||||
import { ControllerApi } from './c300x-controller-api';
|
||||
@@ -26,14 +26,12 @@ import { BticinoMuteSwitch } from './bticino-mute-switch';
|
||||
const STREAM_TIMEOUT = 65000;
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
|
||||
export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor, DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
|
||||
|
||||
private session: SipCallSession
|
||||
private remoteRtpDescription: Promise<RtpDescription>
|
||||
private audioOutForwarder: dgram.Socket
|
||||
private audioOutProcess: ChildProcess
|
||||
private currentMedia: FFmpegInput | MediaStreamUrl
|
||||
private currentMediaMimeType: string
|
||||
private refreshTimeout: NodeJS.Timeout
|
||||
public requestHandlers: CompositeSipMessageHandler = new CompositeSipMessageHandler()
|
||||
public incomingCallRequest : SipRequest
|
||||
@@ -41,16 +39,21 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
private voicemailHandler : VoicemailHandler = new VoicemailHandler(this)
|
||||
private inviteHandler : InviteHandler = new InviteHandler(this)
|
||||
private controllerApi : ControllerApi = new ControllerApi(this)
|
||||
private muteSwitch : BticinoMuteSwitch
|
||||
private aswmSwitch : BticinoAswmSwitch
|
||||
private deferredCleanup
|
||||
private currentMediaObject : Promise<MediaObject>
|
||||
private lastImageRefresh : number
|
||||
//TODO: randomize this
|
||||
private keyAndSalt : string = "/qE7OPGKp9hVGALG2KcvKWyFEZfSSvm7bYVDjT8X"
|
||||
//private decodedSrtpOptions : SrtpOptions = decodeSrtpOptions( this.keyAndSalt )
|
||||
private persistentSipManager : PersistentSipManager
|
||||
public doorbellWebhookUrl : string
|
||||
public doorbellLockWebhookUrl : string
|
||||
private cachedImage : Buffer
|
||||
|
||||
constructor(nativeId: string, public provider: BticinoSipPlugin) {
|
||||
super(nativeId)
|
||||
|
||||
this.requestHandlers.add( this.voicemailHandler ).add( this.inviteHandler )
|
||||
this.persistentSipManager = new PersistentSipManager( this );
|
||||
(async() => {
|
||||
@@ -190,7 +193,21 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
}
|
||||
|
||||
async takePicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
|
||||
const thumbnailCacheTime : number = parseInt( this.storage?.getItem('thumbnailCacheTime') ) * 1000 || 300000
|
||||
const now = new Date().getTime()
|
||||
if( !this.lastImageRefresh || this.lastImageRefresh + thumbnailCacheTime < now ) {
|
||||
// get a proxy object to make sure we pass prebuffer when already watching a stream
|
||||
let cam : VideoCamera = sdk.systemManager.getDeviceById<VideoCamera>(this.id)
|
||||
let vs : MediaObject = await cam.getVideoStream()
|
||||
let buf : Buffer = await mediaManager.convertMediaObjectToBuffer(vs, 'image/jpeg');
|
||||
this.cachedImage = buf
|
||||
this.lastImageRefresh = new Date().getTime()
|
||||
this.console.log(`Camera picture updated and cached: ${this.lastImageRefresh} + cache time: ${thumbnailCacheTime} < ${now}`)
|
||||
|
||||
} else {
|
||||
this.console.log(`Not refreshing camera picture: ${this.lastImageRefresh} + cache time: ${thumbnailCacheTime} < ${now}`)
|
||||
}
|
||||
return mediaManager.createMediaObject(this.cachedImage, 'image/jpeg')
|
||||
}
|
||||
|
||||
async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
@@ -206,8 +223,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
}
|
||||
|
||||
async startIntercom(media: MediaObject): Promise<void> {
|
||||
if (!this.session)
|
||||
throw new Error("not in call");
|
||||
if (!this.session) {
|
||||
const cleanup = () => {
|
||||
this.console.log("STARTINTERCOM CLEANUP CALLED: " + this.session )
|
||||
this.session?.stop()
|
||||
this.session = undefined
|
||||
this.deferredCleanup()
|
||||
this.console.log("STARTINTERCOM CLEANUP ENDED")
|
||||
}
|
||||
this.session = await this.callIntercom( cleanup )
|
||||
}
|
||||
|
||||
|
||||
this.stopIntercom();
|
||||
|
||||
@@ -280,27 +306,24 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
throw new Error('Please configure from/to/domain settings')
|
||||
}
|
||||
|
||||
if (options?.metadata?.refreshAt) {
|
||||
if (!this.currentMedia?.mediaStreamOptions)
|
||||
throw new Error("no stream to refresh");
|
||||
|
||||
const currentMedia = this.currentMedia
|
||||
currentMedia.mediaStreamOptions.refreshAt = Date.now() + STREAM_TIMEOUT;
|
||||
currentMedia.mediaStreamOptions.metadata = {
|
||||
refreshAt: currentMedia.mediaStreamOptions.refreshAt
|
||||
};
|
||||
this.resetStreamTimeout()
|
||||
return mediaManager.createMediaObject(currentMedia, this.currentMediaMimeType)
|
||||
}
|
||||
|
||||
this.console.log("Before stopping session")
|
||||
this.stopSession();
|
||||
const { clientPromise: playbackPromise, port: playbackPort, url: clientUrl } = await listenZeroSingleClient()
|
||||
this.console.log("After stopping session")
|
||||
|
||||
const playbackUrl = clientUrl
|
||||
let rebroadcastEnabled = this.interfaces?.includes( "mixin:@scrypted/prebuffer-mixin")
|
||||
|
||||
const { clientPromise: playbackPromise, port: playbackPort } = await listenZeroSingleClient()
|
||||
|
||||
const playbackUrl = `rtsp://127.0.0.1:${playbackPort}`
|
||||
|
||||
this.console.log("PLAYBACKURL: " +playbackUrl)
|
||||
|
||||
playbackPromise.then(async (client) => {
|
||||
client.setKeepAlive(true, 10000)
|
||||
|
||||
let sip: SipCallSession
|
||||
let audioSplitter
|
||||
let videoSplitter
|
||||
try {
|
||||
if( !this.incomingCallRequest ) {
|
||||
// If this is a "view" call, update the stream endpoint to send it only to "us"
|
||||
@@ -309,61 +332,37 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
}
|
||||
|
||||
let rtsp: RtspServer;
|
||||
|
||||
const cleanup = () => {
|
||||
this.console.log("CLEANUP CALLED")
|
||||
client.destroy();
|
||||
if (this.session === sip)
|
||||
this.session = undefined
|
||||
try {
|
||||
this.log.d('cleanup(): stopping sip session.')
|
||||
sip.stop()
|
||||
sip?.stop()
|
||||
this.currentMediaObject = undefined
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
audioSplitter?.server?.close()
|
||||
videoSplitter?.server?.close()
|
||||
rtsp?.destroy()
|
||||
this.console.log("CLEANUP ENDED")
|
||||
this.deferredCleanup = undefined
|
||||
this.remoteRtpDescription = undefined
|
||||
}
|
||||
this.deferredCleanup = cleanup
|
||||
|
||||
client.on('close', cleanup)
|
||||
client.on('error', cleanup)
|
||||
|
||||
let sipOptions = SipHelper.sipOptions( this )
|
||||
|
||||
sip = await this.persistentSipManager.session( sipOptions );
|
||||
// Validate this sooner
|
||||
if( !sip ) return Promise.reject("Cannot create session")
|
||||
|
||||
sip.onCallEnded.subscribe(cleanup)
|
||||
|
||||
// Call the C300X
|
||||
this.remoteRtpDescription = sip.callOrAcceptInvite(
|
||||
( audio ) => {
|
||||
return [
|
||||
//TODO: Payload types are hardcoded
|
||||
`m=audio 65000 RTP/SAVP 110`,
|
||||
`a=rtpmap:110 speex/8000`,
|
||||
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
|
||||
]
|
||||
}, ( video ) => {
|
||||
if( false ) {
|
||||
//TODO: implement later
|
||||
return [
|
||||
`m=video 0 RTP/SAVP 0`
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
//TODO: Payload types are hardcoded
|
||||
`m=video 65002 RTP/SAVP 96`,
|
||||
`a=rtpmap:96 H264/90000`,
|
||||
`a=fmtp:96 profile-level-id=42801F`,
|
||||
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
|
||||
'a=recvonly'
|
||||
]
|
||||
}
|
||||
}, this.incomingCallRequest );
|
||||
|
||||
this.incomingCallRequest = undefined
|
||||
if( !rebroadcastEnabled || (rebroadcastEnabled && !this.incomingCallRequest ) ) {
|
||||
sip = await this.callIntercom( cleanup )
|
||||
}
|
||||
|
||||
//let sdp: string = replacePorts(this.remoteRtpDescription.sdp, 0, 0 )
|
||||
let sdp : string = [
|
||||
let sdp : string = [
|
||||
"v=0",
|
||||
"m=audio 5000 RTP/AVP 110",
|
||||
"c=IN IP4 127.0.0.1",
|
||||
@@ -375,42 +374,141 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
//sdp = sdp.replaceAll(/a=crypto\:1.*/g, '')
|
||||
//sdp = sdp.replaceAll(/RTP\/SAVP/g, 'RTP\/AVP')
|
||||
//sdp = sdp.replaceAll('\r\n\r\n', '\r\n')
|
||||
sdp = addTrackControls(sdp)
|
||||
sdp = sdp.split('\n').filter(line => !line.includes('a=rtcp-mux')).join('\n')
|
||||
if( sipOptions.debugSip )
|
||||
this.log.d('SIP: Updated SDP:\n' + sdp);
|
||||
|
||||
client.write(sdp)
|
||||
client.end()
|
||||
let vseq = 0;
|
||||
let vseen = 0;
|
||||
let vlost = 0;
|
||||
let aseq = 0;
|
||||
let aseen = 0;
|
||||
let alost = 0;
|
||||
|
||||
sdp = addTrackControls(sdp);
|
||||
sdp = sdp.split('\n').filter(line => !line.includes('a=rtcp-mux')).join('\n');
|
||||
this.console.log('proposed sdp', sdp);
|
||||
|
||||
this.console.log("================= AUDIOSPLITTER CREATING.... ============" )
|
||||
audioSplitter = await createBindUdp(5000)
|
||||
this.console.log("================= AUDIOSPLITTER CREATED ============" )
|
||||
audioSplitter.server.on('close', () => {
|
||||
this.console.log("================= CLOSED AUDIOSPLITTER ================")
|
||||
audioSplitter = undefined
|
||||
})
|
||||
this.console.log("================= VIDEOSPLITTER CREATING.... ============" )
|
||||
videoSplitter = await createBindUdp(5002)
|
||||
this.console.log("================= VIDEOSPLITTER CREATED.... ============" )
|
||||
videoSplitter.server.on('close', () => {
|
||||
this.console.log("================= CLOSED VIDEOSPLITTER ================")
|
||||
videoSplitter = undefined
|
||||
})
|
||||
|
||||
rtsp = new RtspServer(client, sdp, false);
|
||||
|
||||
const parsedSdp = parseSdp(rtsp.sdp);
|
||||
const videoTrack = parsedSdp.msections.find(msection => msection.type === 'video').control;
|
||||
const audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio').control;
|
||||
rtsp.console = this.console;
|
||||
|
||||
await rtsp.handlePlayback();
|
||||
|
||||
this.session = sip
|
||||
|
||||
videoSplitter.server.on('message', (message, rinfo) => {
|
||||
if ( !isStunMessage(message)) {
|
||||
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
|
||||
if (!isRtpMessage)
|
||||
return;
|
||||
vseen++;
|
||||
try {
|
||||
rtsp.sendTrack(videoTrack, message, !isRtpMessage);
|
||||
} catch(e ) {
|
||||
this.console.log(e)
|
||||
}
|
||||
|
||||
const seq = getSequenceNumber(message);
|
||||
if (seq !== (vseq + 1) % 0x0FFFF)
|
||||
vlost++;
|
||||
vseq = seq;
|
||||
}
|
||||
});
|
||||
|
||||
audioSplitter.server.on('message', (message, rinfo ) => {
|
||||
if ( !isStunMessage(message)) {
|
||||
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
|
||||
if (!isRtpMessage)
|
||||
return;
|
||||
aseen++;
|
||||
try {
|
||||
rtsp.sendTrack(audioTrack, message, !isRtpMessage);
|
||||
} catch(e) {
|
||||
this.console.log(e)
|
||||
}
|
||||
const seq = getSequenceNumber(message);
|
||||
if (seq !== (aseq + 1) % 0x0FFFF)
|
||||
alost++;
|
||||
aseq = seq;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await rtsp.handleTeardown();
|
||||
this.console.log('rtsp client ended');
|
||||
} catch (e) {
|
||||
this.console.log('rtsp client ended ungracefully', e);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error(e)
|
||||
sip?.stop()
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
this.resetStreamTimeout();
|
||||
|
||||
const mediaStreamOptions = Object.assign(this.getSipMediaStreamOptions(), {
|
||||
refreshAt: Date.now() + STREAM_TIMEOUT,
|
||||
});
|
||||
|
||||
const ffmpegInput: FFmpegInput = {
|
||||
url: undefined,
|
||||
container: 'sdp',
|
||||
mediaStreamOptions,
|
||||
inputArguments: [
|
||||
'-f', 'sdp',
|
||||
'-i', playbackUrl,
|
||||
],
|
||||
const mediaStreamUrl: MediaStreamUrl = {
|
||||
url: playbackUrl,
|
||||
mediaStreamOptions: this.getSipMediaStreamOptions(),
|
||||
};
|
||||
this.currentMedia = ffmpegInput;
|
||||
this.currentMediaMimeType = ScryptedMimeTypes.FFmpegInput;
|
||||
|
||||
return mediaManager.createFFmpegMediaObject(ffmpegInput);
|
||||
sleep(2500).then( () => this.takePicture() )
|
||||
|
||||
this.currentMediaObject = mediaManager.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl);
|
||||
// Invalidate any cached image and take a picture after some seconds to take into account the opening of the lens
|
||||
this.lastImageRefresh = undefined
|
||||
return this.currentMediaObject
|
||||
}
|
||||
|
||||
async callIntercom( cleanup ) : Promise<SipCallSession> {
|
||||
let sipOptions : SipOptions = SipHelper.sipOptions( this )
|
||||
|
||||
let sip : SipCallSession = await this.persistentSipManager.session( sipOptions );
|
||||
// Validate this sooner
|
||||
if( !sip ) return Promise.reject("Cannot create session")
|
||||
|
||||
sip.onCallEnded.subscribe(cleanup)
|
||||
|
||||
// Call the C300X
|
||||
this.remoteRtpDescription = sip.callOrAcceptInvite(
|
||||
( audio ) => {
|
||||
return [
|
||||
// this SDP is used by the intercom and will send the encrypted packets which we don't care about to the loopback on port 65000 of the intercom
|
||||
`m=audio 65000 RTP/SAVP 110`,
|
||||
`a=rtpmap:110 speex/8000`,
|
||||
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
|
||||
]
|
||||
}, ( video ) => {
|
||||
return [
|
||||
// this SDP is used by the intercom and will send the encrypted packets which we don't care about to the loopback on port 65000 of the intercom
|
||||
`m=video 65002 RTP/SAVP 96`,
|
||||
`a=rtpmap:96 H264/90000`,
|
||||
`a=fmtp:96 profile-level-id=42801F`,
|
||||
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
|
||||
'a=recvonly'
|
||||
]
|
||||
}, this.incomingCallRequest );
|
||||
|
||||
this.incomingCallRequest = undefined
|
||||
|
||||
return sip
|
||||
}
|
||||
|
||||
getSipMediaStreamOptions(): ResponseMediaStreamOptions {
|
||||
@@ -419,13 +517,16 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
name: 'SIP',
|
||||
// this stream is NOT scrypted blessed due to wackiness in the h264 stream.
|
||||
// tool: "scrypted",
|
||||
container: 'sdp',
|
||||
container: 'rtsp',
|
||||
video: {
|
||||
codec: 'h264'
|
||||
},
|
||||
audio: {
|
||||
// this is a hint to let homekit, et al, know that it's speex audio and needs transcoding.
|
||||
codec: 'speex',
|
||||
},
|
||||
source: 'cloud', // to disable prebuffering
|
||||
userConfigurable: false,
|
||||
userConfigurable: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -437,18 +538,26 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
|
||||
|
||||
async getDevice(nativeId: string) : Promise<any> {
|
||||
if( nativeId && nativeId.endsWith('-aswm-switch')) {
|
||||
return new BticinoAswmSwitch(this, this.voicemailHandler)
|
||||
this.aswmSwitch = new BticinoAswmSwitch(this, this.voicemailHandler)
|
||||
return this.aswmSwitch
|
||||
} else if( nativeId && nativeId.endsWith('-mute-switch') ) {
|
||||
return new BticinoMuteSwitch(this)
|
||||
this.muteSwitch = new BticinoMuteSwitch(this)
|
||||
return this.muteSwitch
|
||||
}
|
||||
return new BticinoSipLock(this)
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
this.stopIntercom()
|
||||
this.voicemailHandler.cancelTimer()
|
||||
this.persistentSipManager.cancelTimer()
|
||||
this.controllerApi.cancelTimer()
|
||||
if( nativeId?.endsWith('-aswm-switch') ) {
|
||||
this.aswmSwitch.cancelTimer()
|
||||
} else if( nativeId?.endsWith('mute-switch') ) {
|
||||
this.muteSwitch.cancelTimer()
|
||||
} else {
|
||||
this.stopIntercom()
|
||||
this.voicemailHandler.cancelTimer()
|
||||
this.persistentSipManager.cancelTimer()
|
||||
this.controllerApi.cancelTimer()
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
64
plugins/bticino/src/bticino-mute-switch.ts
Normal file
64
plugins/bticino/src/bticino-mute-switch.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ScryptedDeviceBase, HttpRequest, HttpResponse, HttpRequestHandler, OnOff } from "@scrypted/sdk";
|
||||
import { BticinoSipCamera } from "./bticino-camera";
|
||||
|
||||
export class BticinoMuteSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
|
||||
private timeout : NodeJS.Timeout
|
||||
|
||||
constructor(private camera: BticinoSipCamera) {
|
||||
super( camera.nativeId + "-mute-switch");
|
||||
this.on = false;
|
||||
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
|
||||
}
|
||||
|
||||
turnOff(): Promise<void> {
|
||||
this.on = false
|
||||
return this.camera.muteRinger(false)
|
||||
}
|
||||
|
||||
turnOn(): Promise<void> {
|
||||
this.on = true
|
||||
return this.camera.muteRinger(true)
|
||||
}
|
||||
|
||||
syncStatus() {
|
||||
this.camera.muteStatus().then( (value) => {
|
||||
this.on = value["status"]
|
||||
} ).catch( (e) => { this.camera.console.error(e) } ).finally( () => {
|
||||
this.timeout = setTimeout( () => this.syncStatus() , 60000 )
|
||||
} )
|
||||
}
|
||||
|
||||
cancelTimer() {
|
||||
if( this.timeout ) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
}
|
||||
|
||||
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
||||
if (request.url.endsWith('/disabled')) {
|
||||
this.on = false
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else if( request.url.endsWith('/enabled') ) {
|
||||
this.on = true
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else if( request.url.endsWith('/enable') ) {
|
||||
this.turnOn()
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else if( request.url.endsWith('/disable') ) {
|
||||
this.turnOff()
|
||||
response.send('Success', {
|
||||
code: 200,
|
||||
});
|
||||
} else {
|
||||
response.send('Unsupported operation', {
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,7 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Intercom,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedDeviceType.DeviceProvider,
|
||||
ScryptedInterface.HttpRequestHandler,
|
||||
ScryptedInterface.VideoClips,
|
||||
|
||||
@@ -61,7 +61,7 @@ export class SipHelper {
|
||||
if( !md5 ) {
|
||||
md5 = crypto.createHash('md5').update( camera.nativeId ).digest("hex")
|
||||
md5 = md5.substring(0, 8) + '-' + md5.substring(8, 12) + '-' + md5.substring(12,16) + '-' + md5.substring(16, 32)
|
||||
camera.storage.setItem('md5has', md5)
|
||||
camera.storage.setItem('md5hash', md5)
|
||||
}
|
||||
return md5
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ export class BticinoStorageSettings {
|
||||
defaultValue: 600,
|
||||
placeholder: '600',
|
||||
},
|
||||
thumbnailCacheTime: {
|
||||
title: 'Thumbnail cache time',
|
||||
type: 'number',
|
||||
range: [60, 86400],
|
||||
description: 'How long the snapshot is cached before taking a new one. (in seconds)',
|
||||
defaultValue: 300,
|
||||
placeholder: '300',
|
||||
},
|
||||
sipdebug: {
|
||||
title: 'SIP debug logging',
|
||||
type: 'boolean',
|
||||
|
||||
4
plugins/cloud/package-lock.json
generated
4
plugins/cloud/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^20.4.5"
|
||||
},
|
||||
"version": "0.2.3"
|
||||
"version": "0.2.4"
|
||||
}
|
||||
|
||||
@@ -574,6 +574,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
});
|
||||
|
||||
const { token_info } = this.storageSettings.values;
|
||||
if (!token_info)
|
||||
throw new Error('Scrypted Cloud is not logged in. Skipping home.scrypted.app registration.');
|
||||
const response = await axios(`https://${SCRYPTED_SERVER}/_punch/register?${q}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token_info}`
|
||||
|
||||
4
plugins/core/.vscode/launch.json
vendored
4
plugins/core/.vscode/launch.json
vendored
@@ -10,14 +10,14 @@
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"**/plugin-remote-worker.*",
|
||||
"**/plugin-console.*",
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "pwa-node"
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
plugins/core/package-lock.json
generated
6
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.149",
|
||||
"version": "0.2.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.149",
|
||||
"version": "0.2.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.108",
|
||||
"version": "0.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.149",
|
||||
"version": "0.2.3",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export class Timer {
|
||||
|
||||
}
|
||||
@@ -9,14 +9,12 @@ import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
|
||||
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
|
||||
import { LauncherMixin } from './launcher-mixin';
|
||||
import { MediaCore } from './media-core';
|
||||
import { ScriptCore, ScriptCoreNativeId } from './script-core';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { newScript, ScriptCore, ScriptCoreNativeId } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
|
||||
const { systemManager, deviceManager, endpointManager } = sdk;
|
||||
|
||||
const indexHtml = fs.readFileSync('dist/index.html').toString();
|
||||
|
||||
export function getAddresses() {
|
||||
const addresses: string[] = [];
|
||||
for (const [iface, nif] of Object.entries(os.networkInterfaces())) {
|
||||
@@ -61,10 +59,14 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
},
|
||||
}
|
||||
});
|
||||
indexHtml: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
||||
this.indexHtml = fs.readFileSync('dist/index.html').toString();
|
||||
|
||||
(async () => {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
@@ -226,7 +228,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
const endpoint = await endpointManager.getPublicCloudEndpoint();
|
||||
const u = new URL(endpoint);
|
||||
|
||||
const rewritten = indexHtml
|
||||
const rewritten = this.indexHtml
|
||||
.replace('href="manifest.json"', `href="manifest.json${u.search}"`)
|
||||
.replace('href="img/icons/apple-touch-icon-152x152.png"', `href="img/icons/apple-touch-icon-152x152.png${u.search}"`)
|
||||
.replace('href="img/icons/safari-pinned-tab.svg"', `href="img/icons/safari-pinned-tab.svg${u.search}"`)
|
||||
@@ -269,5 +271,6 @@ export default ScryptedCore;
|
||||
export async function fork() {
|
||||
return {
|
||||
tsCompile,
|
||||
newScript,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class MediaCore extends ScryptedDeviceBase implements DeviceProvider, Buf
|
||||
constructor() {
|
||||
super(MediaCoreNativeId);
|
||||
|
||||
this.fromMimeType = ScryptedMimeTypes.SchemePrefix + 'scrypted-media';
|
||||
this.fromMimeType = ScryptedMimeTypes.SchemePrefix + 'scrypted-media' + ';converter-weight=2';
|
||||
this.toMimeType = ScryptedMimeTypes.MediaObject;
|
||||
|
||||
(async () => {
|
||||
|
||||
@@ -1,33 +1,23 @@
|
||||
import { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
|
||||
import { Script } from "./script";
|
||||
import sdk from '@scrypted/sdk';
|
||||
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from '@scrypted/sdk';
|
||||
import { randomBytes } from "crypto";
|
||||
import fs from 'fs';
|
||||
import path from "path/posix";
|
||||
import { Worker } from "worker_threads";
|
||||
import { Script } from "./script";
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
export const ScriptCoreNativeId = 'scriptcore';
|
||||
|
||||
interface ScriptWorker {
|
||||
script: Script;
|
||||
worker: Worker;
|
||||
}
|
||||
|
||||
export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator, Readme {
|
||||
scripts = new Map<string, Promise<Script>>();
|
||||
scripts = new Map<string, ScriptWorker>();
|
||||
|
||||
constructor() {
|
||||
super(ScriptCoreNativeId);
|
||||
|
||||
for (const nativeId of deviceManager.getNativeIds()) {
|
||||
if (nativeId?.startsWith('script:')) {
|
||||
const script = new Script(nativeId);
|
||||
this.scripts.set(nativeId, (async () => {
|
||||
if (script.providedInterfaces.length > 2) {
|
||||
await script.run();
|
||||
}
|
||||
else {
|
||||
this.reportScript(nativeId);
|
||||
}
|
||||
return script;
|
||||
})());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
@@ -51,6 +41,10 @@ export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, De
|
||||
const nativeId = 'script:' + randomBytes(8).toString('hex');
|
||||
await this.reportScript(nativeId, name?.toString());
|
||||
const script = new Script(nativeId);
|
||||
this.scripts.set(nativeId, {
|
||||
script,
|
||||
worker: undefined,
|
||||
});
|
||||
if (template) {
|
||||
try {
|
||||
await script.saveScript({
|
||||
@@ -65,7 +59,6 @@ export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, De
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
this.scripts.set(nativeId, Promise.resolve(script));
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
@@ -84,10 +77,75 @@ export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, De
|
||||
return await deviceManager.onDeviceDiscovered(device);
|
||||
}
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
return this.scripts.get(nativeId);
|
||||
async getDevice(nativeId: string) {
|
||||
const e = this.scripts.get(nativeId);
|
||||
if (e) {
|
||||
if (e.script)
|
||||
return e.script;
|
||||
e.worker?.terminate();
|
||||
this.scripts.delete(nativeId);
|
||||
}
|
||||
|
||||
let script = new Script(nativeId);
|
||||
let worker: Worker;
|
||||
|
||||
const triggerDeviceDiscover = async (name: string, type: ScryptedDeviceType, interfaces: string[]) => {
|
||||
const e = this.scripts.get(nativeId);
|
||||
if (e?.script == script)
|
||||
e.script = undefined;
|
||||
|
||||
const device: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name,
|
||||
nativeId,
|
||||
type,
|
||||
interfaces,
|
||||
refresh: true,
|
||||
};
|
||||
return await deviceManager.onDeviceDiscovered(device);
|
||||
};
|
||||
|
||||
if (script.providedInterfaces.length > 2) {
|
||||
const fork = sdk.fork<{
|
||||
newScript: typeof newScript,
|
||||
}>();
|
||||
worker = fork.worker;
|
||||
try {
|
||||
script = await (await fork.result).newScript(nativeId, triggerDeviceDiscover);
|
||||
}
|
||||
catch (e) {
|
||||
worker.terminate();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
worker?.on('exit', () => {
|
||||
if (this.scripts.get(nativeId)?.worker === worker) {
|
||||
this.scripts.delete(nativeId);
|
||||
// notify the system that the device needs to be refreshed.
|
||||
if (deviceManager.getNativeIds().includes(nativeId)) {
|
||||
const script = new Script(nativeId);
|
||||
triggerDeviceDiscover(script.providedName, script.providedType, script.providedInterfaces);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.scripts.set(nativeId, {
|
||||
script,
|
||||
worker,
|
||||
});
|
||||
return script;
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
const worker = this.scripts.get(nativeId)?.worker;
|
||||
this.scripts.delete(nativeId);
|
||||
worker?.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
export async function newScript(nativeId: ScryptedNativeId, triggerDeviceDiscover: (name: string, type: ScryptedDeviceType, interfaces: string[]) => Promise<string>) {
|
||||
const script = new Script(nativeId, triggerDeviceDiscover);
|
||||
await script.run();
|
||||
return script;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@ import { createScriptDevice, ScriptDeviceImpl } from "@scrypted/common/src/eval/
|
||||
import { ScriptCoreNativeId } from "./script-core";
|
||||
import { PluginAPIProxy } from "../../../server/src/plugin/plugin-api";
|
||||
|
||||
const { log, deviceManager, systemManager } = sdk;
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export class Script extends ScryptedDeviceBase implements Scriptable, Program, ScriptDeviceImpl {
|
||||
apiProxy: PluginAPIProxy;
|
||||
|
||||
constructor(nativeId: string) {
|
||||
constructor(nativeId: string, public triggerDeviceDiscover?: (name: string, type: ScryptedDeviceType, interfaces: string[]) => Promise<string>) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
@@ -18,6 +16,8 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
|
||||
this.storage.setItem('data', JSON.stringify({
|
||||
'script.ts': source.script,
|
||||
}));
|
||||
|
||||
this.triggerDeviceDiscover?.(this.providedName, this.providedType, this.providedInterfaces);
|
||||
}
|
||||
|
||||
async loadScripts(): Promise<{ [filename: string]: ScriptSource; }> {
|
||||
@@ -70,46 +70,38 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
|
||||
}
|
||||
|
||||
prepareScript() {
|
||||
this.apiProxy?.removeListeners();
|
||||
|
||||
Object.assign(this, createScriptDevice([
|
||||
ScryptedInterface.Scriptable,
|
||||
ScryptedInterface.Program,
|
||||
]));
|
||||
}
|
||||
|
||||
async run(variables?: { [name: string]: any; }): Promise<any> {
|
||||
async runInternal(script: string, variables?: { [name: string]: any; }): Promise<any> {
|
||||
this.prepareScript();
|
||||
|
||||
try {
|
||||
const data = JSON.parse(this.storage.getItem('data'));
|
||||
|
||||
const { value, defaultExport, apiProxy } = await scryptedEval(this, data['script.ts'], Object.assign({
|
||||
const { value, defaultExport } = await scryptedEval(this, script, Object.assign({
|
||||
device: this,
|
||||
}, variables));
|
||||
|
||||
this.apiProxy = apiProxy;
|
||||
|
||||
await this.postRunScript(defaultExport);
|
||||
return value;
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error loading script', e);
|
||||
this.console.error('error evaluating script', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async run(variables?: { [name: string]: any; }): Promise<any> {
|
||||
const data = JSON.parse(this.storage.getItem('data'));
|
||||
return this.runInternal(data['script.ts'], variables)
|
||||
}
|
||||
|
||||
async eval(source: ScriptSource, variables?: { [name: string]: any }) {
|
||||
this.prepareScript();
|
||||
|
||||
const { value, defaultExport, apiProxy } = await scryptedEval(this, source.script, Object.assign({
|
||||
device: this,
|
||||
}, variables));
|
||||
|
||||
this.apiProxy = apiProxy;
|
||||
|
||||
await this.postRunScript(defaultExport);
|
||||
return value;
|
||||
return this.runInternal(source.script, variables);
|
||||
}
|
||||
|
||||
// will be done at runtime
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScryptedDeviceBase, ScryptedNativeId, StreamService } from "@scrypted/sdk";
|
||||
import { IPty, spawn as ptySpawn } from 'node-pty-prebuilt-multiarch';
|
||||
import type { IPty, spawn as ptySpawn } from 'node-pty-prebuilt-multiarch';
|
||||
import { createAsyncQueue } from '@scrypted/common/src/async-queue'
|
||||
import { ChildProcess, spawn as childSpawn } from "child_process";
|
||||
|
||||
@@ -9,8 +9,7 @@ export const TerminalServiceNativeId = 'terminalservice';
|
||||
class InteractiveTerminal {
|
||||
cp: IPty
|
||||
|
||||
constructor(cmd: string[]) {
|
||||
const spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
|
||||
constructor(cmd: string[], spawn: typeof ptySpawn) {
|
||||
if (cmd?.length) {
|
||||
this.cp = spawn(cmd[0], cmd.slice(1), {});
|
||||
} else {
|
||||
@@ -150,8 +149,7 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (cp)
|
||||
cp.kill();
|
||||
cp?.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,37 +160,40 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
continue;
|
||||
|
||||
if (Buffer.isBuffer(message)) {
|
||||
if (cp)
|
||||
cp.write(message);
|
||||
cp?.write(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message.toString());
|
||||
if (parsed.dim) {
|
||||
if (cp)
|
||||
cp.resize(parsed.dim.cols, parsed.dim.rows);
|
||||
cp?.resize(parsed.dim.cols, parsed.dim.rows);
|
||||
} else if (parsed.eof) {
|
||||
if (cp)
|
||||
cp.sendEOF();
|
||||
cp?.sendEOF();
|
||||
} else if ("interactive" in parsed && !cp) {
|
||||
if (parsed.interactive) {
|
||||
cp = new InteractiveTerminal(parsed.cmd);
|
||||
try {
|
||||
const spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
|
||||
cp = new InteractiveTerminal(parsed.cmd, spawn);
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('Error starting pty', e);
|
||||
queue.end(e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
cp = new NoninteractiveTerminal(parsed.cmd);
|
||||
}
|
||||
registerChildListeners();
|
||||
}
|
||||
} catch {
|
||||
if (cp)
|
||||
cp.write(Buffer.from(message));
|
||||
cp?.write(Buffer.from(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.log(e);
|
||||
if (cp)
|
||||
cp.kill();
|
||||
cp?.kill();
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ export default {
|
||||
const version = await info.getVersion();
|
||||
const scryptedEnv = await info.getScryptedEnv();
|
||||
this.currentVersion = version;
|
||||
const { updateAvailable } = await checkServerUpdate(version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
|
||||
const { updateAvailable } = await checkServerUpdate(this.$scrypted.mediaManager, version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
|
||||
this.updateAvailable = updateAvailable;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
// old scrypted servers dont support this call, or it may be unimplemented
|
||||
// in which case fall back and determine what the install type is.
|
||||
const scryptedEnv = await info.getScryptedEnv();
|
||||
const { updateAvailable } = await checkServerUpdate(version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
|
||||
const { updateAvailable } = await checkServerUpdate(this.$scrypted.mediaManager, version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
|
||||
this.updateAvailable = updateAvailable;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DeviceCreator, Scriptable, ScryptedInterface, ScryptedStatic, SystemManager } from '@scrypted/types';
|
||||
import { DeviceCreator, MediaObject, Scriptable, ScryptedInterface, ScryptedStatic, SystemManager, MediaManager } from '@scrypted/types';
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import semver from 'semver';
|
||||
import { getAllDevices } from '../../common/mixin';
|
||||
@@ -66,7 +66,7 @@ export async function checkUpdate(npmPackage: string, npmPackageVersion: string)
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkServerUpdate(version: string, installEnvironment: string): Promise<PluginUpdateCheck> {
|
||||
export async function checkServerUpdate(mediaManager: MediaManager, version: string, installEnvironment: string): Promise<PluginUpdateCheck> {
|
||||
const { updateAvailable, updatePublished, versions } = await checkUpdate(
|
||||
"@scrypted/server",
|
||||
version
|
||||
@@ -78,8 +78,9 @@ export async function checkServerUpdate(version: string, installEnvironment: str
|
||||
// check if there is a new docker image available, using 'latest' tag
|
||||
// this is done so newer server versions in npm are not immediately
|
||||
// displayed until a docker image has been published
|
||||
let response: AxiosResponse<any> = await axios.get("https://corsproxy.io?https://hub.docker.com/v2/namespaces/koush/repositories/scrypted/tags/latest");
|
||||
const { data } = response;
|
||||
// due to CORS restrictions, we query this through scrypted server
|
||||
const mo: MediaObject = await mediaManager.createMediaObjectFromUrl('https://hub.docker.com/v2/namespaces/koush/repositories/scrypted/tags/latest');
|
||||
const data: any = await mediaManager.convertMediaObjectToJSON(mo, 'application/json');
|
||||
const imagePublished = new Date(data.last_updated);
|
||||
console.log(`Latest docker image published ${imagePublished}`);
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ export default {
|
||||
|
||||
const fs = 20;
|
||||
|
||||
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="2" fill="none" />
|
||||
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="1px" fill="none" />
|
||||
<text x="${x}" y="${y}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
|
||||
<text x="${x}" y="${y}" font-size="${fs}" fill="white">${t}</text>
|
||||
`;
|
||||
|
||||
@@ -45,7 +45,7 @@ export default {
|
||||
for (const detection of this.lastDetection.detections || []) {
|
||||
if (!detection.boundingBox) continue;
|
||||
const svgScale = this.svgWidth / 1080;
|
||||
const sw = 6 * svgScale;
|
||||
const sw = 1;
|
||||
const s = "red";
|
||||
const x = detection.boundingBox[0];
|
||||
const y = detection.boundingBox[1];
|
||||
|
||||
162
plugins/ffmpeg-camera/package-lock.json
generated
162
plugins/ffmpeg-camera/package-lock.json
generated
@@ -9,40 +9,37 @@
|
||||
"version": "0.0.22",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"url-parse": "^1.4.7"
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6"
|
||||
"@types/node": "^20.10.8"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"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"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.10.8",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.103",
|
||||
"dev": true,
|
||||
"version": "0.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
@@ -75,15 +72,6 @@
|
||||
"../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
|
||||
@@ -93,82 +81,32 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz",
|
||||
"integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==",
|
||||
"version": "20.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
|
||||
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"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/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"@types/node": "^20.10.8",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
@@ -178,7 +116,7 @@
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
@@ -196,47 +134,19 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz",
|
||||
"integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==",
|
||||
"version": "20.10.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
|
||||
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
|
||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA=="
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6"
|
||||
"@types/node": "^20.10.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"url-parse": "^1.4.7"
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, MediaObject, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
|
||||
import { randomBytes } from "crypto";
|
||||
import https from 'https';
|
||||
|
||||
const { deviceManager, mediaManager } = sdk;
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export interface UrlMediaStreamOptions extends ResponseMediaStreamOptions {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export abstract class CameraBase<T extends ResponseMediaStreamOptions> extends ScryptedDeviceBase implements Camera, VideoCamera, Settings {
|
||||
snapshotAuth: AxiosDigestAuth;
|
||||
|
||||
constructor(nativeId: string, public provider: CameraProviderBase<T>) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
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');
|
||||
takePicture(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.");
|
||||
}
|
||||
|
||||
abstract takePicture(option?: PictureOptions): Promise<MediaObject>;
|
||||
|
||||
async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
@@ -140,8 +117,6 @@ export abstract class CameraBase<T extends ResponseMediaStreamOptions> extends S
|
||||
this.storage.setItem(key, value.toString());
|
||||
}
|
||||
|
||||
this.snapshotAuth = undefined;
|
||||
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { FFmpegInput, MediaObject, PictureOptions, Setting, SettingValue } from "@scrypted/sdk";
|
||||
import sdk, { FFmpegInput, MediaObject, Setting, SettingValue } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { CameraBase, CameraProviderBase, UrlMediaStreamOptions } from "./common";
|
||||
|
||||
@@ -15,10 +15,6 @@ function parseDoubleQuotedArguments(input: string) {
|
||||
}
|
||||
|
||||
class FFmpegCamera 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.");
|
||||
}
|
||||
|
||||
storageSettings = new StorageSettings(this, {
|
||||
ffmpegInputs: {
|
||||
title: 'FFmpeg Input Stream Arguments',
|
||||
|
||||
6143
plugins/gstreamer-camera/package-lock.json
generated
6143
plugins/gstreamer-camera/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,12 +32,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.9.6"
|
||||
"@types/node": "^20.10.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"url-parse": "^1.4.7"
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
import sdk, { ScryptedDeviceBase, DeviceProvider, Settings, Setting, ScryptedDeviceType, VideoCamera, MediaObject, MediaStreamOptions, ScryptedInterface, FFmpegInput, Camera, PictureOptions, SettingValue, DeviceCreator, DeviceCreatorSettings, ResponseMediaStreamOptions } from "@scrypted/sdk";
|
||||
import { recommendRebroadcast } from "./recommend";
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import https from 'https';
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
const { log, deviceManager, mediaManager } = sdk;
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
|
||||
export interface UrlMediaStreamOptions extends MediaStreamOptions {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export abstract class CameraBase<T extends ResponseMediaStreamOptions> extends ScryptedDeviceBase implements Camera, VideoCamera, Settings {
|
||||
snapshotAuth: AxiosDigestAuth;
|
||||
pendingPicture: Promise<MediaObject>;
|
||||
|
||||
constructor(nativeId: string, public provider: CameraProviderBase<T>) {
|
||||
super(nativeId);
|
||||
}
|
||||
|
||||
getSnapshotUrl() {
|
||||
return this.storage.getItem('snapshotUrl');
|
||||
}
|
||||
|
||||
async takePicture(option?: PictureOptions): Promise<MediaObject> {
|
||||
if (!this.pendingPicture) {
|
||||
this.pendingPicture = this.takePictureThrottled(option);
|
||||
this.pendingPicture.finally(() => this.pendingPicture = undefined);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
async getPictureOptions(): Promise<PictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
|
||||
getDefaultOrderedVideoStreamOptions(vsos: T[]) {
|
||||
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;
|
||||
}
|
||||
|
||||
async getVideoStreamOptions(): Promise<T[]> {
|
||||
let vsos = this.getRawVideoStreamOptions();
|
||||
return this.getDefaultOrderedVideoStreamOptions(vsos);
|
||||
}
|
||||
|
||||
abstract getRawVideoStreamOptions(): T[];
|
||||
|
||||
isAudioDisabled() {
|
||||
return this.storage.getItem('noAudio') === 'true';
|
||||
}
|
||||
|
||||
async getVideoStream(options?: T): Promise<MediaObject> {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
const vso = vsos?.find(s => s.id === options?.id) || this.getDefaultStream(vsos);
|
||||
return this.createVideoStream(vso);
|
||||
}
|
||||
|
||||
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(),
|
||||
];
|
||||
}
|
||||
|
||||
getUsername() {
|
||||
return this.storage.getItem('username');
|
||||
}
|
||||
|
||||
getPassword() {
|
||||
return this.storage.getItem('password');
|
||||
}
|
||||
|
||||
async getOtherSettings(): Promise<Setting[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getDefaultStream(vsos: T[]) {
|
||||
let defaultStreamIndex = vsos?.findIndex(vso => vso.id === this.storage.getItem('defaultStream'));
|
||||
if (defaultStreamIndex === -1)
|
||||
defaultStreamIndex = 0;
|
||||
|
||||
defaultStreamIndex = defaultStreamIndex || 0;
|
||||
return vsos?.[defaultStreamIndex];
|
||||
}
|
||||
|
||||
async getStreamSettings(): Promise<Setting[]> {
|
||||
try {
|
||||
const vsos = await this.getVideoStreamOptions();
|
||||
if (!vsos?.length || vsos?.length === 1)
|
||||
return [];
|
||||
|
||||
|
||||
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',
|
||||
}
|
||||
];
|
||||
}
|
||||
catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getUsernameDescription(): string {
|
||||
return 'Optional: Username for snapshot http requests.';
|
||||
}
|
||||
|
||||
getPasswordDescription(): string {
|
||||
return 'Optional: Password for snapshot http requests.';
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'username',
|
||||
title: 'Username',
|
||||
value: this.getUsername(),
|
||||
description: this.getUsernameDescription(),
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
title: 'Password',
|
||||
value: this.getPassword(),
|
||||
type: 'password',
|
||||
description: this.getPasswordDescription(),
|
||||
},
|
||||
...await this.getUrlSettings(),
|
||||
...await this.getStreamSettings(),
|
||||
...await this.getOtherSettings(),
|
||||
{
|
||||
key: 'noAudio',
|
||||
title: 'No Audio',
|
||||
description: 'Enable this setting if the camera does not have audio or to mute audio.',
|
||||
type: 'boolean',
|
||||
value: (this.isAudioDisabled()).toString(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async putSettingBase(key: string, value: SettingValue) {
|
||||
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.snapshotAuth = undefined;
|
||||
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
||||
devices = new Map<string, any>();
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
for (const camId of deviceManager.getNativeIds()) {
|
||||
if (camId)
|
||||
this.getDevice(camId);
|
||||
}
|
||||
|
||||
recommendRebroadcast();
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
|
||||
const nativeId = randomBytes(4).toString('hex');
|
||||
const name = settings.newCamera.toString();
|
||||
await this.updateDevice(nativeId, name, this.getInterfaces());
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'newCamera',
|
||||
title: 'Add Camera',
|
||||
placeholder: 'Camera name, e.g.: Back Yard Camera, Baby Camera, etc',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
getAdditionalInterfaces(): string[] {
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
getInterfaces() {
|
||||
return [ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.Settings, ...this.getAdditionalInterfaces()];
|
||||
}
|
||||
|
||||
updateDevice(nativeId: string, name: string, interfaces: string[], type?: ScryptedDeviceType) {
|
||||
return deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
interfaces,
|
||||
type: type || ScryptedDeviceType.Camera,
|
||||
});
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string | number) {
|
||||
// generate a random id
|
||||
const nativeId = randomBytes(4).toString('hex');
|
||||
const name = value.toString();
|
||||
|
||||
this.updateDevice(nativeId, name, this.getInterfaces());
|
||||
}
|
||||
|
||||
abstract createCamera(nativeId: string): CameraBase<T>;
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
let ret = this.devices.get(nativeId);
|
||||
if (!ret) {
|
||||
ret = this.createCamera(nativeId);
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import sdk, { FFmpegInput, MediaObject, MediaStreamOptions, ResponseMediaStreamOptions, Setting, SettingValue } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from "child_process";
|
||||
import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "./common";
|
||||
import { CameraProviderBase, CameraBase, UrlMediaStreamOptions } from "../../ffmpeg-camera/src/common";
|
||||
// import {} from "../../../common/src/stream-parser"
|
||||
// import {} from "../../../common/src/ffmpeg-rebroadcast"
|
||||
import net from 'net';
|
||||
@@ -81,7 +81,6 @@ class GStreamerCamera extends CameraBase<ResponseMediaStreamOptions> {
|
||||
|
||||
async getUrlSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
...await this.getSnapshotUrlSettings(),
|
||||
...await this.getGStreamerInputSettings(),
|
||||
];
|
||||
}
|
||||
|
||||
232
plugins/hikvision/package-lock.json
generated
232
plugins/hikvision/package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.132",
|
||||
"version": "0.0.133",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.132",
|
||||
"version": "0.0.133",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"axios": "^1.4.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
@@ -30,15 +29,16 @@
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
"@types/node": "^20.10.8",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.103",
|
||||
"version": "0.3.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -76,23 +76,6 @@
|
||||
"../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/@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
|
||||
@@ -114,75 +97,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/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": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"node_modules/http-auth-utils": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.2.tgz",
|
||||
"integrity": "sha512-cQ8957aiUX0lgV1620uIGKGJc0sEuD/QK4ueZ0hb60MGbO0f6ahcuIgPjamAD98D/AUGizKVm+dNvUVHs0f4Ow==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
"yerror": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
"node": ">=12.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
@@ -190,30 +113,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@@ -238,37 +137,27 @@
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yerror": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.2.1.tgz",
|
||||
"integrity": "sha512-WPZgybhCBzsMSSqGYBnj20NZo4FiFKQG0+i/21cYGVd4B7eYtvYDOpjk/0e8UM1eVHJ+4Ja6bZ7TAjHH6mk+ew==",
|
||||
"engines": {
|
||||
"node": ">=12.19.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"@types/node": "^20.10.8",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
@@ -308,52 +197,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"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": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"http-auth-utils": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.2.tgz",
|
||||
"integrity": "sha512-cQ8957aiUX0lgV1620uIGKGJc0sEuD/QK4ueZ0hb60MGbO0f6ahcuIgPjamAD98D/AUGizKVm+dNvUVHs0f4Ow==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
"yerror": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
@@ -361,24 +210,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@@ -397,6 +228,11 @@
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
"yerror": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.2.1.tgz",
|
||||
"integrity": "sha512-WPZgybhCBzsMSSqGYBnj20NZo4FiFKQG0+i/21cYGVd4B7eYtvYDOpjk/0e8UM1eVHJ+4Ja6bZ7TAjHH6mk+ew=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.132",
|
||||
"version": "0.0.133",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -35,11 +35,10 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"axios": "^1.4.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { getDeviceInfo, hikvisionHttpsAgent } from './probe';
|
||||
import { AuthFetchCredentialState, AuthFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { IncomingMessage, RequestOptions } from 'http';
|
||||
import { Readable } from 'stream';
|
||||
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/http-fetch-helpers';
|
||||
import { getDeviceInfo } from './probe';
|
||||
|
||||
export function getChannel(channel: string) {
|
||||
return channel || '101';
|
||||
@@ -28,47 +30,59 @@ export interface HikvisionCameraStreamSetup {
|
||||
}
|
||||
|
||||
export class HikvisionCameraAPI {
|
||||
digestAuth: AxiosDigestAuth;
|
||||
credential: AuthFetchCredentialState;
|
||||
deviceModel: Promise<string>;
|
||||
listenerPromise: Promise<IncomingMessage>;
|
||||
|
||||
constructor(public ip: string, username: string, password: string, public console: Console) {
|
||||
this.digestAuth = new AxiosDigestAuth({
|
||||
this.credential = {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async request<T extends HttpFetchResponseType>(urlOrOptions: string | URL | HttpFetchOptions<T>, body?: Readable) {
|
||||
const options: AuthFetchOptions<T> = {
|
||||
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
|
||||
url: urlOrOptions,
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
credential: this.credential,
|
||||
body,
|
||||
};
|
||||
|
||||
const response = await authHttpFetch(options);
|
||||
return response;
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/ISAPI/System/reboot`,
|
||||
method: "PUT",
|
||||
responseType: 'text',
|
||||
url: `http://${this.ip}/ISAPI/System/reboot`,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async getDeviceInfo() {
|
||||
return getDeviceInfo(this.digestAuth, this.ip);
|
||||
return getDeviceInfo(this.credential, this.ip);
|
||||
}
|
||||
|
||||
async checkTwoWayAudio() {
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/ISAPI/System/TwoWayAudio/channels`,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
return (response.data as string).includes('Speaker');
|
||||
return response.body.includes('Speaker');
|
||||
}
|
||||
|
||||
async checkDeviceModel(): Promise<string> {
|
||||
if (!this.deviceModel) {
|
||||
this.deviceModel = this.getDeviceInfo().then(d => d.deviceModel).catch(e => {
|
||||
this.console.error('error checking NVR model', e);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return await this.deviceModel;
|
||||
@@ -91,17 +105,15 @@ export class HikvisionCameraAPI {
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
const response = await this.request({
|
||||
url: `http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/capabilities`,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
// this is bad:
|
||||
// <videoCodecType opt="H.264,H.265">H.265</videoCodecType>
|
||||
const vcodec = response.data.match(/>(.*?)<\/videoCodecType>/);
|
||||
const acodec = response.data.match(/>(.*?)<\/audioCompressionType>/);
|
||||
const vcodec = response.body.match(/>(.*?)<\/videoCodecType>/);
|
||||
const acodec = response.body.match(/>(.*?)<\/audioCompressionType>/);
|
||||
|
||||
return {
|
||||
videoCodecType: vcodec?.[1],
|
||||
@@ -112,15 +124,12 @@ export class HikvisionCameraAPI {
|
||||
async jpegSnapshot(channel: string): Promise<Buffer> {
|
||||
const url = `http://${this.ip}/ISAPI/Streaming/channels/${getChannel(channel)}/picture?snapShotImageType=JPEG`
|
||||
|
||||
const response = await this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
const response = await this.request({
|
||||
url: url,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async listenEvents() {
|
||||
@@ -128,13 +137,11 @@ export class HikvisionCameraAPI {
|
||||
if (!this.listenerPromise) {
|
||||
const url = `http://${this.ip}/ISAPI/Event/notification/alertStream`;
|
||||
|
||||
this.listenerPromise = this.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
this.listenerPromise = this.request({
|
||||
url,
|
||||
responseType: 'stream',
|
||||
responseType: 'readable',
|
||||
}).then(response => {
|
||||
const stream = response.data as IncomingMessage;
|
||||
const stream = response.body;
|
||||
stream.socket.setKeepAlive(true);
|
||||
|
||||
stream.on('data', (buffer: Buffer) => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
|
||||
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
|
||||
import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
|
||||
import { hikvisionHttpsAgent } from './probe';
|
||||
|
||||
const { mediaManager } = sdk;
|
||||
|
||||
@@ -207,12 +206,11 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo
|
||||
try {
|
||||
let xml: string;
|
||||
try {
|
||||
const response = await client.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
const response = await client.request({
|
||||
url: `http://${this.getHttpAddress()}/ISAPI/Streaming/channels`,
|
||||
responseType: 'text',
|
||||
});
|
||||
xml = response.data;
|
||||
xml = response.body;
|
||||
this.storage.setItem('channels', xml);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -382,12 +380,12 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo
|
||||
|
||||
try {
|
||||
const parameters = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels`;
|
||||
const { data: parametersData } = await this.getClient().digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
const { body } = await this.getClient().request({
|
||||
url: parameters,
|
||||
responseType: 'text',
|
||||
});
|
||||
|
||||
const parsedXml = await xml2js.parseStringPromise(parametersData);
|
||||
const parsedXml = await xml2js.parseStringPromise(body);
|
||||
for (const twoWayChannel of parsedXml.TwoWayAudioChannelList.TwoWayAudioChannel) {
|
||||
const [id] = twoWayChannel.id;
|
||||
if (id !== channel)
|
||||
@@ -423,27 +421,25 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo
|
||||
|
||||
const passthrough = new PassThrough();
|
||||
const open = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/open`;
|
||||
const { data } = await this.getClient().digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: 'PUT',
|
||||
const { body } = await this.getClient().request({
|
||||
url: open,
|
||||
responseType: 'text',
|
||||
method: 'PUT',
|
||||
});
|
||||
this.console.log('two way audio opened', data);
|
||||
this.console.log('two way audio opened', body);
|
||||
|
||||
const url = `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${channel}/audioData`;
|
||||
this.console.log('posting audio data to', url);
|
||||
|
||||
const put = this.getClient().digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: 'PUT',
|
||||
const put = this.getClient().request({
|
||||
url,
|
||||
responseType: 'readable',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
// 'Connection': 'close',
|
||||
'Content-Length': '0'
|
||||
},
|
||||
data: passthrough,
|
||||
});
|
||||
}, passthrough);
|
||||
|
||||
let available = Buffer.alloc(0);
|
||||
this.activeIntercom?.kill();
|
||||
@@ -484,11 +480,10 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom, Reboo
|
||||
}
|
||||
|
||||
const client = this.getClient();
|
||||
await client.digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: 'PUT',
|
||||
await client.request({
|
||||
url: `http://${this.getHttpAddress()}/ISAPI/System/TwoWayAudio/channels/${this.getRtspChannel() || '1'}/close`,
|
||||
})
|
||||
method: 'PUT',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import https from 'https';
|
||||
import AxiosDigestAuth from '@koush/axios-digest-auth';
|
||||
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
|
||||
import { checkStatus } from '../../../server/src/http-fetch-helpers';
|
||||
|
||||
export const hikvisionHttpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
|
||||
try {
|
||||
const response = await digestAuth.request({
|
||||
httpsAgent: hikvisionHttpsAgent,
|
||||
method: "GET",
|
||||
responseType: 'text',
|
||||
url: `http://${address}/ISAPI/System/deviceInfo`,
|
||||
});
|
||||
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
|
||||
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
|
||||
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
|
||||
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
|
||||
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
|
||||
return {
|
||||
deviceModel,
|
||||
deviceName,
|
||||
serialNumber,
|
||||
macAddress,
|
||||
firmwareVersion,
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
if (e?.response?.data?.includes('notActivated'))
|
||||
throw new Error(`Camera must be first be activated at http://${address}.`)
|
||||
throw e;
|
||||
}
|
||||
export async function getDeviceInfo(credential: AuthFetchCredentialState, address: string) {
|
||||
const response = await authHttpFetch({
|
||||
credential,
|
||||
url: `http://${address}/ISAPI/System/deviceInfo`,
|
||||
ignoreStatusCode: true,
|
||||
responseType: 'text',
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
if (response.body.includes('notActivated'))
|
||||
throw new Error(`Camera must be first be activated at http://${address}.`);
|
||||
|
||||
checkStatus(response.statusCode);
|
||||
|
||||
const deviceModel = response.body.match(/>(.*?)<\/model>/)?.[1];
|
||||
const deviceName = response.body.match(/>(.*?)<\/deviceName>/)?.[1];
|
||||
const serialNumber = response.body.match(/>(.*?)<\/serialNumber>/)?.[1];
|
||||
const macAddress = response.body.match(/>(.*?)<\/macAddress>/)?.[1];
|
||||
const firmwareVersion = response.body.match(/>(.*?)<\/firmwareVersion>/)?.[1];
|
||||
return {
|
||||
deviceModel,
|
||||
deviceName,
|
||||
serialNumber,
|
||||
macAddress,
|
||||
firmwareVersion,
|
||||
};
|
||||
}
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.31",
|
||||
"version": "1.2.33",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.31",
|
||||
"version": "1.2.33",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.31",
|
||||
"version": "1.2.33",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -69,10 +69,16 @@ addSupportedType({
|
||||
resolutions: [
|
||||
// 3840x2160@30 (4k).
|
||||
[3840, 2160, 30],
|
||||
// 3K
|
||||
[2880, 1620, 30],
|
||||
// 2MP
|
||||
[2560, 1440, 30],
|
||||
// 1920x1080@30 (1080p).
|
||||
[1920, 1080, 30],
|
||||
// 1280x720@30 (720p).
|
||||
[1280, 720, 30],
|
||||
[960, 540, 30],
|
||||
[640, 360, 30],
|
||||
// 320x240@15 (Apple Watch).
|
||||
[320, 240, 15],
|
||||
]
|
||||
@@ -103,7 +109,7 @@ addSupportedType({
|
||||
const openRecordingStreams = new Map<number, Deferred<any>>();
|
||||
if (isRecordingEnabled) {
|
||||
recordingDelegate = {
|
||||
updateRecordingConfiguration(newConfiguration: CameraRecordingConfiguration ) {
|
||||
updateRecordingConfiguration(newConfiguration: CameraRecordingConfiguration) {
|
||||
configuration = newConfiguration;
|
||||
},
|
||||
handleRecordingStreamRequest(streamId: number): AsyncGenerator<RecordingPacket> {
|
||||
|
||||
@@ -304,10 +304,16 @@ export function createCameraStreamingDelegate(device: ScryptedDevice & VideoCame
|
||||
const mediaOptions: RequestMediaStreamOptions = {
|
||||
destination,
|
||||
destinationId: session.prepareRequest.targetAddress,
|
||||
destinationType: '@scrypted/homekit',
|
||||
adaptive: true,
|
||||
video: {
|
||||
codec: 'h264',
|
||||
bitrate: request.video.max_bit_rate * 1000,
|
||||
// if these are sent as width/height rather than clientWidth/clientHeight,
|
||||
// rebroadcast will always choose substream to treat it as a hard constraint.
|
||||
// send as hint for adaptive bitrate.
|
||||
clientWidth: request.video.width,
|
||||
clientHeight: request.video.height,
|
||||
},
|
||||
audio: {
|
||||
// opus is the preferred/default codec, and can be repacketized to fit any request if in use.
|
||||
|
||||
4
plugins/mqtt/package-lock.json
generated
4
plugins/mqtt/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/mqtt",
|
||||
"version": "0.0.68",
|
||||
"version": "0.0.76",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/mqtt",
|
||||
"version": "0.0.68",
|
||||
"version": "0.0.76",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
"aedes": "^0.46.1",
|
||||
|
||||
@@ -41,5 +41,5 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@types/nunjucks": "^3.2.0"
|
||||
},
|
||||
"version": "0.0.68"
|
||||
"version": "0.0.76"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Settings, Setting, ScryptedDeviceBase, ScryptedInterface } from '@scrypted/sdk';
|
||||
import { connect, Client } from 'mqtt';
|
||||
import { ScriptableDeviceBase } from '../scrypted-eval';
|
||||
import type {MqttProvider} from '../main';
|
||||
|
||||
export class MqttDeviceBase extends ScriptableDeviceBase implements Settings {
|
||||
client: Client;
|
||||
handler: any;
|
||||
pathname: string;
|
||||
|
||||
constructor(nativeId: string) {
|
||||
constructor(public provider: MqttProvider, nativeId: string) {
|
||||
super(nativeId, undefined);
|
||||
}
|
||||
|
||||
@@ -53,9 +54,36 @@ export class MqttDeviceBase extends ScriptableDeviceBase implements Settings {
|
||||
this.client?.removeAllListeners();
|
||||
this.client?.end();
|
||||
this.client = undefined;
|
||||
const url = new URL(this.storage.getItem('url'));
|
||||
this.pathname = url.pathname.substring(1);
|
||||
const urlWithoutPath = new URL(this.storage.getItem('url'));
|
||||
const urlString = this.storage.getItem('url');
|
||||
let url: URL;
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
const externalBroker = this.provider.storage.getItem('externalBroker');
|
||||
if (urlString) {
|
||||
this.console.log('Using device specific broker.', urlString);
|
||||
url = new URL(urlString);
|
||||
username = this.storage.getItem('username') || undefined;
|
||||
password = this.storage.getItem('password') || undefined;
|
||||
this.pathname = url.pathname.substring(1);
|
||||
}
|
||||
else if (externalBroker && !this.provider.isBrokerEnabled) {
|
||||
this.console.log('Using external broker.', externalBroker);
|
||||
url = new URL(externalBroker);
|
||||
username = this.provider.storage.getItem('username') || undefined;
|
||||
password = this.provider.storage.getItem('password') || undefined;
|
||||
this.pathname = `${url.pathname.substring(1)}/${this.id}`;
|
||||
}
|
||||
else {
|
||||
this.console.log('Using built in broker.');
|
||||
const tcpPort = this.provider.storage.getItem('tcpPort') || '';
|
||||
url = new URL(`mqtt://localhost:${tcpPort}/scrypted`);
|
||||
username = this.provider.storage.getItem('username') || undefined;
|
||||
password = this.provider.storage.getItem('password') || undefined;
|
||||
this.pathname = `${url.pathname.substring(1)}/${this.id}`;
|
||||
}
|
||||
|
||||
const urlWithoutPath = new URL(url);
|
||||
urlWithoutPath.pathname = '';
|
||||
|
||||
const client = this.client = connect(urlWithoutPath.toString(), {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Brightness, DeviceProvider, Lock, LockState, OnOff, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings } from "@scrypted/sdk";
|
||||
import { MqttClient, connect } from "mqtt";
|
||||
import { MqttDeviceBase } from "../api/mqtt-device-base";
|
||||
import crypto from 'crypto';
|
||||
import { Brightness, DeviceProvider, Lock, LockState, MixinDeviceBase, OnOff, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceProperty, Setting, Settings } from "@scrypted/sdk";
|
||||
import { Client, MqttClient, connect } from "mqtt";
|
||||
import { MqttDeviceBase } from "./api/mqtt-device-base";
|
||||
import nunjucks from 'nunjucks';
|
||||
import sdk from "@scrypted/sdk";
|
||||
import type { MqttProvider } from './main';
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
@@ -59,8 +61,8 @@ typeMap.set('binary_sensor', {
|
||||
export class MqttAutoDiscoveryProvider extends MqttDeviceBase implements DeviceProvider {
|
||||
devices = new Map<string, MqttAutoDiscoveryDevice>();
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
constructor(provider: MqttProvider, nativeId: string) {
|
||||
super(provider, nativeId);
|
||||
|
||||
this.bind();
|
||||
}
|
||||
@@ -180,7 +182,7 @@ export class MqttAutoDiscoveryProvider extends MqttDeviceBase implements DeviceP
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
|
||||
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string) {
|
||||
@@ -340,3 +342,90 @@ export class MqttAutoDiscoveryDevice extends ScryptedDeviceBase implements OnOff
|
||||
config.value_template, config.payload_unlock, 'UNLOCK');
|
||||
}
|
||||
}
|
||||
|
||||
interface AutoDiscoveryConfig {
|
||||
component: string;
|
||||
create: (mqttId: string, device: MixinDeviceBase<any>, topic: string) => any;
|
||||
}
|
||||
|
||||
const autoDiscoveryMap = new Map<string, AutoDiscoveryConfig>();
|
||||
|
||||
function getAutoDiscoveryDevice(device: MixinDeviceBase<any>, mqttId: string) {
|
||||
return {
|
||||
dev: {
|
||||
name: device.name,
|
||||
// what the hell is this
|
||||
"ids": crypto.createHash('sha256').update(`scrypted-${mqttId}-${device.id}`).digest().toString('hex').substring(0, 8),
|
||||
"sw": device.info?.version,
|
||||
"mdl": device.info?.model,
|
||||
"mf": device.info?.manufacturer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createBinarySensorConfig(mqttId: string, device: MixinDeviceBase<any>, prop: ScryptedInterfaceProperty, topic: string) {
|
||||
return {
|
||||
state_topic: `${topic}/${prop}`,
|
||||
payload_on: 'true',
|
||||
payload_off: 'false',
|
||||
...getAutoDiscoveryDevice(device, mqttId),
|
||||
}
|
||||
}
|
||||
|
||||
function addBinarySensor(iface: ScryptedInterface, prop: ScryptedInterfaceProperty) {
|
||||
autoDiscoveryMap.set(iface, {
|
||||
component: 'binary_sensor',
|
||||
create(mqttId, device, topic) {
|
||||
return createBinarySensorConfig(mqttId, device, prop, topic);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addBinarySensor(ScryptedInterface.MotionSensor, ScryptedInterfaceProperty.motionDetected);
|
||||
addBinarySensor(ScryptedInterface.BinarySensor, ScryptedInterfaceProperty.binaryState);
|
||||
addBinarySensor(ScryptedInterface.OccupancySensor, ScryptedInterfaceProperty.occupied);
|
||||
addBinarySensor(ScryptedInterface.FloodSensor, ScryptedInterfaceProperty.flooded);
|
||||
addBinarySensor(ScryptedInterface.AudioSensor, ScryptedInterfaceProperty.audioDetected);
|
||||
addBinarySensor(ScryptedInterface.Online, ScryptedInterfaceProperty.online);
|
||||
|
||||
autoDiscoveryMap.set(ScryptedInterface.Thermometer, {
|
||||
component: 'sensor',
|
||||
create(mqttId, device, topic) {
|
||||
return {
|
||||
state_topic: `${topic}/${ScryptedInterfaceProperty.temperature}`,
|
||||
value_template: '{{ value_json }}',
|
||||
unit_of_measurement: 'C',
|
||||
...getAutoDiscoveryDevice(device, mqttId),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
autoDiscoveryMap.set(ScryptedInterface.HumiditySensor, {
|
||||
component: 'sensor',
|
||||
create(mqttId, device, topic) {
|
||||
return {
|
||||
state_topic: `${topic}/${ScryptedInterfaceProperty.humidity}`,
|
||||
value_template: '{{ value_json }}',
|
||||
unit_of_measurement: '%',
|
||||
...getAutoDiscoveryDevice(device, mqttId),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function publishAutoDiscovery(mqttId: string, client: Client, device: MixinDeviceBase<any>, topic: string, autoDiscoveryPrefix = 'homeassistant') {
|
||||
for (const iface of device.interfaces) {
|
||||
const found = autoDiscoveryMap.get(iface);
|
||||
if (!found)
|
||||
continue;
|
||||
|
||||
const config = found.create(mqttId, device, topic);
|
||||
const nodeId = `scrypted-${mqttId}-${device.id}`;
|
||||
config.unique_id = `scrypted-${mqttId}-${device.id}-${iface}`;
|
||||
config.name = iface;
|
||||
|
||||
const configTopic = `${autoDiscoveryPrefix}/${found.component}/${nodeId}/${iface}/config`;
|
||||
client.publish(configTopic, JSON.stringify(config), {
|
||||
retain: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
import { createScriptDevice, ScriptDeviceImpl, tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
|
||||
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, EventListenerRegister, MixinProvider, Scriptable, ScriptSource, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, Setting, Settings } from '@scrypted/sdk';
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings"
|
||||
import aedes, { AedesOptions } from 'aedes';
|
||||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
@@ -10,7 +12,7 @@ import ws from 'websocket-stream';
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "../../../common/src/settings-mixin";
|
||||
import { MqttClient, MqttClientPublishOptions, MqttSubscriptions } from './api/mqtt-client';
|
||||
import { MqttDeviceBase } from './api/mqtt-device-base';
|
||||
import { MqttAutoDiscoveryProvider } from './autodiscovery/autodiscovery';
|
||||
import { MqttAutoDiscoveryProvider, publishAutoDiscovery } from './autodiscovery';
|
||||
import { monacoEvalDefaults } from './monaco';
|
||||
import { isPublishable } from './publishable-types';
|
||||
import { scryptedEval } from './scrypted-eval';
|
||||
@@ -29,8 +31,8 @@ const loopbackLight = filterExample('loopback-light.ts');
|
||||
const { log, deviceManager, systemManager } = sdk;
|
||||
|
||||
class MqttDevice extends MqttDeviceBase implements Scriptable {
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
constructor(provider: MqttProvider, nativeId: string) {
|
||||
super(provider, nativeId);
|
||||
}
|
||||
|
||||
async saveScript(source: ScriptSource): Promise<void> {
|
||||
@@ -152,7 +154,7 @@ class MqttDevice extends MqttDeviceBase implements Scriptable {
|
||||
}
|
||||
}
|
||||
|
||||
const brokerProperties = ['httpPort', 'tcpPort', 'enableBroker', 'username', 'password'];
|
||||
const brokerProperties = ['httpPort', 'tcpPort', 'enableBroker', 'username', 'password', 'externalBroker'];
|
||||
|
||||
|
||||
class MqttPublisherMixin extends SettingsMixinDeviceBase<any> {
|
||||
@@ -229,6 +231,18 @@ class MqttPublisherMixin extends SettingsMixinDeviceBase<any> {
|
||||
this.connectClient();
|
||||
}
|
||||
|
||||
publishState(client: Client) {
|
||||
for (const iface of this.device.interfaces) {
|
||||
for (const prop of ScryptedInterfaceDescriptors[iface]?.properties || []) {
|
||||
let str = this[prop];
|
||||
if (typeof str === 'object')
|
||||
str = JSON.stringify(str);
|
||||
|
||||
client.publish(`${this.pathname}/${prop}`, str?.toString() || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectClient() {
|
||||
this.client?.end();
|
||||
this.client = undefined;
|
||||
@@ -236,17 +250,28 @@ class MqttPublisherMixin extends SettingsMixinDeviceBase<any> {
|
||||
let url: URL;
|
||||
let username: string;
|
||||
let password: string;
|
||||
|
||||
const externalBroker = this.provider.storage.getItem('externalBroker');
|
||||
if (urlString) {
|
||||
this.console.log('Using device specific broker.', urlString);
|
||||
url = new URL(urlString);
|
||||
username = this.storage.getItem('username') || undefined;
|
||||
password = this.storage.getItem('password') || undefined;
|
||||
this.pathname = url.pathname.substring(1);
|
||||
}
|
||||
else {
|
||||
const tcpPort = this.provider.storage.getItem('tcpPort') || '';
|
||||
else if (externalBroker && !this.provider.isBrokerEnabled) {
|
||||
this.console.log('Using external broker.', externalBroker);
|
||||
url = new URL(externalBroker);
|
||||
username = this.provider.storage.getItem('username') || undefined;
|
||||
password = this.provider.storage.getItem('password') || undefined;
|
||||
this.pathname = `${url.pathname.substring(1)}/${this.id}`;
|
||||
}
|
||||
else {
|
||||
this.console.log('Using built in broker.');
|
||||
const tcpPort = this.provider.storage.getItem('tcpPort') || '';
|
||||
url = new URL(`mqtt://localhost:${tcpPort}/scrypted`);
|
||||
username = this.provider.storage.getItem('username') || undefined;
|
||||
password = this.provider.storage.getItem('password') || undefined;
|
||||
this.pathname = `${url.pathname.substring(1)}/${this.id}`;
|
||||
}
|
||||
|
||||
@@ -260,24 +285,51 @@ class MqttPublisherMixin extends SettingsMixinDeviceBase<any> {
|
||||
});
|
||||
client.setMaxListeners(Infinity);
|
||||
|
||||
const allProperties: string[] = [];
|
||||
const allMethods: string[] = [];
|
||||
for (const iface of this.device.interfaces) {
|
||||
const methods = ScryptedInterfaceDescriptors[iface]?.methods || [];
|
||||
allMethods.push(...methods);
|
||||
const properties = ScryptedInterfaceDescriptors[iface]?.properties || [];
|
||||
allProperties.push(...properties);
|
||||
}
|
||||
|
||||
client.on('connect', packet => {
|
||||
this.console.log('MQTT client connected, publishing current state.');
|
||||
|
||||
for (const iface of this.device.interfaces) {
|
||||
for (const prop of ScryptedInterfaceDescriptors[iface]?.properties || []) {
|
||||
let str = this[prop];
|
||||
if (typeof str === 'object')
|
||||
str = JSON.stringify(str);
|
||||
|
||||
client.publish(`${this.pathname}/${prop}`, str?.toString() || '');
|
||||
}
|
||||
for (const method of allMethods) {
|
||||
client.subscribe(this.pathname + '/' + method);
|
||||
}
|
||||
})
|
||||
|
||||
publishAutoDiscovery(this.provider.storageSettings.values.mqttId, client, this, this.pathname, 'homeassistant');
|
||||
client.subscribe('homeassistant/status');
|
||||
this.publishState(client);
|
||||
});
|
||||
client.on('disconnect', () => this.console.log('mqtt client disconnected'));
|
||||
client.on('error', e => {
|
||||
this.console.log('mqtt client error', e);
|
||||
});
|
||||
|
||||
client.on('message', async (messageTopic, message) => {
|
||||
if (messageTopic === 'homeassistant/status') {
|
||||
publishAutoDiscovery(this.provider.storageSettings.values.mqttId, client, this, this.pathname, 'homeassistant');
|
||||
this.publishState(client);
|
||||
return;
|
||||
}
|
||||
const method = messageTopic.substring(this.pathname.length + 1);
|
||||
if (!allMethods.includes(method)) {
|
||||
if (!allProperties.includes(method))
|
||||
this.console.warn('unknown topic', method);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const args = JSON.parse(message.toString() || '[]');
|
||||
await this.device[method](...args);
|
||||
}
|
||||
catch (e) {
|
||||
this.console.warn('error invoking method', e);
|
||||
}
|
||||
});
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
@@ -289,10 +341,18 @@ class MqttPublisherMixin extends SettingsMixinDeviceBase<any> {
|
||||
}
|
||||
}
|
||||
|
||||
class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Settings, MixinProvider, DeviceCreator {
|
||||
export class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Settings, MixinProvider, DeviceCreator {
|
||||
devices = new Map<string, any>();
|
||||
netServer: net.Server;
|
||||
httpServer: http.Server;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
mqttId: {
|
||||
group: 'Advanced',
|
||||
title: 'Autodiscovery ID',
|
||||
// hide: true,
|
||||
persistedDefaultValue: crypto.randomBytes(4).toString('hex'),
|
||||
}
|
||||
})
|
||||
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
@@ -344,15 +404,25 @@ class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Setting
|
||||
{
|
||||
title: 'Enable MQTT Broker',
|
||||
key: 'enableBroker',
|
||||
description: 'Enable the Aedes MQTT Broker.',
|
||||
description: 'Enable the built in Aedes MQTT Broker.',
|
||||
// group: 'MQTT Broker',
|
||||
type: 'boolean',
|
||||
value: (this.storage.getItem('enableBroker') === 'true').toString(),
|
||||
},
|
||||
];
|
||||
|
||||
if (!this.isBrokerEnabled)
|
||||
return ret;
|
||||
if (!this.isBrokerEnabled) {
|
||||
ret.push(
|
||||
{
|
||||
title: 'External Broker',
|
||||
group: 'MQTT Broker',
|
||||
key: 'externalBroker',
|
||||
description: 'Specify the mqtt address of an external MQTT broker.',
|
||||
placeholder: 'mqtt://192.168.1.100',
|
||||
value: this.storage.getItem('externalBroker'),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ret.push(
|
||||
{
|
||||
@@ -369,26 +439,33 @@ class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Setting
|
||||
key: 'password',
|
||||
type: 'password',
|
||||
description: 'Optional: Password used to authenticate with the MQTT broker.',
|
||||
},
|
||||
{
|
||||
title: 'TCP Port',
|
||||
key: 'tcpPort',
|
||||
description: 'The port to use for TCP connections',
|
||||
placeholder: '1883',
|
||||
type: 'number',
|
||||
group: 'MQTT Broker',
|
||||
value: this.storage.getItem('tcpPort'),
|
||||
},
|
||||
{
|
||||
title: 'HTTP Port',
|
||||
key: 'httpPort',
|
||||
description: 'The port to use for HTTP connections',
|
||||
placeholder: '8888',
|
||||
type: 'number',
|
||||
group: 'MQTT Broker',
|
||||
value: this.storage.getItem('httpPort'),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (this.isBrokerEnabled) {
|
||||
ret.push(
|
||||
{
|
||||
title: 'TCP Port',
|
||||
key: 'tcpPort',
|
||||
description: 'The port to use for TCP connections',
|
||||
placeholder: '1883',
|
||||
type: 'number',
|
||||
group: 'MQTT Broker',
|
||||
value: this.storage.getItem('tcpPort'),
|
||||
},
|
||||
{
|
||||
title: 'HTTP Port',
|
||||
key: 'httpPort',
|
||||
description: 'The port to use for HTTP connections',
|
||||
placeholder: '8888',
|
||||
type: 'number',
|
||||
group: 'MQTT Broker',
|
||||
value: this.storage.getItem('httpPort'),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ret.push(...await this.storageSettings.getSettings());
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -469,6 +546,9 @@ class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Setting
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string | number) {
|
||||
if (this.storageSettings.keys[key]) {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
this.storage.setItem(key, value.toString());
|
||||
|
||||
if (brokerProperties.includes(key)) {
|
||||
@@ -482,7 +562,7 @@ class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Setting
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
|
||||
|
||||
}
|
||||
|
||||
createMqttDevice(nativeId: string): MqttDevice {
|
||||
@@ -493,10 +573,10 @@ class MqttProvider extends ScryptedDeviceBase implements DeviceProvider, Setting
|
||||
let ret = this.devices.get(nativeId);
|
||||
if (!ret) {
|
||||
if (nativeId.startsWith('autodiscovery:')) {
|
||||
ret = new MqttAutoDiscoveryProvider(nativeId);
|
||||
ret = new MqttAutoDiscoveryProvider(this, nativeId);
|
||||
}
|
||||
else if (nativeId.startsWith('0.')) {
|
||||
ret = new MqttDevice(nativeId);
|
||||
ret = new MqttDevice(this, nativeId);
|
||||
await ret.bind();
|
||||
}
|
||||
if (ret)
|
||||
|
||||
73
plugins/objectdetector/package-lock.json
generated
73
plugins/objectdetector/package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.19",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.19",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"lodash": "^4.17.21",
|
||||
"point-inside-polygon": "^1.0.3",
|
||||
"polygon-overlap": "^1.0.5",
|
||||
"polygon-clipping": "^0.15.3",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,7 +38,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.107",
|
||||
"version": "0.3.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -100,11 +99,6 @@
|
||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lines-intersect": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lines-intersect/-/lines-intersect-1.0.0.tgz",
|
||||
"integrity": "sha1-pgyHo9lXoIcdEU0FSmhatx9ygEI="
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -121,25 +115,14 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/point-inside-polygon": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
|
||||
"integrity": "sha512-ks7+jwmSHj8dcxClSfef2ftms57tGEE4rAwI4DHFX4U5vZqyEaCbHcfdmReWyJ5zDnOpsB5dTfDBmeFNa+449A=="
|
||||
},
|
||||
"node_modules/polygon-overlap": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/polygon-overlap/-/polygon-overlap-1.0.5.tgz",
|
||||
"integrity": "sha1-DONSaovZhnSrBG/JLdAmZLbYJC0=",
|
||||
"node_modules/polygon-clipping": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.3.tgz",
|
||||
"integrity": "sha512-ho0Xx5DLkgxRx/+n4O74XyJ67DcyN3Tu9bGYKsnTukGAW6ssnuak6Mwcyb1wHy9MZc9xsUWqIoiazkZB5weECg==",
|
||||
"dependencies": {
|
||||
"lines-intersect": "1.0.0",
|
||||
"point-inside-polygon": "1.0.1"
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/polygon-overlap/node_modules/point-inside-polygon": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.1.tgz",
|
||||
"integrity": "sha512-qceSGPZXGaELiy5p9f+8DXTnL35qxWhpLSubufeXlVltWKkT9IB0PJcM6mNJ7Nxj0z443qyQrXbWzERheWfC7w=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
@@ -154,6 +137,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
@@ -231,11 +219,6 @@
|
||||
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
|
||||
"dev": true
|
||||
},
|
||||
"lines-intersect": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lines-intersect/-/lines-intersect-1.0.0.tgz",
|
||||
"integrity": "sha1-pgyHo9lXoIcdEU0FSmhatx9ygEI="
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -249,25 +232,12 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"point-inside-polygon": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
|
||||
"integrity": "sha512-ks7+jwmSHj8dcxClSfef2ftms57tGEE4rAwI4DHFX4U5vZqyEaCbHcfdmReWyJ5zDnOpsB5dTfDBmeFNa+449A=="
|
||||
},
|
||||
"polygon-overlap": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/polygon-overlap/-/polygon-overlap-1.0.5.tgz",
|
||||
"integrity": "sha1-DONSaovZhnSrBG/JLdAmZLbYJC0=",
|
||||
"polygon-clipping": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.3.tgz",
|
||||
"integrity": "sha512-ho0Xx5DLkgxRx/+n4O74XyJ67DcyN3Tu9bGYKsnTukGAW6ssnuak6Mwcyb1wHy9MZc9xsUWqIoiazkZB5weECg==",
|
||||
"requires": {
|
||||
"lines-intersect": "1.0.0",
|
||||
"point-inside-polygon": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"point-inside-polygon": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.1.tgz",
|
||||
"integrity": "sha512-qceSGPZXGaELiy5p9f+8DXTnL35qxWhpLSubufeXlVltWKkT9IB0PJcM6mNJ7Nxj0z443qyQrXbWzERheWfC7w=="
|
||||
}
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
@@ -278,6 +248,11 @@
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.19",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -42,13 +42,11 @@
|
||||
],
|
||||
"realfs": true
|
||||
},
|
||||
"optionalDependencies": {},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"lodash": "^4.17.21",
|
||||
"point-inside-polygon": "^1.0.3",
|
||||
"polygon-overlap": "^1.0.5",
|
||||
"polygon-clipping": "^0.15.3",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, DeviceState, EventListenerRegister, Image, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoFrame, VideoFrameGenerator } from '@scrypted/sdk';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, DeviceState, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import os from 'os';
|
||||
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
|
||||
import { getMaxConcurrentObjectDetectionSessions } from './performance-profile';
|
||||
import { insidePolygon, normalizeBox, polygonOverlap } from './polygon';
|
||||
import { serverSupportsMixinEventMasking } from './server-version';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor, createObjectDetectorStorageSetting } from './smart-motionsensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
import { createObjectDetectorStorageSetting, SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
|
||||
const polygonOverlap = require('polygon-overlap');
|
||||
const insidePolygon = require('point-inside-polygon');
|
||||
|
||||
const { systemManager } = sdk;
|
||||
|
||||
const defaultDetectionDuration = 20;
|
||||
const defaultPostMotionAnalysisDuration = 20;
|
||||
const defaultMotionDuration = 30;
|
||||
|
||||
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
|
||||
@@ -25,10 +23,11 @@ const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
|
||||
|
||||
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
|
||||
|
||||
type ClipPath = [number, number][];
|
||||
type ClipPath = Point[];
|
||||
type Zones = { [zone: string]: ClipPath };
|
||||
interface ZoneInfo {
|
||||
exclusion?: boolean;
|
||||
filterMode?: 'include' | 'exclude' | 'observe';
|
||||
type?: 'Intersect' | 'Contain';
|
||||
classes?: string[];
|
||||
scoreThreshold?: number;
|
||||
@@ -42,8 +41,44 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
detections = new Map<string, MediaObject>();
|
||||
cameraDevice: ScryptedDevice & Camera & VideoCamera & MotionSensor & ObjectDetector;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
zones: {
|
||||
title: 'Zones',
|
||||
type: 'string',
|
||||
description: 'Enter the name of a new zone or delete an existing zone.',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
choices: [],
|
||||
},
|
||||
motionSensorSupplementation: {
|
||||
title: 'Built-In Motion Sensor',
|
||||
description: `This camera has a built in motion sensor. Using ${this.objectDetection.name} may be unnecessary and will use additional CPU. Replace will ignore the built in motion sensor. Filter will verify the motion sent by built in motion sensor. The Default is ${BUILTIN_MOTION_SENSOR_REPLACE}.`,
|
||||
choices: [
|
||||
'Default',
|
||||
BUILTIN_MOTION_SENSOR_ASSIST,
|
||||
BUILTIN_MOTION_SENSOR_REPLACE,
|
||||
],
|
||||
defaultValue: "Default",
|
||||
onPut: () => {
|
||||
this.endObjectDetection();
|
||||
this.maybeStartDetection();
|
||||
}
|
||||
},
|
||||
postMotionAnalysisDuration: {
|
||||
title: 'Post Motion Analysis Duration',
|
||||
subgroup: 'Advanced',
|
||||
description: 'The duration in seconds to analyze video after motion ends.',
|
||||
type: 'number',
|
||||
defaultValue: defaultPostMotionAnalysisDuration,
|
||||
},
|
||||
motionDuration: {
|
||||
title: 'Motion Duration',
|
||||
description: 'The duration in seconds to wait to reset the motion sensor.',
|
||||
type: 'number',
|
||||
defaultValue: defaultMotionDuration,
|
||||
},
|
||||
newPipeline: {
|
||||
title: 'Video Pipeline',
|
||||
subgroup: 'Advanced',
|
||||
title: 'Decoder',
|
||||
description: 'Configure how frames are provided to the video analysis pipeline.',
|
||||
onGet: async () => {
|
||||
const choices = [
|
||||
@@ -60,34 +95,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
},
|
||||
defaultValue: 'Default',
|
||||
},
|
||||
motionSensorSupplementation: {
|
||||
title: 'Built-In Motion Sensor',
|
||||
description: `This camera has a built in motion sensor. Using ${this.objectDetection.name} may be unnecessary and will use additional CPU. Replace will ignore the built in motion sensor. Filter will verify the motion sent by built in motion sensor. The Default is ${BUILTIN_MOTION_SENSOR_REPLACE}.`,
|
||||
choices: [
|
||||
'Default',
|
||||
BUILTIN_MOTION_SENSOR_ASSIST,
|
||||
BUILTIN_MOTION_SENSOR_REPLACE,
|
||||
],
|
||||
defaultValue: "Default",
|
||||
onPut: () => {
|
||||
this.endObjectDetection();
|
||||
this.maybeStartDetection();
|
||||
}
|
||||
},
|
||||
detectionDurationDEPRECATED: {
|
||||
hide: true,
|
||||
title: 'Detection Duration',
|
||||
subgroup: 'Advanced',
|
||||
description: 'The duration in seconds to analyze video when motion occurs.',
|
||||
type: 'number',
|
||||
defaultValue: defaultDetectionDuration,
|
||||
},
|
||||
motionDuration: {
|
||||
title: 'Motion Duration',
|
||||
description: 'The duration in seconds to wait to reset the motion sensor.',
|
||||
type: 'number',
|
||||
defaultValue: defaultMotionDuration,
|
||||
},
|
||||
});
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
detectionIntervalTimeout: NodeJS.Timeout;
|
||||
@@ -97,12 +104,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
analyzeStop: number;
|
||||
detectorSignal = new Deferred<void>().resolve();
|
||||
released = false;
|
||||
// settings: Setting[];
|
||||
|
||||
get detectorRunning() {
|
||||
return !this.detectorSignal.finished;
|
||||
}
|
||||
|
||||
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, public model: ObjectDetectionModel, group: string, public hasMotionType: boolean, public settings: Setting[]) {
|
||||
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, public model: ObjectDetectionModel, group: string, public hasMotionType: boolean) {
|
||||
super({
|
||||
mixinDevice, mixinDeviceState,
|
||||
mixinProviderNativeId: providerNativeId,
|
||||
@@ -122,6 +130,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return;
|
||||
this.maybeStartDetection();
|
||||
}, 60000);
|
||||
|
||||
this.storageSettings.settings.zones.mapGet = () => Object.keys(this.zones);
|
||||
this.storageSettings.settings.zones.onGet = async () => {
|
||||
return {
|
||||
group,
|
||||
choices: Object.keys(this.zones),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearMotionTimeout() {
|
||||
@@ -142,13 +158,24 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
getCurrentSettings() {
|
||||
if (!this.settings)
|
||||
const settings = this.model.settings;
|
||||
if (!settings)
|
||||
return;
|
||||
|
||||
const ret: { [key: string]: any } = {};
|
||||
for (const setting of this.settings) {
|
||||
ret[setting.key] = (setting.multiple ? safeParseJson(this.storage.getItem(setting.key)) : this.storage.getItem(setting.key))
|
||||
|| setting.value;
|
||||
for (const setting of settings) {
|
||||
let value: any;
|
||||
if (setting.multiple) {
|
||||
value = safeParseJson(this.storage.getItem(setting.key));
|
||||
if (!value?.length)
|
||||
value = undefined;
|
||||
}
|
||||
else {
|
||||
value = this.storage.getItem(setting.key);
|
||||
}
|
||||
value ||= setting.value;
|
||||
|
||||
ret[setting.key] = value;
|
||||
}
|
||||
|
||||
if (this.hasMotionType)
|
||||
@@ -184,17 +211,28 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
async register() {
|
||||
const model = await this.objectDetection.getDetectionModel();
|
||||
|
||||
if (!this.hasMotionType) {
|
||||
this.motionListener = this.cameraDevice.listen(ScryptedInterface.MotionSensor, async () => {
|
||||
if (!this.cameraDevice.motionDetected) {
|
||||
// const minimumEndTme = this.detectionStartTime + this.storageSettings.values.minimumDetectionDuration * 1000;
|
||||
// const sleepTime = minimumEndTme - Date.now();
|
||||
const sleepTime = this.storageSettings.values.postMotionAnalysisDuration * 1000;
|
||||
|
||||
if (sleepTime > 0) {
|
||||
this.console.log('Motion stopped. Waiting additional time for minimum detection duration:', sleepTime);
|
||||
await sleep(sleepTime);
|
||||
if (this.motionDetected) {
|
||||
this.console.log('Motion resumed during wait. Continuing detection.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.detectorRunning) {
|
||||
// allow anaysis due to user request.
|
||||
if (this.analyzeStop > Date.now())
|
||||
return;
|
||||
|
||||
this.console.log('motion stopped, cancelling ongoing detection')
|
||||
this.console.log('Motion stopped, stopping detection.')
|
||||
this.endObjectDetection();
|
||||
}
|
||||
return;
|
||||
@@ -217,14 +255,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (this.motionDetected)
|
||||
return;
|
||||
if (!this.detectorRunning)
|
||||
this.console.log('built in motion sensor started motion, starting video detection.');
|
||||
this.console.log('Built in motion sensor started motion, starting video detection.');
|
||||
this.startPipelineAnalysis();
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearMotionTimeout();
|
||||
if (this.detectorRunning) {
|
||||
this.console.log('built in motion sensor ended motion, stopping video detection.')
|
||||
this.console.log('Built in motion sensor ended motion, stopping video detection.')
|
||||
this.endObjectDetection();
|
||||
}
|
||||
if (this.motionDetected)
|
||||
@@ -262,6 +300,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
async runPipelineAnalysisLoop(signal: Deferred<void>, options: {
|
||||
suppress?: boolean,
|
||||
}) {
|
||||
await this.updateModel();
|
||||
while (!signal.finished) {
|
||||
if (options.suppress) {
|
||||
this.console.log('Resuming motion processing after active motion timeout.');
|
||||
@@ -342,7 +381,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}, 30000);
|
||||
signal.promise.finally(() => clearInterval(interval));
|
||||
|
||||
const currentDetections = new Set<string>();
|
||||
const currentDetections = new Map<string, number>();
|
||||
let lastReport = 0;
|
||||
|
||||
updatePipelineStatus('waiting result');
|
||||
@@ -354,11 +393,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
continue;
|
||||
for (const [key, zone] of Object.entries(mixin.zones)) {
|
||||
const zi = mixin.zoneInfos[key];
|
||||
if (!zone?.length || zone?.length < 3)
|
||||
if (!zone?.length || zone?.length < 3 || zi?.filterMode === 'observe')
|
||||
continue;
|
||||
const odz: ObjectDetectionZone = {
|
||||
classes: mixin.hasMotionType ? ['motion'] : zi?.classes,
|
||||
exclusion: zi?.exclusion,
|
||||
exclusion: zi?.filterMode ? zi?.filterMode === 'exclude' : zi?.exclusion,
|
||||
path: zone,
|
||||
type: zi?.type,
|
||||
}
|
||||
@@ -417,12 +456,12 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// this.console.log('Zone filtered detections:', numZonedDetections - numOriginalDetections);
|
||||
|
||||
for (const d of detected.detected.detections) {
|
||||
currentDetections.add(d.className);
|
||||
currentDetections.set(d.className, Math.max(currentDetections.get(d.className) || 0, d.score));
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now > lastReport + 10000) {
|
||||
const found = [...currentDetections.values()];
|
||||
const found = [...currentDetections.entries()].map(([className, score]) => `${className} (${score})`);
|
||||
if (!found.length)
|
||||
found.push('[no detections]');
|
||||
this.console.log(`[${Math.round((now - start) / 100) / 10}s] Detected:`, ...found);
|
||||
@@ -462,19 +501,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
}
|
||||
|
||||
normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]) {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x * 100 / inputDimensions[0];
|
||||
y = y * 100 / inputDimensions[1];
|
||||
x2 = x2 * 100 / inputDimensions[0];
|
||||
y2 = y2 * 100 / inputDimensions[1];
|
||||
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
return box;
|
||||
}
|
||||
|
||||
applyZones(detection: ObjectsDetected) {
|
||||
// determine zones of the objects, if configured.
|
||||
if (!detection.detections)
|
||||
@@ -485,7 +511,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
continue;
|
||||
|
||||
o.zones = []
|
||||
const box = this.normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
|
||||
|
||||
let included: boolean;
|
||||
for (const [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
@@ -495,13 +521,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
const zoneInfo = this.zoneInfos[zone];
|
||||
const exclusion = zoneInfo?.filterMode ? zoneInfo.filterMode === 'exclude' : zoneInfo?.exclusion;
|
||||
// track if there are any inclusion zones
|
||||
if (!zoneInfo?.exclusion && !included)
|
||||
if (!exclusion && !included && zoneInfo?.filterMode !== 'observe')
|
||||
included = false;
|
||||
|
||||
let match = false;
|
||||
if (zoneInfo?.type === 'Contain') {
|
||||
match = insidePolygon(box[0], zoneValue) &&
|
||||
match = insidePolygon(box[0] as Point, zoneValue) &&
|
||||
insidePolygon(box[1], zoneValue) &&
|
||||
insidePolygon(box[2], zoneValue) &&
|
||||
insidePolygon(box[3], zoneValue);
|
||||
@@ -510,18 +537,21 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
match = polygonOverlap(box, zoneValue);
|
||||
}
|
||||
|
||||
if (match && zoneInfo?.classes?.length) {
|
||||
match = zoneInfo.classes.includes(o.className);
|
||||
const classes = zoneInfo?.classes?.length ? zoneInfo?.classes : this.model?.classes || [];
|
||||
if (match && classes.length) {
|
||||
match = classes.includes(o.className);
|
||||
}
|
||||
if (match) {
|
||||
o.zones.push(zone);
|
||||
|
||||
if (zoneInfo?.exclusion && match) {
|
||||
copy = copy.filter(c => c !== o);
|
||||
break;
|
||||
}
|
||||
if (zoneInfo?.filterMode !== 'observe') {
|
||||
if (exclusion && match) {
|
||||
copy = copy.filter(c => c !== o);
|
||||
break;
|
||||
}
|
||||
|
||||
included = true;
|
||||
included = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,7 +559,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// use a default inclusion zone that crops the top and bottom to
|
||||
// prevents errant motion from the on screen time changing every second.
|
||||
if (this.hasMotionType && included === undefined) {
|
||||
const defaultInclusionZone = [[0, 10], [100, 10], [100, 90], [0, 90]];
|
||||
const defaultInclusionZone: ClipPath = [[0, 10], [100, 10], [100, 90], [0, 90]];
|
||||
included = polygonOverlap(box, defaultInclusionZone);
|
||||
}
|
||||
|
||||
@@ -550,20 +580,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!this.motionDetected)
|
||||
this.motionDetected = true;
|
||||
|
||||
// if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_ASSIST) {
|
||||
// if (!this.motionDetected) {
|
||||
// this.motionDetected = true;
|
||||
// this.console.log(`${this.objectDetection.name} confirmed motion, stopping video detection.`)
|
||||
// this.endObjectDetection();
|
||||
// this.clearMotionTimeout();
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// if (!this.motionDetected)
|
||||
// this.motionDetected = true;
|
||||
// this.resetMotionTimeout();
|
||||
// }
|
||||
|
||||
const areas = detection.detections.filter(d => d.className === 'motion' && d.score !== 1).map(d => d.score)
|
||||
if (areas.length)
|
||||
this.console.log('detection areas', areas);
|
||||
@@ -597,7 +613,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
const ret = await this.getNativeObjectTypes();
|
||||
if (!ret.classes)
|
||||
ret.classes = [];
|
||||
ret.classes.push(...(await this.objectDetection.getDetectionModel()).classes);
|
||||
ret.classes.push(...(await this.objectDetection.getDetectionModel(this.getCurrentSettings())).classes);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -642,42 +658,45 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return use.id;
|
||||
}
|
||||
|
||||
async getMixinSettings(): Promise<Setting[]> {
|
||||
const settings: Setting[] = [];
|
||||
|
||||
async updateModel() {
|
||||
try {
|
||||
this.settings = (await this.objectDetection.getDetectionModel(this.getCurrentSettings())).settings;
|
||||
this.model = await this.objectDetection.getDetectionModel(this.getCurrentSettings());
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.settings) {
|
||||
settings.push(...this.settings.map(setting =>
|
||||
Object.assign({}, setting, {
|
||||
async getMixinSettings(): Promise<Setting[]> {
|
||||
const settings: Setting[] = [];
|
||||
|
||||
await this.updateModel();
|
||||
const modelSettings = this.model.settings;
|
||||
|
||||
if (modelSettings) {
|
||||
settings.push(...modelSettings.map(setting => {
|
||||
let value: any;
|
||||
if (setting.multiple) {
|
||||
value = safeParseJson(this.storage.getItem(setting.key));
|
||||
if (!value?.length)
|
||||
value = undefined;
|
||||
}
|
||||
else {
|
||||
value = this.storage.getItem(setting.key);
|
||||
}
|
||||
value ||= setting.value;
|
||||
return Object.assign({}, setting, {
|
||||
placeholder: setting.placeholder?.toString(),
|
||||
value: (setting.multiple ? safeParseJson(this.storage.getItem(setting.key)) : this.storage.getItem(setting.key))
|
||||
|| setting.value,
|
||||
} as Setting))
|
||||
);
|
||||
value,
|
||||
} as Setting);
|
||||
}));
|
||||
}
|
||||
|
||||
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
|
||||
this.storageSettings.settings.detectionDurationDEPRECATED.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.postMotionAnalysisDuration.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
|
||||
|
||||
settings.push(...await this.storageSettings.getSettings());
|
||||
|
||||
settings.push({
|
||||
key: 'zones',
|
||||
title: 'Zones',
|
||||
type: 'string',
|
||||
description: 'Enter the name of a new zone or delete an existing zone.',
|
||||
multiple: true,
|
||||
value: Object.keys(this.zones),
|
||||
choices: Object.keys(this.zones),
|
||||
combobox: true,
|
||||
});
|
||||
|
||||
for (const [name, value] of Object.entries(this.zones)) {
|
||||
const zi = this.zoneInfos[name];
|
||||
|
||||
@@ -690,13 +709,26 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
value: JSON.stringify(value),
|
||||
});
|
||||
|
||||
// settings.push({
|
||||
// subgroup,
|
||||
// key: `zoneinfo-exclusion-${name}`,
|
||||
// title: `Exclusion Zone`,
|
||||
// description: 'Detections in this zone will be excluded.',
|
||||
// type: 'boolean',
|
||||
// value: zi?.exclusion,
|
||||
// });
|
||||
settings.push({
|
||||
subgroup,
|
||||
key: `zoneinfo-exclusion-${name}`,
|
||||
title: `Exclusion Zone`,
|
||||
description: 'Detections in this zone will be excluded.',
|
||||
type: 'boolean',
|
||||
value: zi?.exclusion,
|
||||
key: `zoneinfo-filterMode-${name}`,
|
||||
title: `Filter Mode`,
|
||||
description: 'The filter mode used by this zone. The Default is include. Zones set to observe will not affect filtering and can be used for automations.',
|
||||
choices: [
|
||||
'Default',
|
||||
'include',
|
||||
'exclude',
|
||||
'observe',
|
||||
],
|
||||
value: zi?.filterMode || (zi?.exclusion ? 'exclude' : undefined) || 'Default',
|
||||
});
|
||||
|
||||
settings.push({
|
||||
@@ -712,14 +744,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
});
|
||||
|
||||
if (!this.hasMotionType) {
|
||||
const classes = this.model.classes;
|
||||
settings.push(
|
||||
{
|
||||
subgroup,
|
||||
key: `zoneinfo-classes-${name}`,
|
||||
title: `Detection Classes`,
|
||||
description: 'The detection classes to match inside this zone. An empty list will match all classes.',
|
||||
choices: (await this.getObjectTypes())?.classes || [],
|
||||
value: zi?.classes || [],
|
||||
description: 'The detection classes to match inside this zone.',
|
||||
choices: classes || [],
|
||||
value: zi?.classes?.length ? zi?.classes : classes || [],
|
||||
multiple: true,
|
||||
},
|
||||
);
|
||||
@@ -792,7 +825,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
if (value && this.settings?.find(s => s.key === key)?.multiple) {
|
||||
if (value && this.model.settings?.find(s => s.key === key)?.multiple) {
|
||||
vs = JSON.stringify(value);
|
||||
}
|
||||
|
||||
@@ -802,7 +835,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
else {
|
||||
const settings = this.getCurrentSettings();
|
||||
if (settings && settings[key]) {
|
||||
if (settings && key in settings) {
|
||||
this.storage.setItem(key, vs);
|
||||
settings[key] = value;
|
||||
}
|
||||
@@ -870,9 +903,7 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
|
||||
const group = hasMotionType ? 'Motion Detection' : 'Object Detection';
|
||||
// const group = objectDetection.name.replace('Plugin', '').trim();
|
||||
|
||||
const settings = this.model.settings;
|
||||
|
||||
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model, group, hasMotionType, settings);
|
||||
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model, group, hasMotionType);
|
||||
this.currentMixins.add(ret);
|
||||
return ret;
|
||||
}
|
||||
@@ -896,7 +927,7 @@ interface ObjectDetectionStatistics {
|
||||
sampleTime: number;
|
||||
}
|
||||
|
||||
class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings, DeviceProvider, DeviceCreator {
|
||||
export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings, DeviceProvider, DeviceCreator {
|
||||
currentMixins = new Set<ObjectDetectorMixin>();
|
||||
objectDetectionStatistics = new Map<number, ObjectDetectionStatistics>();
|
||||
statsSnapshotTime: number;
|
||||
@@ -1103,7 +1134,7 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
if (nativeId === 'ffmpeg')
|
||||
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartMotionSensor(nativeId);
|
||||
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
|
||||
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
@@ -1164,13 +1195,14 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
|
||||
const sensor = new SmartMotionSensor(nativeId);
|
||||
const sensor = new SmartMotionSensor(this, nativeId);
|
||||
sensor.storageSettings.values.objectDetector = objectDetector?.id;
|
||||
|
||||
return id;
|
||||
|
||||
27
plugins/objectdetector/src/polygon.ts
Normal file
27
plugins/objectdetector/src/polygon.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Point } from '@scrypted/sdk';
|
||||
import polygonClipping from 'polygon-clipping';
|
||||
|
||||
// const polygonOverlap = require('polygon-overlap');
|
||||
// const insidePolygon = require('point-inside-polygon');
|
||||
|
||||
export function polygonOverlap(p1: Point[], p2: Point[]) {
|
||||
const intersect = polygonClipping.intersection([p1], [p2]);
|
||||
return !!intersect.length;
|
||||
}
|
||||
|
||||
export function insidePolygon(point: Point, polygon: Point[]) {
|
||||
const intersect = polygonClipping.intersection([polygon], [[point, [point[0] + 1, point[1]], [point[0] + 1, point[1] + 1]]]);
|
||||
return !!intersect.length;
|
||||
}
|
||||
|
||||
export function normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x * 100 / inputDimensions[0];
|
||||
y = y * 100 / inputDimensions[1];
|
||||
x2 = x2 * 100 / inputDimensions[0];
|
||||
y2 = y2 * 100 / inputDimensions[1];
|
||||
return [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import sdk, { EventListenerRegister, MotionSensor, ObjectDetector, ObjectsDetected, Readme, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
|
||||
import sdk, { Camera, EventListenerRegister, MediaObject, MotionSensor, ObjectDetector, ObjectsDetected, Readme, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
|
||||
export const SMART_MOTIONSENSOR_PREFIX = 'smart-motionsensor-';
|
||||
|
||||
@@ -13,7 +14,7 @@ export function createObjectDetectorStorageSetting(): StorageSetting {
|
||||
};
|
||||
}
|
||||
|
||||
export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, Readme, MotionSensor {
|
||||
export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, Readme, MotionSensor, Camera {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
objectDetector: createObjectDetectorStorageSetting(),
|
||||
detections: {
|
||||
@@ -28,11 +29,20 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
},
|
||||
zones: {
|
||||
title: 'Zones',
|
||||
description: 'Optional: The sensor will only be triggered when an object is in any of the following zones.',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
choices: [
|
||||
],
|
||||
},
|
||||
});
|
||||
listener: EventListenerRegister;
|
||||
timeout: NodeJS.Timeout;
|
||||
lastPicture: Promise<MediaObject>;
|
||||
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
constructor(public plugin: ObjectDetectionPlugin, nativeId?: ScryptedNativeId) {
|
||||
super(nativeId);
|
||||
|
||||
this.storageSettings.settings.detections.onGet = async () => {
|
||||
@@ -48,7 +58,47 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
|
||||
this.storageSettings.settings.objectDetector.onPut = () => this.rebind();
|
||||
|
||||
this.storageSettings.settings.zones.onPut = () => this.rebind();
|
||||
|
||||
this.storageSettings.settings.zones.onGet = async () => {
|
||||
const objectDetector: ObjectDetector & ScryptedDevice = this.storageSettings.values.objectDetector;
|
||||
const objectDetections = [...this.plugin.currentMixins.values()]
|
||||
.map(d => [...d.currentMixins.values()].filter(dd => !dd.hasMotionType)).flat();
|
||||
|
||||
const mixin = objectDetections.find(m => m.id === objectDetector?.id);
|
||||
const zones = new Set(Object.keys(mixin?.getZones() || {}));
|
||||
for (const z of this.storageSettings.values.zones || []) {
|
||||
zones.add(z);
|
||||
}
|
||||
|
||||
return {
|
||||
choices: [...zones],
|
||||
};
|
||||
};
|
||||
|
||||
this.rebind();
|
||||
|
||||
if (!this.providedInterfaces.includes(ScryptedInterface.Camera)) {
|
||||
sdk.deviceManager.onDeviceDiscovered({
|
||||
name: this.providedName,
|
||||
nativeId: this.nativeId,
|
||||
type: this.providedType,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
|
||||
return this.lastPicture;
|
||||
}
|
||||
|
||||
async getPictureOptions(): Promise<ResponsePictureOptions[]> {
|
||||
return;
|
||||
}
|
||||
|
||||
resetTrigger() {
|
||||
@@ -80,7 +130,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
if (!detections?.length)
|
||||
return;
|
||||
|
||||
|
||||
const console = sdk.deviceManager.getMixinConsole(objectDetector.id, this.nativeId);
|
||||
|
||||
this.listener = objectDetector.listen(ScryptedInterface.ObjectDetector, (source, details, data) => {
|
||||
@@ -88,6 +137,23 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
const match = detected.detections?.find(d => {
|
||||
if (!detections.includes(d.className))
|
||||
return false;
|
||||
const zones: string[] = this.storageSettings.values.zones;
|
||||
if (zones?.length) {
|
||||
if (d.zones) {
|
||||
let found = false;
|
||||
for (const z of d.zones) {
|
||||
if (zones.includes(z)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
this.console.warn('Camera does not provide Zones in detection event. Zone filter will not be applied.');
|
||||
}
|
||||
}
|
||||
if (!d.movement)
|
||||
return true;
|
||||
return d.movement.moving;
|
||||
@@ -95,6 +161,8 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
if (match) {
|
||||
if (!this.motionDetected)
|
||||
console.log('Smart Motion Sensor triggered on', match);
|
||||
if (detected.detectionId)
|
||||
this.lastPicture = objectDetector.getDetectionInput(detected.detectionId, details.eventId);
|
||||
this.trigger();
|
||||
}
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user