mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 07:02:13 +00:00
Compare commits
248 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea628a7130 | ||
|
|
ed498ae418 | ||
|
|
5b46036b2d | ||
|
|
7bd4f4053d | ||
|
|
f83cbfa5e7 | ||
|
|
8480713ec6 | ||
|
|
dcae7ce367 | ||
|
|
101d362260 | ||
|
|
73bdca1be6 | ||
|
|
c407fa0b9f | ||
|
|
d26c595fd6 | ||
|
|
38c00f5b9b | ||
|
|
ab4738973d | ||
|
|
ea065f506c | ||
|
|
4b7b66c96b | ||
|
|
0462ad228b | ||
|
|
45ac7f2f4e | ||
|
|
6618129e1d | ||
|
|
1a72eddcc8 | ||
|
|
1c9037dc35 | ||
|
|
2c5b79291f | ||
|
|
cd0ab104ea | ||
|
|
c6f4c1a669 | ||
|
|
9f28e38716 | ||
|
|
ba8f25fde3 | ||
|
|
9f27b2f382 | ||
|
|
96fa6af0fc | ||
|
|
eca5fbecdc | ||
|
|
8e0e2854e9 | ||
|
|
1eb9d938e7 | ||
|
|
095f80e1f9 | ||
|
|
da1f6118c8 | ||
|
|
5060748e9d | ||
|
|
25df4f8376 | ||
|
|
56fdff3545 | ||
|
|
2fbfe2cb65 | ||
|
|
32ede1f7fe | ||
|
|
1a2ec8ab4e | ||
|
|
53cab91b02 | ||
|
|
02a46a9202 | ||
|
|
69f4de66e9 | ||
|
|
aed6e0c446 | ||
|
|
432c178f29 | ||
|
|
bcf698daa3 | ||
|
|
347a957cd3 | ||
|
|
459b95a0e2 | ||
|
|
1c18129449 | ||
|
|
21274df881 | ||
|
|
ca243e79bb | ||
|
|
924394d365 | ||
|
|
23167da88b | ||
|
|
c1d48e1c6b | ||
|
|
7a22e17d84 | ||
|
|
75b2ff22ce | ||
|
|
edde093140 | ||
|
|
8622934c8b | ||
|
|
153cc3ed94 | ||
|
|
2ae6113750 | ||
|
|
4ec001c2a2 | ||
|
|
794ac6c8d2 | ||
|
|
8422ffe55a | ||
|
|
285c07e33e | ||
|
|
ef04398a79 | ||
|
|
6429ea718a | ||
|
|
5f9147e720 | ||
|
|
ea4922d8e5 | ||
|
|
70293ca827 | ||
|
|
878487180e | ||
|
|
f094903ed9 | ||
|
|
5117e217cf | ||
|
|
07c3f2832a | ||
|
|
977666bc3c | ||
|
|
2ec6760308 | ||
|
|
0dbe556835 | ||
|
|
c4f76df255 | ||
|
|
0bf0ec08ab | ||
|
|
1a2216a7de | ||
|
|
d868c3b3bb | ||
|
|
882709ea51 | ||
|
|
df249c554c | ||
|
|
fcbaeb1d1d | ||
|
|
c6dda05fa4 | ||
|
|
953b7812c5 | ||
|
|
7434a5c4ba | ||
|
|
b240a17bb0 | ||
|
|
aab0507805 | ||
|
|
3e091623a8 | ||
|
|
0871898385 | ||
|
|
eea7e4be32 | ||
|
|
8167ca85bb | ||
|
|
a09291114f | ||
|
|
39efe0d994 | ||
|
|
21f56216b0 | ||
|
|
8820bac571 | ||
|
|
47a683e385 | ||
|
|
17f367a373 | ||
|
|
fad0a520ca | ||
|
|
1f0d5dc3b9 | ||
|
|
a965f9b569 | ||
|
|
eb1a388f69 | ||
|
|
b2cf5ac3c7 | ||
|
|
ce10a49f0f | ||
|
|
5e31a0db96 | ||
|
|
8f1a673db5 | ||
|
|
7405476556 | ||
|
|
7dc86f59bf | ||
|
|
2d7cef600d | ||
|
|
5de0f8937b | ||
|
|
8e8d333ea2 | ||
|
|
d66a6317de | ||
|
|
49e3fc1438 | ||
|
|
fbe3daa072 | ||
|
|
670216135b | ||
|
|
ff903fa891 | ||
|
|
ebc6ede275 | ||
|
|
4de91d0673 | ||
|
|
3da1d00f6f | ||
|
|
4ff00a7753 | ||
|
|
f245fb257d | ||
|
|
b1a21a6037 | ||
|
|
0b9f3309a2 | ||
|
|
09b9b33bac | ||
|
|
21d020919a | ||
|
|
02d090cb94 | ||
|
|
817db34357 | ||
|
|
a3eda8cfba | ||
|
|
5a62fdc06b | ||
|
|
5ff8a65c4a | ||
|
|
719dfd2f24 | ||
|
|
7d28d1d9d4 | ||
|
|
aaa924b9b4 | ||
|
|
f69b93c9fa | ||
|
|
12be06adad | ||
|
|
f6fa28b584 | ||
|
|
fc1e5210a5 | ||
|
|
7601b8f0d0 | ||
|
|
b0557704b2 | ||
|
|
572883ed98 | ||
|
|
92927c8b93 | ||
|
|
11ae57b185 | ||
|
|
9f55f0b32a | ||
|
|
ef52e0a723 | ||
|
|
3df6af1fcd | ||
|
|
a283cfb429 | ||
|
|
3ae2dd769a | ||
|
|
3b916e7e20 | ||
|
|
d93f05a228 | ||
|
|
68183775db | ||
|
|
a8db883661 | ||
|
|
4a51caa281 | ||
|
|
c3148b8ed9 | ||
|
|
bc95a15f89 | ||
|
|
8954de3c93 | ||
|
|
cbfad097db | ||
|
|
c9e83c496c | ||
|
|
442e1883c5 | ||
|
|
f819e6d29c | ||
|
|
261c07f330 | ||
|
|
2328c9dd75 | ||
|
|
15639052c3 | ||
|
|
d91c7d89b2 | ||
|
|
fbdefbe06a | ||
|
|
832ee0180c | ||
|
|
a616e95c0e | ||
|
|
7ab9208203 | ||
|
|
9db6808e85 | ||
|
|
5d48760fd8 | ||
|
|
6ce2166e0a | ||
|
|
201dc30650 | ||
|
|
84bb7865fe | ||
|
|
ab1cd379a9 | ||
|
|
9208ca9566 | ||
|
|
e62897e14c | ||
|
|
65559e6685 | ||
|
|
611b7c50bf | ||
|
|
e983526455 | ||
|
|
10c167d4a3 | ||
|
|
0c641ccf6c | ||
|
|
5899ad866a | ||
|
|
17ecb56259 | ||
|
|
ffeade08ca | ||
|
|
49a567fb51 | ||
|
|
aac104f386 | ||
|
|
b4aff117ce | ||
|
|
13d4519a35 | ||
|
|
743102c965 | ||
|
|
315e5bb6e6 | ||
|
|
6ddef853ad | ||
|
|
5848cf1e5e | ||
|
|
f00f650b4f | ||
|
|
e9d73c6faa | ||
|
|
b6d601ebc4 | ||
|
|
1b58b0dd9b | ||
|
|
1b5ef3103e | ||
|
|
78236a54b8 | ||
|
|
ec2e4d64fd | ||
|
|
44644448f5 | ||
|
|
0a86d5c4ea | ||
|
|
20282e05ea | ||
|
|
9d6b405fa9 | ||
|
|
b82ce5ff45 | ||
|
|
f461198e1e | ||
|
|
7505e6907a | ||
|
|
c1046d5706 | ||
|
|
a61c06b607 | ||
|
|
d3df5742e6 | ||
|
|
68ac42ca46 | ||
|
|
bb7c6ef8b9 | ||
|
|
446e8ed61e | ||
|
|
80372b35f2 | ||
|
|
57eff2f296 | ||
|
|
d996088041 | ||
|
|
04be70019b | ||
|
|
51732d0dcd | ||
|
|
e40bc3ddee | ||
|
|
3f4409e1c3 | ||
|
|
63b7616ab3 | ||
|
|
29059691ce | ||
|
|
531a9d28dc | ||
|
|
3314b4d9ca | ||
|
|
37df9810c8 | ||
|
|
47c1cbba3c | ||
|
|
ded7e549bb | ||
|
|
abb2b85cec | ||
|
|
7d157d2882 | ||
|
|
c6c0a225dd | ||
|
|
276fc386ec | ||
|
|
0b21afd193 | ||
|
|
1032e58e3b | ||
|
|
4987b01167 | ||
|
|
28bb8c5b3c | ||
|
|
2160170c3a | ||
|
|
c0eac9053b | ||
|
|
d57501dd42 | ||
|
|
264cb0404f | ||
|
|
dc9f4b39a8 | ||
|
|
653eeceaf2 | ||
|
|
3d8711947a | ||
|
|
38038d5f30 | ||
|
|
e21d9c3a0c | ||
|
|
7b8c014b3b | ||
|
|
55a30864fd | ||
|
|
4f419ff75c | ||
|
|
638a4f28ad | ||
|
|
8970154b8f | ||
|
|
c96debaaed | ||
|
|
fe7b479235 | ||
|
|
aa1486e641 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -11,9 +11,6 @@
|
||||
[submodule "external/werift"]
|
||||
path = external/werift
|
||||
url = ../../koush/werift-webrtc
|
||||
[submodule "sdk/developer.scrypted.app"]
|
||||
path = sdk/developer.scrypted.app
|
||||
url = ../../koush/developer.scrypted.app
|
||||
[submodule "plugins/sample-cameraprovider"]
|
||||
path = plugins/sample-cameraprovider
|
||||
url = ../../koush/scrypted-sample-cameraprovider
|
||||
|
||||
@@ -160,7 +160,7 @@ export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
(async() => {
|
||||
try {
|
||||
for await (const i of generator) {
|
||||
q.submit(i);
|
||||
await q.enqueue(i);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
@@ -4,6 +4,41 @@ import os from 'os';
|
||||
|
||||
export type Zygote<T> = () => PluginFork<T>;
|
||||
|
||||
export function createService<T, V>(options: ForkOptions, create: (t: Promise<T>) => Promise<V>): {
|
||||
getResult: () => Promise<V>,
|
||||
terminate: () => void,
|
||||
} {
|
||||
let killed = false;
|
||||
let currentResult: Promise<V>;
|
||||
let currentFork: ReturnType<typeof sdk.fork<T>>;
|
||||
|
||||
return {
|
||||
getResult() {
|
||||
if (killed)
|
||||
throw new Error('service terminated');
|
||||
|
||||
if (currentResult)
|
||||
return currentResult;
|
||||
|
||||
currentFork = sdk.fork<T>(options);
|
||||
currentFork.worker.on('exit', () => currentResult = undefined);
|
||||
currentResult = create(currentFork.result);
|
||||
currentResult.catch(() => currentResult = undefined);
|
||||
return currentResult;
|
||||
},
|
||||
|
||||
terminate() {
|
||||
if (killed)
|
||||
return;
|
||||
|
||||
killed = true;
|
||||
currentFork.worker.terminate();
|
||||
currentFork = undefined;
|
||||
currentResult = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
|
||||
let zygote = sdk.fork<T>(options);
|
||||
function* next() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "v0.118.0-jammy-full"
|
||||
version: "v0.120.0-jammy-full"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -14,12 +14,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
@@ -40,16 +35,12 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# 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
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
# python-codecs pygobject dependencies
|
||||
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
|
||||
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-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
@@ -71,11 +62,17 @@ RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu and npu for openvino
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
|
||||
# intel opencl for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
# Disable NPU on docker, because level-zero crashes openvino on older systems.
|
||||
# RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
# NPU driver will SIGILL on openvino prior to 2024.5.0
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# amd opencl
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -98,6 +98,9 @@ services:
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
# "/dev/dri:/dev/dri",
|
||||
|
||||
# AMD GPU
|
||||
# "/dev/kfd:/dev/kfd",
|
||||
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
|
||||
|
||||
36
install/docker/install-amd-graphics.sh
Normal file
36
install/docker/install-amd-graphics.sh
Normal file
@@ -0,0 +1,36 @@
|
||||
if [ "$(uname -m)" != "x86_64" ]
|
||||
then
|
||||
echo "AMD graphics will not be installed on this architecture."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "AMD graphics package can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="jammy"
|
||||
else
|
||||
distro="noble"
|
||||
fi
|
||||
|
||||
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
|
||||
|
||||
FILENAME="amdgpu-install_6.2.60202-1_all.deb"
|
||||
set -e
|
||||
mkdir -p /tmp/amd
|
||||
cd /tmp/amd
|
||||
curl -O -L http://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
|
||||
apt -y install rsync
|
||||
dpkg -i $FILENAME
|
||||
amdgpu-install --usecase=opencl --no-dkms -y --accept-eula
|
||||
cd /tmp
|
||||
rm -rf /tmp/amd
|
||||
|
||||
@@ -30,8 +30,8 @@ apt-get -y install intel-media-va-driver-non-free &&
|
||||
apt-get -y dist-upgrade;
|
||||
|
||||
# manual installation
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
|
||||
# these debs are seemingly ubuntu 22.04 only.
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
@@ -39,12 +39,12 @@ apt-get install -y ocl-icd-libopencl1
|
||||
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-core_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-opencl_1.0.17537.20_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu_1.3.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd_24.35.30872.22_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/libigdgmm12_22.5.0_amd64.deb
|
||||
|
||||
@@ -7,7 +7,7 @@ fi
|
||||
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ]
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
|
||||
if [ -d "/etc/pve" ]
|
||||
@@ -23,6 +23,13 @@ then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
distro="22.04_amd64"
|
||||
else
|
||||
distro="24.04_amd64"
|
||||
fi
|
||||
|
||||
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
|
||||
|
||||
# no errors beyond this point
|
||||
@@ -30,27 +37,23 @@ set -e
|
||||
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# different npu downloads for ubuntu versions
|
||||
if [ -n "$UBUNTU_22_04" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-driver-compiler-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-fw-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-level-zero-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
|
||||
else
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-driver-compiler-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-fw-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-level-zero-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
|
||||
fi
|
||||
# level zero must also be installed
|
||||
LEVEL_ZERO_VERSION=1.18.5
|
||||
# https://github.com/oneapi-src/level-zero
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb
|
||||
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
|
||||
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
|
||||
# npu driver
|
||||
# https://github.com/intel/linux-npu-driver
|
||||
NPU_VERSION=1.10.0
|
||||
NPU_VERSION_DATE=20241107-11729849322
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
# firmware can only be installed on host. will cause problems inside container.
|
||||
if [ -n "$INTEL_FW_NPU" ]
|
||||
then
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-fw-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
fi
|
||||
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-level-zero-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
|
||||
|
||||
apt -y update
|
||||
apt -y install libtbb12
|
||||
|
||||
@@ -2,9 +2,13 @@ UBUNTU_22_04=$(lsb_release -r | grep "22.04")
|
||||
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
|
||||
|
||||
set -e
|
||||
|
||||
# Install CUDA for 22.04
|
||||
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
|
||||
# need this apt for nvidia-utils
|
||||
# needs either ubuntu 22.0.4 or 24.04
|
||||
# Install CUDA for 24.04
|
||||
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
|
||||
# Do not apt install nvidia-open, must use cuda-drivers.
|
||||
|
||||
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
|
||||
then
|
||||
echo "NVIDIA container toolkit can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
|
||||
|
||||
@@ -75,10 +75,14 @@ echo "Created $DOCKER_COMPOSE_YML"
|
||||
|
||||
if [ -z "$SCRYPTED_LXC" ]
|
||||
then
|
||||
if [ -d /dev/dri ]
|
||||
if [ -e /dev/dri ]
|
||||
then
|
||||
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
if [ -e /dev/kfd ]
|
||||
then
|
||||
sed -i 's/'#' "\/dev\/kfd/"\/dev\/kfd/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
else
|
||||
# uncomment lxc specific stuff
|
||||
sed -i 's/'#' lxc //g' $DOCKER_COMPOSE_YML
|
||||
|
||||
@@ -3,11 +3,17 @@
|
||||
################################################################
|
||||
FROM header as base
|
||||
|
||||
# intel opencl gpu and npu for openvino
|
||||
# vulkan
|
||||
RUN apt -y install libvulkan1
|
||||
|
||||
# intel opencl for openvino
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
|
||||
|
||||
# Disable NPU on docker, because level-zero crashes openvino on older systems.
|
||||
# RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
# NPU driver will SIGILL on openvino prior to 2024.5.0
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
|
||||
|
||||
# amd opencl
|
||||
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
|
||||
@@ -11,12 +11,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# base tools and development stuff
|
||||
RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
pkg-config && \
|
||||
apt-get -y update && \
|
||||
apt-get -y upgrade
|
||||
@@ -37,16 +32,12 @@ RUN apt-get -y install \
|
||||
python3-setuptools \
|
||||
python3-wheel
|
||||
|
||||
# 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
|
||||
|
||||
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
|
||||
RUN echo "Installing gstreamer."
|
||||
# python-codecs pygobject dependencies
|
||||
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
|
||||
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-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
|
||||
gstreamer1.0-vaapi
|
||||
|
||||
# python3 gstreamer bindings
|
||||
|
||||
@@ -40,8 +40,6 @@ echo "Installing Scrypted dependencies..."
|
||||
RUN_IGNORE xcode-select --install
|
||||
RUN brew update
|
||||
RUN_IGNORE brew install node@20
|
||||
# snapshot plugin and others
|
||||
RUN brew install libvips
|
||||
# dlib
|
||||
RUN brew install cmake
|
||||
|
||||
|
||||
@@ -10,14 +10,15 @@ export DEBIAN_FRONTEND=noninteractive
|
||||
if [ -e "volume/.pull" ]
|
||||
then
|
||||
rm -rf volume/.pull
|
||||
docker compose pull && docker container prune -f && docker image prune -a -f
|
||||
PULL="--pull"
|
||||
(sleep 300 && docker container prune -f && docker image prune -a -f) &
|
||||
else
|
||||
# always background pull in case there's a broken image.
|
||||
(docker compose pull && docker container prune -f && docker image prune -a -f) &
|
||||
(sleep 300 && docker compose pull && docker container prune -f && docker image prune -a -f) &
|
||||
fi
|
||||
|
||||
# do not daemonize, when it exits, systemd will restart it.
|
||||
# force a recreate as .env may have changed.
|
||||
# furthermore force recreate gets the container back into a known state
|
||||
# which is preferable in case the user has made manual changes and then restarts.
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit
|
||||
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit $PULL
|
||||
|
||||
@@ -18,17 +18,30 @@ function readyn() {
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
SCRYPTED_VERSION=v0.116.0
|
||||
SCRYPTED_VERSION=v0.120.0
|
||||
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
|
||||
if [ -z "$VMID" ]
|
||||
then
|
||||
VMID=10443
|
||||
fi
|
||||
|
||||
SCRYPTED_BACKUP_VMID=10445
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
pct config $VMID 2>&1 > /dev/null
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "VMID $VMID not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# append existing mac address.
|
||||
HWADDR=",hwaddr=$(pct config $VMID | grep -oE 'hwaddr=[A-Z0-9:]+' | cut -d '=' -f 2)"
|
||||
RESTORE_HOSTNAME=$(pct config $VMID | grep -oE 'hostname: [^[:space:]]+' | cut -d ':' -f 2- | tr -d ' ')
|
||||
|
||||
pct destroy $SCRYPTED_BACKUP_VMID 2>&1 > /dev/null
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=10444
|
||||
VMID=$SCRYPTED_BACKUP_VMID
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
fi
|
||||
|
||||
@@ -39,18 +52,49 @@ then
|
||||
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
|
||||
fi
|
||||
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
if [[ "$@" =~ "--force" ]]
|
||||
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 ""
|
||||
IGNORE_EXISTING=true
|
||||
fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
IGNORE_EXISTING=true
|
||||
fi
|
||||
|
||||
if [ -z "$IGNORE_EXISTING" ]
|
||||
then
|
||||
echo "Checking for existing container."
|
||||
pct config $VMID
|
||||
if [ "$?" == "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "==============================================================="
|
||||
echo "Existing container $VMID found."
|
||||
echo "Please choose from the following options to resolve this error."
|
||||
echo "==============================================================="
|
||||
echo ""
|
||||
echo "1. To reinstall and reset Scrypted, run this script with --force to overwrite the existing container."
|
||||
echo "THIS WILL WIPE THE EXISTING CONFIGURATION:"
|
||||
echo ""
|
||||
echo "VMID=$VMID bash $0 --force"
|
||||
echo ""
|
||||
echo "2. To reinstall Scrypted and and retain existing configuration, run this script with the environment variable SCRYPTED_RESTORE=true."
|
||||
echo "This preserves existing data. Creating a backup within Scrypted is highly recommended in case the reset fails."
|
||||
echo "THIS WILL WIPE ADDITIONAL VOLUMES SUCH AS NVR STORAGE. NVR volumes will need to be readded after the restore:"
|
||||
echo ""
|
||||
echo "SCRYPTED_RESTORE=true VMID=$VMID bash $0"
|
||||
echo ""
|
||||
echo "3. To install and run multiple Scrypted containers, run this script with the environment variable specifying"
|
||||
echo "the new VMID=<number>. For example, to create a new LXC with VMID 12345:"
|
||||
echo ""
|
||||
echo "VMID=12345 bash $0"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
pct stop $VMID 2>&1 > /dev/null
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
@@ -73,7 +117,7 @@ then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto
|
||||
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto$HWADDR
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
@@ -82,6 +126,18 @@ then
|
||||
echo "Ignoring... Please verify your container's network settings."
|
||||
fi
|
||||
|
||||
if [ -n "$RESTORE_HOSTNAME" ]
|
||||
then
|
||||
pct set $VMID --hostname $RESTORE_HOSTNAME
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo ""
|
||||
echo "pct hostname restore failed"
|
||||
echo ""
|
||||
echo "Ignoring... Please verify your container's dns settings."
|
||||
fi
|
||||
fi
|
||||
|
||||
CONF=/etc/pve/lxc/$VMID.conf
|
||||
if [ -f "$CONF" ]
|
||||
then
|
||||
@@ -92,23 +148,89 @@ fi
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
|
||||
echo ""
|
||||
echo ""
|
||||
echo "Running this script will reset the Scrypted container to a factory state while preserving existing data."
|
||||
echo "IT IS RECOMMENDED TO CREATE A BACKUP INSIDE SCRYPTED FIRST."
|
||||
readyn "Are you sure you want to continue?"
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Stopping scrypted..."
|
||||
pct stop $RESTORE_VMID 2>&1 > /dev/null
|
||||
|
||||
echo "Preparing rootfs reset..."
|
||||
# this copies the
|
||||
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
|
||||
|
||||
rm *.tar
|
||||
vzdump 10444 --dumpdir /tmp
|
||||
# remove the empty data volume from the downloaded image.
|
||||
pct set $SCRYPTED_BACKUP_VMID --delete mp0 && pct set $SCRYPTED_BACKUP_VMID --delete unused0
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Failed to remove data volume from image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# create a backup that contains only the root disk.
|
||||
rm -f *.tar
|
||||
vzdump $SCRYPTED_BACKUP_VMID --dumpdir /tmp
|
||||
|
||||
# this moves the data volume from the current scrypted instance to the backup target to preserve it during
|
||||
# the restore.
|
||||
pct move-volume $RESTORE_VMID mp0 --target-vmid $SCRYPTED_BACKUP_VMID --target-volume mp0
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
echo "Failed to move data volume to backup."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# arguments: from to mp hide-warning
|
||||
function move_volume() {
|
||||
HAS_VOLUME=$(pct config $1 | grep $3:)
|
||||
if [ -n "$HAS_VOLUME" ]
|
||||
then
|
||||
echo "Moving $3..."
|
||||
# this may error and there may be recording loss. bailing at ths point is already too late.
|
||||
pct move-volume $1 $3 --target-vmid $2 --target-volume $3
|
||||
|
||||
# volume must be inside /mnt to get into docker container
|
||||
INSIDE_MNT=$(echo $HAS_VOLUME | grep /mnt)
|
||||
if [ -z "$INSIDE_MNT" -a -z "$4" ]
|
||||
then
|
||||
echo "##################################################################"
|
||||
echo "The following mount point is not visible to the"
|
||||
echo "Scrypted docker container within the LXC:"
|
||||
echo ""
|
||||
echo "$HAS_VOLUME"
|
||||
echo ""
|
||||
echo "This recordings directory will be unavailable."
|
||||
echo "The mount point must be updated to a path within /mnt."
|
||||
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#proxmox-ve-mount-point"
|
||||
echo "##################################################################"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# try moving 5 volumes, any more than that seems unlikely
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp1 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp2 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp3 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp4 hide-warning
|
||||
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp5 hide-warning
|
||||
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Moving data volume to backup..."
|
||||
pct restore $VMID *.tar $@
|
||||
echo "Restoring with reset image..."
|
||||
pct restore --force 1 $VMID *.tar $@
|
||||
|
||||
pct destroy 10444
|
||||
echo "Restoring volumes..."
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp0 hide-warning
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp1
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp2
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp3
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp4
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp5
|
||||
|
||||
pct destroy $SCRYPTED_BACKUP_VMID
|
||||
fi
|
||||
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
@@ -117,6 +239,7 @@ then
|
||||
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"drm\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"kfd\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
|
||||
sh -c "echo 'SUBSYSTEM==\"accel\", 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"
|
||||
@@ -143,4 +266,5 @@ then
|
||||
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."
|
||||
echo ""
|
||||
echo "Scrypted NVR servers should run the disk setup script in the documentation to add storage prior to starting the container."
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
NVR_STORAGE=$1
|
||||
NVR_STORAGE_DIRECTORY=$2
|
||||
|
||||
DISK_TYPE="large"
|
||||
if [ ! -z "$FAST_DISK" ]
|
||||
@@ -10,9 +11,9 @@ fi
|
||||
|
||||
if [ -z "$NVR_STORAGE" ]; then
|
||||
echo ""
|
||||
echo "Error: Proxmox Directory Disk not provided. Usage:"
|
||||
echo "Error: Directory name not provided. Usage:"
|
||||
echo ""
|
||||
echo "bash $0 <proxmox-directory-disk>"
|
||||
echo "bash $0 directory-name [/optional/path/to/storage]"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@@ -30,20 +31,30 @@ if [ ! -f "$FILE" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STORAGE="/mnt/pve/$NVR_STORAGE"
|
||||
|
||||
if [ ! -d "$STORAGE" ]
|
||||
if [ ! -z "$NVR_STORAGE_DIRECTORY" ]
|
||||
then
|
||||
echo "Error: $STORAGE not found."
|
||||
echo "The Proxmox Directory Storage must be created using the UI prior to running this script."
|
||||
exit 1
|
||||
if [ ! -d "$NVR_STORAGE_DIRECTORY" ]
|
||||
then
|
||||
echo ""
|
||||
echo "Error: $NVR_STORAGE_DIRECTORY directory not found."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
STORAGE="/mnt/pve/$NVR_STORAGE"
|
||||
if [ ! -d "$STORAGE" ]
|
||||
then
|
||||
echo "Error: $STORAGE not found."
|
||||
echo "The Proxmox Directory Storage must be created using the UI prior to running this script."
|
||||
exit 1
|
||||
fi
|
||||
# use subdirectory doesn't conflict with Proxmox storage of backups etc.
|
||||
NVR_STORAGE_DIRECTORY="$STORAGE/mounts/scrypted-nvr"
|
||||
fi
|
||||
|
||||
# use subdirectory doesn't conflict with Proxmox storage of backups etc.
|
||||
STORAGE="$STORAGE/mounts/scrypted-nvr"
|
||||
# create the hidden folder that can be used as a marker.
|
||||
mkdir -p $STORAGE/.nvr
|
||||
chmod 0777 $STORAGE
|
||||
mkdir -p $NVR_STORAGE_DIRECTORY/.nvr
|
||||
chmod 0777 $NVR_STORAGE_DIRECTORY
|
||||
|
||||
echo "Stopping Scrypted..."
|
||||
pct stop "$VMID"
|
||||
@@ -57,7 +68,7 @@ then
|
||||
fi
|
||||
|
||||
echo "Adding new $DISK_TYPE lxc.mount.entry."
|
||||
echo "lxc.mount.entry: $STORAGE mnt/nvr/$DISK_TYPE/$NVR_STORAGE none bind,optional,create=dir" >> "$FILE"
|
||||
echo "lxc.mount.entry: $NVR_STORAGE_DIRECTORY mnt/nvr/$DISK_TYPE/$NVR_STORAGE none bind,optional,create=dir" >> "$FILE"
|
||||
|
||||
echo "Starting Scrypted..."
|
||||
pct start $VMID
|
||||
|
||||
@@ -27,7 +27,7 @@ echo "external/werift > npm install"
|
||||
npm install
|
||||
popd
|
||||
|
||||
for directory in ffmpeg-camera rtsp amcrest onvif hikvision unifi-protect webrtc homekit
|
||||
for directory in rtsp amcrest onvif hikvision reolink unifi-protect webrtc homekit
|
||||
do
|
||||
echo "$directory > npm install"
|
||||
pushd plugins/$directory
|
||||
|
||||
8
packages/client/package-lock.json
generated
8
packages/client/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.3.6",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.60",
|
||||
"@scrypted/types": "^0.3.66",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
@@ -75,9 +75,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.60",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.60.tgz",
|
||||
"integrity": "sha512-oapFYQvyHLp0odCSx//USNnGNegS9ZL6a1HFIZzjDdMj2MNszTqiucAcu/wAlBwqjgURlP4/8xeLGVHEa4S2uQ=="
|
||||
"version": "0.3.66",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.66.tgz",
|
||||
"integrity": "sha512-POHpVgW6Ce8mnJRaXZRm+2RtvFuPP+ZehsDrhUqkQdxmnV81m8K2+3M6Vhrt+07kNDXmrznAijoj/OzXkdZWgw=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.60",
|
||||
"@scrypted/types": "^0.3.66",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
|
||||
5
plugins/alexa/package-lock.json
generated
5
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
@@ -203,4 +202,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -53,6 +53,12 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
title: "Pairing Key",
|
||||
description: "The pairing key used to validate requests from Alexa. Clear this key or delete the plugin to allow pairing with a different Alexa login.",
|
||||
},
|
||||
disableAutoAdd: {
|
||||
title: "Disable auto add",
|
||||
description: "Disable automatic enablement of devices.",
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
accessToken: Promise<string>;
|
||||
@@ -116,6 +122,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
if (!supportedTypes.has(device.type))
|
||||
return DeviceMixinStatus.NotSupported;
|
||||
|
||||
if (this.storageSettings.values.disableAutoAdd) {
|
||||
return DeviceMixinStatus.Skip;
|
||||
}
|
||||
|
||||
mixins.push(this.id);
|
||||
|
||||
const plugins = await systemManager.getComponent('plugins');
|
||||
@@ -671,7 +681,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
|
||||
enum DeviceMixinStatus {
|
||||
NotSupported = 0,
|
||||
Setup = 1,
|
||||
AlreadySetup = 2
|
||||
AlreadySetup = 2,
|
||||
Skip = 3,
|
||||
}
|
||||
|
||||
class HttpResponseLoggingImpl implements AlexaHttpResponse {
|
||||
|
||||
2
plugins/core/.vscode/settings.json
vendored
2
plugins/core/.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.82",
|
||||
"version": "0.3.86",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.82",
|
||||
"version": "0.3.86",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.82",
|
||||
"version": "0.3.86",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -17,9 +17,13 @@ export class PluginSocketService extends ScryptedDeviceBase implements StreamSer
|
||||
throw new Error('must provide pluginId');
|
||||
|
||||
const plugins = await sdk.systemManager.getComponent('plugins');
|
||||
const replPort: number = await plugins.getRemoteServicePort(pluginId, this.serviceName);
|
||||
const servicePort = await plugins.getRemoteServicePort(pluginId, this.serviceName) as number | [number, string];
|
||||
const [port, host] = Array.isArray(servicePort) ? servicePort : [servicePort, undefined];
|
||||
|
||||
const socket = net.connect(replPort);
|
||||
const socket = net.connect({
|
||||
port,
|
||||
host,
|
||||
});
|
||||
await once(socket, 'connect');
|
||||
|
||||
const queue = createAsyncQueue<Buffer>();
|
||||
|
||||
2
plugins/coreml/.vscode/settings.json
vendored
2
plugins/coreml/.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
68
plugins/coreml/package-lock.json
generated
68
plugins/coreml/package-lock.json
generated
@@ -1,36 +1,35 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.70",
|
||||
"version": "0.1.76",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.70",
|
||||
"version": "0.1.76",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.31",
|
||||
"version": "0.3.77",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -42,11 +41,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -61,25 +60,24 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,18 @@
|
||||
"interfaces": [
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"ClusterForkInterface",
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"@scrypted/coreml"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.70"
|
||||
"version": "0.1.76"
|
||||
}
|
||||
|
||||
@@ -69,9 +69,13 @@ def parse_labels(userDefined):
|
||||
return parse_label_contents(classes)
|
||||
|
||||
|
||||
class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
class CoreMLPlugin(
|
||||
PredictPlugin,
|
||||
scrypted_sdk.Settings,
|
||||
scrypted_sdk.DeviceProvider,
|
||||
):
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
@@ -139,7 +143,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
@@ -148,6 +154,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "CoreML Face Recognition",
|
||||
@@ -160,6 +167,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "CoreML Text Recognition",
|
||||
@@ -176,10 +184,10 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
|
||||
async def getDevice(self, nativeId: str) -> Any:
|
||||
if nativeId == "facerecognition":
|
||||
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(nativeId)
|
||||
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(self, nativeId)
|
||||
return self.faceDevice
|
||||
if nativeId == "textrecognition":
|
||||
self.textDevice = self.textDevice or CoreMLTextRecognition(nativeId)
|
||||
self.textDevice = self.textDevice or CoreMLTextRecognition(self, nativeId)
|
||||
return self.textDevice
|
||||
raise Exception("unknown device")
|
||||
|
||||
@@ -227,7 +235,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
return ret
|
||||
|
||||
if self.scrypted_yolo_nas:
|
||||
predictions = list(out_dict.values())
|
||||
predictions = list(out_dict.values())
|
||||
objs = yolo.parse_yolo_nas(predictions)
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
return ret
|
||||
@@ -250,13 +258,13 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
|
||||
for r in objects:
|
||||
obj = Prediction(
|
||||
r["classId"].astype(float),
|
||||
r["confidence"].astype(float),
|
||||
r["classId"],
|
||||
r["confidence"],
|
||||
Rectangle(
|
||||
r["xmin"].astype(float),
|
||||
r["ymin"].astype(float),
|
||||
r["xmax"].astype(float),
|
||||
r["ymax"].astype(float),
|
||||
r["xmin"],
|
||||
r["ymin"],
|
||||
r["xmax"],
|
||||
r["ymax"],
|
||||
),
|
||||
)
|
||||
objs.append(obj)
|
||||
@@ -275,9 +283,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
|
||||
),
|
||||
)
|
||||
|
||||
coordinatesList = out_dict["coordinates"].astype(float)
|
||||
coordinatesList = out_dict["coordinates"]
|
||||
|
||||
for index, confidenceList in enumerate(out_dict["confidence"].astype(float)):
|
||||
for index, confidenceList in enumerate(out_dict["confidence"]):
|
||||
values = confidenceList
|
||||
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
|
||||
maxConfidence = confidenceList[maxConfidenceIndex]
|
||||
|
||||
@@ -29,8 +29,8 @@ def cosine_similarity(vector_a, vector_b):
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
|
||||
|
||||
class CoreMLFaceRecognition(FaceRecognizeDetection):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin, nativeId)
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-face")
|
||||
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-face")
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from predict.text_recognize import TextRecognition
|
||||
|
||||
|
||||
class CoreMLTextRecognition(TextRecognition):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin, nativeId)
|
||||
|
||||
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-text")
|
||||
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-text")
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from coreml import CoreMLPlugin
|
||||
import predict
|
||||
|
||||
def create_scrypted_plugin():
|
||||
return CoreMLPlugin()
|
||||
|
||||
async def fork():
|
||||
return predict.Fork(CoreMLPlugin)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
|
||||
numpy==1.26.4
|
||||
coremltools==7.2
|
||||
coremltools==8.0
|
||||
Pillow==10.3.0
|
||||
opencv-python==4.10.0.84
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
2
plugins/diagnostics/.vscode/settings.json
vendored
2
plugins/diagnostics/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "koushik-winvm",
|
||||
}
|
||||
38
plugins/diagnostics/package-lock.json
generated
38
plugins/diagnostics/package-lock.json
generated
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.19",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
@@ -12,8 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4"
|
||||
},
|
||||
"version": "0.0.17"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
@@ -32,13 +33,13 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.62",
|
||||
"version": "0.3.69",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
@@ -46,7 +47,7 @@
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -59,11 +60,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.5"
|
||||
"typedoc": "^0.26.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
@@ -719,12 +720,12 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/node": "^22.1.0",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
@@ -733,9 +734,9 @@
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.5",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
@@ -843,6 +844,5 @@
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
}
|
||||
},
|
||||
"version": "0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/diagnostics",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.19",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -128,6 +128,9 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
await device.sendNotification('Scrypted Diagnostics', {
|
||||
body: 'Body',
|
||||
subtitle: 'Subtitle',
|
||||
android: {
|
||||
channel: 'diagnostics',
|
||||
}
|
||||
}, mo);
|
||||
|
||||
this.warnStep(console, 'Check the device for the notification.');
|
||||
@@ -291,6 +294,8 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
|
||||
const nvrPlugin = sdk.systemManager.getDeviceById('@scrypted/nvr');
|
||||
const cloudPlugin = sdk.systemManager.getDeviceById('@scrypted/cloud');
|
||||
const hasCUDA = process.env.NVIDIA_VISIBLE_DEVICES && process.env.NVIDIA_DRIVER_CAPABILITIES;
|
||||
const onnxPlugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>('@scrypted/onnx');
|
||||
const openvinoPlugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>('@scrypted/openvino');
|
||||
|
||||
await this.validate(this.console, 'Scrypted Installation', async () => {
|
||||
@@ -364,10 +369,14 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
});
|
||||
|
||||
if (process.platform === 'linux' && nvrPlugin) {
|
||||
// ensure /dev/dri/renderD128 is available
|
||||
// ensure /dev/dri/renderD128 or /dev/dri/renderD129 is available
|
||||
await this.validate(this.console, 'GPU Passthrough', async () => {
|
||||
if (!fs.existsSync('/dev/dri/renderD128'))
|
||||
throw new Error('GPU device unvailable or not passed through to container.');
|
||||
if (!fs.existsSync('/dev/dri/renderD128') && !fs.existsSync('/dev/dri/renderD129'))
|
||||
throw new Error('GPU device unvailable or not passed through to container. (/dev/dri/renderD128, /dev/dri/renderD129)');
|
||||
// also check /dev/kfd for AMD CPU
|
||||
const amdCPU = os.cpus().find(c => c.model.includes('AMD'));
|
||||
if (amdCPU && !fs.existsSync('/dev/kfd'))
|
||||
throw new Error('GPU device unvailable or not passed through to container. (/dev/kfd)');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -403,7 +412,22 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
throw new Error('Invalid response received from short lived URL.');
|
||||
});
|
||||
|
||||
if (openvinoPlugin) {
|
||||
if ((hasCUDA || process.platform === 'win32') && onnxPlugin) {
|
||||
await this.validate(this.console, 'ONNX Plugin', async () => {
|
||||
const settings = await onnxPlugin.getSettings();
|
||||
const executionDevice = settings.find(s => s.key === 'execution_device');
|
||||
if (executionDevice?.value?.toString().includes('CPU'))
|
||||
this.warnStep(this.console, 'GPU device unvailable or not passed through to container.');
|
||||
|
||||
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
|
||||
const detected = await onnxPlugin.detectObjects(zidane);
|
||||
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
|
||||
if (!personFound)
|
||||
throw new Error('Person not detected in test image.');
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasCUDA && openvinoPlugin && (process.platform !== 'win32' || !onnxPlugin)) {
|
||||
await this.validate(this.console, 'OpenVINO Plugin', async () => {
|
||||
const settings = await openvinoPlugin.getSettings();
|
||||
const availbleDevices = settings.find(s => s.key === 'available_devices');
|
||||
@@ -512,6 +536,7 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
|
||||
|
||||
await this.validate(this.console, 'Deprecated Plugins', async () => {
|
||||
const defunctPlugins = [
|
||||
'@scrypted/electron-core',
|
||||
'@scrypted/opencv',
|
||||
'@scrypted/python-codecs',
|
||||
'@scrypted/pam-diff',
|
||||
|
||||
@@ -111,6 +111,12 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
hide: true,
|
||||
description: 'The last home hub to request a recording. Internally used to determine if a streaming request is coming from remote wifi.',
|
||||
},
|
||||
autoAdd: {
|
||||
title: "Auto enable",
|
||||
description: "Automatically enable this mixin on new devices.",
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
mergedDevices = new Set<string>();
|
||||
|
||||
@@ -218,7 +224,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
|
||||
|
||||
try {
|
||||
const mixins = (device.mixins || []).slice();
|
||||
if (!mixins.includes(this.id)) {
|
||||
const autoAdd = this.storageSettings.values.autoAdd ?? true;
|
||||
if (!mixins.includes(this.id) && autoAdd) {
|
||||
// don't sync this by default, as it's solely for automations
|
||||
if (device.type === ScryptedDeviceType.Notifier)
|
||||
continue;
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.46",
|
||||
"version": "0.1.47",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.46",
|
||||
"version": "0.1.47",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.46",
|
||||
"version": "0.1.47",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -162,7 +162,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
getCurrentSettings() {
|
||||
const settings = this.model.settings;
|
||||
if (!settings)
|
||||
return { id : this.id };
|
||||
return { id: this.id };
|
||||
|
||||
const ret: { [key: string]: any } = {};
|
||||
for (const setting of settings) {
|
||||
@@ -338,7 +338,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
if (this.model.decoder) {
|
||||
if (!options?.suppress)
|
||||
this.console.log(this.objectDetection.name, '(with builtin decoder)');
|
||||
this.console.log(this.objectDetection.name, '(with builtin decoder)');
|
||||
return stream;
|
||||
}
|
||||
|
||||
@@ -456,10 +456,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!this.hasMotionType) {
|
||||
this.plugin.trackDetection();
|
||||
|
||||
// const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
|
||||
// const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
|
||||
// if (numZonedDetections !== numOriginalDetections)
|
||||
// this.console.log('Zone filtered detections:', numZonedDetections - numOriginalDetections);
|
||||
const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
|
||||
const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
|
||||
if (numZonedDetections !== numOriginalDetections)
|
||||
currentDetections.set('filtered', (currentDetections.get('filtered') || 0) + 1);
|
||||
|
||||
for (const d of detected.detected.detections) {
|
||||
currentDetections.set(d.className, Math.max(currentDetections.get(d.className) || 0, d.score));
|
||||
|
||||
5
plugins/onnx/.vscode/launch.json
vendored
5
plugins/onnx/.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "${config:scrypted.debugHost}",
|
||||
@@ -21,9 +21,8 @@
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/src",
|
||||
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
|
||||
"remoteRoot": "."
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
7
plugins/onnx/.vscode/settings.json
vendored
7
plugins/onnx/.vscode/settings.json
vendored
@@ -1,12 +1,8 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
"scrypted.debugHost": "koushik-ubuntuvm",
|
||||
"scrypted.debugHost": "koushik-winvm",
|
||||
"scrypted.serverRoot": "/server",
|
||||
|
||||
// lxc
|
||||
// "scrypted.debugHost": "scrypted-server",
|
||||
// "scrypted.serverRoot": "/root/.scrypted",
|
||||
|
||||
// pi local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
@@ -18,7 +14,6 @@
|
||||
// "scrypted.debugHost": "koushik-winvm",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
4
plugins/onnx/package-lock.json
generated
4
plugins/onnx/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/onnx",
|
||||
"version": "0.1.113",
|
||||
"version": "0.1.119",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/onnx",
|
||||
"version": "0.1.113",
|
||||
"version": "0.1.119",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -35,12 +35,18 @@
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings",
|
||||
"ClusterForkInterface",
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"@scrypted/onnx"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.113"
|
||||
"version": "0.1.119"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from ort import ONNXPlugin
|
||||
import predict
|
||||
|
||||
def create_scrypted_plugin():
|
||||
return ONNXPlugin()
|
||||
|
||||
async def fork():
|
||||
return predict.Fork(ONNXPlugin)
|
||||
|
||||
@@ -40,6 +40,7 @@ availableModels = [
|
||||
"scrypted_yolov8n_320",
|
||||
]
|
||||
|
||||
|
||||
def parse_labels(names):
|
||||
j = ast.literal_eval(names)
|
||||
ret = {}
|
||||
@@ -47,11 +48,15 @@ def parse_labels(names):
|
||||
ret[int(k)] = v
|
||||
return ret
|
||||
|
||||
|
||||
class ONNXPlugin(
|
||||
PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider
|
||||
PredictPlugin,
|
||||
scrypted_sdk.BufferConverter,
|
||||
scrypted_sdk.Settings,
|
||||
scrypted_sdk.DeviceProvider,
|
||||
):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
@@ -67,7 +72,11 @@ class ONNXPlugin(
|
||||
|
||||
print(f"model {model}")
|
||||
|
||||
onnxmodel = model if self.scrypted_yolo_nas else "best" if self.scrypted_model else model
|
||||
onnxmodel = (
|
||||
model
|
||||
if self.scrypted_yolo_nas
|
||||
else "best" if self.scrypted_model else model
|
||||
)
|
||||
|
||||
model_version = "v3"
|
||||
onnxfile = self.downloadFile(
|
||||
@@ -83,30 +92,37 @@ class ONNXPlugin(
|
||||
deviceIds = ["0"]
|
||||
self.deviceIds = deviceIds
|
||||
|
||||
compiled_models = []
|
||||
self.compiled_models = {}
|
||||
compiled_models: list[onnxruntime.InferenceSession] = []
|
||||
self.compiled_models: dict[str, onnxruntime.InferenceSession] = {}
|
||||
self.provider = "Unknown"
|
||||
|
||||
try:
|
||||
for deviceId in deviceIds:
|
||||
sess_options = onnxruntime.SessionOptions()
|
||||
|
||||
providers: list[str] = []
|
||||
if sys.platform == 'darwin':
|
||||
if sys.platform == "darwin":
|
||||
providers.append("CoreMLExecutionProvider")
|
||||
|
||||
if ('linux' in sys.platform or 'win' in sys.platform) and (platform.machine() == 'x86_64' or platform.machine() == 'AMD64'):
|
||||
if ("linux" in sys.platform or "win" in sys.platform) and (
|
||||
platform.machine() == "x86_64" or platform.machine() == "AMD64"
|
||||
):
|
||||
deviceId = int(deviceId)
|
||||
providers.append(("CUDAExecutionProvider", { "device_id": deviceId }))
|
||||
providers.append(("CUDAExecutionProvider", {"device_id": deviceId}))
|
||||
|
||||
providers.append('CPUExecutionProvider')
|
||||
providers.append("CPUExecutionProvider")
|
||||
|
||||
compiled_model = onnxruntime.InferenceSession(onnxfile, sess_options=sess_options, providers=providers)
|
||||
compiled_model = onnxruntime.InferenceSession(
|
||||
onnxfile, sess_options=sess_options, providers=providers
|
||||
)
|
||||
compiled_models.append(compiled_model)
|
||||
|
||||
input = compiled_model.get_inputs()[0]
|
||||
self.model_dim = input.shape[2]
|
||||
self.input_name = input.name
|
||||
self.labels = parse_labels(compiled_model.get_modelmeta().custom_metadata_map['names'])
|
||||
self.labels = parse_labels(
|
||||
compiled_model.get_modelmeta().custom_metadata_map["names"]
|
||||
)
|
||||
|
||||
except:
|
||||
import traceback
|
||||
@@ -121,7 +137,15 @@ class ONNXPlugin(
|
||||
thread_name = threading.current_thread().name
|
||||
interpreter = compiled_models.pop()
|
||||
self.compiled_models[thread_name] = interpreter
|
||||
print('Runtime initialized on thread {}'.format(thread_name))
|
||||
# remove CPUExecutionProider from providers
|
||||
providers = interpreter.get_providers()
|
||||
if not len(providers):
|
||||
providers = ["CPUExecutionProvider"]
|
||||
if "CPUExecutionProvider" in providers:
|
||||
providers.remove("CPUExecutionProvider")
|
||||
# join the remaining providers string
|
||||
self.provider = ", ".join(providers)
|
||||
print("Runtime initialized on thread {}".format(thread_name))
|
||||
|
||||
self.executor = concurrent.futures.ThreadPoolExecutor(
|
||||
initializer=executor_initializer,
|
||||
@@ -134,9 +158,13 @@ class ONNXPlugin(
|
||||
thread_name_prefix="onnx-prepare",
|
||||
)
|
||||
|
||||
self.executor.submit(lambda: None)
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def prepareRecognitionModels(self):
|
||||
try:
|
||||
@@ -145,6 +173,7 @@ class ONNXPlugin(
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "ONNX Face Recognition",
|
||||
@@ -157,6 +186,7 @@ class ONNXPlugin(
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "ONNX Text Recognition",
|
||||
@@ -206,12 +236,12 @@ class ONNXPlugin(
|
||||
"key": "execution_device",
|
||||
"title": "Execution Device",
|
||||
"readonly": True,
|
||||
"value": onnxruntime.get_device(),
|
||||
}
|
||||
"value": self.provider,
|
||||
},
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: SettingValue):
|
||||
if (key == 'deviceIds'):
|
||||
if key == "deviceIds":
|
||||
value = json.dumps(value)
|
||||
self.storage.setItem(key, value)
|
||||
await self.onDeviceEvent(scrypted_sdk.ScryptedInterface.Settings.value, None)
|
||||
@@ -225,7 +255,7 @@ class ONNXPlugin(
|
||||
return [self.model_dim, self.model_dim]
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def prepare():
|
||||
def prepare():
|
||||
im = np.array(input)
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
@@ -235,7 +265,7 @@ class ONNXPlugin(
|
||||
|
||||
def predict(input_tensor):
|
||||
compiled_model = self.compiled_models[threading.current_thread().name]
|
||||
output_tensors = compiled_model.run(None, { self.input_name: input_tensor })
|
||||
output_tensors = compiled_model.run(None, {self.input_name: input_tensor})
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output_tensors[0][0])
|
||||
if self.scrypted_yolo_nas:
|
||||
|
||||
@@ -14,11 +14,6 @@ from predict.face_recognize import FaceRecognizeDetection
|
||||
|
||||
|
||||
class ONNXFaceRecognition(FaceRecognizeDetection):
|
||||
def __init__(self, plugin, nativeId: str | None = None):
|
||||
self.plugin = plugin
|
||||
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
def downloadModel(self, model: str):
|
||||
onnxmodel = "best" if "scrypted" in model else model
|
||||
model_version = "v1"
|
||||
|
||||
@@ -14,11 +14,6 @@ from predict.text_recognize import TextRecognition
|
||||
|
||||
|
||||
class ONNXTextRecognition(TextRecognition):
|
||||
def __init__(self, plugin, nativeId: str | None = None):
|
||||
self.plugin = plugin
|
||||
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
def downloadModel(self, model: str):
|
||||
onnxmodel = model
|
||||
model_version = "v4"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
|
||||
numpy==1.26.4
|
||||
|
||||
# uncomment to require cuda 12, but most stuff is still targetting cuda 11.
|
||||
# however, stuff targetted for cuda 11 can still run on cuda 12.
|
||||
# --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
|
||||
@@ -11,4 +8,4 @@ onnxruntime; 'darwin' in sys_platform or platform_machine == 'aarch64'
|
||||
# ort-nightly-gpu==1.17.3.dev20240409002
|
||||
|
||||
Pillow==10.3.0
|
||||
opencv-python==4.10.0.84
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
4
plugins/opencv/.vscode/launch.json
vendored
4
plugins/opencv/.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "${config:scrypted.debugHost}",
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/src",
|
||||
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
|
||||
"remoteRoot": "."
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
9
plugins/opencv/.vscode/settings.json
vendored
9
plugins/opencv/.vscode/settings.json
vendored
@@ -5,16 +5,15 @@
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted",
|
||||
|
||||
// docker installation
|
||||
// "scrypted.debugHost": "koushik-ubuntu",
|
||||
// "scrypted.serverRoot": "/server",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.serverRoot": "/server",
|
||||
|
||||
// local checkout
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "127.0.0.1",
|
||||
// "scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "koushik-windows",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
4
plugins/opencv/package-lock.json
generated
4
plugins/opencv/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.91",
|
||||
"version": "0.0.92",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.91",
|
||||
"version": "0.0.92",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.91"
|
||||
"version": "0.0.92"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
|
||||
numpy==1.26.4
|
||||
imutils>=0.5.0
|
||||
opencv-python==4.10.0.82
|
||||
opencv-python-headless==4.10.0.84
|
||||
Pillow==10.3.0
|
||||
|
||||
18
plugins/openvino/.vscode/settings.json
vendored
18
plugins/openvino/.vscode/settings.json
vendored
@@ -1,24 +1,6 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
// "scrypted.debugHost": "scrypted-demo",
|
||||
// "scrypted.serverRoot": "/server",
|
||||
|
||||
// proxmox installation
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.serverRoot": "/root/.scrypted",
|
||||
|
||||
// pi local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted",
|
||||
|
||||
// local checkout
|
||||
// "scrypted.debugHost": "127.0.0.1",
|
||||
// "scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "koushik-winvm",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
4
plugins/openvino/package-lock.json
generated
4
plugins/openvino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.118",
|
||||
"version": "0.1.137",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.118",
|
||||
"version": "0.1.137",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -35,12 +35,18 @@
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings",
|
||||
"ClusterForkInterface",
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"@scrypted/openvino"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.118"
|
||||
"version": "0.1.137"
|
||||
}
|
||||
|
||||
@@ -33,4 +33,22 @@ async def ensureRGBAData(data: bytes, size: Tuple[int, int], format: str):
|
||||
return rgb.convert('RGBA')
|
||||
finally:
|
||||
rgb.close()
|
||||
return await to_thread(convert)
|
||||
return await to_thread(convert)
|
||||
|
||||
async def ensureYCbCrAData(data: bytes, size: Tuple[int, int], format: str):
|
||||
# if the format is already yuvj444p, just return the data as is.
|
||||
if format == 'yuvj444p':
|
||||
# return RGB as a hack to indicate the data is already yuv planar.
|
||||
return Image.frombuffer('RGB', size, data)
|
||||
|
||||
def convert():
|
||||
if format == 'rgb':
|
||||
tmp = Image.frombuffer('RGB', size, data)
|
||||
else:
|
||||
tmp = Image.frombuffer('RGBA', size, data)
|
||||
|
||||
try:
|
||||
return tmp.convert('YCbCr')
|
||||
finally:
|
||||
tmp.close()
|
||||
return await to_thread(convert)
|
||||
|
||||
@@ -12,11 +12,11 @@ def parse_yolov10(results, threshold = defaultThreshold, scale = None, confidenc
|
||||
for indices in keep:
|
||||
class_id = indices[0]
|
||||
index = indices[1]
|
||||
confidence = results[class_id + 4, index].astype(float)
|
||||
l = results[0][index].astype(float)
|
||||
t = results[1][index].astype(float)
|
||||
r = results[2][index].astype(float)
|
||||
b = results[3][index].astype(float)
|
||||
confidence = results[class_id + 4, index]
|
||||
l = results[0][index]
|
||||
t = results[1][index]
|
||||
r = results[2][index]
|
||||
b = results[3][index]
|
||||
if scale:
|
||||
l = scale(l)
|
||||
t = scale(t)
|
||||
@@ -47,7 +47,7 @@ def parse_yolo_nas(predictions):
|
||||
pred_cls_label = j[:]
|
||||
for box, conf, label in zip(pred_bboxes, pred_cls_conf, pred_cls_label):
|
||||
obj = Prediction(
|
||||
int(label), conf.astype(float), Rectangle(box[0].astype(float), box[1].astype(float), box[2].astype(float), box[3].astype(float))
|
||||
int(label), conf, Rectangle(box[0], box[1], box[2], box[3])
|
||||
)
|
||||
objs.append(obj)
|
||||
return objs
|
||||
@@ -58,11 +58,11 @@ def parse_yolov9(results, threshold = defaultThreshold, scale = None, confidence
|
||||
for indices in keep:
|
||||
class_id = indices[0]
|
||||
index = indices[1]
|
||||
confidence = results[class_id + 4, index].astype(float)
|
||||
x = results[0][index].astype(float)
|
||||
y = results[1][index].astype(float)
|
||||
w = results[2][index].astype(float)
|
||||
h = results[3][index].astype(float)
|
||||
confidence = results[class_id + 4, index]
|
||||
x = results[0][index]
|
||||
y = results[1][index]
|
||||
w = results[2][index]
|
||||
h = results[3][index]
|
||||
if scale:
|
||||
x = scale(x)
|
||||
y = scale(y)
|
||||
@@ -190,12 +190,12 @@ def parse_yolo_region(blob, original_im_shape, anchors, sigmoid = True):
|
||||
ymax = y + height /2
|
||||
objects.append(
|
||||
{
|
||||
'xmin': xmin.astype(float),
|
||||
'xmax': xmax.astype(float),
|
||||
'ymin': ymin.astype(float),
|
||||
'ymax': ymax.astype(float),
|
||||
'confidence': confidence.astype(float),
|
||||
'classId': class_id.astype(float),
|
||||
'xmin': xmin,
|
||||
'xmax': xmax,
|
||||
'ymin': ymin,
|
||||
'ymax': ymax,
|
||||
'confidence': confidence,
|
||||
'classId': class_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -4,13 +4,22 @@ import asyncio
|
||||
from typing import Any, Tuple
|
||||
|
||||
import scrypted_sdk
|
||||
from scrypted_sdk.types import (MediaObject, ObjectDetection,
|
||||
ObjectDetectionGeneratorSession,
|
||||
ObjectDetectionModel, ObjectDetectionSession,
|
||||
ObjectsDetected, ScryptedMimeTypes, Setting)
|
||||
from scrypted_sdk.types import (
|
||||
MediaObject,
|
||||
ObjectDetection,
|
||||
ObjectDetectionGeneratorSession,
|
||||
ObjectDetectionModel,
|
||||
ObjectDetectionSession,
|
||||
ObjectsDetected,
|
||||
ScryptedMimeTypes,
|
||||
Setting,
|
||||
)
|
||||
|
||||
|
||||
class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
|
||||
class DetectPlugin(
|
||||
scrypted_sdk.ScryptedDeviceBase,
|
||||
ObjectDetection,
|
||||
):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
@@ -33,47 +42,55 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
|
||||
|
||||
async def getDetectionModel(self, settings: Any = None) -> ObjectDetectionModel:
|
||||
d: ObjectDetectionModel = {
|
||||
'name': self.modelName,
|
||||
'classes': self.getClasses(),
|
||||
'triggerClasses': self.getTriggerClasses(),
|
||||
'inputSize': self.get_input_details(),
|
||||
'inputFormat': self.get_input_format(),
|
||||
'settings': [],
|
||||
"name": self.modelName,
|
||||
"classes": self.getClasses(),
|
||||
"triggerClasses": self.getTriggerClasses(),
|
||||
"inputSize": self.get_input_details(),
|
||||
"inputFormat": self.get_input_format(),
|
||||
"settings": [],
|
||||
}
|
||||
|
||||
d['settings'] += self.getModelSettings(settings)
|
||||
d["settings"] += self.getModelSettings(settings)
|
||||
|
||||
return d
|
||||
|
||||
def get_detection_input_size(self, src_size):
|
||||
pass
|
||||
|
||||
async def run_detection_image(self, videoFrame: scrypted_sdk.Image, detection_session: ObjectDetectionSession) -> ObjectsDetected:
|
||||
async def run_detection_image(
|
||||
self, videoFrame: scrypted_sdk.Image, detection_session: ObjectDetectionSession
|
||||
) -> ObjectsDetected:
|
||||
pass
|
||||
|
||||
async def generateObjectDetections(self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None) -> Any:
|
||||
|
||||
async def generateObjectDetections(
|
||||
self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None
|
||||
) -> Any:
|
||||
try:
|
||||
videoFrames = await scrypted_sdk.sdk.connectRPCObject(videoFrames)
|
||||
videoFrame: scrypted_sdk.VideoFrame
|
||||
async for videoFrame in videoFrames:
|
||||
image = await scrypted_sdk.sdk.connectRPCObject(videoFrame['image'])
|
||||
detected = await self.run_detection_image(image, session)
|
||||
yield {
|
||||
'__json_copy_serialize_children': True,
|
||||
'detected': detected,
|
||||
'videoFrame': videoFrame,
|
||||
}
|
||||
image = await scrypted_sdk.sdk.connectRPCObject(videoFrame["image"])
|
||||
detected = await self.run_detection_image(image, session)
|
||||
yield {
|
||||
"__json_copy_serialize_children": True,
|
||||
"detected": detected,
|
||||
"videoFrame": videoFrame,
|
||||
}
|
||||
finally:
|
||||
try:
|
||||
await videoFrames.aclose()
|
||||
except:
|
||||
pass
|
||||
|
||||
async def detectObjects(self, mediaObject: MediaObject, session: ObjectDetectionSession = None) -> ObjectsDetected:
|
||||
async def detectObjects(
|
||||
self, mediaObject: MediaObject, session: ObjectDetectionSession = None
|
||||
) -> ObjectsDetected:
|
||||
image: scrypted_sdk.Image
|
||||
if mediaObject.mimeType == ScryptedMimeTypes.Image.value:
|
||||
image = await scrypted_sdk.sdk.connectRPCObject(mediaObject)
|
||||
else:
|
||||
image = await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(mediaObject, ScryptedMimeTypes.Image.value)
|
||||
image = await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(
|
||||
mediaObject, ScryptedMimeTypes.Image.value
|
||||
)
|
||||
|
||||
return await self.run_detection_image(image, session)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from ov import OpenVINOPlugin
|
||||
import predict
|
||||
|
||||
def create_scrypted_plugin():
|
||||
return OpenVINOPlugin()
|
||||
|
||||
async def fork():
|
||||
return predict.Fork(OpenVINOPlugin)
|
||||
|
||||
@@ -30,6 +30,7 @@ prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "OpenVINO-Prepare")
|
||||
|
||||
availableModels = [
|
||||
"Default",
|
||||
"scrypted_yolov9c_relu_int8_320",
|
||||
"scrypted_yolov10m_320",
|
||||
"scrypted_yolov10s_320",
|
||||
"scrypted_yolov10n_320",
|
||||
@@ -37,6 +38,7 @@ availableModels = [
|
||||
"scrypted_yolov6n_320",
|
||||
"scrypted_yolov6s_320",
|
||||
"scrypted_yolov9c_320",
|
||||
"scrypted_yolov9m_320",
|
||||
"scrypted_yolov9s_320",
|
||||
"scrypted_yolov9t_320",
|
||||
"scrypted_yolov8n_320",
|
||||
@@ -46,6 +48,7 @@ availableModels = [
|
||||
"yolo-v4-tiny-tf",
|
||||
]
|
||||
|
||||
|
||||
def parse_label_contents(contents: str):
|
||||
lines = contents.splitlines()
|
||||
lines = [line for line in lines if line.strip()]
|
||||
@@ -87,10 +90,13 @@ def dump_device_properties(core):
|
||||
|
||||
|
||||
class OpenVINOPlugin(
|
||||
PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider
|
||||
PredictPlugin,
|
||||
scrypted_sdk.BufferConverter,
|
||||
scrypted_sdk.Settings,
|
||||
scrypted_sdk.DeviceProvider,
|
||||
):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, nativeId: str | None = None, forked: bool = False):
|
||||
super().__init__(nativeId=nativeId, forked=forked)
|
||||
|
||||
self.core = ov.Core()
|
||||
dump_device_properties(self.core)
|
||||
@@ -158,16 +164,22 @@ class OpenVINOPlugin(
|
||||
else:
|
||||
model = "scrypted_yolov9t_320"
|
||||
self.yolo = "yolo" in model
|
||||
self.scrypted_yolov9 = "scrypted_yolov9" in model
|
||||
self.scrypted_yolov10 = "scrypted_yolov10" in model
|
||||
self.scrypted_yolo_nas = "scrypted_yolo_nas" in model
|
||||
self.scrypted_yolo = "scrypted_yolo" in model
|
||||
self.scrypted_model = "scrypted" in model
|
||||
self.scrypted_yuv = "yuv" in model
|
||||
self.sigmoid = model == "yolo-v4-tiny-tf"
|
||||
self.modelName = model
|
||||
|
||||
ovmodel = "best" if self.scrypted_model else model
|
||||
ovmodel = (
|
||||
"best-converted"
|
||||
if self.scrypted_yolov9
|
||||
else "best" if self.scrypted_model else model
|
||||
)
|
||||
|
||||
model_version = "v5"
|
||||
model_version = "v7"
|
||||
xmlFile = self.downloadFile(
|
||||
f"https://github.com/koush/openvino-models/raw/main/{model}/{precision}/{ovmodel}.xml",
|
||||
f"{model_version}/{model}/{precision}/{ovmodel}.xml",
|
||||
@@ -203,11 +215,12 @@ class OpenVINOPlugin(
|
||||
self.compiled_model = self.core.compile_model(xmlFile, mode)
|
||||
except:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
if mode == "GPU":
|
||||
if "GPU" in mode:
|
||||
try:
|
||||
print("GPU mode failed, reverting to AUTO.")
|
||||
print(f"{mode} mode failed, reverting to AUTO.")
|
||||
mode = "AUTO"
|
||||
self.mode = mode
|
||||
self.compiled_model = self.core.compile_model(xmlFile, mode)
|
||||
@@ -222,7 +235,7 @@ class OpenVINOPlugin(
|
||||
"EXECUTION_DEVICES",
|
||||
self.compiled_model.get_property("EXECUTION_DEVICES"),
|
||||
)
|
||||
print(f"model/mode/precision: {model}/{mode}/{precision}")
|
||||
print(f"model/mode: {model}/{mode}")
|
||||
|
||||
# mobilenet 1,300,300,3
|
||||
# yolov3/4 1,416,416,3
|
||||
@@ -235,7 +248,9 @@ class OpenVINOPlugin(
|
||||
|
||||
self.faceDevice = None
|
||||
self.textDevice = None
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
if not self.forked:
|
||||
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
mode = self.storage.getItem("mode") or "Default"
|
||||
@@ -283,6 +298,11 @@ class OpenVINOPlugin(
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
return [self.model_dim, self.model_dim]
|
||||
|
||||
def get_input_format(self):
|
||||
if self.scrypted_yuv:
|
||||
return "yuvj444p"
|
||||
return super().get_input_format()
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def predict(input_tensor):
|
||||
infer_request = self.compiled_model.create_infer_request()
|
||||
@@ -329,7 +349,7 @@ class OpenVINOPlugin(
|
||||
return objs
|
||||
|
||||
output = infer_request.get_output_tensor(0)
|
||||
for values in output.data[0][0].astype(float):
|
||||
for values in output.data[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
@@ -347,14 +367,24 @@ class OpenVINOPlugin(
|
||||
|
||||
return objs
|
||||
|
||||
|
||||
def prepare():
|
||||
def prepare():
|
||||
# the input_tensor can be created with the shared_memory=True parameter,
|
||||
# but that seems to cause issues on some platforms.
|
||||
if self.scrypted_yolo:
|
||||
im = np.array(input)
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
if not self.scrypted_yuv:
|
||||
im = np.expand_dims(input, axis=0)
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
else:
|
||||
# when a yuv image is requested, it may be either planar or interleaved
|
||||
# as as hack, the input will come as RGB if already planar.
|
||||
if input.mode != "RGB":
|
||||
im = np.array(input)
|
||||
im = im.reshape((1, self.model_dim, self.model_dim, 3))
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
|
||||
else:
|
||||
im = np.array(input)
|
||||
im = im.reshape((1, 3, self.model_dim, self.model_dim))
|
||||
im = im.astype(np.float32) / 255.0
|
||||
im = np.ascontiguousarray(im) # contiguous
|
||||
input_tensor = ov.Tensor(array=im)
|
||||
@@ -388,6 +418,7 @@ class OpenVINOPlugin(
|
||||
"nativeId": "facerecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "OpenVINO Face Recognition",
|
||||
@@ -400,6 +431,7 @@ class OpenVINOPlugin(
|
||||
"nativeId": "textrecognition",
|
||||
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
|
||||
"interfaces": [
|
||||
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
|
||||
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
|
||||
],
|
||||
"name": "OpenVINO Text Recognition",
|
||||
|
||||
@@ -16,15 +16,11 @@ faceRecognizePrepare, faceRecognizePredict = async_infer.create_executors(
|
||||
|
||||
|
||||
class OpenVINOFaceRecognition(FaceRecognizeDetection):
|
||||
def __init__(self, plugin, nativeId: str | None = None):
|
||||
self.plugin = plugin
|
||||
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
def downloadModel(self, model: str):
|
||||
ovmodel = "best"
|
||||
scrypted_yolov9 = "scrypted_yolov9" in model
|
||||
ovmodel = "best-converted" if scrypted_yolov9 else "best"
|
||||
precision = self.plugin.precision
|
||||
model_version = "v5"
|
||||
model_version = "v7"
|
||||
xmlFile = self.downloadFile(
|
||||
f"https://github.com/koush/openvino-models/raw/main/{model}/{precision}/{ovmodel}.xml",
|
||||
f"{model_version}/{model}/{precision}/{ovmodel}.xml",
|
||||
|
||||
@@ -15,11 +15,6 @@ textRecognizePrepare, textRecognizePredict = async_infer.create_executors(
|
||||
|
||||
|
||||
class OpenVINOTextRecognition(TextRecognition):
|
||||
def __init__(self, plugin, nativeId: str | None = None):
|
||||
self.plugin = plugin
|
||||
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
def downloadModel(self, model: str):
|
||||
ovmodel = "best"
|
||||
precision = self.plugin.precision
|
||||
|
||||
@@ -2,48 +2,77 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import traceback
|
||||
import urllib.request
|
||||
from typing import Any, List, Tuple
|
||||
from typing import Any, List, Tuple, Mapping
|
||||
|
||||
import scrypted_sdk
|
||||
from PIL import Image
|
||||
from scrypted_sdk.types import (ObjectDetectionResult, ObjectDetectionSession,
|
||||
ObjectsDetected, Setting)
|
||||
from scrypted_sdk.types import (
|
||||
ObjectDetectionResult,
|
||||
ObjectDetectionSession,
|
||||
ObjectsDetected,
|
||||
Setting,
|
||||
)
|
||||
|
||||
import common.colors
|
||||
from detect import DetectPlugin
|
||||
from predict.rectangle import Rectangle
|
||||
|
||||
|
||||
class Prediction:
|
||||
def __init__(self, id: int, score: float, bbox: Tuple[float, float, float, float], embedding: str = None):
|
||||
self.id = id
|
||||
self.score = score
|
||||
self.bbox = bbox
|
||||
def __init__(self, id: int, score: float, bbox: Rectangle, embedding: str = None):
|
||||
# these may be numpy values. sanitize them.
|
||||
self.id = int(id)
|
||||
self.score = float(score)
|
||||
# ensure all floats from numpy
|
||||
self.bbox = Rectangle(
|
||||
float(bbox.xmin),
|
||||
float(bbox.ymin),
|
||||
float(bbox.xmax),
|
||||
float(bbox.ymax),
|
||||
)
|
||||
self.embedding = embedding
|
||||
|
||||
class PredictPlugin(DetectPlugin):
|
||||
|
||||
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
|
||||
labels: dict
|
||||
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
plugin: PredictPlugin = None,
|
||||
nativeId: str | None = None,
|
||||
forked: bool = False,
|
||||
):
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
self.plugin = plugin
|
||||
# self.clusterIndex = 0
|
||||
|
||||
# periodic restart of main plugin because there seems to be leaks in tflite or coral API.
|
||||
if not nativeId:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.call_later(4 * 60 * 60, lambda: self.requestRestart())
|
||||
|
||||
|
||||
self.batch: List[Tuple[Any, asyncio.Future]] = []
|
||||
self.batching = 0
|
||||
self.batch_flush = None
|
||||
|
||||
self.forked = forked
|
||||
if not self.forked:
|
||||
self.forks: Mapping[str, scrypted_sdk.PluginFork] = {}
|
||||
|
||||
if not self.plugin and not self.forked:
|
||||
asyncio.ensure_future(self.startCluster(), loop=self.loop)
|
||||
|
||||
def downloadFile(self, url: str, filename: str):
|
||||
try:
|
||||
filesPath = os.path.join(os.environ['SCRYPTED_PLUGIN_VOLUME'], 'files')
|
||||
filesPath = os.path.join(os.environ["SCRYPTED_PLUGIN_VOLUME"], "files")
|
||||
fullpath = os.path.join(filesPath, filename)
|
||||
if os.path.isfile(fullpath):
|
||||
return fullpath
|
||||
tmp = fullpath + '.tmp'
|
||||
tmp = fullpath + ".tmp"
|
||||
print("Creating directory for", tmp)
|
||||
os.makedirs(os.path.dirname(fullpath), exist_ok=True)
|
||||
print("Downloading", url)
|
||||
@@ -64,6 +93,7 @@ class PredictPlugin(DetectPlugin):
|
||||
except:
|
||||
print("Error downloading", url)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
@@ -71,7 +101,7 @@ class PredictPlugin(DetectPlugin):
|
||||
return list(self.labels.values())
|
||||
|
||||
def getTriggerClasses(self) -> list[str]:
|
||||
return ['motion']
|
||||
return ["motion"]
|
||||
|
||||
def requestRestart(self):
|
||||
asyncio.ensure_future(scrypted_sdk.deviceManager.requestRestart())
|
||||
@@ -84,35 +114,47 @@ class PredictPlugin(DetectPlugin):
|
||||
return []
|
||||
|
||||
def get_input_format(self) -> str:
|
||||
return 'rgb'
|
||||
return "rgb"
|
||||
|
||||
def create_detection_result(self, objs: List[Prediction], size, convert_to_src_size=None) -> ObjectsDetected:
|
||||
def create_detection_result(
|
||||
self, objs: List[Prediction], size, convert_to_src_size=None
|
||||
) -> ObjectsDetected:
|
||||
detections: List[ObjectDetectionResult] = []
|
||||
detection_result: ObjectsDetected = {}
|
||||
detection_result['detections'] = detections
|
||||
detection_result['inputDimensions'] = size
|
||||
detection_result["detections"] = detections
|
||||
detection_result["inputDimensions"] = size
|
||||
|
||||
for obj in objs:
|
||||
className = self.labels.get(obj.id, obj.id)
|
||||
detection: ObjectDetectionResult = {}
|
||||
detection['boundingBox'] = (
|
||||
obj.bbox.xmin, obj.bbox.ymin, obj.bbox.xmax - obj.bbox.xmin, obj.bbox.ymax - obj.bbox.ymin)
|
||||
detection['className'] = className
|
||||
detection['score'] = obj.score
|
||||
if hasattr(obj, 'embedding') and obj.embedding is not None:
|
||||
detection['embedding'] = obj.embedding
|
||||
detection["boundingBox"] = (
|
||||
obj.bbox.xmin,
|
||||
obj.bbox.ymin,
|
||||
obj.bbox.xmax - obj.bbox.xmin,
|
||||
obj.bbox.ymax - obj.bbox.ymin,
|
||||
)
|
||||
# check bounding box for nan
|
||||
if any(map(lambda x: not math.isfinite(x), detection["boundingBox"])):
|
||||
print("unexpected nan detected", obj.bbox)
|
||||
continue
|
||||
detection["className"] = className
|
||||
detection["score"] = obj.score
|
||||
if hasattr(obj, "embedding") and obj.embedding is not None:
|
||||
detection["embedding"] = obj.embedding
|
||||
detections.append(detection)
|
||||
|
||||
if convert_to_src_size:
|
||||
detections = detection_result['detections']
|
||||
detection_result['detections'] = []
|
||||
detections = detection_result["detections"]
|
||||
detection_result["detections"] = []
|
||||
for detection in detections:
|
||||
bb = detection['boundingBox']
|
||||
bb = detection["boundingBox"]
|
||||
x, y = convert_to_src_size((bb[0], bb[1]))
|
||||
x2, y2 = convert_to_src_size(
|
||||
(bb[0] + bb[2], bb[1] + bb[3]))
|
||||
detection['boundingBox'] = (x, y, x2 - x + 1, y2 - y + 1)
|
||||
detection_result['detections'].append(detection)
|
||||
x2, y2 = convert_to_src_size((bb[0] + bb[2], bb[1] + bb[3]))
|
||||
detection["boundingBox"] = (x, y, x2 - x + 1, y2 - y + 1)
|
||||
if any(map(lambda x: not math.isfinite(x), detection["boundingBox"])):
|
||||
print("unexpected nan detected", obj.bbox)
|
||||
continue
|
||||
detection_result["detections"].append(detection)
|
||||
|
||||
# print(detection_result)
|
||||
return detection_result
|
||||
@@ -127,7 +169,9 @@ class PredictPlugin(DetectPlugin):
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
pass
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
|
||||
async def detect_once(
|
||||
self, input: Image.Image, settings: Any, src_size, cvss
|
||||
) -> ObjectsDetected:
|
||||
pass
|
||||
|
||||
async def detect_batch(self, inputs: List[Any]) -> List[Any]:
|
||||
@@ -153,33 +197,62 @@ class PredictPlugin(DetectPlugin):
|
||||
await self.run_batch()
|
||||
|
||||
async def queue_batch(self, input: Any) -> List[Any]:
|
||||
future = asyncio.Future(loop = asyncio.get_event_loop())
|
||||
future = asyncio.Future(loop=asyncio.get_event_loop())
|
||||
self.batch.append((input, future))
|
||||
if self.batching:
|
||||
self.batching = self.batching - 1
|
||||
if self.batching:
|
||||
# if there is any sort of error or backlog, .
|
||||
if not self.batch_flush:
|
||||
self.batch_flush = self.loop.call_later(.5, lambda: asyncio.ensure_future(self.flush_batch()))
|
||||
self.batch_flush = self.loop.call_later(
|
||||
0.5, lambda: asyncio.ensure_future(self.flush_batch())
|
||||
)
|
||||
return await future
|
||||
await self.run_batch()
|
||||
return await future
|
||||
|
||||
async def safe_detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
|
||||
async def safe_detect_once(
|
||||
self, input: Image.Image, settings: Any, src_size, cvss
|
||||
) -> ObjectsDetected:
|
||||
try:
|
||||
f = self.detect_once(input, settings, src_size, cvss)
|
||||
return await asyncio.wait_for(f, 60)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
print(
|
||||
"encountered an error while detecting. requesting plugin restart."
|
||||
)
|
||||
print("encountered an error while detecting. requesting plugin restart.")
|
||||
self.requestRestart()
|
||||
raise
|
||||
|
||||
async def run_detection_image(self, image: scrypted_sdk.Image, detection_session: ObjectDetectionSession) -> ObjectsDetected:
|
||||
settings = detection_session and detection_session.get('settings')
|
||||
batch = (detection_session and detection_session.get('batch')) or 0
|
||||
# async def detectObjects(
|
||||
# self, mediaObject: scrypted_sdk.MediaObject, session: ObjectDetectionSession = None
|
||||
# ) -> ObjectsDetected:
|
||||
# # main plugin can dispatch
|
||||
# plugin: PredictPlugin = None
|
||||
# if scrypted_sdk.clusterManager and scrypted_sdk.clusterManager.getClusterMode() and not self.forked:
|
||||
# if session:
|
||||
# del session['batch']
|
||||
# if len(self.forks):
|
||||
# totalWorkers = len(self.forks)
|
||||
# if not self.forked:
|
||||
# totalWorkers += 1
|
||||
|
||||
# self.clusterIndex += 1
|
||||
# self.clusterIndex %= totalWorkers
|
||||
# if len(self.forks) != self.clusterIndex:
|
||||
# fork = list(self.forks.values())[self.clusterIndex]
|
||||
# result = await fork.result
|
||||
# plugin = await result.getPlugin()
|
||||
|
||||
# if not plugin:
|
||||
# return await super().detectObjects(mediaObject, session)
|
||||
|
||||
# return await plugin.detectObjects(mediaObject, session)
|
||||
|
||||
async def run_detection_image(
|
||||
self, image: scrypted_sdk.Image, detection_session: ObjectDetectionSession
|
||||
) -> ObjectsDetected:
|
||||
settings = detection_session and detection_session.get("settings")
|
||||
batch = (detection_session and detection_session.get("batch")) or 0
|
||||
self.batching += batch
|
||||
|
||||
iw, ih = image.width, image.height
|
||||
@@ -189,34 +262,142 @@ class PredictPlugin(DetectPlugin):
|
||||
resize = None
|
||||
w = image.width
|
||||
h = image.height
|
||||
|
||||
def cvss(point):
|
||||
return point
|
||||
|
||||
else:
|
||||
resize = None
|
||||
xs = w / iw
|
||||
ys = h / ih
|
||||
|
||||
def cvss(point):
|
||||
return point[0] / xs, point[1] / ys
|
||||
|
||||
if iw != w or ih != h:
|
||||
resize = {
|
||||
'width': w,
|
||||
'height': h,
|
||||
"width": w,
|
||||
"height": h,
|
||||
}
|
||||
|
||||
format = image.format or self.get_input_format()
|
||||
b = await image.toBuffer({
|
||||
'resize': resize,
|
||||
'format': format,
|
||||
})
|
||||
|
||||
if self.get_input_format() == 'rgb':
|
||||
# if the model requires yuvj444p, convert the image to yuvj444p directly
|
||||
# if possible, otherwise use whatever is available and convert in the detection plugin
|
||||
if self.get_input_format() == "yuvj444p":
|
||||
if image.ffmpegFormats != True:
|
||||
format = image.format or "rgb"
|
||||
|
||||
b = await image.toBuffer(
|
||||
{
|
||||
"resize": resize,
|
||||
"format": format,
|
||||
}
|
||||
)
|
||||
|
||||
if self.get_input_format() == "rgb":
|
||||
data = await common.colors.ensureRGBData(b, (w, h), format)
|
||||
elif self.get_input_format() == 'rgba':
|
||||
elif self.get_input_format() == "rgba":
|
||||
data = await common.colors.ensureRGBAData(b, (w, h), format)
|
||||
elif self.get_input_format() == "yuvj444p":
|
||||
data = await common.colors.ensureYCbCrAData(b, (w, h), format)
|
||||
else:
|
||||
raise Exception("unsupported format")
|
||||
|
||||
try:
|
||||
ret = await self.safe_detect_once(data, settings, (iw, ih), cvss)
|
||||
return ret
|
||||
finally:
|
||||
data.close()
|
||||
|
||||
async def forkInterfaceInternal(self, options: dict):
|
||||
if self.plugin:
|
||||
return await self.plugin.forkInterfaceInternal(options)
|
||||
clusterWorkerId = options.get("clusterWorkerId", None)
|
||||
|
||||
if not clusterWorkerId:
|
||||
raise Exception("clusterWorkerId required")
|
||||
|
||||
if self.forked:
|
||||
raise Exception("cannot fork a fork")
|
||||
|
||||
forked = self.forks.get(clusterWorkerId, None)
|
||||
if not forked:
|
||||
forked = scrypted_sdk.fork(
|
||||
{"labels": {"require": [self.pluginId]}, **(options or {})}
|
||||
)
|
||||
|
||||
def clusterWorkerExit(result):
|
||||
print("cluster worker exit", clusterWorkerId)
|
||||
self.forks.pop(clusterWorkerId)
|
||||
|
||||
forked.exit.add_done_callback(clusterWorkerExit)
|
||||
self.forks[clusterWorkerId] = forked
|
||||
|
||||
result = await forked.result
|
||||
return result
|
||||
|
||||
async def forkInterface(self, forkInterface, options: dict = None):
|
||||
if forkInterface != scrypted_sdk.ScryptedInterface.ObjectDetection.value:
|
||||
raise Exception("unsupported fork interface")
|
||||
|
||||
result = await self.forkInterfaceInternal(options)
|
||||
if not self.nativeId:
|
||||
ret = await result.getPlugin()
|
||||
elif self.nativeId == "textrecognition":
|
||||
ret = await result.getTextRecognition()
|
||||
elif self.nativeId == "facerecognition":
|
||||
ret = await result.getFaceRecognition()
|
||||
return ret
|
||||
|
||||
async def startCluster(self):
|
||||
try:
|
||||
clusterManager = scrypted_sdk.clusterManager
|
||||
if not clusterManager:
|
||||
return
|
||||
workers = await clusterManager.getClusterWorkers()
|
||||
thisClusterWorkerId = clusterManager.getClusterWorkerId()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
for cwid in workers:
|
||||
if cwid == thisClusterWorkerId:
|
||||
selfFork = Fork(None)
|
||||
selfFork.plugin = self
|
||||
|
||||
pf = scrypted_sdk.PluginFork()
|
||||
pf.result = asyncio.Future(loop=self.loop)
|
||||
pf.result.set_result(selfFork)
|
||||
|
||||
self.forks[cwid] = pf
|
||||
continue
|
||||
|
||||
async def startClusterWorker(clusterWorkerId=cwid):
|
||||
print("starting cluster worker", clusterWorkerId)
|
||||
try:
|
||||
await self.forkInterfaceInternal(
|
||||
{"clusterWorkerId": clusterWorkerId}
|
||||
)
|
||||
except:
|
||||
# traceback.print_exc()
|
||||
pass
|
||||
|
||||
asyncio.ensure_future(startClusterWorker(), loop=self.loop)
|
||||
|
||||
|
||||
class Fork:
|
||||
def __init__(self, PluginType: Any):
|
||||
if PluginType:
|
||||
self.plugin = PluginType(forked=True)
|
||||
else:
|
||||
self.plugin = None
|
||||
|
||||
async def getPlugin(self):
|
||||
return self.plugin
|
||||
|
||||
async def getTextRecognition(self):
|
||||
return await self.plugin.getDevice("textrecognition")
|
||||
|
||||
async def getFaceRecognition(self):
|
||||
return await self.plugin.getDevice("facerecognition")
|
||||
|
||||
@@ -23,8 +23,8 @@ def cosine_similarity(vector_a, vector_b):
|
||||
return similarity
|
||||
|
||||
class FaceRecognizeDetection(PredictPlugin):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, plugin: PredictPlugin, nativeId: str):
|
||||
super().__init__(nativeId=nativeId, plugin=plugin)
|
||||
|
||||
self.inputheight = 320
|
||||
self.inputwidth = 320
|
||||
|
||||
@@ -23,8 +23,8 @@ predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "TextDetect")
|
||||
|
||||
|
||||
class TextRecognition(PredictPlugin):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
def __init__(self, plugin: PredictPlugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
|
||||
self.inputheight = 640
|
||||
self.inputwidth = 640
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
|
||||
numpy==1.26.4
|
||||
# openvino 2024.3.0 crashes on older CPU (J4105 and older) if level-zero is installed via apt.
|
||||
# openvino 2024.2.0 and older crashes on arc dGPU.
|
||||
openvino==2024.4.0
|
||||
Pillow==10.3.0
|
||||
opencv-python==4.10.0.84
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
4
plugins/prebuffer-mixin/package-lock.json
generated
4
plugins/prebuffer-mixin/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.38",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.38",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.36",
|
||||
"version": "0.10.38",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { createActivityTimeout } from '@scrypted/common/src/activity-timeout';
|
||||
import { cloneDeep } from '@scrypted/common/src/clone-deep';
|
||||
import { Deferred } from "@scrypted/common/src/deferred";
|
||||
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
|
||||
import { createActivityTimeout } from '@scrypted/common/src/activity-timeout';
|
||||
import { createRtspParser } from "@scrypted/common/src/rtsp-server";
|
||||
import { parseSdp } from "@scrypted/common/src/sdp-utils";
|
||||
import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
||||
import sdk, { FFmpegInput, RequestMediaStreamOptions, ResponseMediaStreamOptions } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess, StdioOptions } from 'child_process';
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import net from 'net';
|
||||
import sdk from '@scrypted/sdk';
|
||||
|
||||
export async function getUrlLocalAdresses(console: Console, url: string) {
|
||||
let urls: string[];
|
||||
try {
|
||||
const addresses = await sdk.endpointManager.getLocalAddresses();
|
||||
if (addresses) {
|
||||
const [address] = addresses;
|
||||
if (address) {
|
||||
const u = new URL(url);
|
||||
u.hostname = address;
|
||||
url = u.toString();
|
||||
}
|
||||
urls = addresses.map(address => {
|
||||
const u = new URL(url);
|
||||
u.hostname = address;
|
||||
return u.toString();
|
||||
});
|
||||
}
|
||||
if (!addresses)
|
||||
return;
|
||||
const urls = addresses.map(address => {
|
||||
const u = new URL(url);
|
||||
u.hostname = net.isIPv6(address) ? `[${address}]` : address;
|
||||
return u.toString();
|
||||
});
|
||||
return urls;
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Error determining external addresses. Is Scrypted Server Address configured?', e);
|
||||
return
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/commo
|
||||
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
|
||||
import { ListenZeroSingleClientTimeoutError, closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
import { H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_B, H265_NAL_TYPE_SPS, RtspServer, RtspTrack, createRtspParser, findH264NaluType, findH265NaluType, getNaluTypes, listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
|
||||
import { H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_B, RtspServer, RtspTrack, createRtspParser, findH264NaluType, getNaluTypes, listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
|
||||
import { addTrackControls, getSpsPps, parseSdp } from '@scrypted/common/src/sdp-utils';
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
@@ -15,9 +15,7 @@ import { once } from 'events';
|
||||
import { parse as h264SpsParse } from "h264-sps-parser";
|
||||
import net, { AddressInfo } from 'net';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import { Duplex } from 'stream';
|
||||
import { Worker } from 'worker_threads';
|
||||
import { ParserOptions, ParserSession, startParserSession } from './ffmpeg-rebroadcast';
|
||||
import { FileRtspServer } from './file-rtsp-server';
|
||||
import { getUrlLocalAdresses } from './local-addresses';
|
||||
@@ -196,13 +194,33 @@ class PrebufferSession {
|
||||
return;
|
||||
this.console.log(this.streamName, 'prebuffer session started');
|
||||
this.parserSessionPromise = this.startPrebufferSession();
|
||||
this.parserSessionPromise.then(pso => pso.killed.finally(() => {
|
||||
this.console.error(this.streamName, 'prebuffer session ended');
|
||||
this.parserSessionPromise = undefined;
|
||||
}))
|
||||
let active = false;
|
||||
this.parserSessionPromise.then(pso => {
|
||||
pso.once('rtsp', () => {
|
||||
active = true;
|
||||
if (!this.mixin.online)
|
||||
this.mixin.online = true;
|
||||
});
|
||||
|
||||
pso.killed.finally(() => {
|
||||
this.console.error(this.streamName, 'prebuffer session ended');
|
||||
this.parserSessionPromise = undefined;
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
this.console.error(this.streamName, 'prebuffer session ended with error', e);
|
||||
this.parserSessionPromise = undefined;
|
||||
|
||||
if (!active) {
|
||||
// find sessions that arent this one, and check their prebuffers to see if any data has been received.
|
||||
// if there's no data, then consider this camera offline.
|
||||
const others = [...this.mixin.sessions.values()].filter(s => s !== this);
|
||||
if (others.length) {
|
||||
const hasData = others.some(s => s.rtspPrebuffer.length);
|
||||
if (!hasData && this.mixin.online)
|
||||
this.mixin.online = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1415,11 +1433,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledIds.length)
|
||||
this.online = true;
|
||||
|
||||
let active = 0;
|
||||
|
||||
// figure out the default stream and streams that may have been removed due to
|
||||
// a config change.
|
||||
const toRemove = new Set(this.sessions.keys());
|
||||
@@ -1462,23 +1475,13 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
session.ensurePrebufferSession();
|
||||
let wasActive = false;
|
||||
try {
|
||||
this.console.log(name, 'prebuffer session starting');
|
||||
const ps = await session.parserSessionPromise;
|
||||
active++;
|
||||
wasActive = true;
|
||||
this.online = !!active;
|
||||
await ps.killed;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
finally {
|
||||
if (wasActive)
|
||||
active--;
|
||||
wasActive = false;
|
||||
this.online = !!active;
|
||||
}
|
||||
this.console.log(this.name, 'restarting prebuffer session in 5 seconds');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
|
||||
13
plugins/python-codecs/.vscode/launch.json
vendored
13
plugins/python-codecs/.vscode/launch.json
vendored
@@ -21,21 +21,10 @@
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/src",
|
||||
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
|
||||
"remoteRoot": "."
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Python: Test",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/test.py",
|
||||
"console": "internalConsole",
|
||||
"justMyCode": true,
|
||||
"env": {
|
||||
"GST_PLUGIN_PATH": "/opt/homebrew/lib/gstreamer-1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
plugins/python-codecs/.vscode/settings.json
vendored
25
plugins/python-codecs/.vscode/settings.json
vendored
@@ -1,31 +1,20 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
// "scrypted.debugHost": "scrypted-server",
|
||||
// "scrypted.serverRoot": "/server",
|
||||
|
||||
// lxc installation
|
||||
// "scrypted.debugHost": "scrypted-server",
|
||||
// "scrypted.serverRoot": "/root/.scrypted",
|
||||
|
||||
// windows installation
|
||||
// "scrypted.debugHost": "koushik-windows",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.serverRoot": "/server",
|
||||
|
||||
// pi local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted",
|
||||
|
||||
// local checkout
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "127.0.0.1",
|
||||
// "scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "koushik-winvm",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
],
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
},
|
||||
"python.formatting.provider": "none"
|
||||
]
|
||||
}
|
||||
4
plugins/python-codecs/package-lock.json
generated
4
plugins/python-codecs/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.96",
|
||||
"version": "0.1.97",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.96",
|
||||
"version": "0.1.97",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/python-codecs",
|
||||
"version": "0.1.96",
|
||||
"version": "0.1.97",
|
||||
"description": "Python Codecs for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
|
||||
@@ -3,7 +3,4 @@ numpy>=1.16.2
|
||||
|
||||
av>=10.0.0
|
||||
|
||||
# in case pyvips fails to load, use a pillow fallback.
|
||||
# pillow for anything not intel linux, pillow-simd is available on x64 linux
|
||||
Pillow>=5.4.1; 'linux' not in sys_platform or platform_machine != 'x86_64'
|
||||
pillow-simd; 'linux' in sys_platform and platform_machine == 'x86_64'
|
||||
Pillow>=5.4.1
|
||||
|
||||
2
plugins/reolink/.vscode/settings.json
vendored
2
plugins/reolink/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
6
plugins/reolink/package-lock.json
generated
6
plugins/reolink/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.100",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.100",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.65",
|
||||
"version": "0.3.67",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/reolink",
|
||||
"version": "0.0.97",
|
||||
"version": "0.0.100",
|
||||
"description": "Reolink Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
|
||||
import sdk, { Brightness, Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import { EventEmitter } from "stream";
|
||||
import { createRtspMediaStreamOptions, Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
|
||||
@@ -50,13 +50,44 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
||||
}
|
||||
}
|
||||
|
||||
class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brightness {
|
||||
constructor(public camera: ReolinkCamera, nativeId: string) {
|
||||
super(nativeId);
|
||||
this.on = false;
|
||||
}
|
||||
|
||||
async setBrightness(brightness: number): Promise<void> {
|
||||
this.brightness = brightness;
|
||||
await this.setFloodlight(undefined, brightness);
|
||||
}
|
||||
|
||||
async turnOff() {
|
||||
this.on = false;
|
||||
await this.setFloodlight(false);
|
||||
}
|
||||
|
||||
async turnOn() {
|
||||
this.on = true;
|
||||
await this.setFloodlight(true);
|
||||
}
|
||||
|
||||
private async setFloodlight(on?: boolean, brightness?: number) {
|
||||
const api = this.camera.getClientWithToken();
|
||||
|
||||
await api.setWhiteLedState(on, brightness);
|
||||
}
|
||||
}
|
||||
|
||||
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom {
|
||||
client: ReolinkCameraClient;
|
||||
clientWithToken: ReolinkCameraClient;
|
||||
onvifClient: OnvifCameraAPI;
|
||||
onvifIntercom = new OnvifIntercom(this);
|
||||
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
siren: ReolinkCameraSiren;
|
||||
floodlight: ReolinkCameraFloodlight;
|
||||
batteryTimeout: NodeJS.Timeout;
|
||||
|
||||
storageSettings = new StorageSettings(this, {
|
||||
doorbell: {
|
||||
@@ -180,18 +211,11 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
const api = this.getClient();
|
||||
const deviceInfo = await api.getDeviceInfo();
|
||||
this.console.log('deviceInfo', JSON.stringify(deviceInfo));
|
||||
this.storageSettings.values.deviceInfo = deviceInfo;
|
||||
await this.updateAbilities();
|
||||
await this.updateDevice();
|
||||
if (this.hasSiren()) {
|
||||
this.reportSirenDevice();
|
||||
}
|
||||
else {
|
||||
sdk.deviceManager.onDevicesChanged({
|
||||
providerNativeId: this.nativeId,
|
||||
devices: []
|
||||
});
|
||||
}
|
||||
await this.reportDevices();
|
||||
})()
|
||||
.catch(e => {
|
||||
this.console.log('device refresh failed', e);
|
||||
@@ -217,7 +241,13 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
|
||||
async updateAbilities() {
|
||||
const api = this.getClient();
|
||||
const abilities = await api.getAbility();
|
||||
const apiWithToken = this.getClientWithToken();
|
||||
let abilities;
|
||||
try {
|
||||
abilities = await api.getAbility();
|
||||
} catch (e) {
|
||||
abilities = await apiWithToken.getAbility();
|
||||
}
|
||||
this.storageSettings.values.abilities = abilities;
|
||||
this.console.log('getAbility', JSON.stringify(abilities));
|
||||
}
|
||||
@@ -286,6 +316,26 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
&& this.storageSettings.values.abilities?.value?.Ability?.supportAudioAlarm?.ver !== 0;
|
||||
}
|
||||
|
||||
hasFloodlight() {
|
||||
const channel = this.getRtspChannel();
|
||||
|
||||
const channelData = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[channel];
|
||||
if (channelData) {
|
||||
const floodLightConfigVer = channelData.floodLight?.ver ?? 0;
|
||||
const supportFLswitchConfigVer = channelData.supportFLswitch?.ver ?? 0;
|
||||
const supportFLBrightnessConfigVer = channelData.supportFLBrightness?.ver ?? 0;
|
||||
|
||||
return floodLightConfigVer > 0 || supportFLswitchConfigVer > 0 || supportFLBrightnessConfigVer > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasBattery() {
|
||||
const batteryConfigVer = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[this.getRtspChannel()]?.battery?.ver ?? 0;
|
||||
return batteryConfigVer > 0;
|
||||
}
|
||||
|
||||
async updateDevice() {
|
||||
const interfaces = this.provider.getInterfaces();
|
||||
let type = ScryptedDeviceType.Camera;
|
||||
@@ -309,10 +359,33 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
if (this.storageSettings.values.hasObjectDetector) {
|
||||
interfaces.push(ScryptedInterface.ObjectDetector);
|
||||
}
|
||||
if (this.hasSiren())
|
||||
if (this.hasSiren() || this.hasFloodlight())
|
||||
interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
if (this.hasBattery()) {
|
||||
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Online);
|
||||
this.startBatteryCheckInterval();
|
||||
}
|
||||
|
||||
await this.provider.updateDevice(this.nativeId, name, interfaces, type);
|
||||
await this.provider.updateDevice(this.nativeId, this.name ?? name, interfaces, type);
|
||||
}
|
||||
|
||||
startBatteryCheckInterval() {
|
||||
if (this.batteryTimeout) {
|
||||
clearInterval(this.batteryTimeout);
|
||||
}
|
||||
|
||||
this.batteryTimeout = setInterval(async () => {
|
||||
const api = this.getClientWithToken();
|
||||
|
||||
try {
|
||||
const { batteryPercent, sleep } = await api.getBatteryInfo();
|
||||
this.batteryLevel = batteryPercent;
|
||||
this.online = !sleep;
|
||||
}
|
||||
catch (e) {
|
||||
this.console.log('Error in getting battery info', e);
|
||||
}
|
||||
}, 1000 * 60 * 30);
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
@@ -341,6 +414,12 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
return this.client;
|
||||
}
|
||||
|
||||
getClientWithToken() {
|
||||
if (!this.clientWithToken)
|
||||
this.clientWithToken = new ReolinkCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.getRtspChannel(), this.console, true);
|
||||
return this.clientWithToken;
|
||||
}
|
||||
|
||||
async getOnvifClient() {
|
||||
if (!this.onvifClient)
|
||||
this.onvifClient = await this.createOnvifClient();
|
||||
@@ -567,7 +646,8 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
this.console.error("Codec query failed. Falling back to known defaults.", e);
|
||||
}
|
||||
|
||||
const channel = (this.getRtspChannel() + 1).toString().padStart(2, '0');
|
||||
const rtspChannel = this.getRtspChannel();
|
||||
const channel = (rtspChannel + 1).toString().padStart(2, '0');
|
||||
|
||||
const streams: UrlMediaStreamOptions[] = [
|
||||
{
|
||||
@@ -612,7 +692,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
// 1: support main/extern/sub stream
|
||||
// 2: support main/sub stream
|
||||
|
||||
const live = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[0].live?.ver;
|
||||
const live = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[rtspChannel].live?.ver;
|
||||
const [rtmpMain, rtmpExt, rtmpSub, rtspMain, rtspSub] = streams;
|
||||
streams.splice(0, streams.length);
|
||||
|
||||
@@ -621,7 +701,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
// 1: main stream enc type is H265
|
||||
|
||||
// anecdotally, encoders of type h265 do not have a working RTMP main stream.
|
||||
const mainEncType = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[0].mainEncType?.ver;
|
||||
const mainEncType = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[rtspChannel].mainEncType?.ver;
|
||||
|
||||
if (live === 2) {
|
||||
if (mainEncType === 1) {
|
||||
@@ -639,7 +719,14 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
}
|
||||
|
||||
|
||||
if (deviceInfo?.model == "Reolink TrackMix PoE") {
|
||||
// https://github.com/starkillerOG/reolink_aio/blob/main/reolink_aio/api.py#L93C1-L97C2
|
||||
// single motion models have 2*2 RTSP channels
|
||||
if (deviceInfo?.model &&
|
||||
[
|
||||
"Reolink TrackMix PoE",
|
||||
"Reolink TrackMix WiFi",
|
||||
"RLC-81MA"
|
||||
].includes(deviceInfo?.model)) {
|
||||
streams.push({
|
||||
name: '',
|
||||
id: 'autotrack.bcs',
|
||||
@@ -647,14 +734,30 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
video: { width: 896, height: 512 },
|
||||
url: '',
|
||||
});
|
||||
|
||||
if (rtspChannel === 0) {
|
||||
streams.push({
|
||||
name: '',
|
||||
id: `h264Preview_02_main`,
|
||||
container: 'rtsp',
|
||||
video: { codec: 'h264', width: 3840, height: 2160 },
|
||||
url: ''
|
||||
}, {
|
||||
name: '',
|
||||
id: `h264Preview_02_sub`,
|
||||
container: 'rtsp',
|
||||
video: { codec: 'h264', width: 640, height: 480 },
|
||||
url: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const stream of streams) {
|
||||
var streamUrl;
|
||||
if (stream.container === 'rtmp') {
|
||||
streamUrl = new URL(`rtmp://${this.getRtmpAddress()}/bcs/channel${this.getRtspChannel()}_${stream.id}`)
|
||||
streamUrl = new URL(`rtmp://${this.getRtmpAddress()}/bcs/channel${rtspChannel}_${stream.id}`)
|
||||
const params = streamUrl.searchParams;
|
||||
params.set("channel", this.getRtspChannel().toString())
|
||||
params.set("channel", rtspChannel.toString())
|
||||
params.set("stream", '0')
|
||||
stream.url = streamUrl.toString();
|
||||
stream.name = `RTMP ${stream.id}`;
|
||||
@@ -715,48 +818,83 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
|
||||
];
|
||||
}
|
||||
|
||||
getOtherSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
async getOtherSettings(): Promise<Setting[]> {
|
||||
const ret = await super.getOtherSettings();
|
||||
return [
|
||||
...await this.storageSettings.getSettings(),
|
||||
...ret,
|
||||
];
|
||||
}
|
||||
|
||||
getRtmpAddress() {
|
||||
return `${this.getIPAddress()}:${this.storage.getItem('rtmpPort') || 1935}`;
|
||||
}
|
||||
|
||||
reportSirenDevice() {
|
||||
const sirenNativeId = `${this.nativeId}-siren`;
|
||||
const sirenDevice: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: 'Reolink Siren',
|
||||
nativeId: sirenNativeId,
|
||||
info: {
|
||||
...this.info,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
type: ScryptedDeviceType.Siren,
|
||||
};
|
||||
async reportDevices() {
|
||||
const hasSiren = this.hasSiren();
|
||||
const hasFloodlight = this.hasFloodlight();
|
||||
|
||||
const devices: Device[] = [];
|
||||
|
||||
if (hasSiren) {
|
||||
const sirenNativeId = `${this.nativeId}-siren`;
|
||||
const sirenDevice: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: `${this.name} Siren`,
|
||||
nativeId: sirenNativeId,
|
||||
info: {
|
||||
...this.info,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
type: ScryptedDeviceType.Siren,
|
||||
};
|
||||
|
||||
devices.push(sirenDevice);
|
||||
}
|
||||
|
||||
if (hasFloodlight) {
|
||||
const floodlightNativeId = `${this.nativeId}-floodlight`;
|
||||
const floodlightDevice: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: `${this.name} Floodlight`,
|
||||
nativeId: floodlightNativeId,
|
||||
info: {
|
||||
...this.info,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.OnOff
|
||||
],
|
||||
type: ScryptedDeviceType.Light,
|
||||
};
|
||||
|
||||
devices.push(floodlightDevice);
|
||||
}
|
||||
|
||||
sdk.deviceManager.onDevicesChanged({
|
||||
providerNativeId: this.nativeId,
|
||||
devices: [sirenDevice]
|
||||
devices
|
||||
});
|
||||
|
||||
return sirenNativeId;
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string): Promise<any> {
|
||||
if (nativeId.endsWith('-siren')) {
|
||||
this.siren ||= new ReolinkCameraSiren(this, nativeId);
|
||||
return this.siren;
|
||||
} else if (nativeId.endsWith('-floodlight')) {
|
||||
this.floodlight ||= new ReolinkCameraFloodlight(this, nativeId);
|
||||
return this.floodlight;
|
||||
}
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string) {
|
||||
if (nativeId.endsWith('-siren')) {
|
||||
delete this.siren;
|
||||
}
|
||||
} else
|
||||
if (nativeId.endsWith('-floodlight')) {
|
||||
delete this.floodlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,6 +928,7 @@ class ReolinkProvider extends RtspProvider {
|
||||
const rtspChannel = parseInt(settings.rtspChannel?.toString()) || 0;
|
||||
if (!skipValidate) {
|
||||
const api = new ReolinkCameraClient(httpAddress, username, password, rtspChannel, this.console);
|
||||
const apiWithToken = new ReolinkCameraClient(httpAddress, username, password, rtspChannel, this.console, true);
|
||||
try {
|
||||
await api.jpegSnapshot();
|
||||
}
|
||||
@@ -803,7 +942,11 @@ class ReolinkProvider extends RtspProvider {
|
||||
doorbell = deviceInfo.type === 'BELL';
|
||||
name = deviceInfo.name ?? 'Reolink Camera';
|
||||
ai = await api.getAiState();
|
||||
abilities = await api.getAbility();
|
||||
try {
|
||||
abilities = await api.getAbility();
|
||||
} catch (e) {
|
||||
abilities = await apiWithToken.getAbility();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('Reolink camera does not support AI events', e);
|
||||
|
||||
@@ -46,25 +46,27 @@ async function getDeviceInfo(host: string, username: string, password: string):
|
||||
return response.body?.[0]?.value?.DevInfo;
|
||||
}
|
||||
|
||||
export async function getLoginParameters(host: string, username: string, password: string) {
|
||||
try {
|
||||
await getDeviceInfo(host, username, password);
|
||||
return {
|
||||
parameters: {
|
||||
user: username,
|
||||
password,
|
||||
},
|
||||
leaseTimeSeconds: Infinity,
|
||||
export async function getLoginParameters(host: string, username: string, password: string, forceToken?: boolean) {
|
||||
if (!forceToken) {
|
||||
try {
|
||||
await getDeviceInfo(host, username, password);
|
||||
return {
|
||||
parameters: {
|
||||
user: username,
|
||||
password,
|
||||
},
|
||||
leaseTimeSeconds: Infinity,
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(`http://${host}/api.cgi`);
|
||||
const params = url.searchParams;
|
||||
params.set('cmd', 'Login');
|
||||
|
||||
|
||||
const response = await httpFetch({
|
||||
url,
|
||||
method: 'POST',
|
||||
@@ -83,7 +85,7 @@ export async function getLoginParameters(host: string, username: string, passwor
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
const token = response.body?.[0]?.value?.Token?.name || response.body?.value?.Token?.name;
|
||||
if (!token)
|
||||
throw new Error('unable to login');
|
||||
|
||||
@@ -49,7 +49,7 @@ export class ReolinkCameraClient {
|
||||
parameters: Record<string, string>;
|
||||
tokenLease: number;
|
||||
|
||||
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console) {
|
||||
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console, public readonly forceToken?: boolean) {
|
||||
this.credential = {
|
||||
username,
|
||||
password,
|
||||
@@ -80,7 +80,7 @@ export class ReolinkCameraClient {
|
||||
|
||||
this.console.log(`token expired at ${this.tokenLease}, renewing...`);
|
||||
|
||||
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password);
|
||||
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password, this.forceToken);
|
||||
this.parameters = parameters
|
||||
this.tokenLease = Date.now() + 1000 * leaseTimeSeconds;
|
||||
}
|
||||
@@ -153,15 +153,37 @@ export class ReolinkCameraClient {
|
||||
const params = url.searchParams;
|
||||
params.set('cmd', 'GetAbility');
|
||||
params.set('channel', this.channelId.toString());
|
||||
const response = await this.requestWithLogin({
|
||||
let response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
});
|
||||
const error = response.body?.[0]?.error;
|
||||
let error = response.body?.[0]?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getAbility', error);
|
||||
throw new Error('error during call to getAbility');
|
||||
this.console.error('error during call to getAbility GET, Trying with POST', error);
|
||||
|
||||
url.search = '';
|
||||
|
||||
const body = [
|
||||
{
|
||||
cmd: "GetAbility",
|
||||
action: 0,
|
||||
param: { User: { userName: this.username } }
|
||||
}
|
||||
];
|
||||
|
||||
response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
method: 'POST',
|
||||
}, this.createReadable(body));
|
||||
|
||||
error = response.body?.[0]?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getAbility GET, Trying with POST', error);
|
||||
throw new Error('error during call to getAbility');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: response.body?.[0]?.value || response.body?.value,
|
||||
data: response.body,
|
||||
@@ -210,7 +232,46 @@ export class ReolinkCameraClient {
|
||||
this.console.error('error during call to getDeviceInfo', error);
|
||||
throw new Error('error during call to getDeviceInfo');
|
||||
}
|
||||
return response.body?.[0]?.value?.DevInfo;
|
||||
|
||||
const deviceInfo: DevInfo = await response.body?.[0]?.value?.DevInfo;
|
||||
|
||||
// Will need to check if it's valid for NVR and NVR_WIFI
|
||||
if (!['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType)) {
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
// If the device is listed as homehub, fetch the channel specific information
|
||||
url.search = '';
|
||||
const body = [
|
||||
{ cmd: "GetChnTypeInfo", action: 0, param: { channel: this.channelId } },
|
||||
{ cmd: "GetChannelstatus", action: 0, param: {} },
|
||||
]
|
||||
|
||||
const additionalInfoResponse = await this.requestWithLogin({
|
||||
url,
|
||||
method: 'POST',
|
||||
responseType: 'json'
|
||||
}, this.createReadable(body));
|
||||
|
||||
const chnTypeInfo = additionalInfoResponse?.body?.find(elem => elem.cmd === 'GetChnTypeInfo');
|
||||
const chnStatus = additionalInfoResponse?.body?.find(elem => elem.cmd === 'GetChannelstatus');
|
||||
|
||||
if (chnTypeInfo?.value) {
|
||||
deviceInfo.firmVer = chnTypeInfo.value.firmVer;
|
||||
deviceInfo.model = chnTypeInfo.value.typeInfo;
|
||||
deviceInfo.pakSuffix = chnTypeInfo.value.pakSuffix;
|
||||
}
|
||||
|
||||
if (chnStatus?.value) {
|
||||
const specificChannelStatus = chnStatus.value?.status?.find(elem => elem.channel === this.channelId);
|
||||
|
||||
if (specificChannelStatus) {
|
||||
deviceInfo.name = specificChannelStatus.name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
async getPtzPresets(): Promise<PtzPreset[]> {
|
||||
@@ -369,4 +430,69 @@ export class ReolinkCameraClient {
|
||||
data: response.body,
|
||||
};
|
||||
}
|
||||
|
||||
async setWhiteLedState(on?: boolean, brightness?: number) {
|
||||
const url = new URL(`http://${this.host}/api.cgi`);
|
||||
|
||||
const settings: any = { channel: this.channelId };
|
||||
|
||||
if (on !== undefined) {
|
||||
settings.state = on ? 1 : 0;
|
||||
}
|
||||
|
||||
if (brightness !== undefined) {
|
||||
settings.bright = brightness;
|
||||
}
|
||||
|
||||
const body = [{
|
||||
cmd: 'SetWhiteLed',
|
||||
param: { WhiteLed: settings }
|
||||
}];
|
||||
|
||||
const response = await this.requestWithLogin({
|
||||
url,
|
||||
method: 'POST',
|
||||
responseType: 'json',
|
||||
}, this.createReadable(body));
|
||||
|
||||
const error = response.body?.[0]?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to setWhiteLedState', JSON.stringify(body), error);
|
||||
}
|
||||
}
|
||||
|
||||
async getBatteryInfo() {
|
||||
const url = new URL(`http://${this.host}/api.cgi`);
|
||||
|
||||
const body = [
|
||||
{
|
||||
cmd: "GetBatteryInfo",
|
||||
action: 0,
|
||||
param: { channel: this.channelId }
|
||||
},
|
||||
{
|
||||
cmd: "GetChannelstatus",
|
||||
}
|
||||
];
|
||||
|
||||
const response = await this.requestWithLogin({
|
||||
url,
|
||||
responseType: 'json',
|
||||
method: 'POST',
|
||||
}, this.createReadable(body));
|
||||
|
||||
const error = response.body?.find(elem => elem.error)?.error;
|
||||
if (error) {
|
||||
this.console.error('error during call to getBatteryInfo', error);
|
||||
}
|
||||
|
||||
const batteryInfoEntry = response.body.find(entry => entry.cmd === 'GetBatteryInfo')?.value?.Battery;
|
||||
const channelStatusEntry = response.body.find(entry => entry.cmd === 'GetChannelstatus')?.value?.status
|
||||
?.find(chStatus => chStatus.channel === this.channelId)
|
||||
|
||||
return {
|
||||
batteryPercent: batteryInfoEntry?.batteryPercent,
|
||||
sleep: channelStatusEntry?.sleep === 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
plugins/ring/.vscode/settings.json
vendored
2
plugins/ring/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
}
|
||||
5
plugins/tensorflow-lite/.vscode/launch.json
vendored
5
plugins/tensorflow-lite/.vscode/launch.json
vendored
@@ -6,7 +6,7 @@
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "${config:scrypted.debugHost}",
|
||||
@@ -21,9 +21,8 @@
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/src",
|
||||
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
|
||||
"remoteRoot": "."
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
13
plugins/tensorflow-lite/.vscode/settings.json
vendored
13
plugins/tensorflow-lite/.vscode/settings.json
vendored
@@ -1,24 +1,19 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
// "scrypted.debugHost": "koushik-ubuntu",
|
||||
// "scrypted.serverRoot": "/server",
|
||||
"scrypted.debugHost": "koushik-ubuntuvm",
|
||||
"scrypted.serverRoot": "/server",
|
||||
|
||||
// pi local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted",
|
||||
|
||||
// lxc installation
|
||||
// "scrypted.debugHost": "scrypted-test",
|
||||
// "scrypted.serverRoot": "/root/.scrypted",
|
||||
|
||||
// local checkout
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "127.0.0.1",
|
||||
// "scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "koushik-windows",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
4
plugins/tensorflow-lite/package-lock.json
generated
4
plugins/tensorflow-lite/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.68",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.68",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -48,10 +48,15 @@
|
||||
"Settings",
|
||||
"ObjectDetection",
|
||||
"ObjectDetectionPreview"
|
||||
]
|
||||
],
|
||||
"labels": {
|
||||
"require": [
|
||||
"@scrypted/tensorflow-lite"
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.65"
|
||||
"version": "0.1.68"
|
||||
}
|
||||
|
||||
359
plugins/unifi-protect/package-lock.json
generated
359
plugins/unifi-protect/package-lock.json
generated
@@ -1,23 +1,26 @@
|
||||
{
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.156",
|
||||
"version": "0.0.164",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.156",
|
||||
"version": "0.0.164",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/unifi-protect": "file:../../external/unifi-protect",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^1.4.0",
|
||||
"ws": "^8.13.0"
|
||||
"axios": "^1.7.8",
|
||||
"unifi-protect": "^4.16.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/ws": "^8.5.5"
|
||||
"@types/node": "^22.9.4",
|
||||
"@types/ws": "^8.5.13"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@adobe/fetch": "^4.1.9"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -26,58 +29,33 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../external/unifi-protect": {
|
||||
"name": "@koush/unifi-protect",
|
||||
"version": "3.0.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"node-fetch": "^3.3.0",
|
||||
"util": "^0.12.4",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17.0.18",
|
||||
"@types/ws": "^8.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"eslint": "^8.9.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.31",
|
||||
"version": "0.3.88",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"rimraf": "^6.0.1",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -89,19 +67,28 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@koush/unifi-protect": {
|
||||
"resolved": "../../external/unifi-protect",
|
||||
"link": true
|
||||
"node_modules/@adobe/fetch": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/fetch/-/fetch-4.1.9.tgz",
|
||||
"integrity": "sha512-FWIzm4vvl1OtCarTBgWDW6otj4gxrNmMf/DdriqBUw7DjjmckjT3ZQA43b4HzBkzkuAHhajwMy91btp9fWgTEw==",
|
||||
"dependencies": {
|
||||
"debug": "4.3.7",
|
||||
"http-cache-semantics": "4.1.1",
|
||||
"lru-cache": "7.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
@@ -112,15 +99,18 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
|
||||
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
|
||||
"dev": true
|
||||
"version": "22.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.4.tgz",
|
||||
"integrity": "sha512-d9RWfoR7JC/87vj7n+PVTzGg9hDyuFjir3RxUHbjFSKNd9mpxbxwMEyaCim/ddCmy4IuW7HjTzF3g9p3EtWEOg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -132,15 +122,28 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"version": "1.7.8",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
|
||||
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bufferutil": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -152,6 +155,22 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -161,9 +180,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -192,6 +211,19 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -211,15 +243,55 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"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/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/unifi-protect": {
|
||||
"version": "4.16.0",
|
||||
"resolved": "https://registry.npmjs.org/unifi-protect/-/unifi-protect-4.16.0.tgz",
|
||||
"integrity": "sha512-M8/VUTKhPxlzagIQdpjvXbdUPp4a/3F051CghaLXWT9JfnVuJZGLYC3U1zYOXtKVIfqP+KcTmn6sSTawHTGADQ==",
|
||||
"dependencies": {
|
||||
"@adobe/fetch": "4.1.9",
|
||||
"ws": "8.18.0"
|
||||
},
|
||||
"bin": {
|
||||
"ufp": "dist/util/ufp.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "4.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -238,68 +310,63 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/unifi-protect": {
|
||||
"version": "file:../../external/unifi-protect",
|
||||
"@adobe/fetch": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/fetch/-/fetch-4.1.9.tgz",
|
||||
"integrity": "sha512-FWIzm4vvl1OtCarTBgWDW6otj4gxrNmMf/DdriqBUw7DjjmckjT3ZQA43b4HzBkzkuAHhajwMy91btp9fWgTEw==",
|
||||
"requires": {
|
||||
"@types/node": "^17.0.18",
|
||||
"@types/ws": "^8.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.12.0",
|
||||
"@typescript-eslint/parser": "^5.12.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "^8.9.0",
|
||||
"node-fetch": "^3.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.5.5",
|
||||
"util": "^0.12.4",
|
||||
"ws": "^8.5.0"
|
||||
"debug": "4.3.7",
|
||||
"http-cache-semantics": "4.1.1",
|
||||
"lru-cache": "7.18.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^1.6.5",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/node": "^22.8.1",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.7",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.10",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
|
||||
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
|
||||
"dev": true
|
||||
"version": "22.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.4.tgz",
|
||||
"integrity": "sha512-d9RWfoR7JC/87vj7n+PVTzGg9hDyuFjir3RxUHbjFSKNd9mpxbxwMEyaCim/ddCmy4IuW7HjTzF3g9p3EtWEOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"undici-types": "~6.19.8"
|
||||
}
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "8.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
|
||||
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
@@ -311,15 +378,24 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"version": "1.7.8",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
|
||||
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"bufferutil": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"node-gyp-build": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -328,15 +404,23 @@
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.3"
|
||||
}
|
||||
},
|
||||
"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.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
@@ -348,6 +432,16 @@
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -361,15 +455,42 @@
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"optional": true
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
},
|
||||
"unifi-protect": {
|
||||
"version": "4.16.0",
|
||||
"resolved": "https://registry.npmjs.org/unifi-protect/-/unifi-protect-4.16.0.tgz",
|
||||
"integrity": "sha512-M8/VUTKhPxlzagIQdpjvXbdUPp4a/3F051CghaLXWT9JfnVuJZGLYC3U1zYOXtKVIfqP+KcTmn6sSTawHTGADQ==",
|
||||
"requires": {
|
||||
"@adobe/fetch": "4.1.9",
|
||||
"bufferutil": "4.0.8",
|
||||
"ws": "8.18.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"version": "0.0.156",
|
||||
"type": "module",
|
||||
"version": "0.0.164",
|
||||
"description": "Unifi Protect Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -28,20 +29,22 @@
|
||||
"DeviceProvider",
|
||||
"Settings"
|
||||
],
|
||||
"babel": true,
|
||||
"pluginDependencies": [
|
||||
"@scrypted/prebuffer-mixin"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/ws": "^8.5.5"
|
||||
"@types/node": "^22.9.4",
|
||||
"@types/ws": "^8.5.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/unifi-protect": "file:../../external/unifi-protect",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^1.4.0",
|
||||
"ws": "^8.13.0"
|
||||
"axios": "^1.7.8",
|
||||
"unifi-protect": "^4.16.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@adobe/fetch": "^4.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
28
plugins/unifi-protect/src/camera-sensors.ts
Normal file
28
plugins/unifi-protect/src/camera-sensors.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const MOTION_SENSOR_TIMEOUT = 25000;
|
||||
export const FINGERPRINT_SENSOR_TIMEOUT = 5000;
|
||||
|
||||
export interface UnifiMotionDevice {
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
setMotionDetected(motionDetected: boolean): void;
|
||||
}
|
||||
|
||||
export interface UnifiFingerprintDevice {
|
||||
fingerprintTimeout: NodeJS.Timeout;
|
||||
setFingerprintDetected(fingerprintDetected: boolean): void;
|
||||
}
|
||||
|
||||
export function debounceMotionDetected(device: UnifiMotionDevice) {
|
||||
device.setMotionDetected(true);
|
||||
clearTimeout(device.motionTimeout);
|
||||
device.motionTimeout = setTimeout(() => {
|
||||
device.setMotionDetected(false);
|
||||
}, MOTION_SENSOR_TIMEOUT);
|
||||
}
|
||||
|
||||
export function debounceFingerprintDetected(device: UnifiFingerprintDevice) {
|
||||
device.setFingerprintDetected(true);
|
||||
clearTimeout(device.fingerprintTimeout);
|
||||
device.fingerprintTimeout = setTimeout(() => {
|
||||
device.setFingerprintDetected(false);
|
||||
}, FINGERPRINT_SENSOR_TIMEOUT);
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
import { fitHeightToWidth } from "@scrypted/common/src/resolution-utils";
|
||||
import sdk, { Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, Online, PanTiltZoom, PanTiltZoomCommand, PictureOptions, PrivacyMasks, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration, VideoCameraMask } from "@scrypted/sdk";
|
||||
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamConfiguration, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, Online, PanTiltZoom, PanTiltZoomCommand, PictureOptions, PrivacyMasks, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration, VideoCameraMask } from "@scrypted/sdk";
|
||||
import child_process, { ChildProcess } from 'child_process';
|
||||
import { once } from "events";
|
||||
import { PassThrough, Readable } from "stream";
|
||||
import { Readable } from "stream";
|
||||
import WS from 'ws';
|
||||
import { UnifiProtect } from "./main";
|
||||
import { MOTION_SENSOR_TIMEOUT, UnifiMotionDevice, debounceMotionDetected } from './motion';
|
||||
import { MOTION_SENSOR_TIMEOUT, UnifiFingerprintDevice, UnifiMotionDevice, debounceMotionDetected } from './camera-sensors';
|
||||
import { FeatureFlagsShim, PrivacyZone } from "./shim";
|
||||
import { ProtectCameraChannelConfig, ProtectCameraConfigInterface, ProtectCameraLcdMessagePayload } from "./unifi-protect";
|
||||
import { readLength } from '@scrypted/common/src/read-stream';
|
||||
|
||||
const { log, deviceManager, mediaManager } = sdk;
|
||||
const { deviceManager, mediaManager } = sdk;
|
||||
|
||||
export const defaultSensorTimeout = 30;
|
||||
|
||||
export class UnifiPackageCamera extends ScryptedDeviceBase implements Camera, VideoCamera, MotionSensor {
|
||||
constructor(public protectCamera: UnifiCamera, nativeId: string) {
|
||||
super(nativeId);
|
||||
this.console.log(protectCamera);
|
||||
}
|
||||
async takePicture(options?: PictureOptions): Promise<MediaObject> {
|
||||
const buffer = await this.protectCamera.getSnapshot(options, 'package-snapshot?');
|
||||
@@ -40,8 +39,13 @@ export class UnifiPackageCamera extends ScryptedDeviceBase implements Camera, Vi
|
||||
return [options[options.length - 1]];
|
||||
}
|
||||
}
|
||||
export class UnifiFingerprintSensor extends ScryptedDeviceBase implements BinarySensor {
|
||||
constructor(public protectCamera: UnifiCamera, nativeId: string) {
|
||||
super(nativeId);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff, PanTiltZoom, Online, UnifiMotionDevice, VideoCameraMask {
|
||||
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff, PanTiltZoom, Online, UnifiMotionDevice, VideoCameraMask, UnifiFingerprintDevice {
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
detectionTimeout: NodeJS.Timeout;
|
||||
ringTimeout: NodeJS.Timeout;
|
||||
@@ -49,6 +53,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
lastSeen: number;
|
||||
intercomProcess?: ChildProcess;
|
||||
packageCamera?: UnifiPackageCamera;
|
||||
fingerprintSensor?: UnifiFingerprintSensor;
|
||||
fingerprintTimeout: NodeJS.Timeout;
|
||||
|
||||
constructor(public protect: UnifiProtect, nativeId: string, protectCamera: Readonly<ProtectCameraConfigInterface>) {
|
||||
super(nativeId);
|
||||
@@ -90,16 +96,16 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
});
|
||||
|
||||
const camera = this.findCamera() as any;
|
||||
const camera = this.findCamera();
|
||||
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
await this.protect.api.updateDevice(camera, {
|
||||
privacyZones,
|
||||
} as any);
|
||||
}
|
||||
|
||||
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
|
||||
const camera = this.findCamera() as any;
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
const camera = this.findCamera();
|
||||
await this.protect.api.updateDevice(camera, {
|
||||
ispSettings: {
|
||||
zoomPosition: Math.abs(command.zoom * 100),
|
||||
}
|
||||
@@ -107,8 +113,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
|
||||
async setStatusLight(on: boolean) {
|
||||
const camera = this.findCamera() as any;
|
||||
await this.protect.api.updateCamera(camera, {
|
||||
const camera = this.findCamera();
|
||||
await this.protect.api.updateDevice(camera, {
|
||||
ledSettings: {
|
||||
isEnabled: on,
|
||||
}
|
||||
@@ -127,15 +133,33 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
return this.nativeId + '-packageCamera';
|
||||
}
|
||||
|
||||
get fingerprintSensorNativeId() {
|
||||
return this.nativeId + '-fingerprintSensor';
|
||||
}
|
||||
|
||||
ensurePackageCamera() {
|
||||
if (!this.packageCamera) {
|
||||
this.packageCamera = new UnifiPackageCamera(this, this.packageCameraNativeId);
|
||||
}
|
||||
}
|
||||
async getDevice(nativeId: string) {
|
||||
this.ensurePackageCamera();
|
||||
return this.packageCamera;
|
||||
|
||||
ensureFingerprintSensor() {
|
||||
if (!this.fingerprintSensor) {
|
||||
this.fingerprintSensor = new UnifiFingerprintSensor(this, this.fingerprintSensorNativeId);
|
||||
}
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
if (nativeId === this.packageCameraNativeId) {
|
||||
this.ensurePackageCamera();
|
||||
return this.packageCamera;
|
||||
}
|
||||
if (nativeId === this.fingerprintSensorNativeId) {
|
||||
this.ensureFingerprintSensor();
|
||||
return this.fingerprintSensor;
|
||||
}
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
@@ -146,8 +170,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
|
||||
|
||||
const camera = this.findCamera();
|
||||
const params = new URLSearchParams({ camera: camera.id });
|
||||
const response = await this.protect.loginFetch(this.protect.api.wsUrl() + "/talkback?" + params.toString());
|
||||
const endpoint = new URL(this.protect.api.getApiEndpoint("talkback"));
|
||||
endpoint.searchParams.set('camera', camera.id);
|
||||
const response = await this.protect.loginFetch(endpoint.toString());
|
||||
const tb = response.data as Record<string, string>;
|
||||
|
||||
// Adjust the URL for our address.
|
||||
@@ -251,6 +276,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
classes.push('ring');
|
||||
if (this.interfaces.includes(ScryptedInterface.ObjectDetector))
|
||||
classes.push(...this.findCamera().featureFlags.smartDetectTypes);
|
||||
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor)
|
||||
classes.push('fingerprintIdentified');
|
||||
return {
|
||||
classes,
|
||||
};
|
||||
@@ -349,7 +376,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
findCamera() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.cameras.find(camera => camera.id === id);
|
||||
return this.protect.api.bootstrap.cameras.find(camera => camera.id === id);
|
||||
}
|
||||
async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
|
||||
const camera = this.findCamera();
|
||||
@@ -382,7 +409,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
minBitrate: channel.minBitrate,
|
||||
maxBitrate: channel.maxBitrate,
|
||||
fps: channel.fps,
|
||||
idrIntervalMillis: channel.idrInterval * 1000,
|
||||
keyframeInterval: channel.idrInterval * channel.fps,
|
||||
},
|
||||
audio: {
|
||||
codec: 'aac',
|
||||
@@ -404,7 +431,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
return vsos;
|
||||
}
|
||||
|
||||
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
|
||||
async setVideoStreamOptions(options: MediaStreamOptions): Promise<MediaStreamConfiguration> {
|
||||
const bitrate = options?.video?.bitrate;
|
||||
if (!bitrate)
|
||||
return;
|
||||
@@ -415,7 +442,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
const sanitizedBitrate = Math.min(channel.maxBitrate, Math.max(channel.minBitrate, bitrate));
|
||||
this.console.log(channel.name, 'bitrate change requested', bitrate, 'clamped to', sanitizedBitrate);
|
||||
channel.bitrate = sanitizedBitrate;
|
||||
const cameraResult = await this.protect.api.updateCameraChannels(camera);
|
||||
const cameraResult = await this.protect.api.updateDevice(camera, {
|
||||
channels: camera.channels,
|
||||
});
|
||||
if (!cameraResult) {
|
||||
throw new Error("setVideoStreamOptions failed")
|
||||
}
|
||||
@@ -432,7 +461,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
|
||||
setMotionDetected(motionDetected: boolean) {
|
||||
this.motionDetected = motionDetected;
|
||||
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
|
||||
if (this.findCamera().featureFlags.hasPackageCamera) {
|
||||
if (deviceManager.getNativeIds().includes(this.packageCameraNativeId)) {
|
||||
this.ensurePackageCamera();
|
||||
this.packageCamera.motionDetected = motionDetected;
|
||||
@@ -440,12 +469,21 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
}
|
||||
}
|
||||
|
||||
setFingerprintDetected(fingerprintDetected: boolean) {
|
||||
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
|
||||
if (deviceManager.getNativeIds().includes(this.fingerprintSensorNativeId)) {
|
||||
this.ensureFingerprintSensor();
|
||||
this.fingerprintSensor.binaryState = fingerprintDetected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendNotification(title: string, options?: NotifierOptions, media?: MediaObject | string, icon?: MediaObject | string) {
|
||||
const payload: ProtectCameraLcdMessagePayload = {
|
||||
text: title.substring(0, 30),
|
||||
type: 'CUSTOM_MESSAGE',
|
||||
};
|
||||
this.protect.api.updateCamera(this.findCamera(), {
|
||||
this.protect.api.updateDevice(this.findCamera(), {
|
||||
lcdMessage: payload,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Brightness, MotionSensor, OnOff, ScryptedDeviceBase, TemperatureUnit } from "@scrypted/sdk";
|
||||
import { UnifiProtect } from "./main";
|
||||
import { UnifiMotionDevice, debounceMotionDetected } from "./motion";
|
||||
import { UnifiMotionDevice, debounceMotionDetected } from "./camera-sensors";
|
||||
import { ProtectLightConfig } from "./unifi-protect";
|
||||
|
||||
export class UnifiLight extends ScryptedDeviceBase implements OnOff, Brightness, MotionSensor, UnifiMotionDevice {
|
||||
@@ -14,23 +14,23 @@ export class UnifiLight extends ScryptedDeviceBase implements OnOff, Brightness,
|
||||
this.console.log(protectLight);
|
||||
}
|
||||
async turnOff(): Promise<void> {
|
||||
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
|
||||
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
|
||||
if (!result)
|
||||
this.console.error('turnOff failed.');
|
||||
}
|
||||
async turnOn(): Promise<void> {
|
||||
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
|
||||
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
|
||||
if (!result)
|
||||
this.console.error('turnOn failed.');
|
||||
}
|
||||
async setBrightness(brightness: number): Promise<void> {
|
||||
const ledLevel = Math.round(((brightness as number) / 20) + 1);
|
||||
this.protect.api.updateLight(this.findLight(), { lightDeviceSettings: { ledLevel } });
|
||||
this.protect.api.updateDevice(this.findLight(), { lightDeviceSettings: { ledLevel } });
|
||||
}
|
||||
|
||||
findLight() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.lights.find(light => light.id === id);
|
||||
return this.protect.api.bootstrap.lights.find(light => light.id === id);
|
||||
}
|
||||
|
||||
updateState(light?: Readonly<ProtectLightConfig>) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Lock, LockState, ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
import { UnifiProtect } from "./main";
|
||||
import { ProtectDoorLockConfig } from "./unifi-protect";
|
||||
|
||||
export class UnifiLock extends ScryptedDeviceBase implements Lock {
|
||||
constructor(public protect: UnifiProtect, nativeId: string, protectLock: Readonly<ProtectDoorLockConfig>) {
|
||||
constructor(public protect: UnifiProtect, nativeId: string, protectLock: any) {
|
||||
super(nativeId);
|
||||
|
||||
this.updateState(protectLock);
|
||||
@@ -11,23 +10,23 @@ export class UnifiLock extends ScryptedDeviceBase implements Lock {
|
||||
}
|
||||
|
||||
async lock(): Promise<void> {
|
||||
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/close`, {
|
||||
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/close`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
async unlock(): Promise<void> {
|
||||
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/open`, {
|
||||
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/open`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
findLock() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.doorlocks.find(doorlock => doorlock.id === id);
|
||||
return (this.protect.api.bootstrap.doorlocks as any).find(doorlock => doorlock.id === id);
|
||||
}
|
||||
|
||||
updateState(lock?: Readonly<ProtectDoorLockConfig>) {
|
||||
updateState(lock?: any) {
|
||||
lock = lock || this.findLock();
|
||||
if (!lock)
|
||||
return;
|
||||
|
||||
@@ -4,12 +4,12 @@ import sdk, { Device, DeviceProvider, ObjectDetectionResult, ObjectsDetected, Sc
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import axios from "axios";
|
||||
import { UnifiCamera } from "./camera";
|
||||
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
|
||||
import { UnifiLight } from "./light";
|
||||
import { UnifiLock } from "./lock";
|
||||
import { debounceMotionDetected } from "./motion";
|
||||
import { UnifiSensor } from "./sensor";
|
||||
import { FeatureFlagsShim, LastSeenShim } from "./shim";
|
||||
import { ProtectApi, ProtectApiUpdates, ProtectNvrUpdatePayloadCameraUpdate, ProtectNvrUpdatePayloadEventAdd } from "./unifi-protect";
|
||||
import { FeatureFlagsShim } from "./shim";
|
||||
import { ProtectApi, ProtectCameraConfigInterface, ProtectEventAddInterface, ProtectEventPacket } from "./unifi-protect";
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
@@ -54,10 +54,10 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
return;
|
||||
}
|
||||
|
||||
const device = this.api.cameras?.find(c => c.id === packet.action.id)
|
||||
|| this.api.lights?.find(c => c.id === packet.action.id)
|
||||
|| this.api.doorlocks?.find(c => c.id === packet.action.id)
|
||||
|| this.api.sensors?.find(c => c.id === packet.action.id);
|
||||
const device = this.api.bootstrap.cameras?.find(c => c.id === packet.action.id)
|
||||
|| this.api.bootstrap.lights?.find(c => c.id === packet.action.id)
|
||||
|| (this.api.bootstrap.doorlocks as any)?.find(c => c.id === packet.action.id)
|
||||
|| this.api.bootstrap.sensors?.find(c => c.id === packet.action.id);
|
||||
|
||||
if (!device) {
|
||||
return;
|
||||
@@ -100,8 +100,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
})
|
||||
}
|
||||
|
||||
listener(event: Buffer) {
|
||||
const updatePacket = ProtectApiUpdates.decodeUpdatePacket(this.console, event);
|
||||
listener(updatePacket: ProtectEventPacket) {
|
||||
if (!updatePacket)
|
||||
return;
|
||||
|
||||
@@ -109,27 +108,27 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
|
||||
const unifiDevice = this.handleUpdatePacket(updatePacket);
|
||||
|
||||
switch (updatePacket.action.modelKey) {
|
||||
switch (updatePacket.header.modelKey) {
|
||||
case "sensor":
|
||||
case "doorlock":
|
||||
case "light":
|
||||
case "camera": {
|
||||
if (!unifiDevice) {
|
||||
this.console.log('unknown device, sync needed?', updatePacket.action.id);
|
||||
this.console.log('unknown device, sync needed?', updatePacket.header.id);
|
||||
return;
|
||||
}
|
||||
if (updatePacket.action.action !== "update") {
|
||||
unifiDevice.console.log('non update', updatePacket.action.action);
|
||||
if (updatePacket.header.action !== "update") {
|
||||
unifiDevice.console.log('non update', updatePacket.header.action);
|
||||
return;
|
||||
}
|
||||
unifiDevice.updateState();
|
||||
|
||||
if (updatePacket.action.modelKey === "doorlock")
|
||||
if (updatePacket.header.modelKey === "doorlock")
|
||||
return;
|
||||
|
||||
const payload = updatePacket.payload as any as ProtectNvrUpdatePayloadCameraUpdate & LastSeenShim;
|
||||
const payload = updatePacket.payload as ProtectCameraConfigInterface;
|
||||
|
||||
if (updatePacket.action.modelKey !== "camera")
|
||||
if (updatePacket.header.modelKey !== "camera")
|
||||
return;
|
||||
|
||||
const unifiCamera = unifiDevice as UnifiCamera;
|
||||
@@ -142,19 +141,19 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
break;
|
||||
}
|
||||
case "event": {
|
||||
if (updatePacket.action.action !== "add") {
|
||||
if ((updatePacket?.payload as any)?.end && updatePacket.action.id) {
|
||||
const payload = updatePacket.payload as ProtectEventAddInterface;
|
||||
if (updatePacket.header.action !== "add") {
|
||||
if (payload.end && updatePacket.header.id) {
|
||||
// unifi reports the event ended but it seems to take a moment before the snapshot
|
||||
// is actually ready.
|
||||
setTimeout(() => {
|
||||
const running = this.runningEvents.get(updatePacket.action.id);
|
||||
const running = this.runningEvents.get(updatePacket.header.id);
|
||||
running?.resolve?.(undefined)
|
||||
}, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = updatePacket.payload as ProtectNvrUpdatePayloadEventAdd;
|
||||
if (!payload.camera)
|
||||
return;
|
||||
const nativeId = this.getNativeId({ id: payload.camera }, false);
|
||||
@@ -166,7 +165,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
const detectionId = payload.id;
|
||||
const actionId = updatePacket.action.id;
|
||||
const actionId = updatePacket.header.id;
|
||||
|
||||
let resolve: (value: unknown) => void;
|
||||
const promise = new Promise(r => resolve = r);
|
||||
@@ -220,6 +219,15 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
else if (payload.type === 'motion') {
|
||||
debounceMotionDetected(unifiCamera);
|
||||
}
|
||||
else if (payload.type === 'fingerprintIdentified') {
|
||||
const anypay = payload as any;
|
||||
const userId: string = anypay.metadata?.fingerprint?.userId || anypay.metadata?.fingerprint?.ulpId;
|
||||
if (userId) {
|
||||
debounceFingerprintDetected(unifiCamera);
|
||||
detections[0].label = userId;
|
||||
detections[0].labelScore = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const detection: ObjectsDetected = {
|
||||
@@ -240,7 +248,26 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
this.console.log(message, ...parameters);
|
||||
}
|
||||
|
||||
|
||||
reconnecting = false;
|
||||
wsTimeout: NodeJS.Timeout;
|
||||
reconnect(reason: string) {
|
||||
return async () => {
|
||||
if (this.reconnecting)
|
||||
return;
|
||||
this.reconnecting = true;
|
||||
this.api?.reset();
|
||||
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
|
||||
await sleep(10000);
|
||||
this.discoverDevices(0);
|
||||
}
|
||||
}
|
||||
|
||||
async discoverDevices(duration: number) {
|
||||
this.api?.reset();
|
||||
this.reconnecting = false;
|
||||
clearTimeout(this.wsTimeout);
|
||||
|
||||
const ip = this.getSetting('ip');
|
||||
const username = this.getSetting('username');
|
||||
const password = this.getSetting('password');
|
||||
@@ -262,10 +289,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
return
|
||||
}
|
||||
|
||||
this.api?.eventsWs?.removeAllListeners();
|
||||
this.api?.eventsWs?.close();
|
||||
if (!this.api) {
|
||||
this.api = new ProtectApi(ip, username, password, {
|
||||
this.api = new ProtectApi({
|
||||
debug() { },
|
||||
error: (...args) => {
|
||||
this.console.error(...args);
|
||||
@@ -275,48 +300,37 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
});
|
||||
}
|
||||
|
||||
let reconnecting = false;
|
||||
const reconnect = (reason: string) => {
|
||||
return async () => {
|
||||
if (reconnecting)
|
||||
return;
|
||||
reconnecting = true;
|
||||
this.api?.eventsWs?.close();
|
||||
this.api?.eventsWs?.emit('close');
|
||||
this.api?.eventsWs?.removeAllListeners();
|
||||
if (this.api.eventsWs) {
|
||||
this.console.warn('Event Listener failed to close. Requesting plugin restart.');
|
||||
deviceManager.requestRestart();
|
||||
}
|
||||
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
|
||||
await sleep(10000);
|
||||
this.discoverDevices(0);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!await this.api.refreshDevices()) {
|
||||
reconnect('refresh failed')();
|
||||
const loginResult = await this.api.login(ip, username, password);
|
||||
if (!loginResult) {
|
||||
this.log.a('Login failed. Check credentials.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.api.getBootstrap()) {
|
||||
this.reconnect('refresh failed')();
|
||||
return;
|
||||
}
|
||||
|
||||
let wsTimeout: NodeJS.Timeout;
|
||||
const resetWsTimeout = () => {
|
||||
clearTimeout(wsTimeout);
|
||||
wsTimeout = setTimeout(reconnect('timeout'), 5 * 60 * 1000);
|
||||
clearTimeout(this.wsTimeout);
|
||||
this.wsTimeout = setTimeout(() => this.reconnect('timeout'), 5 * 60 * 1000);
|
||||
};
|
||||
resetWsTimeout();
|
||||
|
||||
this.api.eventsWs?.on('message', (data) => {
|
||||
this.api.on('message', message => {
|
||||
resetWsTimeout();
|
||||
this.listener(data as Buffer);
|
||||
});
|
||||
this.api.eventsWs?.on('close', reconnect('close'));
|
||||
this.api.eventsWs?.on('error', reconnect('error'));
|
||||
this.listener(message);
|
||||
})
|
||||
|
||||
const devices: Device[] = [];
|
||||
|
||||
for (let camera of this.api.cameras || []) {
|
||||
if (!this.api.bootstrap.cameras.length) {
|
||||
this.console.warn('no cameras found. is this an admin account? cancelling sync.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (let camera of this.api.bootstrap.cameras || []) {
|
||||
if (camera.isAdoptedByOther) {
|
||||
this.console.log('skipping camera that is adopted by another nvr', camera.id, camera.name);
|
||||
continue;
|
||||
@@ -338,7 +352,9 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
if (needUpdate) {
|
||||
camera = await this.api.updateCameraChannels(camera);
|
||||
camera = await this.api.updateDevice(camera, {
|
||||
channels: camera.channels,
|
||||
});
|
||||
if (!camera) {
|
||||
this.log.a('Unable to enable RTSP and IDR interval on camera. Is this an admin account?');
|
||||
continue;
|
||||
@@ -383,7 +399,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
if (camera.featureFlags.hasLcdScreen) {
|
||||
d.interfaces.push(ScryptedInterface.Notifier);
|
||||
}
|
||||
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
|
||||
if (camera.featureFlags.hasPackageCamera) {
|
||||
d.interfaces.push(ScryptedInterface.DeviceProvider);
|
||||
}
|
||||
if (camera.featureFlags.hasLedStatus) {
|
||||
@@ -396,7 +412,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const sensor of this.api.sensors || []) {
|
||||
for (const sensor of this.api.bootstrap.sensors || []) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: sensor.name,
|
||||
@@ -404,7 +420,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
info: {
|
||||
manufacturer: 'Ubiquiti',
|
||||
model: sensor.type,
|
||||
ip: sensor.host,
|
||||
ip: (sensor as any).host,
|
||||
firmware: sensor.firmwareVersion,
|
||||
version: sensor.hardwareRevision,
|
||||
serialNumber: sensor.id,
|
||||
@@ -424,7 +440,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const light of this.api.lights || []) {
|
||||
for (const light of this.api.bootstrap.lights || []) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: light.name,
|
||||
@@ -449,7 +465,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
for (const lock of this.api.doorlocks || []) {
|
||||
for (const lock of (this.api.bootstrap.doorlocks as any) || []) {
|
||||
const d: Device = {
|
||||
providerNativeId: this.nativeId,
|
||||
name: lock.name,
|
||||
@@ -481,32 +497,61 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
// handle package cameras as a sub device
|
||||
for (const camera of this.api.cameras) {
|
||||
if (!(camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera)
|
||||
for (const camera of this.api.bootstrap.cameras) {
|
||||
const devices: Device[] = [];
|
||||
|
||||
const providerNativeId = this.getNativeId(camera, true);
|
||||
|
||||
if (camera.featureFlags.hasPackageCamera) {
|
||||
const nativeId = providerNativeId + '-packageCamera';
|
||||
const d: Device = {
|
||||
providerNativeId,
|
||||
name: camera.name + ' Package Camera',
|
||||
nativeId,
|
||||
info: {
|
||||
manufacturer: 'Ubiquiti',
|
||||
model: camera.type,
|
||||
firmware: camera.firmwareVersion,
|
||||
version: camera.hardwareRevision,
|
||||
serialNumber: camera.id,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
],
|
||||
type: ScryptedDeviceType.Camera,
|
||||
};
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
if ((camera.featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
|
||||
const nativeId = providerNativeId + '-fingerprintSensor';
|
||||
const d: Device = {
|
||||
providerNativeId,
|
||||
name: camera.name + ' Fingerprint Sensor',
|
||||
nativeId,
|
||||
info: {
|
||||
manufacturer: 'Ubiquiti',
|
||||
model: camera.type,
|
||||
firmware: camera.firmwareVersion,
|
||||
version: camera.hardwareRevision,
|
||||
serialNumber: camera.id,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.BinarySensor,
|
||||
],
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
};
|
||||
devices.push(d);
|
||||
}
|
||||
|
||||
if (!devices.length)
|
||||
continue;
|
||||
const nativeId = camera.id + '-packageCamera';
|
||||
const d: Device = {
|
||||
providerNativeId: this.getNativeId(camera, true),
|
||||
name: camera.name + ' Package Camera',
|
||||
nativeId,
|
||||
info: {
|
||||
manufacturer: 'Ubiquiti',
|
||||
model: camera.type,
|
||||
firmware: camera.firmwareVersion,
|
||||
version: camera.hardwareRevision,
|
||||
serialNumber: camera.id,
|
||||
},
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.VideoCamera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
],
|
||||
type: ScryptedDeviceType.Camera,
|
||||
};
|
||||
|
||||
await deviceManager.onDevicesChanged({
|
||||
providerNativeId: this.getNativeId(camera, true),
|
||||
devices: [d],
|
||||
devices,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -531,25 +576,25 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
return this.locks.get(nativeId);
|
||||
|
||||
const id = this.findId(nativeId);
|
||||
const camera = this.api.cameras.find(camera => camera.id === id);
|
||||
const camera = this.api.bootstrap.cameras.find(camera => camera.id === id);
|
||||
if (camera) {
|
||||
const ret = new UnifiCamera(this, nativeId, camera);
|
||||
this.cameras.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
const sensor = this.api.sensors.find(sensor => sensor.id === id);
|
||||
const sensor = this.api.bootstrap.sensors.find(sensor => sensor.id === id);
|
||||
if (sensor) {
|
||||
const ret = new UnifiSensor(this, nativeId, sensor);
|
||||
this.sensors.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
const light = this.api.lights.find(light => light.id === id);
|
||||
const light = this.api.bootstrap.lights.find(light => light.id === id);
|
||||
if (light) {
|
||||
const ret = new UnifiLight(this, nativeId, light);
|
||||
this.lights.set(nativeId, ret);
|
||||
return ret;
|
||||
}
|
||||
const lock = this.api.doorlocks?.find(lock => lock.id === id);
|
||||
const lock = (this.api.bootstrap.doorlocks as any)?.find(lock => lock.id === id);
|
||||
if (lock) {
|
||||
const ret = new UnifiLock(this, nativeId, lock);
|
||||
this.locks.set(nativeId, ret);
|
||||
@@ -613,7 +658,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
getNativeId(device: { id?: string, mac?: string; anonymousDeviceId?: string, host?: string }, update: boolean) {
|
||||
const { id, mac, anonymousDeviceId,host } = device;
|
||||
const { id, mac, anonymousDeviceId, host } = device;
|
||||
const idMaps = this.storageSettings.values.idMaps;
|
||||
|
||||
// try to find an existing nativeId given the mac and anonymous device id
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user