mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea628a7130 |
4
.github/workflows/docker-common.yml
vendored
4
.github/workflows/docker-common.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
NODE_VERSION: '20'
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["noble"]
|
||||
BASE: ["jammy"]
|
||||
FLAVOR: ["full", "lite"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["noble"]
|
||||
BASE: ["jammy"]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
22
.github/workflows/docker.yml
vendored
22
.github/workflows/docker.yml
vendored
@@ -20,9 +20,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: [
|
||||
["noble-nvidia", ".s6"],
|
||||
["noble-full", ".s6"],
|
||||
["noble-lite", ""],
|
||||
["jammy-nvidia", ".s6"],
|
||||
["jammy-full", ".s6"],
|
||||
["jammy-lite", ""],
|
||||
]
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
@@ -95,15 +95,15 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'noble-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-lite' && 'koush/scrypted:lite' || '' }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'koush/scrypted:lite' || '' }}
|
||||
|
||||
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
|
||||
${{ matrix.BASE[0] == 'noble-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
${{ matrix.BASE[0] == 'jammy-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'ghcr.io/koush/scrypted:full' || '' }}
|
||||
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -63,6 +63,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
|
||||
|
||||
const allParams = Object.assign({}, params, {
|
||||
sdk,
|
||||
fs: require('realfs'),
|
||||
ScryptedDeviceBase,
|
||||
MixinDeviceBase,
|
||||
StorageSettings,
|
||||
|
||||
@@ -19,7 +19,7 @@ function isPi(model: string) {
|
||||
export function isRaspberryPi() {
|
||||
let cpuInfo: string;
|
||||
try {
|
||||
cpuInfo = require('fs').readFileSync('/proc/cpuinfo', { encoding: 'utf8' });
|
||||
cpuInfo = require('realfs').readFileSync('/proc/cpuinfo', { encoding: 'utf8' });
|
||||
}
|
||||
catch (e) {
|
||||
// if this fails, this is probably not a pi
|
||||
@@ -70,7 +70,11 @@ export function getH264DecoderArgs(): CodecArgs {
|
||||
],
|
||||
};
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
if (isRaspberryPi()) {
|
||||
ret['Raspberry Pi'] = ['-c:v', 'h264_mmal'];
|
||||
ret[V4L2] = ['-c:v', 'h264_v4l2m2m'];
|
||||
}
|
||||
else if (os.platform() === 'linux') {
|
||||
ret[V4L2] = ['-c:v', 'h264_v4l2m2m'];
|
||||
}
|
||||
else if (os.platform() === 'win32') {
|
||||
|
||||
@@ -247,7 +247,6 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'tcp',
|
||||
...(options?.vcodec || []),
|
||||
...(options?.acodec || []),
|
||||
'-pkt_size', '64000',
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
@@ -395,7 +394,7 @@ export class RtspClient extends RtspBase {
|
||||
hasGetParameter = true;
|
||||
contentBase: string;
|
||||
|
||||
constructor(public readonly url: string) {
|
||||
constructor(public url: string) {
|
||||
super();
|
||||
const u = new URL(url);
|
||||
const port = parseInt(u.port) || 554;
|
||||
@@ -512,42 +511,6 @@ export class RtspClient extends RtspBase {
|
||||
}
|
||||
}
|
||||
|
||||
async *handleStream(): AsyncGenerator<{
|
||||
rtcp: boolean,
|
||||
header: Buffer,
|
||||
packet: Buffer,
|
||||
channel: number,
|
||||
}> {
|
||||
while (true) {
|
||||
const header = await readLength(this.client, 4);
|
||||
// can this even happen? since the RTSP request method isn't a fixed
|
||||
// value like the "RTSP" in the RTSP response, I don't think so?
|
||||
if (header[0] !== RTSP_FRAME_MAGIC) {
|
||||
if (header.toString() !== 'RTSP')
|
||||
throw this.createBadHeader(header);
|
||||
|
||||
this.client.unshift(header);
|
||||
|
||||
// do what with this?
|
||||
const message = await super.readMessage();
|
||||
const body = await this.readBody(parseHeaders(message));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const length = header.readUInt16BE(2);
|
||||
const packet = await readLength(this.client, length);
|
||||
const id = header.readUInt8(1);
|
||||
|
||||
yield {
|
||||
channel: id,
|
||||
rtcp: id % 2 === 1,
|
||||
header,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readLoop() {
|
||||
const deferred = new Deferred<void>();
|
||||
|
||||
@@ -650,8 +613,7 @@ export class RtspClient extends RtspBase {
|
||||
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
|
||||
|
||||
if (this.wwwAuthenticate.includes('Basic')) {
|
||||
const parsedUrl = new URL(this.url);
|
||||
const hash = BASIC.computeHash({ username: parsedUrl.username, password: parsedUrl.password });
|
||||
const hash = BASIC.computeHash(url);
|
||||
return `Basic ${hash}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20250101"
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -51,9 +51,8 @@ RUN apt-get -y install \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
|
||||
# RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
@@ -86,8 +85,8 @@ RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
@@ -95,20 +94,16 @@ RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" |
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# set default shell to bash
|
||||
RUN chsh -s /bin/bash
|
||||
ENV SHELL="/bin/bash"
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.12"
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.12"
|
||||
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.10"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -46,6 +46,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION="20250101"
|
||||
ENV SCRYPTED_BASE_VERSION="20240321"
|
||||
|
||||
CMD ["/bin/sh", "-c", "ulimit -c 0; exec npm --prefix /server exec scrypted-serve"]
|
||||
|
||||
@@ -75,8 +75,7 @@ services:
|
||||
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
|
||||
|
||||
# Default volume for the Scrypted database. Typically should not be changed.
|
||||
# The volume will be placed relative to this docker-compose.yml.
|
||||
- ./volume:/server/volume
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
|
||||
# LXC usage only
|
||||
# lxc - /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
@@ -23,13 +23,7 @@ fi
|
||||
|
||||
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
|
||||
|
||||
FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
|
||||
if [ -z "$FILENAME" ]
|
||||
then
|
||||
echo "AMD graphics package can not be installed. Could not find the package name."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILENAME="amdgpu-install_6.2.60202-1_all.deb"
|
||||
set -e
|
||||
mkdir -p /tmp/amd
|
||||
cd /tmp/amd
|
||||
|
||||
@@ -29,50 +29,27 @@ apt-get -y update &&
|
||||
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.35.30872.22
|
||||
# these debs are seemingly ubuntu 22.04 only.
|
||||
|
||||
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
|
||||
|
||||
apt-get install -y ocl-icd-libopencl1
|
||||
|
||||
# very stupid legacy + current install process conflict.
|
||||
# install 24.35.30872.22 for legacy support. Then install latest.
|
||||
# https://github.com/intel/compute-runtime/issues/770#issuecomment-2515166915
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
|
||||
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
|
||||
|
||||
dpkg -i *.deb
|
||||
rm -f *.deb
|
||||
|
||||
# https://github.com/intel/compute-runtime/releases/tag/24.45.31740.9
|
||||
# note that at time of commit, IGC supports ubuntu 24.04 only possibly due to their builder being on 24.04.
|
||||
IGC_VERSION=2_2.1.12+18087_amd64
|
||||
COMPUTE_VERSION=24.45.31740.9
|
||||
ZERO_GPU_VERSION=1.6.31740.9_amd64
|
||||
LIBIGDGMM_VERSION=22.5.2_amd64
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.1.12/intel-igc-core-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.1.12/intel-igc-opencl-$IGC_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu-dbgsym_$ZERO_GPU_VERSION.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu_$ZERO_GPU_VERSION.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd-dbgsym_"$COMPUTE_VERSION"_amd64.ddeb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd_"$COMPUTE_VERSION"_amd64.deb
|
||||
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/libigdgmm12_$LIBIGDGMM_VERSION.deb
|
||||
|
||||
set +e
|
||||
dpkg -i *.deb
|
||||
set -e
|
||||
# the legacy + latest process says this may be necessary but it does not seem to be in a clean environment.
|
||||
apt-get install --fix-broken
|
||||
|
||||
|
||||
cd /tmp && rm -rf /tmp/gpu
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ set -e
|
||||
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
|
||||
|
||||
# level zero must also be installed
|
||||
LEVEL_ZERO_VERSION=1.19.2
|
||||
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
|
||||
|
||||
@@ -128,7 +128,7 @@ then
|
||||
set -e
|
||||
removescryptedfstab
|
||||
mkdir -p /mnt/scrypted-nvr
|
||||
echo "UUID=$UUID /mnt/scrypted-nvr ext4 defaults,nofail,noatime,x-systemd.automount 0 0" >> /etc/fstab
|
||||
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail,noatime 0 0" >> /etc/fstab
|
||||
mount -a
|
||||
systemctl daemon-reload
|
||||
set +e
|
||||
|
||||
@@ -26,8 +26,8 @@ RUN add-apt-repository -y ppa:deadsnakes/ppa && \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
# Coral Edge TPU
|
||||
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
|
||||
@@ -35,20 +35,16 @@ RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" |
|
||||
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
RUN apt-get -y update && apt-get -y install libedgetpu1-std
|
||||
|
||||
# set default shell to bash
|
||||
RUN chsh -s /bin/bash
|
||||
ENV SHELL="/bin/bash"
|
||||
|
||||
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
|
||||
ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.12"
|
||||
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
|
||||
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
|
||||
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
|
||||
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
|
||||
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
|
||||
@@ -48,9 +48,8 @@ RUN apt-get -y install \
|
||||
# allow pip to install to system
|
||||
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
|
||||
|
||||
# ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
|
||||
# RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy
|
||||
RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
|
||||
@@ -69,14 +69,11 @@ then
|
||||
fi
|
||||
|
||||
RUN python$PYTHON_VERSION -m pip install --upgrade pip
|
||||
# besides debugpy, none of these dependencies are needed anymore?
|
||||
# portable python includes typing and does not need typing_extensions.
|
||||
# opencv-python-headless has wheels for macos.
|
||||
if [ "$PYTHON_VERSION" != "3.10" ]
|
||||
then
|
||||
RUN python$PYTHON_VERSION -m pip install typing
|
||||
fi
|
||||
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python
|
||||
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python psutil
|
||||
|
||||
echo "Installing Scrypted Launch Agent..."
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#Requires -RunAsAdministrator
|
||||
|
||||
# Set-PSDebug -Trace 1
|
||||
|
||||
# stop existing service if any
|
||||
@@ -10,7 +8,7 @@ sc.exe stop scrypted.exe
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
|
||||
|
||||
# Install node.js
|
||||
choco upgrade -y nodejs-lts --version=20.18.0
|
||||
choco upgrade -y nodejs-lts --version=20.11.1
|
||||
|
||||
# Install VC Redist, which is necessary for portable python
|
||||
choco install -y vcredist140
|
||||
@@ -24,19 +22,11 @@ $SCRYPTED_WINDOWS_PYTHON_VERSION="-3.9"
|
||||
# Refresh environment variables for py and npx to work
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
# Workaround Windows Node no longer creating %APPDATA%\npm which causes npx to fail
|
||||
# Fixed in newer versions of NPM but not the one bundled with Node 20
|
||||
# https://github.com/nodejs/node/issues/53538
|
||||
npm i -g npm
|
||||
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install --upgrade pip
|
||||
# besides debugpy, none of these dependencies are needed anymore?
|
||||
# portable python includes typing and does not need typing_extensions.
|
||||
# opencv-python-headless has wheels for windows.
|
||||
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
|
||||
|
||||
$SCRYPTED_INSTALL_VERSION=[System.Environment]::GetEnvironmentVariable("SCRYPTED_INSTALL_VERSION","User")
|
||||
|
||||
if ($SCRYPTED_INSTALL_VERSION -eq $null) {
|
||||
npx -y scrypted@latest install-server
|
||||
} else {
|
||||
@@ -51,8 +41,6 @@ npm install --prefix $SCRYPTED_HOME @koush/node-windows --save
|
||||
$NPX_PATH = (Get-Command npx).Path
|
||||
# The path needs double quotes to handle spaces in the directory path
|
||||
$NPX_PATH_ESCAPED = '"' + $NPX_PATH.replace('\', '\\') + '"'
|
||||
# On newer versions of NPM, the NPX might be a .ps1 file which doesn't work with child_process.spawn, change to .cmd
|
||||
$NPX_PATH_ESCAPED = $NPX_PATH_ESCAPED.replace('.ps1', '.cmd')
|
||||
|
||||
$SERVICE_JS = @"
|
||||
const fs = require('fs');
|
||||
@@ -66,8 +54,6 @@ child_process.spawn('$NPX_PATH_ESCAPED', ['-y', 'scrypted', 'serve'], {
|
||||
stdio: 'inherit',
|
||||
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
|
||||
shell: true,
|
||||
}).on('error', (err) => {
|
||||
console.error('Error spawning child process', err);
|
||||
});
|
||||
"@
|
||||
|
||||
@@ -113,9 +99,6 @@ svc.on("install", () => {
|
||||
svc.on("start", () => {
|
||||
console.log("Service started");
|
||||
});
|
||||
svc.on("error", (err) => {
|
||||
console.log("Service error", err);
|
||||
});
|
||||
svc.install();
|
||||
"@
|
||||
|
||||
|
||||
18
install/proxmox/docker-compose.sh
Executable file → Normal file
18
install/proxmox/docker-compose.sh
Executable file → Normal file
@@ -4,15 +4,21 @@ cd /root/.scrypted
|
||||
# always immediately upgrade everything in case there's a broken update.
|
||||
# this will also be preferable for troubleshooting via lxc reboot.
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
yes | dpkg --configure -a
|
||||
apt -y --fix-broken install && apt -y update && apt -y dist-upgrade
|
||||
(apt -y --fix-broken install && (yes | dpkg --configure -a) && apt -y update && apt -y dist-upgrade) &
|
||||
|
||||
# force a pull to ensure we have the latest images.
|
||||
# not using --pull always cause that fails everything on network down
|
||||
docker compose pull
|
||||
# foreground pull if requested.
|
||||
if [ -e "volume/.pull" ]
|
||||
then
|
||||
rm -rf volume/.pull
|
||||
PULL="--pull"
|
||||
(sleep 300 && docker container prune -f && docker image prune -a -f) &
|
||||
else
|
||||
# always background pull in case there's a broken image.
|
||||
(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
|
||||
|
||||
@@ -26,7 +26,8 @@ then
|
||||
fi
|
||||
|
||||
SCRYPTED_BACKUP_VMID=10445
|
||||
function prepareScryptedRestore() {
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
pct config $VMID 2>&1 > /dev/null
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
@@ -42,11 +43,6 @@ function prepareScryptedRestore() {
|
||||
RESTORE_VMID=$VMID
|
||||
VMID=$SCRYPTED_BACKUP_VMID
|
||||
pct destroy $VMID 2>&1 > /dev/null
|
||||
}
|
||||
|
||||
if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
prepareScryptedRestore
|
||||
fi
|
||||
|
||||
echo "Downloading scrypted container backup."
|
||||
@@ -75,56 +71,31 @@ then
|
||||
echo ""
|
||||
echo "==============================================================="
|
||||
echo "Existing container $VMID found."
|
||||
echo "Please choose from the following options to resolve this error."
|
||||
echo "==============================================================="
|
||||
echo ""
|
||||
echo "This script can be used ro reinstall Scrypted and reset the container to a factory state."
|
||||
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:"
|
||||
readyn "Reinstall Scrypted and and retain existing configuration?"
|
||||
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"
|
||||
|
||||
if [ "$yn" != "y" ]
|
||||
then
|
||||
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
|
||||
|
||||
SCRYPTED_RESTORE=true
|
||||
prepareScryptedRestore
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! "$@" =~ "--storage" ]]
|
||||
then
|
||||
HAS_LOCAL_LVM=$(pvesm status | grep local-lvm | grep active)
|
||||
HAS_LOCAL_ZFS=$(pvesm status | grep local-zfs | grep active)
|
||||
if [ ! -z "$HAS_LOCAL_LVM" ]
|
||||
then
|
||||
RESTORE_STORAGE="--storage local-lvm"
|
||||
elif [ ! -z "$HAS_LOCAL_ZFS" ]
|
||||
then
|
||||
RESTORE_STORAGE="--storage local-zfs"
|
||||
else
|
||||
echo "Could not determine a valid storage device. One may need to be specified manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
pct stop $VMID 2>&1 > /dev/null
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $RESTORE_STORAGE $@
|
||||
pct restore $VMID $SCRYPTED_TAR_ZST $@
|
||||
|
||||
if [ "$?" != "0" ]
|
||||
then
|
||||
@@ -179,7 +150,7 @@ if [ -n "$SCRYPTED_RESTORE" ]
|
||||
then
|
||||
echo ""
|
||||
echo ""
|
||||
echo "This script will reset the Scrypted container to a factory state while preserving existing data."
|
||||
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" ]
|
||||
@@ -249,7 +220,7 @@ then
|
||||
|
||||
VMID=$RESTORE_VMID
|
||||
echo "Restoring with reset image..."
|
||||
pct restore --force 1 $VMID *.tar $RESTORE_STORAGE $@
|
||||
pct restore --force 1 $VMID *.tar $@
|
||||
|
||||
echo "Restoring volumes..."
|
||||
move_volume $SCRYPTED_BACKUP_VMID $VMID mp0 hide-warning
|
||||
@@ -262,9 +233,6 @@ then
|
||||
pct destroy $SCRYPTED_BACKUP_VMID
|
||||
fi
|
||||
|
||||
echo "Enabling startup on boot..."
|
||||
pct set $VMID -onboot 1
|
||||
|
||||
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
|
||||
if [ "$yn" == "y" ]
|
||||
then
|
||||
|
||||
31
packages/client/package-lock.json
generated
31
packages/client/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.6",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"@scrypted/types": "^0.3.66",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
@@ -17,7 +17,6 @@
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
@@ -76,10 +75,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.3.92",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
|
||||
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
|
||||
"license": "ISC"
|
||||
"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",
|
||||
@@ -128,16 +126,6 @@
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
@@ -223,10 +211,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.6",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -14,12 +14,11 @@
|
||||
"devDependencies": {
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/ws": "^8.5.13",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.3.92",
|
||||
"@scrypted/types": "^0.3.66",
|
||||
"engine.io-client": "^6.6.1",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"rimraf": "^6.0.1"
|
||||
|
||||
@@ -700,7 +700,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager,
|
||||
clusterManager,
|
||||
} = scrypted;
|
||||
console.log('api attached', Date.now() - start);
|
||||
|
||||
@@ -860,7 +859,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
|
||||
connectionType,
|
||||
admin,
|
||||
systemManager,
|
||||
clusterManager,
|
||||
deviceManager,
|
||||
endpointManager,
|
||||
mediaManager,
|
||||
|
||||
@@ -56,13 +56,13 @@ Scrypted Cloud automatically creates a login free tunnel for remote access.
|
||||
|
||||
The following steps are only necessary if you want to associate the tunnel with your existing Cloudflare account to manage it remotely.
|
||||
|
||||
1. Navigate to the Cloud Plugin's Cloudflare Settings.
|
||||
2. Enter the Cloudflare subdomain, e.g. `scrypted.example.org`.
|
||||
3. Open the authorization link printed in the Log in a browser.
|
||||
4. Log in to Cloudflare if prompted. Then open the authorization link again.
|
||||
5. Select the domain for the specified the subdomain.
|
||||
6. Authorization should now be complete.
|
||||
1. Create the Tunnel in the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com).
|
||||
2. Copy the token shown for the tunnel shown in the `install [token]` command. For example, if you see `cloudflared service install eyJhI344aA...`, then `eyJhI344aA...` is the token you need to copy.
|
||||
3. Paste the token into the Cloud Plugin Advanced Settings.
|
||||
4. Add a `Public Hostname` to the tunnel.
|
||||
* Choose a (sub)domain.
|
||||
* Service `Type` is `HTTPS` and `URL` is `localhost:port`. Replace the port with `Forward Port` from Cloud Plugin Settings.
|
||||
* Expand `Additional Application Settings` -> `TLS` menus and enable `No TLS Verify`.
|
||||
|
||||
::: info
|
||||
Visiting the authorization link twice as directed in the above instructions may be necessary. Cloudflare will not prompt a with a list of domains unless the browser session is already logged in.
|
||||
:::
|
||||
5. Reload Cloud Plugin.
|
||||
6. Verify Cloudflare successfully connected by observing the `Console` Logs.
|
||||
|
||||
2275
plugins/cloud/package-lock.json
generated
2275
plugins/cloud/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^4.3.0",
|
||||
"@eneris/push-receiver": "^4.2.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"bpmux": "^8.2.1",
|
||||
@@ -48,9 +48,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/http-proxy": "^1.17.15",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/nat-upnp": "^1.1.5",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^22.5.2",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"version": "0.2.49"
|
||||
"version": "0.2.47"
|
||||
}
|
||||
|
||||
@@ -183,7 +183,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
this.storageSettings.values.cloudflaredTunnelCredentials = undefined;
|
||||
this.doCloudflaredLogin(nv);
|
||||
},
|
||||
console: true,
|
||||
},
|
||||
cloudflaredTunnelLoginUrl: {
|
||||
group: 'Cloudflare',
|
||||
@@ -1046,27 +1045,24 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
args['--url'] = tunnelUrl;
|
||||
}
|
||||
|
||||
// if error messages are detected after 10 minutes from tunnel attempt start,
|
||||
// kill the tunnel.
|
||||
const tenMinutesMs = 10 * 60 * 1000;
|
||||
const tunnelStart = Date.now();
|
||||
const deferred = new Deferred<string>();
|
||||
|
||||
const cloudflareTunnel = cloudflared.tunnel(args);
|
||||
deferred.resolvePromise(cloudflareTunnel.url);
|
||||
|
||||
const processData = (string: string) => {
|
||||
this.console.error(string);
|
||||
|
||||
const lines = string.split('\n');
|
||||
for (const line of lines) {
|
||||
if ((line.includes('Unregistered tunnel connection')
|
||||
|| line.includes('Connection terminated error')
|
||||
|| line.includes('Register tunnel error')
|
||||
|| line.includes('Failed to serve tunnel')
|
||||
|| line.includes('Failed to get tunnel'))
|
||||
&& (deferred.finished || Date.now() - tunnelStart > tenMinutesMs)) {
|
||||
this.console.warn('Cloudflare registration failure detected. Terminating.');
|
||||
&& deferred.finished) {
|
||||
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');
|
||||
cloudflareTunnel.child.kill();
|
||||
}
|
||||
if (line.includes('hostname'))
|
||||
this.console.log(line);
|
||||
const match = /config=(".*?}")/gm.exec(line)
|
||||
if (match) {
|
||||
const json = match[1];
|
||||
@@ -1111,10 +1107,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
throw e;
|
||||
}
|
||||
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
|
||||
return {
|
||||
url: deferred.promise,
|
||||
child: cloudflareTunnel.child,
|
||||
};
|
||||
return cloudflareTunnel;
|
||||
}, {
|
||||
startingDelay: 60000,
|
||||
timeMultiple: 1.2,
|
||||
|
||||
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",
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../../../install/proxmox/docker-compose.sh
|
||||
22
plugins/core/fs/lxc/scrypted.service
Normal file
22
plugins/core/fs/lxc/scrypted.service
Normal file
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=Scrypted service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/npx -y scrypted serve
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
Environment="NODE_OPTIONS=--dns-result-order=ipv4first"
|
||||
Environment="SCRYPTED_PYTHON_PATH=/usr/bin/python3"
|
||||
Environment="SCRYPTED_PYTHON39_PATH=/usr/bin/python3.9"
|
||||
Environment="SCRYPTED_PYTHON310_PATH=/usr/bin/python3.10"
|
||||
Environment="SCRYPTED_FFMPEG_PATH=/usr/bin/ffmpeg"
|
||||
Environment="SCRYPTED_INSTALL_ENVIRONMENT=lxc"
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
56
plugins/core/package-lock.json
generated
56
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.103",
|
||||
"version": "0.3.86",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.103",
|
||||
"version": "0.3.86",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
@@ -88,28 +88,21 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.63",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
},
|
||||
"bin": {
|
||||
@@ -122,9 +115,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"typedoc": "^0.26.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -286,28 +281,23 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/stringify-object": "^4.0.5",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"typedoc": "^0.26.5",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.93.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.3.103",
|
||||
"version": "0.3.86",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk';
|
||||
import { AggregateDevice } from './aggregate';
|
||||
import { AggregateDevice, createAggregateDevice } from './aggregate';
|
||||
|
||||
const { deviceManager } = sdk;
|
||||
export const AggregateCoreNativeId = 'aggregatecore';
|
||||
@@ -13,6 +13,24 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
|
||||
this.systemDevice = {
|
||||
deviceCreator: 'Device Group',
|
||||
};
|
||||
|
||||
for (const nativeId of deviceManager.getNativeIds()) {
|
||||
if (nativeId?.startsWith('aggregate:')) {
|
||||
const aggregate = createAggregateDevice(nativeId);
|
||||
this.aggregate.set(nativeId, aggregate);
|
||||
this.reportAggregate(nativeId, aggregate.computeInterfaces(), aggregate.providedName);
|
||||
}
|
||||
}
|
||||
|
||||
sdk.systemManager.listen((eventSource, eventDetails, eventData) => {
|
||||
if (eventDetails.eventInterface === 'Storage') {
|
||||
const ids = [...this.aggregate.values()].map(a => a.id);
|
||||
if (ids.includes(eventSource.id)) {
|
||||
const aggregate = [...this.aggregate.values()].find(a => a.id === eventSource.id);
|
||||
this.reportAggregate(aggregate.nativeId, aggregate.computeInterfaces(), aggregate.providedName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
@@ -33,8 +51,7 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
|
||||
const { name } = settings;
|
||||
const nativeId = `aggregate:${Math.random()}`;
|
||||
await this.reportAggregate(nativeId, [], name?.toString());
|
||||
const aggregate = new AggregateDevice(this, nativeId);
|
||||
aggregate.computeInterfaces();
|
||||
const aggregate = createAggregateDevice(nativeId);
|
||||
this.aggregate.set(nativeId, aggregate);
|
||||
return nativeId;
|
||||
}
|
||||
@@ -51,17 +68,9 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
let device = this.aggregate.get(nativeId);
|
||||
if (device)
|
||||
return device;
|
||||
device = new AggregateDevice(this, nativeId);
|
||||
device.computeInterfaces();
|
||||
this.aggregate.set(nativeId, device);
|
||||
return device;
|
||||
return this.aggregate.get(nativeId);
|
||||
}
|
||||
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
const device = this.aggregate.get(nativeId);
|
||||
device?.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import sdk, { EventListener, EventListenerRegister, FFmpegInput, LockState, MediaStreamDestination, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import type { AggregateCore } from "./aggregate-core";
|
||||
const { systemManager, mediaManager, deviceManager } = sdk;
|
||||
|
||||
export interface AggregateDevice extends ScryptedDeviceBase {
|
||||
computeInterfaces(): string[];
|
||||
}
|
||||
|
||||
interface Aggregator<T> {
|
||||
(values: T[]): T;
|
||||
}
|
||||
@@ -138,144 +141,143 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
|
||||
}
|
||||
}
|
||||
|
||||
export class AggregateDevice extends ScryptedDeviceBase implements Settings {
|
||||
listeners: EventListenerRegister[] = [];
|
||||
storageSettings = new StorageSettings(this, {
|
||||
deviceInterfaces: {
|
||||
title: 'Selected Device Interfaces',
|
||||
description: 'The components of other devices to combine into this device group.',
|
||||
type: 'interface',
|
||||
multiple: true,
|
||||
deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`,
|
||||
onPut: () => {
|
||||
this.core.reportAggregate(this.nativeId, this.computeInterfaces(), this.providedName);
|
||||
export function createAggregateDevice(nativeId: string): AggregateDevice {
|
||||
class AggregateDeviceImpl extends ScryptedDeviceBase implements Settings {
|
||||
listeners: EventListenerRegister[] = [];
|
||||
storageSettings = new StorageSettings(this, {
|
||||
deviceInterfaces: {
|
||||
title: 'Selected Device Interfaces',
|
||||
description: 'The components of other devices to combine into this device group.',
|
||||
type: 'interface',
|
||||
multiple: true,
|
||||
deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
constructor(public core: AggregateCore, nativeId: string) {
|
||||
super(nativeId);
|
||||
constructor() {
|
||||
super(nativeId);
|
||||
|
||||
try {
|
||||
const data = this.storage.getItem('data');
|
||||
if (data) {
|
||||
const { deviceInterfaces } = JSON.parse(data);
|
||||
this.storageSettings.values.deviceInterfaces = deviceInterfaces;
|
||||
try {
|
||||
const data = this.storage.getItem('data');
|
||||
if (data) {
|
||||
const { deviceInterfaces } = JSON.parse(data);
|
||||
this.storageSettings.values.deviceInterfaces = deviceInterfaces;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
this.storage.removeItem('data');
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
this.storage.removeItem('data');
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
makeListener(iface: string, devices: ScryptedDevice[]) {
|
||||
const aggregator = aggregators.get(iface);
|
||||
if (!aggregator) {
|
||||
const ds = deviceManager.getDeviceState(this.nativeId);
|
||||
// if this device can't be aggregated for whatever reason, pass property through.
|
||||
for (const device of devices) {
|
||||
const register = device.listen({
|
||||
event: iface,
|
||||
watch: true,
|
||||
}, (source, details, data) => {
|
||||
if (details.property)
|
||||
ds[details.property] = data;
|
||||
});
|
||||
this.listeners.push(register);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0];
|
||||
if (!property) {
|
||||
this.console.warn('aggregating interface with no property?', iface);
|
||||
return;
|
||||
}
|
||||
|
||||
const runAggregator = () => {
|
||||
const values = devices.map(device => device[property]);
|
||||
(this as any)[property] = aggregator(values);
|
||||
}
|
||||
|
||||
const listener: EventListener = () => runAggregator();
|
||||
|
||||
makeListener(iface: string, devices: ScryptedDevice[]) {
|
||||
const aggregator = aggregators.get(iface);
|
||||
if (!aggregator) {
|
||||
const ds = deviceManager.getDeviceState(this.nativeId);
|
||||
// if this device can't be aggregated for whatever reason, pass property through.
|
||||
for (const device of devices) {
|
||||
const register = device.listen({
|
||||
event: iface,
|
||||
watch: true,
|
||||
}, (source, details, data) => {
|
||||
if (details.property)
|
||||
ds[details.property] = data;
|
||||
});
|
||||
}, listener);
|
||||
this.listeners.push(register);
|
||||
}
|
||||
return;
|
||||
|
||||
return runAggregator;
|
||||
}
|
||||
|
||||
const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0];
|
||||
if (!property) {
|
||||
this.console.warn('aggregating interface with no property?', iface);
|
||||
return;
|
||||
}
|
||||
computeInterfaces(): string[] {
|
||||
this.listeners.forEach(listener => listener.removeListener());
|
||||
this.listeners = [];
|
||||
|
||||
const runAggregator = () => {
|
||||
const values = devices.map(device => device[property]);
|
||||
(this as any)[property] = aggregator(values);
|
||||
}
|
||||
|
||||
const listener: EventListener = () => runAggregator();
|
||||
|
||||
for (const device of devices) {
|
||||
const register = device.listen({
|
||||
event: iface,
|
||||
watch: true,
|
||||
}, listener);
|
||||
this.listeners.push(register);
|
||||
}
|
||||
|
||||
return runAggregator;
|
||||
}
|
||||
|
||||
release() {
|
||||
this.listeners.forEach(listener => listener.removeListener());
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
computeInterfaces(): string[] {
|
||||
this.release();
|
||||
|
||||
try {
|
||||
const interfaces = new Map<string, string[]>();
|
||||
for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) {
|
||||
const parts = deviceInterface.split('#');
|
||||
const id = parts[0];
|
||||
const iface = parts[1];
|
||||
if (!interfaces.has(iface))
|
||||
interfaces.set(iface, []);
|
||||
interfaces.get(iface).push(id);
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const runAggregator = this.makeListener(iface, devices);
|
||||
runAggregator?.();
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const descriptor = ScryptedInterfaceDescriptors[iface];
|
||||
if (!descriptor) {
|
||||
this.console.warn(`descriptor not found for ${iface}, skipping method generation`);
|
||||
continue;
|
||||
try {
|
||||
const interfaces = new Map<string, string[]>();
|
||||
for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) {
|
||||
const parts = deviceInterface.split('#');
|
||||
const id = parts[0];
|
||||
const iface = parts[1];
|
||||
if (!interfaces.has(iface))
|
||||
interfaces.set(iface, []);
|
||||
interfaces.get(iface).push(id);
|
||||
}
|
||||
|
||||
if (iface === ScryptedInterface.VideoCamera) {
|
||||
const camera = createVideoCamera(devices as any, this.console);
|
||||
for (const method of descriptor.methods) {
|
||||
this[method] = (...args: any[]) => camera[method](...args);
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const runAggregator = this.makeListener(iface, devices);
|
||||
runAggregator?.();
|
||||
}
|
||||
|
||||
for (const [iface, ids] of interfaces.entries()) {
|
||||
const devices = ids.map(id => systemManager.getDeviceById(id));
|
||||
const descriptor = ScryptedInterfaceDescriptors[iface];
|
||||
if (!descriptor) {
|
||||
this.console.warn(`descriptor not found for ${iface}, skipping method generation`);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const method of descriptor.methods) {
|
||||
this[method] = async function (...args: any[]) {
|
||||
const ret: Promise<any>[] = [];
|
||||
for (const device of devices) {
|
||||
ret.push(device[method](...args));
|
||||
if (iface === ScryptedInterface.VideoCamera) {
|
||||
const camera = createVideoCamera(devices as any, this.console);
|
||||
for (const method of descriptor.methods) {
|
||||
AggregateDeviceImpl.prototype[method] = (...args: any[]) => camera[method](...args);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const results = await Promise.all(ret);
|
||||
return results[0];
|
||||
for (const method of descriptor.methods) {
|
||||
AggregateDeviceImpl.prototype[method] = async function (...args: any[]) {
|
||||
const ret: Promise<any>[] = [];
|
||||
for (const device of devices) {
|
||||
ret.push(device[method](...args));
|
||||
}
|
||||
|
||||
const results = await Promise.all(ret);
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...interfaces.keys()];
|
||||
}
|
||||
catch (e) {
|
||||
// this.console.error('error loading aggregate device', e);
|
||||
return [];
|
||||
return [...interfaces.keys()];
|
||||
}
|
||||
catch (e) {
|
||||
// this.console.error('error loading aggregate device', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ret = new AggregateDeviceImpl();
|
||||
ret.computeInterfaces();
|
||||
return new AggregateDeviceImpl();
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import { createAsyncQueue } from "@scrypted/common/src/async-queue";
|
||||
import sdk, { Readme, ScryptedDeviceBase, ScryptedInterface, ScryptedSettings, Setting, Settings } from "@scrypted/sdk";
|
||||
|
||||
export const ClusterCoreNativeId = 'clustercore';
|
||||
|
||||
export class ClusterCore extends ScryptedDeviceBase implements Settings, Readme, ScryptedSettings {
|
||||
writeQueue = createAsyncQueue<() => Promise<void>>();
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
|
||||
(async () => {
|
||||
for await (const write of this.writeQueue.queue) {
|
||||
try {
|
||||
await write();
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('error writing settings', e);
|
||||
}
|
||||
finally {
|
||||
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
const mode = sdk.clusterManager?.getClusterMode?.();
|
||||
if (!mode)
|
||||
return [];
|
||||
|
||||
const workers = await sdk.clusterManager.getClusterWorkers();
|
||||
|
||||
const ret: Setting[] = [];
|
||||
|
||||
const clientWorkers = Object.values(workers);
|
||||
|
||||
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
|
||||
|
||||
for (const worker of clientWorkers) {
|
||||
const group = `Worker: ${worker.name}`;
|
||||
const name: Setting = {
|
||||
key: `${worker.id}:name`,
|
||||
group,
|
||||
title: 'Name',
|
||||
description: 'The friendly name of the worker.',
|
||||
value: worker.name,
|
||||
};
|
||||
ret.push(name);
|
||||
|
||||
const mode: Setting = {
|
||||
key: `${worker.id}:mode`,
|
||||
group,
|
||||
title: 'Mode',
|
||||
description: 'The mode of the worker.',
|
||||
value: worker.mode,
|
||||
readonly: true,
|
||||
};
|
||||
ret.push(mode);
|
||||
|
||||
|
||||
const envControl = await clusterFork.getEnvControl(worker.id);
|
||||
// catch in case env is coming from vscode launch.json and no .env actually exists.
|
||||
const dotEnv: string = await envControl.getDotEnv().catch(() => {});
|
||||
const dotEnvLines = dotEnv?.split('\n') || worker.labels;
|
||||
const dotEnvParsed = dotEnvLines.map(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('#')) {
|
||||
return { line };
|
||||
}
|
||||
const [key, ...value] = trimmed.split('=');
|
||||
return { key, value: value.join('='), line };
|
||||
});
|
||||
|
||||
const workerLabels = dotEnvParsed.find(line => line.key === 'SCRYPTED_CLUSTER_LABELS')?.value?.split(',') || [];
|
||||
|
||||
const labelChoices = new Set<string>([
|
||||
...workerLabels,
|
||||
'storage',
|
||||
'compute',
|
||||
'compute.preferred',
|
||||
'@scrypted/coreml',
|
||||
'@scrypted/openvino',
|
||||
'@scrypted/onnx',
|
||||
'@scrypted/tensorflow-lite',
|
||||
]);
|
||||
const labels: Setting = {
|
||||
key: `${worker.id}:labels`,
|
||||
group,
|
||||
title: 'Labels',
|
||||
description: 'The labels to apply to this worker. Modifying the labels will restart the worker. Some labels, such as the host OS and architecture, cannot be changed.',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
choices: [...labelChoices],
|
||||
value: workerLabels,
|
||||
};
|
||||
ret.push(labels);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: any) {
|
||||
await this.writeQueue.enqueue(async () => {
|
||||
const split = key.split(':');
|
||||
const [workerId, setting] = split;
|
||||
const workers = await sdk.clusterManager.getClusterWorkers();
|
||||
const worker = workers[workerId];
|
||||
if (!worker)
|
||||
return;
|
||||
|
||||
|
||||
switch (setting) {
|
||||
case 'name':
|
||||
case 'labels':
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
|
||||
const envControl = await clusterFork.getEnvControl(worker.id);
|
||||
const dotEnv: string = await envControl.getDotEnv().catch(() => {});
|
||||
|
||||
const dotEnvLines = dotEnv?.split('\n') || worker.labels;
|
||||
const dotEnvParsed = dotEnvLines.map(line => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('#')) {
|
||||
return { line };
|
||||
}
|
||||
const [key, ...value] = trimmed.split('=');
|
||||
return { key, value: value.join('='), line };
|
||||
});
|
||||
|
||||
const updateDotEnv = async (key: string, newValue: string) => {
|
||||
let entry = dotEnvParsed.find(line => line.key === key);
|
||||
if (!entry) {
|
||||
entry = { key, value: '', line: '' };
|
||||
dotEnvParsed.push(entry);
|
||||
}
|
||||
entry.line = `${key}=${newValue}`;
|
||||
await envControl.setDotEnv(dotEnvParsed.filter(line => line).map(line => line.line).join('\n'));
|
||||
};
|
||||
|
||||
if (setting === 'labels') {
|
||||
await updateDotEnv('SCRYPTED_CLUSTER_LABELS', value.join(','));
|
||||
} else if (setting === 'name') {
|
||||
await updateDotEnv('SCRYPTED_CLUSTER_WORKER_NAME', value);
|
||||
}
|
||||
setTimeout(async () => {
|
||||
const serviceControl = await clusterFork.getServiceControl(worker.id);
|
||||
await serviceControl.restart().catch(() => { });
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
return `Manage Scrypted's cluster mode. Run storage devices and compute services on separate servers.`;
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,11 @@ import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
|
||||
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
|
||||
import { LauncherMixin } from './launcher-mixin';
|
||||
import { MediaCore } from './media-core';
|
||||
import { checkLegacyLxc, checkLxc } from './platform/lxc';
|
||||
import { checkLxcDependencies } from './platform/lxc';
|
||||
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
|
||||
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
|
||||
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
|
||||
import { UsersCore, UsersNativeId } from './user';
|
||||
import { ClusterCore, ClusterCoreNativeId } from './cluster';
|
||||
|
||||
const { deviceManager, endpointManager } = sdk;
|
||||
|
||||
@@ -28,7 +27,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
publicRouter: any = Router();
|
||||
mediaCore: MediaCore;
|
||||
scriptCore: ScriptCore;
|
||||
clusterCore: ClusterCore;
|
||||
aggregateCore: AggregateCore;
|
||||
automationCore: AutomationCore;
|
||||
users: UsersCore;
|
||||
@@ -98,23 +96,12 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
settings: "General",
|
||||
}
|
||||
|
||||
checkLegacyLxc();
|
||||
checkLxc();
|
||||
checkLxcDependencies();
|
||||
|
||||
this.storageSettings.settings.releaseChannel.hide = process.env.SCRYPTED_INSTALL_ENVIRONMENT !== 'lxc-docker';
|
||||
|
||||
this.indexHtml = readFileAsString('dist/index.html');
|
||||
|
||||
(async () => {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
name: 'Cluster',
|
||||
nativeId: ClusterCoreNativeId,
|
||||
interfaces: [ScryptedInterface.Settings, ScryptedInterface.Readme, ScryptedInterface.ScryptedSettings],
|
||||
type: ScryptedDeviceType.Builtin,
|
||||
},
|
||||
);
|
||||
})();
|
||||
(async () => {
|
||||
await deviceManager.onDeviceDiscovered(
|
||||
{
|
||||
@@ -227,8 +214,6 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
if (nativeId === ClusterCoreNativeId)
|
||||
return this.clusterCore ||= new ClusterCore(ClusterCoreNativeId);
|
||||
if (nativeId === 'launcher')
|
||||
return new LauncherMixin('launcher');
|
||||
if (nativeId === 'mediacore')
|
||||
|
||||
@@ -1,35 +1,121 @@
|
||||
import fs from 'fs';
|
||||
import sdk from '@scrypted/sdk';
|
||||
import child_process from 'child_process';
|
||||
import { once } from 'events';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
||||
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC = 'lxc';
|
||||
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER = 'lxc-docker';
|
||||
|
||||
export async function checkLegacyLxc() {
|
||||
export async function checkLxcDependencies() {
|
||||
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC)
|
||||
return;
|
||||
|
||||
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/install/proxmox-ve.html#proxmox-ve-container-reset');
|
||||
}
|
||||
|
||||
const DOCKER_COMPOSE_SH_PATH = '/root/.scrypted/docker-compose.sh';
|
||||
const LXC_DOCKER_COMPOSE_SH_PATH = 'lxc/docker-compose.sh';
|
||||
|
||||
export async function checkLxc() {
|
||||
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER)
|
||||
return;
|
||||
|
||||
const foundDockerComposeSh = await fs.promises.readFile(DOCKER_COMPOSE_SH_PATH, 'utf8');
|
||||
const dockerComposeSh = await fs.promises.readFile(LXC_DOCKER_COMPOSE_SH_PATH, 'utf8');
|
||||
|
||||
if (foundDockerComposeSh === dockerComposeSh) {
|
||||
// check if the file is executable
|
||||
const stats = await fs.promises.stat(DOCKER_COMPOSE_SH_PATH);
|
||||
if (stats.mode & 0o111)
|
||||
return;
|
||||
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
|
||||
return;
|
||||
let needRestart = false;
|
||||
if (!process.version.startsWith('v20.')) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'apt update -y && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt install -y nodejs']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install Node.js 20.x.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
|
||||
await fs.promises.copyFile(LXC_DOCKER_COMPOSE_SH_PATH, DOCKER_COMPOSE_SH_PATH);
|
||||
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
|
||||
if (!fs.existsSync('/var/run/avahi-daemon/socket')) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'apt update -y && apt install -y avahi-daemon && apt upgrade -y']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install avahi-daemon.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
|
||||
const scryptedService = fs.readFileSync('lxc/scrypted.service').toString();
|
||||
const installedScryptedService = fs.readFileSync('/etc/systemd/system/scrypted.service').toString();
|
||||
|
||||
if (installedScryptedService !== scryptedService) {
|
||||
fs.writeFileSync('/etc/systemd/system/scrypted.service', scryptedService);
|
||||
needRestart = true;
|
||||
|
||||
const cp = child_process.spawn('systemctl', ['daemon-reload']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to daemon-reload systemd.');
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await new Promise<string>((r, f) => child_process.exec("sh -c 'apt list --installed | grep level-zero/'", (err, stdout, stderr) => {
|
||||
if (err && !stdout && !stderr)
|
||||
f(err);
|
||||
else
|
||||
r(stdout + '\n' + stderr);
|
||||
}));
|
||||
|
||||
const cpuModel = os.cpus()[0].model;
|
||||
if (cpuModel.includes('Core') && cpuModel.includes('Ultra')) {
|
||||
if (
|
||||
// apt
|
||||
!output.includes('level-zero/')
|
||||
) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install intel-driver-compiler-npu.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// level-zero crashes openvino on older CPU due to illegal instruction.
|
||||
// so ensure it is not installed if this is not a core ultra system with npu.
|
||||
if (
|
||||
// apt
|
||||
output.includes('level-zero/')
|
||||
) {
|
||||
const cp = child_process.spawn('apt', ['-y', 'remove', 'level-zero']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
console.log('level-zero removed', exitCode);
|
||||
needRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
sdk.log.a('Failed to verify/install intel-driver-compiler-npu.');
|
||||
}
|
||||
|
||||
try {
|
||||
// intel opencl icd is broken from their official apt repos on kernel versions 6.8, which ships with ubuntu 24.04 and proxmox 8.2.
|
||||
// the intel apt repo has not been updated yet.
|
||||
// the current workaround is to install the release manually.
|
||||
// https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
|
||||
const output = await new Promise<string>((r, f) => child_process.exec("sh -c 'apt show versions intel-opencl-icd'", (err, stdout, stderr) => {
|
||||
if (err && !stdout && !stderr)
|
||||
f(err);
|
||||
else
|
||||
r(stdout + '\n' + stderr);
|
||||
}));
|
||||
|
||||
if (
|
||||
// apt
|
||||
output.includes('Version: 23')
|
||||
// was installed via script at some point
|
||||
|| output.includes('Version: 24.13.29138.7')
|
||||
|| output.includes('Version: 24.26.30049.6')
|
||||
|| output.includes('Version: 24.31.30508.7')
|
||||
// current script version: 24.35.30872.22
|
||||
) {
|
||||
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash']);
|
||||
const [exitCode] = await once(cp, 'exit');
|
||||
if (exitCode !== 0)
|
||||
sdk.log.a('Failed to install intel-opencl-icd.');
|
||||
else
|
||||
needRestart = true;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
sdk.log.a('Failed to verify/install intel-opencl-icd version.');
|
||||
}
|
||||
|
||||
if (needRestart)
|
||||
sdk.log.a('A system update is pending. Please restart Scrypted to apply changes.');
|
||||
}
|
||||
|
||||
@@ -206,7 +206,14 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
|
||||
if (parsed.interactive) {
|
||||
let spawn: typeof ptySpawn;
|
||||
try {
|
||||
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
|
||||
try {
|
||||
spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
|
||||
if (!spawn)
|
||||
throw new Error();
|
||||
}
|
||||
catch (e) {
|
||||
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
|
||||
}
|
||||
cp = new InteractiveTerminal(cmd, extraPaths, spawn);
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
2
plugins/dummy-switch/.vscode/settings.json
vendored
2
plugins/dummy-switch/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
130
plugins/dummy-switch/package-lock.json
generated
130
plugins/dummy-switch/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/dummy-switch",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.24",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/dummy-switch",
|
||||
"version": "0.0.25",
|
||||
"version": "0.0.24",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.6.1",
|
||||
"axios": "^1.3.6"
|
||||
@@ -23,41 +23,35 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.106",
|
||||
"version": "0.2.97",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.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"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -69,9 +63,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -96,11 +92,11 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz",
|
||||
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -125,9 +121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -186,39 +182,35 @@
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@types/node": "^20.11.0",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.5.3"
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
"rimraf": "^3.0.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -232,11 +224,11 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.9",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.6.tgz",
|
||||
"integrity": "sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
@@ -255,9 +247,9 @@
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.25"
|
||||
"version": "0.0.24"
|
||||
}
|
||||
|
||||
@@ -2,60 +2,11 @@ import { BinarySensor, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Loc
|
||||
import sdk from '@scrypted/sdk';
|
||||
import { ReplaceMotionSensor, ReplaceMotionSensorNativeId } from './replace-motion-sensor';
|
||||
import { ReplaceBinarySensor, ReplaceBinarySensorNativeId } from './replace-binary-sensor';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
|
||||
const { log, deviceManager } = sdk;
|
||||
|
||||
class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop, OccupancySensor, MotionSensor, BinarySensor, Settings {
|
||||
timeout: NodeJS.Timeout;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
reset: {
|
||||
title: 'Reset Sensor',
|
||||
description: 'Reset the motion sensor and binary sensor after the given seconds. Enter 0 to never reset.',
|
||||
defaultValue: 10,
|
||||
type: 'number',
|
||||
placeholder: '10',
|
||||
onPut: () => {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
},
|
||||
actionTypes: {
|
||||
title: 'Action Types',
|
||||
description: 'Select the action types to expose.',
|
||||
defaultValue: [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.StartStop,
|
||||
ScryptedInterface.Lock,
|
||||
],
|
||||
multiple: true,
|
||||
choices: [
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.StartStop,
|
||||
ScryptedInterface.Lock,
|
||||
],
|
||||
onPut: () => {
|
||||
this.reportInterfaces();
|
||||
},
|
||||
},
|
||||
sensorTypes: {
|
||||
title: 'Sensor Types',
|
||||
description: 'Select the sensor types to expose.',
|
||||
defaultValue: [
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.OccupancySensor,
|
||||
],
|
||||
multiple: true,
|
||||
choices: [
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.BinarySensor,
|
||||
ScryptedInterface.OccupancySensor,
|
||||
],
|
||||
onPut: () => {
|
||||
this.reportInterfaces();
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
constructor(nativeId: string) {
|
||||
super(nativeId);
|
||||
@@ -68,22 +19,6 @@ class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop,
|
||||
this.occupied = false;
|
||||
}
|
||||
|
||||
async reportInterfaces() {
|
||||
const interfaces: ScryptedInterface[] = this.storageSettings.values.sensorTypes || [];
|
||||
if (!interfaces.length)
|
||||
interfaces.push(ScryptedInterface.MotionSensor, ScryptedInterface.BinarySensor, ScryptedInterface.OccupancySensor);
|
||||
const actionTyoes = this.storageSettings.values.actionTypes || [];
|
||||
if (!actionTyoes.length)
|
||||
actionTyoes.push(ScryptedInterface.OnOff, ScryptedInterface.StartStop, ScryptedInterface.Lock);
|
||||
|
||||
await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId: this.nativeId,
|
||||
interfaces: [...interfaces, ...actionTyoes, ScryptedInterface.Settings],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
name: this.providedName,
|
||||
});
|
||||
}
|
||||
|
||||
lock(): Promise<void> {
|
||||
return this.turnOff();
|
||||
}
|
||||
@@ -96,12 +31,20 @@ class DummyDevice extends ScryptedDeviceBase implements OnOff, Lock, StartStop,
|
||||
stop(): Promise<void> {
|
||||
return this.turnOff();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
return [
|
||||
{
|
||||
key: 'reset',
|
||||
title: 'Reset Sensor',
|
||||
description: 'Reset the motion sensor and binary sensor after the given seconds. Enter 0 to never reset.',
|
||||
value: this.storage.getItem('reset') || '10',
|
||||
placeholder: '10',
|
||||
}
|
||||
]
|
||||
}
|
||||
async putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
this.storage.setItem(key, value.toString());
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
// note that turnOff locks the lock
|
||||
@@ -188,6 +131,12 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
const nativeId = 'shell:' + Math.random().toString();
|
||||
const name = settings.name?.toString();
|
||||
|
||||
await this.onDiscovered(nativeId, name);
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async onDiscovered(nativeId: string, name: string) {
|
||||
await deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
@@ -202,8 +151,6 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
],
|
||||
type: ScryptedDeviceType.Switch,
|
||||
});
|
||||
|
||||
return nativeId;
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
@@ -216,6 +163,11 @@ class DummyDeviceProvider extends ScryptedDeviceBase implements DeviceProvider,
|
||||
if (!ret) {
|
||||
ret = new DummyDevice(nativeId);
|
||||
|
||||
// remove legacy scriptable interface
|
||||
if (ret.interfaces.includes(ScryptedInterface.Scriptable)) {
|
||||
setTimeout(() => this.onDiscovered(ret.nativeId, ret.providedName), 2000);
|
||||
}
|
||||
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"module": "commonjs",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.62",
|
||||
"version": "1.2.61",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.62",
|
||||
"version": "1.2.61",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.4.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.62",
|
||||
"version": "1.2.61",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -519,15 +519,11 @@ export class H264Repacketizer {
|
||||
// after the codec information. so codec information can be changed between
|
||||
// idr and non-idr? maybe it is not applied until next idr?
|
||||
}
|
||||
else if (nalType === NAL_TYPE_IDR) {
|
||||
// this is uncommon but has been seen on tapo.
|
||||
// i have no clue how they can fit an idr frame into a single packet stapa.
|
||||
}
|
||||
else if (nalType === 0) {
|
||||
// nal delimiter or something. usually empty.
|
||||
}
|
||||
else {
|
||||
this.console.warn('Skipped a stapa type.', nalType)
|
||||
this.console.warn('Skipped a stapa type. Please report this to @koush on Discord.', nalType)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
2
plugins/mqtt/.vscode/settings.json
vendored
2
plugins/mqtt/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
4
plugins/mqtt/package-lock.json
generated
4
plugins/mqtt/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/mqtt",
|
||||
"version": "0.0.86",
|
||||
"version": "0.0.82",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/mqtt",
|
||||
"version": "0.0.86",
|
||||
"version": "0.0.82",
|
||||
"dependencies": {
|
||||
"aedes": "^0.46.1",
|
||||
"axios": "^0.23.0",
|
||||
|
||||
@@ -43,5 +43,5 @@
|
||||
"@types/node": "^18.4.2",
|
||||
"@types/nunjucks": "^3.2.0"
|
||||
},
|
||||
"version": "0.0.86"
|
||||
"version": "0.0.82"
|
||||
}
|
||||
|
||||
@@ -1,63 +1,3 @@
|
||||
import type { ScryptedDeviceBase } from "@scrypted/sdk";
|
||||
import type { MqttClient, MqttEvent, MqttSubscriptions } from "./mqtt-client";
|
||||
import type { MqttClient } from "./mqtt-client";
|
||||
|
||||
declare const device: ScryptedDeviceBase;
|
||||
export declare const mqtt: MqttClient;
|
||||
|
||||
export function createSensor(options: {
|
||||
type: string,
|
||||
topic: string,
|
||||
when: (message: MqttEvent) => boolean;
|
||||
set: (value: boolean) => void,
|
||||
delay?: number;
|
||||
}) {
|
||||
const subscriptions: MqttSubscriptions = {};
|
||||
let timeout: NodeJS.Timeout;
|
||||
subscriptions[options.topic] = message => {
|
||||
const detected = options.when(message);
|
||||
|
||||
if (!options.delay) {
|
||||
options.set(detected);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!detected)
|
||||
return;
|
||||
|
||||
options.set(true);
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => options.set(false), options.delay * 1000);
|
||||
};
|
||||
|
||||
mqtt.subscribe(subscriptions);
|
||||
|
||||
mqtt.handleTypes(options.type);
|
||||
}
|
||||
|
||||
export function createMotionSensor(options: {
|
||||
topic: string,
|
||||
when: (message: MqttEvent) => boolean;
|
||||
delay?: number;
|
||||
}) {
|
||||
return createSensor({
|
||||
type: "MotionSensor",
|
||||
topic: options.topic,
|
||||
set: (value: boolean) => device.motionDetected = value,
|
||||
when: options.when,
|
||||
delay: options.delay,
|
||||
})
|
||||
}
|
||||
|
||||
export function createBinarySensor(options: {
|
||||
topic: string,
|
||||
when: (message: MqttEvent) => boolean;
|
||||
delay?: number;
|
||||
}) {
|
||||
return createSensor({
|
||||
type: "BinarySensor",
|
||||
topic: options.topic,
|
||||
set: (value: boolean) => device.binaryState = value,
|
||||
when: options.when,
|
||||
delay: options.delay,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ScriptDeviceImpl, scryptedEval as scryptedEvalBase } from "@scrypted/co
|
||||
|
||||
const util = require("!!raw-loader!./api/util.ts").default;
|
||||
const libs = {
|
||||
util: util.replace('export', ''),
|
||||
util,
|
||||
};
|
||||
|
||||
export async function scryptedEval(device: ScryptedDeviceBase, script: string, params: { [name: string]: any }) {
|
||||
|
||||
2
plugins/objectdetector/.vscode/settings.json
vendored
2
plugins/objectdetector/.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
@@ -13,8 +13,4 @@ benefits to HomeKit, which does its own detection processing.
|
||||
|
||||
## Smart Motion Sensors
|
||||
|
||||
This plugin can be used to create smart motion sensors that trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires cameras with hardware or software object detection capability.
|
||||
|
||||
## Smart Occupancy Sensors
|
||||
|
||||
This plugin can be used to create smart occupancy sensors remains triggered when a specific type of object (vehicle, person, animal, etc) is detected on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.
|
||||
This plugin can be used to create smart motion sensors that trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This feature requires cameras with hardware or software object detection capability.
|
||||
|
||||
198
plugins/objectdetector/package-lock.json
generated
198
plugins/objectdetector/package-lock.json
generated
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.47",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.47",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0"
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.6"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -22,40 +25,34 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^5.0.1",
|
||||
"typescript": "^5.5.3"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.106",
|
||||
"version": "0.3.12",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@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",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.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"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -67,9 +64,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
@@ -89,12 +88,67 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/polygon-clipping": {
|
||||
"version": "0.15.7",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
|
||||
"integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2",
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node-moving-things-tracker": {
|
||||
"version": "0.9.1",
|
||||
"extraneous": true,
|
||||
@@ -118,39 +172,35 @@
|
||||
"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.5.3"
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@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",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
"rimraf": "^3.0.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
@@ -162,11 +212,57 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
|
||||
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
|
||||
"dev": true
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"polygon-clipping": {
|
||||
"version": "0.15.7",
|
||||
"resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz",
|
||||
"integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==",
|
||||
"requires": {
|
||||
"robust-predicates": "^3.0.2",
|
||||
"splaytree": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"robust-predicates": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"splaytree": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz",
|
||||
"integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.1.65",
|
||||
"version": "0.1.47",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -46,9 +46,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"polygon-clipping": "^0.15.7",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0"
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/semver": "^7.5.6"
|
||||
}
|
||||
}
|
||||
|
||||
48
plugins/objectdetector/src/cpu-timer.ts
Normal file
48
plugins/objectdetector/src/cpu-timer.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { CpuInfo, cpus } from 'os';
|
||||
|
||||
function getIdleTotal(cpu: CpuInfo) {
|
||||
const t = cpu.times;
|
||||
const total = t.user + t.nice + t.sys + t.idle + t.irq;
|
||||
const idle = t.idle;
|
||||
return {
|
||||
idle,
|
||||
total,
|
||||
}
|
||||
}
|
||||
|
||||
export class CpuTimer {
|
||||
previousSample: ReturnType<typeof cpus>;
|
||||
maxSpeed = 0;
|
||||
|
||||
sample(): number {
|
||||
const sample = cpus();
|
||||
const previousSample = this.previousSample;
|
||||
this.previousSample = sample;
|
||||
|
||||
// can cpu count change at runtime, who knows
|
||||
if (!previousSample || previousSample.length !== sample.length)
|
||||
return 0;
|
||||
|
||||
// cpu may be throttled in low power mode, so observe total speed to scale
|
||||
let totalSpeed = 0;
|
||||
|
||||
const times = sample.map((v, i) => {
|
||||
totalSpeed += v.speed;
|
||||
const c = getIdleTotal(v);
|
||||
const p = getIdleTotal(previousSample[i]);
|
||||
const total = c.total - p.total;
|
||||
const idle = c.idle - p.idle;
|
||||
return 1 - idle / total;
|
||||
});
|
||||
|
||||
this.maxSpeed = Math.max(this.maxSpeed, totalSpeed);
|
||||
|
||||
// will return a value between 0 and 1, where 1 is full cpu speed
|
||||
// the cpu usage is scaled by the clock speed
|
||||
// so if the cpu is running at 1ghz out of 3ghz, the cpu usage is scaled by 1/3
|
||||
const clockScale = totalSpeed / this.maxSpeed;
|
||||
|
||||
const total = times.reduce((p, c) => p + c, 0);
|
||||
return total / sample.length * clockScale;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator, WritableDeviceState } from '@scrypted/sdk';
|
||||
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, DeviceState, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator, WritableDeviceState } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { CpuTimer } from './cpu-timer';
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
|
||||
import { fixLegacyClipPath, normalizeBox, polygonContainsBoundingBox, polygonIntersectsBoundingBox } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
|
||||
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
|
||||
import { insidePolygon, normalizeBox, polygonOverlap } from './polygon';
|
||||
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor, createObjectDetectorStorageSetting } from './smart-motionsensor';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
|
||||
|
||||
@@ -20,17 +20,6 @@ const defaultMotionDuration = 30;
|
||||
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
|
||||
const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
|
||||
|
||||
// at 5fps object detection speed, the camera is considered throttled.
|
||||
// throttling may be due to cpu, gpu, npu or whatever.
|
||||
// regardless, purging low fps object detection sessions will likely
|
||||
// restore performance.
|
||||
const fpsKillWaterMark = 5
|
||||
const fpsLowWaterMark = 7;
|
||||
// cameras may have low performance due to low framerate or intensive tasks such as
|
||||
// LPR and face recognition. if multiple cams are in low performance mode, then
|
||||
// the system may be struggling.
|
||||
const lowPerformanceMinThreshold = 2;
|
||||
|
||||
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
|
||||
|
||||
type ClipPath = Point[];
|
||||
@@ -96,7 +85,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
|
||||
];
|
||||
return {
|
||||
hide: this.model?.decoder,
|
||||
choices,
|
||||
}
|
||||
},
|
||||
@@ -115,7 +103,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
analyzeStop: number;
|
||||
detectorSignal = new Deferred<void>().resolve();
|
||||
released = false;
|
||||
sampleHistory: number[] = [];
|
||||
// settings: Setting[];
|
||||
|
||||
get detectorRunning() {
|
||||
@@ -187,8 +174,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
else {
|
||||
value = this.storage.getItem(setting.key);
|
||||
if (setting.type === 'number')
|
||||
value = parseFloat(value);
|
||||
}
|
||||
value ||= setting.value;
|
||||
|
||||
@@ -449,18 +434,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
break;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// stop when analyze period ends.
|
||||
if (!this.hasMotionType && this.analyzeStop && now > this.analyzeStop) {
|
||||
if (!this.hasMotionType && this.analyzeStop && Date.now() > this.analyzeStop) {
|
||||
this.analyzeStop = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
this.purgeSampleHistory(now);
|
||||
this.sampleHistory.push(now);
|
||||
|
||||
if (!longObjectDetectionWarning && !this.hasMotionType && now - start > 5 * 60 * 1000) {
|
||||
if (!longObjectDetectionWarning && !this.hasMotionType && Date.now() - start > 5 * 60 * 1000) {
|
||||
longObjectDetectionWarning = true;
|
||||
this.console.warn('Camera has been performing object detection for 5 minutes due to persistent motion. This may adversely affect system performance. Read the Optimizing System Performance guide for tips and tricks. https://github.com/koush/nvr.scrypted.app/wiki/Optimizing-System-Performance')
|
||||
}
|
||||
@@ -471,6 +451,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
const zonedDetections = this.applyZones(detected.detected);
|
||||
detected.detected.detections = zonedDetections;
|
||||
|
||||
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
|
||||
|
||||
if (!this.hasMotionType) {
|
||||
this.plugin.trackDetection();
|
||||
|
||||
@@ -483,6 +465,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
currentDetections.set(d.className, Math.max(currentDetections.get(d.className) || 0, d.score));
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (now > lastReport + 10000) {
|
||||
const found = [...currentDetections.entries()].map(([className, score]) => `${className} (${score})`);
|
||||
if (!found.length)
|
||||
@@ -495,20 +478,23 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
if (detected.detected.detectionId) {
|
||||
updatePipelineStatus('creating jpeg');
|
||||
// const start = Date.now();
|
||||
let { image } = detected.videoFrame;
|
||||
image = await sdk.connectRPCObject(image);
|
||||
const jpeg = await image.toBuffer({
|
||||
format: 'jpg',
|
||||
});
|
||||
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
|
||||
// this.console.log('retain took', Date.now() -start);
|
||||
this.setDetection(detected.detected, mo);
|
||||
// this.console.log('image saved', detected.detected.detections);
|
||||
}
|
||||
const motionFound = this.reportObjectDetections(detected.detected);
|
||||
if (this.hasMotionType) {
|
||||
// if motion is detected, stop processing and exit loop allowing it to sleep.
|
||||
if (motionFound) {
|
||||
// however, when running in analyze mode, continue to allow viewing motion boxes for test purposes.
|
||||
if (!this.analyzeStop || now > this.analyzeStop) {
|
||||
if (!this.analyzeStop || Date.now() > this.analyzeStop) {
|
||||
this.analyzeStop = undefined;
|
||||
clearInterval(interval);
|
||||
return true;
|
||||
@@ -517,25 +503,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
await sleep(250);
|
||||
}
|
||||
updatePipelineStatus('waiting result');
|
||||
// this.handleDetectionEvent(detected.detected);
|
||||
}
|
||||
}
|
||||
|
||||
purgeSampleHistory(now: number) {
|
||||
while (this.sampleHistory.length && now - this.sampleHistory[0] > 10000) {
|
||||
this.sampleHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
get detectionFps() {
|
||||
const now = Date.now();
|
||||
this.purgeSampleHistory(now);
|
||||
const first = this.sampleHistory[0];
|
||||
// require at least 5 seconds of samples.
|
||||
if (!first || (now - first) < 8000)
|
||||
return Infinity;
|
||||
return this.sampleHistory.length / ((now - first) / 1000);
|
||||
}
|
||||
|
||||
applyZones(detection: ObjectsDetected) {
|
||||
// determine zones of the objects, if configured.
|
||||
if (!detection.detections)
|
||||
@@ -553,17 +524,12 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
included = true;
|
||||
else
|
||||
o.zones = [];
|
||||
for (let [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
zoneValue = fixLegacyClipPath(zoneValue);
|
||||
for (const [zone, zoneValue] of Object.entries(this.zones)) {
|
||||
if (zoneValue.length < 3) {
|
||||
// this.console.warn(zone, 'Zone is unconfigured, skipping.');
|
||||
continue;
|
||||
}
|
||||
|
||||
// object detection may report motion, don't filter these at all.
|
||||
if (!this.hasMotionType && o.className === 'motion')
|
||||
continue;
|
||||
|
||||
const zoneInfo = this.zoneInfos[zone];
|
||||
const exclusion = zoneInfo?.filterMode ? zoneInfo.filterMode === 'exclude' : zoneInfo?.exclusion;
|
||||
// track if there are any inclusion zones
|
||||
@@ -572,10 +538,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
let match = false;
|
||||
if (zoneInfo?.type === 'Contain') {
|
||||
match = polygonContainsBoundingBox(zoneValue, box);
|
||||
match = insidePolygon(box[0] as Point, zoneValue) &&
|
||||
insidePolygon(box[1], zoneValue) &&
|
||||
insidePolygon(box[2], zoneValue) &&
|
||||
insidePolygon(box[3], zoneValue);
|
||||
}
|
||||
else {
|
||||
match = polygonIntersectsBoundingBox(zoneValue, box);
|
||||
match = polygonOverlap(box, zoneValue);
|
||||
}
|
||||
|
||||
const classes = zoneInfo?.classes?.length ? zoneInfo?.classes : this.model?.classes || [];
|
||||
@@ -600,8 +569,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// use a default inclusion zone that crops the top and bottom to
|
||||
// prevents errant motion from the on screen time changing every second.
|
||||
if (this.hasMotionType && included === undefined) {
|
||||
const defaultInclusionZone: ClipPath = [[0, .1], [1, .1], [1, .9], [0, .9]];
|
||||
included = polygonIntersectsBoundingBox(defaultInclusionZone, box);
|
||||
const defaultInclusionZone: ClipPath = [[0, 10], [100, 10], [100, 90], [0, 90]];
|
||||
included = polygonOverlap(box, defaultInclusionZone);
|
||||
}
|
||||
|
||||
// if there are inclusion zones and this object
|
||||
@@ -848,7 +817,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (key.startsWith('zone-')) {
|
||||
const zoneName = key.substring('zone-'.length);
|
||||
if (this.zones[zoneName]) {
|
||||
this.zones[zoneName] = Array.isArray(value) ? value : JSON.parse(vs);
|
||||
this.zones[zoneName] = JSON.parse(vs);
|
||||
this.storage.setItem('zones', JSON.stringify(this.zones));
|
||||
}
|
||||
return;
|
||||
@@ -865,10 +834,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const found = this.model.settings?.find(s => s.key === key);
|
||||
if (found?.multiple || found?.type === 'clippath')
|
||||
vs = JSON.stringify(value);
|
||||
if (value && this.model.settings?.find(s => s.key === key)?.multiple) {
|
||||
vs = JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (key === 'analyzeButton') {
|
||||
@@ -1040,12 +1007,14 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
},
|
||||
});
|
||||
devices = new Map<string, any>();
|
||||
cpuTimer = new CpuTimer();
|
||||
cpuUsage = 0;
|
||||
|
||||
constructor(nativeId?: ScryptedNativeId) {
|
||||
super(nativeId, 'v5');
|
||||
|
||||
this.systemDevice = {
|
||||
deviceCreator: 'Smart Sensor',
|
||||
deviceCreator: 'Smart Motion Sensor',
|
||||
};
|
||||
|
||||
process.nextTick(() => {
|
||||
@@ -1059,28 +1028,19 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
})
|
||||
});
|
||||
|
||||
// on an interval check to see if system load allows squelched detectors to start up.
|
||||
setInterval(() => {
|
||||
this.cpuUsage = this.cpuTimer.sample();
|
||||
// this.console.log('cpu usage', Math.round(this.cpuUsage * 100));
|
||||
|
||||
const runningDetections = this.runningObjectDetections;
|
||||
|
||||
// don't allow too many cams to start up at once if resuming from a low performance state.
|
||||
let allowStart = 2;
|
||||
|
||||
// allow minimum amount of concurrent cameras regardless of system specs
|
||||
if (runningDetections.length > lowPerformanceMinThreshold) {
|
||||
// if anything is below the kill threshold, do not start
|
||||
const killable = runningDetections.filter(o => o.detectionFps < fpsKillWaterMark && !o.analyzeStop);
|
||||
if (killable.length > lowPerformanceMinThreshold) {
|
||||
const cameraNames = runningDetections.map(o => `${o.name} ${o.detectionFps}`).join(', ');
|
||||
const first = killable[0];
|
||||
first.console.warn(`System at capacity. Ending object detection.`, cameraNames);
|
||||
first.endObjectDetection();
|
||||
// always allow 2 cameras to push past cpu throttling
|
||||
if (runningDetections.length > 2) {
|
||||
const cpuPerDetector = this.cpuUsage / runningDetections.length;
|
||||
allowStart = Math.ceil(1 / cpuPerDetector) - runningDetections.length;
|
||||
if (allowStart <= 0)
|
||||
return;
|
||||
}
|
||||
|
||||
const lowWatermark = runningDetections.filter(o => o.detectionFps < fpsLowWaterMark);
|
||||
if (lowWatermark.length > lowPerformanceMinThreshold)
|
||||
allowStart = 1;
|
||||
}
|
||||
|
||||
const idleDetectors = [...this.currentMixins.values()]
|
||||
@@ -1094,7 +1054,7 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, 5000)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
checkHasEnabledMixin(device: ScryptedDevice): boolean {
|
||||
@@ -1121,28 +1081,26 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
if (runningDetections.find(o => o.id === mixin.id))
|
||||
return false;
|
||||
|
||||
// allow minimum amount of concurrent cameras regardless of system specs
|
||||
if (runningDetections.length < lowPerformanceMinThreshold)
|
||||
// always allow 2 cameras to push past cpu throttling
|
||||
if (runningDetections.length < 2)
|
||||
return true;
|
||||
|
||||
// find any cameras struggling with a with low detection fps.
|
||||
const lowWatermark = runningDetections.filter(o => o.detectionFps < fpsLowWaterMark);
|
||||
if (lowWatermark.length > lowPerformanceMinThreshold) {
|
||||
const [first] = lowWatermark;
|
||||
// if cameras have been detecting enough to catch the activity, kill it for new camera.
|
||||
const cameraNames = runningDetections.map(o => `${o.name} ${o.detectionFps}`).join(', ');
|
||||
const cpuPerDetector = this.cpuUsage / runningDetections.length;
|
||||
const cpuPercent = Math.round(this.cpuUsage * 100);
|
||||
if (cpuPerDetector * (runningDetections.length + 1) > .9) {
|
||||
const [first] = runningDetections;
|
||||
if (Date.now() - first.detectionStartTime > 30000) {
|
||||
first.console.warn(`System at capacity. Ending object detection to process activity on ${mixin.name}.`, cameraNames);
|
||||
first.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Ending object detection to process activity on ${mixin.name}.`);
|
||||
first.endObjectDetection();
|
||||
mixin.console.warn(`System at capacity. Ending object detection on ${first.name} to process activity.`, cameraNames);
|
||||
mixin.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Ending object detection on ${first.name} to process activity.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
mixin.console.warn(`System at capacity. Not starting object detection to continue processing recent activity on ${first.name}.`, cameraNames);
|
||||
mixin.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Not starting object detection to continue processing recent activity on ${first.name}.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// System capacity is fine. Start the detection.
|
||||
// CPU capacity is fine
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1197,8 +1155,6 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
|
||||
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))
|
||||
ret = this.devices.get(nativeId) || new SmartOccupancySensor(this, nativeId);
|
||||
|
||||
if (ret)
|
||||
this.devices.set(nativeId, ret);
|
||||
@@ -1209,13 +1165,6 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX)) {
|
||||
const smart = this.devices.get(nativeId) as SmartMotionSensor;
|
||||
smart?.detectionListener?.removeListener();
|
||||
smart?.resetMotionTimeout();
|
||||
}
|
||||
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX)) {
|
||||
const smart = this.devices.get(nativeId) as SmartOccupancySensor;
|
||||
smart?.detectionListener?.removeListener();
|
||||
smart?.resetOccupiedTimeout();
|
||||
smart?.clearOccupancyInterval();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1251,71 +1200,32 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
|
||||
|
||||
async getCreateDeviceSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
key: 'sensorType',
|
||||
title: 'Sensor Type',
|
||||
description: 'Select the type of sensor to create.',
|
||||
choices: [
|
||||
'Smart Motion Sensor',
|
||||
'Smart Occupancy Sensor',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'camera',
|
||||
title: 'Camera',
|
||||
description: 'Select a camera or doorbell.',
|
||||
type: 'device',
|
||||
deviceFilter: `type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}'`,
|
||||
},
|
||||
createObjectDetectorStorageSetting(),
|
||||
];
|
||||
}
|
||||
|
||||
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
|
||||
const sensorType = settings.sensorType;
|
||||
const camera = sdk.systemManager.getDeviceById(settings.camera as string);
|
||||
if (sensorType === 'Smart Motion Sensor') {
|
||||
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
|
||||
let name = camera.name || 'New';
|
||||
name += ' Smart Motion Sensor'
|
||||
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
|
||||
const objectDetector = sdk.systemManager.getDeviceById(settings.objectDetector as string);
|
||||
let name = objectDetector.name || 'New';
|
||||
name += ' Smart Motion Sensor'
|
||||
|
||||
const id = await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
const id = await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.Camera,
|
||||
ScryptedInterface.MotionSensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
|
||||
const sensor = new SmartMotionSensor(this, nativeId);
|
||||
sensor.storageSettings.values.objectDetector = camera?.id;
|
||||
const sensor = new SmartMotionSensor(this, nativeId);
|
||||
sensor.storageSettings.values.objectDetector = objectDetector?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
else if (sensorType === 'Smart Occupancy Sensor') {
|
||||
const nativeId = SMART_OCCUPANCYSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
|
||||
let name = camera.name || 'New';
|
||||
name += ' Smart Occupancy Sensor'
|
||||
|
||||
const id = await sdk.deviceManager.onDeviceDiscovered({
|
||||
nativeId,
|
||||
name,
|
||||
type: ScryptedDeviceType.Sensor,
|
||||
interfaces: [
|
||||
ScryptedInterface.OccupancySensor,
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.Readme,
|
||||
]
|
||||
});
|
||||
|
||||
const sensor = new SmartOccupancySensor(this, nativeId);
|
||||
sensor.storageSettings.values.camera = camera?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,118 +1,38 @@
|
||||
import type { ClipPath, Point } from '@scrypted/sdk';
|
||||
import { Point } from '@scrypted/sdk';
|
||||
import polygonClipping from 'polygon-clipping';
|
||||
|
||||
// x y w h
|
||||
export type BoundingBox = [number, number, number, number];
|
||||
/**
|
||||
* Checks if a line segment intersects with another line segment
|
||||
*/
|
||||
function lineIntersects(
|
||||
[x1, y1]: Point,
|
||||
[x2, y2]: Point,
|
||||
[x3, y3]: Point,
|
||||
[x4, y4]: Point
|
||||
): boolean {
|
||||
// Calculate the denominators for intersection check
|
||||
const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
||||
if (denom === 0) return false; // Lines are parallel
|
||||
// const polygonOverlap = require('polygon-overlap');
|
||||
// const insidePolygon = require('point-inside-polygon');
|
||||
|
||||
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
|
||||
const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
|
||||
|
||||
// Check if intersection point lies within both line segments
|
||||
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
|
||||
export function polygonOverlap(p1: Point[], p2: Point[]) {
|
||||
const intersect = polygonClipping.intersection([p1], [p2]);
|
||||
return !!intersect.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a point is inside a polygon using ray casting algorithm
|
||||
*/
|
||||
function pointInPolygon([x, y]: Point, polygon: ClipPath): boolean {
|
||||
let inside = false;
|
||||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||||
const [xi, yi] = polygon[i];
|
||||
const [xj, yj] = polygon[j];
|
||||
export function insidePolygon(point: Point, polygon: Point[]) {
|
||||
const intersect = polygonClipping.intersection([polygon], [[point, [point[0] + 1, point[1]], [point[0] + 1, point[1] + 1]]]);
|
||||
return !!intersect.length;
|
||||
}
|
||||
|
||||
const intersect = ((yi > y) !== (yj > y)) &&
|
||||
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
|
||||
export function normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
|
||||
let [x, y, width, height] = boundingBox;
|
||||
let x2 = x + width;
|
||||
let y2 = y + height;
|
||||
// the zones are point paths in percentage format
|
||||
x = x * 100 / inputDimensions[0];
|
||||
y = y * 100 / inputDimensions[1];
|
||||
x2 = x2 * 100 / inputDimensions[0];
|
||||
y2 = y2 * 100 / inputDimensions[1];
|
||||
return [[x, y], [x2, y], [x2, y2], [x, y2]];
|
||||
}
|
||||
|
||||
if (intersect) inside = !inside;
|
||||
export function polygonArea(p: Point[]): number {
|
||||
let area = 0;
|
||||
const n = p.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
area += p[i][0] * p[j][1];
|
||||
area -= p[j][0] * p[i][1];
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a bounding box to an array of its corner points
|
||||
*/
|
||||
function boundingBoxToPoints([x, y, w, h]: BoundingBox): Point[] {
|
||||
return [
|
||||
[x, y], // top-left
|
||||
[x + w, y], // top-right
|
||||
[x + w, y + h], // bottom-right
|
||||
[x, y + h] // bottom-left
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a polygon intersects with a bounding box
|
||||
*/
|
||||
export function polygonIntersectsBoundingBox(polygon: ClipPath, boundingBox: BoundingBox): boolean {
|
||||
// Get bounding box corners
|
||||
const boxPoints = boundingBoxToPoints(boundingBox);
|
||||
|
||||
// Check if any polygon edge intersects with any bounding box edge
|
||||
for (let i = 0; i < polygon.length; i++) {
|
||||
const nextI = (i + 1) % polygon.length;
|
||||
const polygonPoint1 = polygon[i];
|
||||
const polygonPoint2 = polygon[nextI];
|
||||
|
||||
// Check against all bounding box edges
|
||||
for (let j = 0; j < boxPoints.length; j++) {
|
||||
const nextJ = (j + 1) % boxPoints.length;
|
||||
const boxPoint1 = boxPoints[j];
|
||||
const boxPoint2 = boxPoints[nextJ];
|
||||
|
||||
if (lineIntersects(polygonPoint1, polygonPoint2, boxPoint1, boxPoint2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no edges intersect, check if either shape contains a point from the other
|
||||
if (pointInPolygon(polygon[0], boxPoints) || pointInPolygon(boxPoints[0], polygon))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a polygon completely contains a bounding box
|
||||
*/
|
||||
export function polygonContainsBoundingBox(polygon: ClipPath, boundingBox: BoundingBox): boolean {
|
||||
// Check if all corners of the bounding box are inside the polygon
|
||||
const boxPoints = boundingBoxToPoints(boundingBox);
|
||||
return boxPoints.every(point => pointInPolygon(point, polygon));
|
||||
}
|
||||
|
||||
|
||||
export function normalizeBox(box: BoundingBox, dims: Point): BoundingBox {
|
||||
return [box[0] / dims[0], box[1] / dims[1], box[2] / dims[0], box[3] / dims[1]];
|
||||
}
|
||||
|
||||
export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
|
||||
if (!clipPath)
|
||||
return;
|
||||
|
||||
// if any value is over abs 2, then divide by 100.
|
||||
// this is a workaround for the old scrypted bug where the path was not normalized.
|
||||
// this is a temporary workaround until the path is normalized in the UI.
|
||||
let needNormalize = false;
|
||||
for (const p of clipPath) {
|
||||
for (const c of p) {
|
||||
if (Math.abs(c) >= 2)
|
||||
needNormalize = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needNormalize)
|
||||
return clipPath;
|
||||
|
||||
return clipPath.map(p => p.map(c => c / 100)) as ClipPath;
|
||||
return Math.abs(area / 2);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import sdk, { Camera, EventListenerRegister, MediaObject, MotionSensor, ObjectDetector, ObjectsDetected, Readme, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { levenshteinDistance } from "./edit-distance";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
|
||||
export const SMART_MOTIONSENSOR_PREFIX = 'smart-motionsensor-';
|
||||
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
|
||||
|
||||
export function createObjectDetectorStorageSetting(): StorageSetting {
|
||||
return {
|
||||
key: 'objectDetector',
|
||||
title: 'Object Detector',
|
||||
description: 'Select the camera or doorbell that provides smart detection event.',
|
||||
type: 'device',
|
||||
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
|
||||
};
|
||||
}
|
||||
|
||||
export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, Readme, MotionSensor, Camera {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
objectDetector: {
|
||||
title: 'Camera',
|
||||
description: 'Select a camera or doorbell that provides smart detection events.',
|
||||
type: 'device',
|
||||
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
|
||||
},
|
||||
objectDetector: createObjectDetectorStorageSetting(),
|
||||
detections: {
|
||||
title: 'Detections',
|
||||
description: 'The detections that will trigger this smart motion sensor.',
|
||||
@@ -85,7 +91,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
|
||||
this.storageSettings.settings.detections.onGet = async () => {
|
||||
const objectDetector: ObjectDetector = this.storageSettings.values.objectDetector;
|
||||
const choices = (await objectDetector?.getObjectTypes?.())?.classes || [];
|
||||
const choices = (await objectDetector?.getObjectTypes())?.classes || [];
|
||||
return {
|
||||
hide: !objectDetector,
|
||||
choices,
|
||||
@@ -139,13 +145,13 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
return;
|
||||
}
|
||||
|
||||
resetMotionTimeout() {
|
||||
resetTrigger() {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
}
|
||||
|
||||
trigger() {
|
||||
this.resetMotionTimeout();
|
||||
this.resetTrigger();
|
||||
this.motionDetected = true;
|
||||
const duration: number = this.storageSettings.values.detectionTimeout;
|
||||
if (!duration)
|
||||
@@ -161,7 +167,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
this.detectionListener = undefined;
|
||||
this.motionListener?.removeListener();
|
||||
this.motionListener = undefined;
|
||||
this.resetMotionTimeout();
|
||||
this.resetTrigger();
|
||||
|
||||
|
||||
const objectDetector: ObjectDetector & MotionSensor & ScryptedDevice = this.storageSettings.values.objectDetector;
|
||||
@@ -172,6 +178,8 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
if (!detections?.length)
|
||||
return;
|
||||
|
||||
const console = sdk.deviceManager.getMixinConsole(objectDetector.id, this.nativeId);
|
||||
|
||||
this.motionListener = objectDetector.listen({
|
||||
event: ScryptedInterface.MotionSensor,
|
||||
watch: true,
|
||||
@@ -250,7 +258,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
|
||||
if (match) {
|
||||
if (!this.motionDetected)
|
||||
this.console.log('Smart Motion Sensor triggered on', match);
|
||||
console.log('Smart Motion Sensor triggered on', match);
|
||||
if (detected.detectionId)
|
||||
this.lastPicture = objectDetector.getDetectionInput(detected.detectionId, details.eventId);
|
||||
this.trigger();
|
||||
@@ -270,6 +278,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
|
||||
return `
|
||||
## Smart Motion Sensor
|
||||
|
||||
This Smart Motion Sensor can trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
|
||||
This Smart Motion Sensor can trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
import sdk, { Camera, ClipPath, EventListenerRegister, Image, ObjectDetection, ObjectDetector, ObjectsDetected, OccupancySensor, Readme, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { levenshteinDistance } from "./edit-distance";
|
||||
import type { ObjectDetectionPlugin } from "./main";
|
||||
import { normalizeBox, polygonIntersectsBoundingBox } from "./polygon";
|
||||
|
||||
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
|
||||
|
||||
const nvrAcceleratedMotionSensorId = sdk.systemManager.getDeviceById('@scrypted/nvr', 'motion')?.id;
|
||||
|
||||
export class SmartOccupancySensor extends ScryptedDeviceBase implements Settings, Readme, OccupancySensor {
|
||||
storageSettings = new StorageSettings(this, {
|
||||
camera: {
|
||||
title: 'Camera',
|
||||
description: 'Select the camera or doorbell image to analyze periodically.',
|
||||
type: 'device',
|
||||
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.Camera}')`,
|
||||
immediate: true,
|
||||
},
|
||||
objectDetection: {
|
||||
title: 'Object Detector',
|
||||
description: 'Select the object detection plugin to use for detecting objects.',
|
||||
type: 'device',
|
||||
deviceFilter: `interfaces.includes('ObjectDetectionPreview') && id !== '${nvrAcceleratedMotionSensorId}'`,
|
||||
immediate: true,
|
||||
},
|
||||
detections: {
|
||||
title: 'Detections',
|
||||
description: 'The detections that will trigger this occupancy sensor.',
|
||||
multiple: true,
|
||||
choices: [],
|
||||
},
|
||||
occupancyInterval: {
|
||||
title: 'Occupancy Check Interval',
|
||||
description: 'The interval in minutes that the sensor will check for occupancy.',
|
||||
type: 'number',
|
||||
defaultValue: 60,
|
||||
// save and restore in seconds for consistency.
|
||||
mapPut(oldValue, newValue) {
|
||||
return newValue * 60;
|
||||
},
|
||||
mapGet(value) {
|
||||
return value / 60;
|
||||
},
|
||||
},
|
||||
zone: {
|
||||
title: 'Edit Intersect Zone',
|
||||
description: 'Optional: Configure the intersect zone for the occupancy check. Objects intersecting this zone will trigger the occupancy sensor.',
|
||||
type: 'clippath',
|
||||
},
|
||||
captureZone: {
|
||||
title: 'Edit Crop Zone',
|
||||
description: 'Optional: Configure the capture zone for the occupancy check. The image will be cropped to this zone before detection. Cropping to desired location will improve detection performance.',
|
||||
type: 'clippath',
|
||||
},
|
||||
minScore: {
|
||||
title: 'Minimum Score',
|
||||
description: 'The minimum score required for a detection to trigger the occupancy sensor.',
|
||||
type: 'number',
|
||||
defaultValue: 0.4,
|
||||
},
|
||||
labels: {
|
||||
group: 'Recognition',
|
||||
title: 'Labels',
|
||||
description: 'The labels (license numbers, names) that will trigger this smart occupancy sensor.',
|
||||
multiple: true,
|
||||
combobox: true,
|
||||
choices: [],
|
||||
},
|
||||
labelDistance: {
|
||||
group: 'Recognition',
|
||||
title: 'Label Distance',
|
||||
description: 'The maximum edit distance between the detected label and the desired label. Ie, a distance of 1 will match "abcde" to "abcbe" or "abcd".',
|
||||
type: 'number',
|
||||
defaultValue: 2,
|
||||
},
|
||||
labelScore: {
|
||||
group: 'Recognition',
|
||||
title: 'Label Score',
|
||||
description: 'The minimum score required for a label to trigger the occupancy sensor.',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
}
|
||||
});
|
||||
|
||||
detectionListener: EventListenerRegister;
|
||||
occupancyTimeout: NodeJS.Timeout;
|
||||
occupancyInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(public plugin: ObjectDetectionPlugin, nativeId?: ScryptedNativeId) {
|
||||
super(nativeId);
|
||||
|
||||
this.storageSettings.settings.zone.onGet = async () => {
|
||||
return {
|
||||
deviceFilter: this.storageSettings.values.camera?.id,
|
||||
}
|
||||
};
|
||||
|
||||
this.storageSettings.settings.captureZone.onGet = async () => {
|
||||
return {
|
||||
deviceFilter: this.storageSettings.values.camera?.id,
|
||||
}
|
||||
};
|
||||
|
||||
this.storageSettings.settings.detections.onGet = async () => {
|
||||
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
|
||||
const choices = (await objectDetection?.getDetectionModel())?.classes || [];
|
||||
return {
|
||||
hide: !objectDetection,
|
||||
choices,
|
||||
};
|
||||
};
|
||||
|
||||
this.storageSettings.settings.detections.onPut = () => this.rebind();
|
||||
this.storageSettings.settings.objectDetection.onPut = () => this.rebind();
|
||||
this.storageSettings.settings.zone.onPut = () => this.rebind();
|
||||
this.storageSettings.settings.captureZone.onPut = () => this.rebind();
|
||||
|
||||
this.rebind();
|
||||
}
|
||||
|
||||
resetOccupiedTimeout() {
|
||||
clearTimeout(this.occupancyTimeout);
|
||||
this.occupancyTimeout = undefined;
|
||||
}
|
||||
|
||||
clearOccupancyInterval() {
|
||||
clearInterval(this.occupancyInterval);
|
||||
this.occupancyInterval = undefined;
|
||||
}
|
||||
|
||||
trigger() {
|
||||
this.resetOccupiedTimeout();
|
||||
this.occupied = true;
|
||||
const duration: number = this.storageSettings.values.occupancyInterval;
|
||||
if (!duration)
|
||||
return;
|
||||
this.occupancyTimeout = setTimeout(() => {
|
||||
this.occupied = false;
|
||||
}, duration * 60000 + 10000);
|
||||
}
|
||||
|
||||
checkDetection(detections: string[], labels: string[], labelDistance: number, labelScore: number, detected: ObjectsDetected) {
|
||||
const match = detected.detections?.find(d => {
|
||||
if (d.score && d.score < this.storageSettings.values.minScore)
|
||||
return false;
|
||||
if (!detections?.includes(d.className))
|
||||
return false;
|
||||
const zone: ClipPath = this.storageSettings.values.zone;
|
||||
if (zone?.length >= 3) {
|
||||
if (!d.boundingBox)
|
||||
return false;
|
||||
const detectionBox = normalizeBox(d.boundingBox, detected.inputDimensions);
|
||||
if (!polygonIntersectsBoundingBox(zone, detectionBox))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!labels?.length)
|
||||
return true;
|
||||
|
||||
if (!d.label)
|
||||
return false;
|
||||
|
||||
for (const label of labels) {
|
||||
if (label === d.label) {
|
||||
if (!labelScore || d.labelScore >= labelScore)
|
||||
return true;
|
||||
this.console.log('Label score too low.', d.labelScore);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!labelDistance)
|
||||
continue;
|
||||
|
||||
if (levenshteinDistance(label, d.label) > labelDistance) {
|
||||
this.console.log('Label does not match.', label, d.label, d.labelScore);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!labelScore || d.labelScore >= labelScore)
|
||||
return true;
|
||||
this.console.log('Label score too low.', d.labelScore);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (match) {
|
||||
if (!this.occupied)
|
||||
this.console.log('Occupancy Sensor triggered on', match);
|
||||
this.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
async runDetection() {
|
||||
try {
|
||||
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
|
||||
if (!objectDetection) {
|
||||
this.console.error('no object detection plugin selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const camera: ScryptedDevice & Camera = this.storageSettings.values.camera;
|
||||
if (!camera) {
|
||||
this.console.error('no camera selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const picture = await camera.takePicture({
|
||||
reason: 'event',
|
||||
});
|
||||
const zone: ClipPath = this.storageSettings.values.captureZone;
|
||||
let detected: ObjectsDetected;
|
||||
if (zone?.length >= 3) {
|
||||
const image = await sdk.mediaManager.convertMediaObject<Image>(picture, ScryptedMimeTypes.Image);
|
||||
let left = image.width;
|
||||
let top = image.height;
|
||||
let right = 0;
|
||||
let bottom = 0;
|
||||
for (const point of zone) {
|
||||
left = Math.min(left, point[0]);
|
||||
top = Math.min(top, point[1]);
|
||||
right = Math.max(right, point[0]);
|
||||
bottom = Math.max(bottom, point[1]);
|
||||
}
|
||||
|
||||
left = left * image.width;
|
||||
top = top * image.height;
|
||||
right = right * image.width;
|
||||
bottom = bottom * image.height;
|
||||
|
||||
let width = right - left;
|
||||
let height = bottom - top;
|
||||
// square it for standard detection
|
||||
width = height = Math.max(width, height);
|
||||
// recenter it
|
||||
left = left + (right - left - width) / 2;
|
||||
top = top + (bottom - top - height) / 2;
|
||||
// ensure bounds are within image.
|
||||
left = Math.max(0, left);
|
||||
top = Math.max(0, top);
|
||||
width = Math.min(width, image.width - left);
|
||||
height = Math.min(height, image.height - top);
|
||||
|
||||
const cropped = await image.toImage({
|
||||
crop: {
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
});
|
||||
detected = await objectDetection.detectObjects(cropped);
|
||||
|
||||
// adjust the origin of the bounding boxes for the crop.
|
||||
for (const d of detected.detections) {
|
||||
d.boundingBox[0] += left;
|
||||
d.boundingBox[1] += top;
|
||||
}
|
||||
detected.inputDimensions = [image.width, image.height];
|
||||
}
|
||||
else {
|
||||
detected = await objectDetection.detectObjects(picture);
|
||||
}
|
||||
|
||||
this.checkDetection(this.storageSettings.values.detections, this.storageSettings.values.labels, this.storageSettings.values.labelDistance, this.storageSettings.values.labelScore, detected);
|
||||
}
|
||||
catch (e) {
|
||||
this.console.error('failed to take picture', e);
|
||||
}
|
||||
}
|
||||
|
||||
rebind() {
|
||||
this.occupied = false;
|
||||
this.detectionListener?.removeListener();
|
||||
this.detectionListener = undefined;
|
||||
this.resetOccupiedTimeout();
|
||||
this.clearOccupancyInterval();
|
||||
|
||||
this.runDetection();
|
||||
this.occupancyInterval = setInterval(() => {
|
||||
this.runDetection();
|
||||
}, this.storageSettings.values.occupancyInterval * 60000);
|
||||
|
||||
// camera may have an object detector that can also be observed for occupancy for free.
|
||||
const objectDetector: ObjectDetector & ScryptedDevice = this.storageSettings.values.camera;
|
||||
if (!objectDetector)
|
||||
return;
|
||||
|
||||
const detections: string[] = this.storageSettings.values.detections;
|
||||
if (!detections?.length)
|
||||
return;
|
||||
|
||||
const { labels, labelDistance, labelScore } = this.storageSettings.values;
|
||||
|
||||
this.detectionListener = objectDetector.listen(ScryptedInterface.ObjectDetector, (source, details, data) => {
|
||||
const detected: ObjectsDetected = data;
|
||||
this.checkDetection(detections, labels, labelDistance, labelScore, detected);
|
||||
});
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async getReadmeMarkdown(): Promise<string> {
|
||||
return `
|
||||
## Smart Occupancy Sensor
|
||||
|
||||
This Occupancy Sensor remains triggered while specified objects (vehicle, person, animal, etc) are detected on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.`;
|
||||
}
|
||||
}
|
||||
4
plugins/openvino/package-lock.json
generated
4
plugins/openvino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.137",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.153",
|
||||
"version": "0.1.137",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.153"
|
||||
"version": "0.1.137"
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@ from predict.rectangle import Rectangle
|
||||
|
||||
defaultThreshold = .2
|
||||
|
||||
def parse_yolov10(results, threshold = defaultThreshold, scale = None, confidence_scale = None, threshold_scale = None):
|
||||
def parse_yolov10(results, threshold = defaultThreshold, scale = None, confidence_scale = None):
|
||||
objs: list[Prediction] = []
|
||||
if not threshold_scale:
|
||||
keep = np.argwhere(results[4:] > threshold)
|
||||
else:
|
||||
keep = np.argwhere(results[4:] > threshold_scale(results[4:]))
|
||||
keep = np.argwhere(results[4:] > threshold)
|
||||
for indices in keep:
|
||||
class_id = indices[0]
|
||||
index = indices[1]
|
||||
@@ -55,12 +52,9 @@ def parse_yolo_nas(predictions):
|
||||
objs.append(obj)
|
||||
return objs
|
||||
|
||||
def parse_yolov9(results, threshold = defaultThreshold, scale = None, confidence_scale = None, threshold_scale = None):
|
||||
def parse_yolov9(results, threshold = defaultThreshold, scale = None, confidence_scale = None):
|
||||
objs: list[Prediction] = []
|
||||
if not threshold_scale:
|
||||
keep = np.argwhere(results[4:] > threshold)
|
||||
else:
|
||||
keep = np.argwhere(threshold_scale(results[4:]) > threshold)
|
||||
keep = np.argwhere(results[4:] > threshold)
|
||||
for indices in keep:
|
||||
class_id = indices[0]
|
||||
index = indices[1]
|
||||
|
||||
@@ -25,22 +25,12 @@ try:
|
||||
except:
|
||||
OpenVINOTextRecognition = None
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Predict"
|
||||
)
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
thread_name_prefix="OpenVINO-Prepare"
|
||||
)
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "OpenVINO-Predict")
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "OpenVINO-Prepare")
|
||||
|
||||
availableModels = [
|
||||
"Default",
|
||||
"scrypted_yolov9c_relu_int8_320",
|
||||
"scrypted_yolov9s_relu_int8_320",
|
||||
"scrypted_yolov9t_relu_int8_320",
|
||||
"scrypted_yolov9c_int8_320",
|
||||
"scrypted_yolov9m_int8_320",
|
||||
"scrypted_yolov9s_int8_320",
|
||||
"scrypted_yolov9t_int8_320",
|
||||
"scrypted_yolov10m_320",
|
||||
"scrypted_yolov10s_320",
|
||||
"scrypted_yolov10n_320",
|
||||
@@ -52,6 +42,10 @@ availableModels = [
|
||||
"scrypted_yolov9s_320",
|
||||
"scrypted_yolov9t_320",
|
||||
"scrypted_yolov8n_320",
|
||||
"ssd_mobilenet_v1_coco",
|
||||
"ssdlite_mobilenet_v2",
|
||||
"yolo-v3-tiny-tf",
|
||||
"yolo-v4-tiny-tf",
|
||||
]
|
||||
|
||||
|
||||
@@ -137,25 +131,21 @@ class OpenVINOPlugin(
|
||||
except:
|
||||
pass
|
||||
|
||||
# AUTO mode can cause conflicts or hide errors with NPU and GPU
|
||||
# so try to be explicit and fall back accordingly.
|
||||
mode = self.storage.getItem("mode") or "Default"
|
||||
if mode == "Default":
|
||||
mode = "AUTO"
|
||||
|
||||
if npu:
|
||||
mode = "NPU"
|
||||
if gpu:
|
||||
mode = f"AUTO:NPU,GPU,CPU"
|
||||
else:
|
||||
mode = f"AUTO:NPU,CPU"
|
||||
elif len(dgpus):
|
||||
mode = f"AUTO:{','.join(dgpus)},CPU"
|
||||
# forcing GPU can cause crashes on older GPU.
|
||||
elif gpu:
|
||||
mode = f"GPU"
|
||||
|
||||
# recognition models are not supported on NPU.
|
||||
self.recognition_mode = mode
|
||||
if "NPU" in mode:
|
||||
self.recognition_mode = "AUTO"
|
||||
|
||||
mode = mode or "AUTO"
|
||||
self.mode = mode
|
||||
|
||||
@@ -165,18 +155,14 @@ class OpenVINOPlugin(
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default" or model not in availableModels:
|
||||
# relu + int8 wins out by a mile on gpu and npu.
|
||||
# observation is that silu + float is faster than silu + int8 on gpu.
|
||||
# possibly due to quantization causing complexity at the activation function?
|
||||
# however, silu + int8 is faster than silu + float on cpu. (tested on wyse 5070)
|
||||
if model != "Default":
|
||||
self.storage.setItem("model", "Default")
|
||||
if arc or nvidia or npu:
|
||||
model = "scrypted_yolov9c_relu_int8_320"
|
||||
model = "scrypted_yolov9c_320"
|
||||
elif iris_xe:
|
||||
model = "scrypted_yolov9s_relu_int8_320"
|
||||
model = "scrypted_yolov9s_320"
|
||||
else:
|
||||
model = "scrypted_yolov9t_relu_int8_320"
|
||||
model = "scrypted_yolov9t_320"
|
||||
self.yolo = "yolo" in model
|
||||
self.scrypted_yolov9 = "scrypted_yolov9" in model
|
||||
self.scrypted_yolov10 = "scrypted_yolov10" in model
|
||||
@@ -245,45 +231,6 @@ class OpenVINOPlugin(
|
||||
self.storage.removeItem("precision")
|
||||
self.requestRestart()
|
||||
|
||||
self.infer_queue = ov.AsyncInferQueue(self.compiled_model)
|
||||
|
||||
def predict(output):
|
||||
if not self.yolo:
|
||||
objs = []
|
||||
for values in output[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
r = torelative(r)
|
||||
b = torelative(b)
|
||||
|
||||
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output[0])
|
||||
if self.scrypted_yolo_nas:
|
||||
return yolo.parse_yolo_nas([output[1], output[0]])
|
||||
return yolo.parse_yolov9(output[0])
|
||||
|
||||
def callback(infer_request, future: asyncio.Future):
|
||||
try:
|
||||
output = infer_request.get_output_tensor(0).data
|
||||
objs = predict(output)
|
||||
self.loop.call_soon_threadsafe(future.set_result, objs)
|
||||
except Exception as e:
|
||||
self.loop.call_soon_threadsafe(future.set_exception, e)
|
||||
|
||||
self.infer_queue.set_callback(callback)
|
||||
|
||||
print(
|
||||
"EXECUTION_DEVICES",
|
||||
self.compiled_model.get_property("EXECUTION_DEVICES"),
|
||||
@@ -357,6 +304,69 @@ class OpenVINOPlugin(
|
||||
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()
|
||||
infer_request.set_input_tensor(input_tensor)
|
||||
output_tensors = infer_request.infer()
|
||||
|
||||
objs = []
|
||||
|
||||
if self.scrypted_yolo:
|
||||
if self.scrypted_yolov10:
|
||||
return yolo.parse_yolov10(output_tensors[0][0])
|
||||
if self.scrypted_yolo_nas:
|
||||
return yolo.parse_yolo_nas([output_tensors[1], output_tensors[0]])
|
||||
return yolo.parse_yolov9(output_tensors[0][0])
|
||||
|
||||
if self.yolo:
|
||||
# index 2 will always either be 13 or 26
|
||||
# index 1 may be 13/26 or 255 depending on yolo 3 vs 4
|
||||
if infer_request.outputs[0].data.shape[2] == 13:
|
||||
out_blob = infer_request.outputs[0]
|
||||
else:
|
||||
out_blob = infer_request.outputs[1]
|
||||
|
||||
# 13 13
|
||||
objects = yolo.parse_yolo_region(
|
||||
out_blob.data,
|
||||
(input.width, input.height),
|
||||
(81, 82, 135, 169, 344, 319),
|
||||
self.sigmoid,
|
||||
)
|
||||
|
||||
for r in objects:
|
||||
obj = Prediction(
|
||||
r["classId"],
|
||||
r["confidence"],
|
||||
Rectangle(r["xmin"], r["ymin"], r["xmax"], r["ymax"]),
|
||||
)
|
||||
objs.append(obj)
|
||||
|
||||
# what about output[1]?
|
||||
# 26 26
|
||||
# objects = yolo.parse_yolo_region(out_blob, (input.width, input.height), (,27, 37,58, 81,82))
|
||||
|
||||
return objs
|
||||
|
||||
output = infer_request.get_output_tensor(0)
|
||||
for values in output.data[0][0]:
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
r = torelative(r)
|
||||
b = torelative(b)
|
||||
|
||||
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
def prepare():
|
||||
# the input_tensor can be created with the shared_memory=True parameter,
|
||||
# but that seems to cause issues on some platforms.
|
||||
@@ -377,19 +387,23 @@ class OpenVINOPlugin(
|
||||
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)
|
||||
elif self.yolo:
|
||||
im = np.expand_dims(np.array(input), axis=0).astype(np.float32)
|
||||
input_tensor = ov.Tensor(
|
||||
array=np.expand_dims(np.array(input), axis=0).astype(np.float32)
|
||||
)
|
||||
else:
|
||||
im = np.expand_dims(np.array(input), axis=0)
|
||||
return im
|
||||
input_tensor = ov.Tensor(array=np.expand_dims(np.array(input), axis=0))
|
||||
return input_tensor
|
||||
|
||||
try:
|
||||
input_tensor = await asyncio.get_event_loop().run_in_executor(
|
||||
prepareExecutor, lambda: prepare()
|
||||
)
|
||||
f = asyncio.Future(loop=self.loop)
|
||||
self.infer_queue.start_async(input_tensor, f)
|
||||
objs = await f
|
||||
objs = await asyncio.get_event_loop().run_in_executor(
|
||||
predictExecutor, lambda: predict(input_tensor)
|
||||
)
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
@@ -16,10 +16,6 @@ faceRecognizePrepare, faceRecognizePredict = async_infer.create_executors(
|
||||
|
||||
|
||||
class OpenVINOFaceRecognition(FaceRecognizeDetection):
|
||||
def __init__(self, plugin, nativeId: str):
|
||||
super().__init__(plugin=plugin, nativeId=nativeId)
|
||||
self.prefer_relu = True
|
||||
|
||||
def downloadModel(self, model: str):
|
||||
scrypted_yolov9 = "scrypted_yolov9" in model
|
||||
ovmodel = "best-converted" if scrypted_yolov9 else "best"
|
||||
@@ -34,7 +30,7 @@ class OpenVINOFaceRecognition(FaceRecognizeDetection):
|
||||
f"{model_version}/{model}/{precision}/{ovmodel}.bin",
|
||||
)
|
||||
print(xmlFile, binFile)
|
||||
return self.plugin.core.compile_model(xmlFile, self.plugin.recognition_mode)
|
||||
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
|
||||
|
||||
async def predictDetectModel(self, input: Image.Image):
|
||||
def predict():
|
||||
|
||||
@@ -28,7 +28,7 @@ class OpenVINOTextRecognition(TextRecognition):
|
||||
f"{model_version}/{model}/{precision}/{ovmodel}.bin",
|
||||
)
|
||||
print(xmlFile, binFile)
|
||||
return self.plugin.core.compile_model(xmlFile, self.plugin.recognition_mode)
|
||||
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
|
||||
|
||||
async def predictDetectModel(self, input: np.ndarray):
|
||||
def predict():
|
||||
|
||||
@@ -26,9 +26,6 @@ class FaceRecognizeDetection(PredictPlugin):
|
||||
def __init__(self, plugin: PredictPlugin, nativeId: str):
|
||||
super().__init__(nativeId=nativeId, plugin=plugin)
|
||||
|
||||
if not hasattr(self, "prefer_relu"):
|
||||
self.prefer_relu = False
|
||||
|
||||
self.inputheight = 320
|
||||
self.inputwidth = 320
|
||||
|
||||
@@ -38,7 +35,7 @@ class FaceRecognizeDetection(PredictPlugin):
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.minThreshold = 0.5
|
||||
|
||||
self.detectModel = self.downloadModel("scrypted_yolov9t_relu_face_320" if self.prefer_relu else "scrypted_yolov9t_face_320")
|
||||
self.detectModel = self.downloadModel("scrypted_yolov9t_face_320")
|
||||
self.faceModel = self.downloadModel("inception_resnet_v1")
|
||||
|
||||
def downloadModel(self, model: str):
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# openvino 2024.5.0 crashes NPU. Update: NPU can not be used with AUTO in this version
|
||||
# openvino 2024.4.0 crashes legacy systems.
|
||||
# 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.5.0
|
||||
openvino==2024.4.0
|
||||
Pillow==10.3.0
|
||||
opencv-python-headless==4.10.0.84
|
||||
|
||||
@@ -17,3 +17,7 @@ Medium: 720p (500 Kbps)
|
||||
Low (if available): 320p (100 Kbps)
|
||||
|
||||
The `Key Frame (IDR) Interval` should be set to `4` seconds. This setting is usually configured in frames. So if the camera frame rate is `30`, the interval would be `120`. If the camera frame rate is `15` the interval would be `60`. The value can be calculated as `IDR Interval = FPS * 4`.
|
||||
|
||||
## Transcoding
|
||||
|
||||
Some cameras may not allow configuration of the video codec (h264) or IDR Interval. The camera may also only have a single high bitrate stream which will fail to stream when viewing on low bandwidth remote connections. In this case, Transcoding should be enabled for `Remote Stream` and `Remote Recording Stream` to ensure there isn't a bandwidth issue.
|
||||
|
||||
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.43",
|
||||
"version": "0.10.38",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.38",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.38",
|
||||
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
@@ -26,7 +26,7 @@
|
||||
"name": "Rebroadcast Plugin",
|
||||
"type": "API",
|
||||
"interfaces": [
|
||||
"Settings",
|
||||
"DeviceProvider",
|
||||
"MixinProvider",
|
||||
"BufferConverter"
|
||||
],
|
||||
|
||||
@@ -2,33 +2,19 @@ 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)
|
||||
return;
|
||||
urls = addresses.map(address => {
|
||||
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
|
||||
}
|
||||
|
||||
if (process.env.SCRYPTED_CLUSTER_ADDRESS) {
|
||||
try {
|
||||
const clusterUrl = new URL(url);
|
||||
clusterUrl.hostname = process.env.SCRYPTED_CLUSTER_ADDRESS;
|
||||
const str = clusterUrl.toString();
|
||||
if (!urls.includes(str))
|
||||
urls.push(str);
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Error determining external addresses. Is Scrypted Cluster Address configured?', e);
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
|
||||
import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/common/src/ffmpeg-hardware-acceleration';
|
||||
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';
|
||||
@@ -7,7 +8,7 @@ import { addTrackControls, getSpsPps, parseSdp } from '@scrypted/common/src/sdp-
|
||||
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
|
||||
import sdk, { BufferConverter, ChargeState, EventListenerRegister, FFmpegInput, ForkWorker, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoCameraConfiguration, WritableDeviceState } from '@scrypted/sdk';
|
||||
import sdk, { BufferConverter, ChargeState, DeviceProvider, EventListenerRegister, FFmpegInput, ForkWorker, H264Info, MediaObject, MediaStreamDestination, MediaStreamOptions, MixinProvider, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, SettingValue, Settings, VideoCamera, VideoCameraConfiguration, WritableDeviceState } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import { once } from 'events';
|
||||
@@ -23,6 +24,7 @@ import { connectRFC4571Parser, startRFC4571Parser } from './rfc4571';
|
||||
import { RtspSessionParserSpecific, startRtspSession } from './rtsp-session';
|
||||
import { getSpsResolution } from './sps-resolution';
|
||||
import { createStreamSettings } from './stream-settings';
|
||||
import { TRANSCODE_MIXIN_PROVIDER_NATIVE_ID, TranscodeMixinProvider, getTranscodeMixinProviderId } from './transcode-settings';
|
||||
|
||||
const { mediaManager, log, systemManager, deviceManager } = sdk;
|
||||
|
||||
@@ -71,7 +73,7 @@ class PrebufferSession {
|
||||
|
||||
activeClients = 0;
|
||||
inactivityTimeout: NodeJS.Timeout;
|
||||
syntheticInputIdKey: string;
|
||||
audioConfigurationKey: string;
|
||||
ffmpegInputArgumentsKey: string;
|
||||
ffmpegOutputArgumentsKey: string;
|
||||
lastDetectedAudioCodecKey: string;
|
||||
@@ -87,7 +89,7 @@ class PrebufferSession {
|
||||
this.storage = mixin.storage;
|
||||
this.console = mixin.console;
|
||||
this.mixinDevice = mixin.mixinDevice;
|
||||
this.syntheticInputIdKey = 'syntheticInputIdKey-' + this.streamId;
|
||||
this.audioConfigurationKey = 'audioConfiguration-' + this.streamId;
|
||||
this.ffmpegInputArgumentsKey = 'ffmpegInputArguments-' + this.streamId;
|
||||
this.ffmpegOutputArgumentsKey = 'ffmpegOutputArguments-' + this.streamId;
|
||||
this.lastDetectedAudioCodecKey = 'lastDetectedAudioCodec-' + this.streamId;
|
||||
@@ -115,6 +117,10 @@ class PrebufferSession {
|
||||
return !this.enabled || this.shouldDisableBatteryPrebuffer();
|
||||
}
|
||||
|
||||
get canPrebuffer() {
|
||||
return (this.advertisedMediaStreamOptions.container !== 'rawvideo' && this.advertisedMediaStreamOptions.container !== 'ffmpeg') || this.storage.getItem(this.ffmpegOutputArgumentsKey);
|
||||
}
|
||||
|
||||
getLastH264Probe(): H264Info {
|
||||
const str = this.storage.getItem(this.lastH264ProbeKey);
|
||||
if (!str) {
|
||||
@@ -224,20 +230,12 @@ class PrebufferSession {
|
||||
|
||||
getParser(mediaStreamOptions: MediaStreamOptions) {
|
||||
let parser: string;
|
||||
let rtspParser = this.storage.getItem(this.rtspParserKey);
|
||||
|
||||
let isDefault = !rtspParser || rtspParser === 'Default';
|
||||
const rtspParser = this.storage.getItem(this.rtspParserKey);
|
||||
|
||||
if (!this.canUseRtspParser(mediaStreamOptions)) {
|
||||
parser = STRING_DEFAULT;
|
||||
isDefault = true;
|
||||
rtspParser = undefined;
|
||||
}
|
||||
else {
|
||||
if (isDefault) {
|
||||
// use the plugin default
|
||||
rtspParser = localStorage.getItem('defaultRtspParser');
|
||||
}
|
||||
switch (rtspParser) {
|
||||
case FFMPEG_PARSER_TCP:
|
||||
case FFMPEG_PARSER_UDP:
|
||||
@@ -253,7 +251,7 @@ class PrebufferSession {
|
||||
|
||||
return {
|
||||
parser,
|
||||
isDefault,
|
||||
isDefault: !rtspParser || rtspParser === 'Default',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,19 +326,6 @@ class PrebufferSession {
|
||||
const group = "Streams";
|
||||
const subgroup = `Stream: ${this.streamName}`;
|
||||
|
||||
if (this.mixin.streamSettings.storageSettings.values.synthenticStreams.includes(this.streamId)) {
|
||||
const nonSynthetic = [...this.mixin.sessions.keys()].filter(s => s && !s.startsWith('synthetic:'));
|
||||
settings.push({
|
||||
group,
|
||||
subgroup,
|
||||
key: this.syntheticInputIdKey,
|
||||
title: 'Synthetic Stream Source',
|
||||
description: 'The source stream to transcode.',
|
||||
choices: nonSynthetic,
|
||||
value: this.storage.getItem(this.syntheticInputIdKey),
|
||||
});
|
||||
}
|
||||
|
||||
const addFFmpegInputSettings = () => {
|
||||
settings.push(
|
||||
{
|
||||
@@ -366,7 +351,7 @@ class PrebufferSession {
|
||||
key: this.ffmpegOutputArgumentsKey,
|
||||
value: this.storage.getItem(this.ffmpegOutputArgumentsKey),
|
||||
choices: [
|
||||
'-c:v libx264 -pix_fmt yuvj420p -preset ultrafast -bf 0 -g 60 -r 15 -b:v 500000 -bufsize 1000000 -maxrate 500000'
|
||||
'-c:v libx264 -pix_fmt yuvj420p -preset ultrafast -bf 0'
|
||||
],
|
||||
combobox: true,
|
||||
},
|
||||
@@ -379,6 +364,11 @@ class PrebufferSession {
|
||||
const parser = this.getParser(this.advertisedMediaStreamOptions);
|
||||
const defaultValue = parser.parser;
|
||||
|
||||
const scryptedOptions = [
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
];
|
||||
|
||||
const currentParser = parser.isDefault ? STRING_DEFAULT : parser.parser;
|
||||
|
||||
settings.push(
|
||||
@@ -391,8 +381,7 @@ class PrebufferSession {
|
||||
value: currentParser,
|
||||
choices: [
|
||||
STRING_DEFAULT,
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
...scryptedOptions,
|
||||
FFMPEG_PARSER_TCP,
|
||||
FFMPEG_PARSER_UDP,
|
||||
],
|
||||
@@ -529,19 +518,7 @@ class PrebufferSession {
|
||||
};
|
||||
this.parsers = rbo.parsers;
|
||||
|
||||
let mo: MediaObject;
|
||||
if (this.mixin.streamSettings.storageSettings.values.synthenticStreams.includes(this.streamId)) {
|
||||
const syntheticInputId = this.storage.getItem(this.syntheticInputIdKey);
|
||||
if (!syntheticInputId)
|
||||
throw new Error('synthetic stream has not been configured with an input');
|
||||
const realDevice = systemManager.getDeviceById<VideoCamera>(this.mixin.id);
|
||||
mo = await realDevice.getVideoStream({
|
||||
id: syntheticInputId,
|
||||
});
|
||||
}
|
||||
else {
|
||||
mo = await this.mixinDevice.getVideoStream(mso);
|
||||
}
|
||||
const mo = await this.mixinDevice.getVideoStream(mso);
|
||||
const isRfc4571 = mo.mimeType === 'x-scrypted/x-rfc4571';
|
||||
|
||||
let session: ParserSession<PrebufferParsers>;
|
||||
@@ -1305,6 +1282,9 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
async getVideoStream(options?: RequestMediaStreamOptions): Promise<MediaObject> {
|
||||
if (options?.route === 'direct')
|
||||
return this.mixinDevice.getVideoStream(options);
|
||||
|
||||
await this.ensurePrebufferSessions();
|
||||
|
||||
let id = options?.id;
|
||||
@@ -1314,6 +1294,8 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
let videoFilterArguments: string;
|
||||
let destinationVideoBitrate: number;
|
||||
|
||||
const transcodingEnabled = this.mixins?.includes(getTranscodeMixinProviderId());
|
||||
|
||||
const msos = await this.mixinDevice.getVideoStreamOptions();
|
||||
let result: {
|
||||
stream: ResponseMediaStreamOptions,
|
||||
@@ -1373,23 +1355,58 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
}
|
||||
|
||||
id = result.stream.id;
|
||||
// this.console.log('Selected stream', result.stream.name);
|
||||
// transcoding video should never happen transparently since it is CPU intensive.
|
||||
// encourage users at every step to configure proper codecs.
|
||||
// for this reason, do not automatically supply h264 encoder arguments
|
||||
// even if h264 is requested, to force a visible failure.
|
||||
if (transcodingEnabled && this.streamSettings.storageSettings.values.transcodeStreams?.includes(result.title)) {
|
||||
h264EncoderArguments = transcodeStorageSettings.h264EncoderArguments?.split(' ');
|
||||
if (this.streamSettings.storageSettings.values.videoFilterArguments)
|
||||
videoFilterArguments = this.streamSettings.storageSettings.values.videoFilterArguments;
|
||||
}
|
||||
}
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
let ffmpegInput: FFmpegInput;
|
||||
if (!session)
|
||||
throw new Error('stream not found');
|
||||
|
||||
ffmpegInput = await session.getVideoStream(true, options);
|
||||
if (!session.canPrebuffer) {
|
||||
this.console.log('Source container can not be prebuffered. Using a direct media stream.');
|
||||
session = undefined;
|
||||
}
|
||||
if (!session) {
|
||||
const mo = await this.mixinDevice.getVideoStream(options);
|
||||
if (!transcodingEnabled)
|
||||
return mo;
|
||||
ffmpegInput = await mediaManager.convertMediaObjectToJSON(mo, ScryptedMimeTypes.FFmpegInput);
|
||||
}
|
||||
else {
|
||||
// ffmpeg probing works better if the stream does NOT start on a sync frame. the pre-sps/pps data is used
|
||||
// as part of the stream analysis, and sync frame is immediately used. otherwise the sync frame is
|
||||
// read and tossed during rtsp analysis.
|
||||
// if ffmpeg is not in used (ie, not transcoding or implicitly rtsp),
|
||||
// trust that downstream is not using ffmpeg and start with a sync frame.
|
||||
const findSyncFrame = !transcodingEnabled
|
||||
&& (!options?.container || options?.container === 'rtsp')
|
||||
&& options?.tool !== 'ffmpeg';
|
||||
ffmpegInput = await session.getVideoStream(findSyncFrame, options);
|
||||
}
|
||||
|
||||
ffmpegInput.h264EncoderArguments = h264EncoderArguments;
|
||||
ffmpegInput.destinationVideoBitrate = destinationVideoBitrate;
|
||||
|
||||
if (transcodingEnabled && this.streamSettings.storageSettings.values.missingCodecParameters) {
|
||||
if (!ffmpegInput.mediaStreamOptions)
|
||||
ffmpegInput.mediaStreamOptions = { id };
|
||||
ffmpegInput.mediaStreamOptions.oobCodecParameters = true;
|
||||
}
|
||||
|
||||
if (ffmpegInput.h264FilterArguments && videoFilterArguments)
|
||||
addVideoFilterArguments(ffmpegInput.h264FilterArguments, videoFilterArguments)
|
||||
else if (videoFilterArguments)
|
||||
ffmpegInput.h264FilterArguments = ['-filter_complex', videoFilterArguments];
|
||||
|
||||
if (transcodingEnabled)
|
||||
ffmpegInput.videoDecoderArguments = this.streamSettings.storageSettings.values.videoDecoderArguments?.split(' ');
|
||||
return mediaManager.createFFmpegMediaObject(ffmpegInput, {
|
||||
sourceId: this.id,
|
||||
});
|
||||
@@ -1472,22 +1489,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
|
||||
})();
|
||||
}
|
||||
|
||||
for (const synthetic of this.streamSettings.storageSettings.values.synthenticStreams) {
|
||||
const id = `synthetic:${synthetic}`;
|
||||
toRemove.delete(id);
|
||||
|
||||
let session = this.sessions.get(id);
|
||||
|
||||
if (session)
|
||||
continue;
|
||||
|
||||
session = new PrebufferSession(this, {
|
||||
id: synthetic,
|
||||
}, false, false);
|
||||
this.sessions.set(id, session);
|
||||
this.console.log('stream', synthetic, 'is synthetic and will be rebroadcast on demand.');
|
||||
}
|
||||
|
||||
if (!this.sessions.has(undefined)) {
|
||||
const defaultStreamName = this.streamSettings.storageSettings.values.defaultStream;
|
||||
let defaultSession = this.sessions.get(msos?.find(mso => mso.name === defaultStreamName)?.id);
|
||||
@@ -1616,39 +1617,30 @@ function millisUntilMidnight() {
|
||||
return (midnight.getTime() - new Date().getTime());
|
||||
}
|
||||
|
||||
export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, Settings {
|
||||
export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinProvider, BufferConverter, Settings, DeviceProvider {
|
||||
// no longer in use, but kept for future use.
|
||||
storageSettings = new StorageSettings(this, {
|
||||
defaultRtspParser: {
|
||||
group: 'Advanced',
|
||||
title: 'Default RTSP Parser',
|
||||
description: `Experimental: The Default parser used to read RTSP streams. The default is "${SCRYPTED_PARSER_TCP}".`,
|
||||
defaultValue: STRING_DEFAULT,
|
||||
choices: [
|
||||
STRING_DEFAULT,
|
||||
SCRYPTED_PARSER_TCP,
|
||||
SCRYPTED_PARSER_UDP,
|
||||
FFMPEG_PARSER_TCP,
|
||||
FFMPEG_PARSER_UDP,
|
||||
],
|
||||
onPut: () => {
|
||||
this.log.a('Rebroadcast Plugin will restart momentarily.');
|
||||
sdk.deviceManager.requestRestart();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
storageSettings = new StorageSettings(this, {});
|
||||
transcodeStorageSettings = new StorageSettings(this, {
|
||||
remoteStreamingBitrate: {
|
||||
group: 'Advanced',
|
||||
title: 'Remote Streaming Bitrate',
|
||||
type: 'number',
|
||||
defaultValue: 500000,
|
||||
defaultValue: 1000000,
|
||||
description: 'The bitrate to use when remote streaming. This setting will only be used when transcoding or adaptive bitrate is enabled on a camera.',
|
||||
onPut() {
|
||||
sdk.deviceManager.onDeviceEvent('transcode', ScryptedInterface.Settings, undefined);
|
||||
},
|
||||
},
|
||||
h264EncoderArguments: {
|
||||
title: 'H264 Encoder Arguments',
|
||||
description: 'FFmpeg arguments used to encode h264 video. This is not camera specific and is used to setup the hardware accelerated encoder on your Scrypted server. This setting will only be used when transcoding is enabled on a camera.',
|
||||
choices: Object.keys(getH264EncoderArgs()),
|
||||
defaultValue: getDebugModeH264EncoderArgs().join(' '),
|
||||
combobox: true,
|
||||
mapPut: (oldValue, newValue) => getH264EncoderArgs()[newValue]?.join(' ') || newValue || getDebugModeH264EncoderArgs().join(' '),
|
||||
onPut() {
|
||||
sdk.deviceManager.onDeviceEvent('transcode', ScryptedInterface.Settings, undefined);
|
||||
},
|
||||
}
|
||||
});
|
||||
currentMixins = new Map<PrebufferMixin, {
|
||||
worker: ForkWorker,
|
||||
@@ -1658,8 +1650,6 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
||||
constructor(nativeId?: string) {
|
||||
super(nativeId);
|
||||
|
||||
this.log.clearAlerts();
|
||||
|
||||
this.fromMimeType = 'x-scrypted/x-rfc4571';
|
||||
this.toMimeType = ScryptedMimeTypes.FFmpegInput;
|
||||
|
||||
@@ -1679,24 +1669,40 @@ export class RebroadcastPlugin extends AutoenableMixinProvider implements MixinP
|
||||
}
|
||||
});
|
||||
|
||||
// legacy transcode extension that needs to be removed.
|
||||
if (sdk.deviceManager.getNativeIds().includes('transcode')) {
|
||||
process.nextTick(() => {
|
||||
deviceManager.onDeviceRemoved('transcode');
|
||||
// schedule restarts at 2am
|
||||
// removed as the mp4 containerization leak used way back when is defunct.
|
||||
// const midnight = millisUntilMidnight();
|
||||
// const twoAM = midnight + 2 * 60 * 60 * 1000;
|
||||
// this.log.i(`Rebroadcaster scheduled for restart at 2AM: ${Math.round(twoAM / 1000 / 60)} minutes`)
|
||||
// setTimeout(() => deviceManager.requestRestart(), twoAM);
|
||||
|
||||
process.nextTick(() => {
|
||||
deviceManager.onDeviceDiscovered({
|
||||
nativeId: TRANSCODE_MIXIN_PROVIDER_NATIVE_ID,
|
||||
name: 'Transcoding',
|
||||
interfaces: [
|
||||
"SystemSettings",
|
||||
ScryptedInterface.Settings,
|
||||
ScryptedInterface.MixinProvider,
|
||||
],
|
||||
type: ScryptedDeviceType.API,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
...await this.storageSettings.getSettings(),
|
||||
...await this.transcodeStorageSettings.getSettings(),
|
||||
];
|
||||
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
||||
}
|
||||
|
||||
async getDevice(nativeId: string) {
|
||||
if (nativeId === TRANSCODE_MIXIN_PROVIDER_NATIVE_ID)
|
||||
return new TranscodeMixinProvider(this);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.storageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
if (this.transcodeStorageSettings.keys[key])
|
||||
return this.transcodeStorageSettings.putSetting(key, value);
|
||||
return this.storageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getH264DecoderArgs } from "@scrypted/common/src/ffmpeg-hardware-acceleration";
|
||||
import { MixinDeviceBase, ResponseMediaStreamOptions, VideoCamera } from "@scrypted/sdk";
|
||||
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import { MixinDeviceBase, ResponseMediaStreamOptions, VideoCamera } from "@scrypted/sdk";
|
||||
import { getTranscodeMixinProviderId } from "./transcode-settings";
|
||||
|
||||
export type StreamStorageSetting = StorageSetting & {
|
||||
prefersPrebuffer: boolean,
|
||||
@@ -101,16 +102,45 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
type: 'number',
|
||||
hide: false,
|
||||
},
|
||||
synthenticStreams: {
|
||||
subgroup,
|
||||
title: 'Synthetic Streams',
|
||||
description: 'Create additional streams by transcoding the existing streams. This can be useful for creating streams with different resolutions or bitrates.',
|
||||
immediate: true,
|
||||
transcodeStreams: {
|
||||
group: 'Transcoding',
|
||||
title: 'Transcode Streams',
|
||||
description: 'The media streams to transcode. Transcoding audio and video is not recommended and should only be used when necessary. The Rebroadcast Plugin manages the system-wide Transcode settings. See the Rebroadcast Readme for optimal configuration.',
|
||||
multiple: true,
|
||||
choices: Object.values(streamTypes).map(st => st.title),
|
||||
hide: true,
|
||||
},
|
||||
videoDecoderArguments: {
|
||||
group: 'Transcoding',
|
||||
title: 'Video Decoder Arguments',
|
||||
description: 'FFmpeg arguments used to decode input video when transcoding a stream.',
|
||||
placeholder: '-hwaccel auto',
|
||||
choices: Object.keys(getH264DecoderArgs()),
|
||||
combobox: true,
|
||||
choices: [],
|
||||
defaultValue: [],
|
||||
}
|
||||
mapPut: (oldValue, newValue) => getH264DecoderArgs()[newValue]?.join(' ') || newValue || '',
|
||||
hide: true,
|
||||
},
|
||||
videoFilterArguments: {
|
||||
group: 'Transcoding',
|
||||
title: 'Video Filter Arguments',
|
||||
description: 'FFmpeg arguments used to filter input video when transcoding a stream. This can be used to crops, scale, rotates, etc.',
|
||||
placeholder: 'transpose=1',
|
||||
hide: true,
|
||||
},
|
||||
// 3/6/2022
|
||||
// Ran into an issue where the RTSP source had SPS/PPS in the SDP,
|
||||
// and none in the bitstream. Codec copy will not add SPS/PPS before IDR frames
|
||||
// unless this flag is used.
|
||||
// 3/7/2022
|
||||
// This flag was enabled by default, but I believe this is causing issues with some users.
|
||||
// Make it a setting.
|
||||
missingCodecParameters: {
|
||||
group: 'Transcoding',
|
||||
title: 'Out of Band Codec Parameters',
|
||||
description: 'Some cameras do not include H264 codec parameters in the stream and this causes live streaming to always fail (but recordings may be working). This is a inexpensive video filter and does not perform a transcode. Enable this setting only as necessary.',
|
||||
type: 'boolean',
|
||||
hide: true,
|
||||
},
|
||||
});
|
||||
|
||||
function getDefaultPrebufferedStreams(msos: ResponseMediaStreamOptions[]) {
|
||||
@@ -147,18 +177,10 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
const v: StreamStorageSetting = storageSettings.settings[key];
|
||||
const value = storageSettings.values[key];
|
||||
let isDefault = value === 'Default';
|
||||
|
||||
let stream = msos?.find(mso => mso.name === value);
|
||||
if (storageSettings.values.synthenticStreams.includes(value)) {
|
||||
stream = {
|
||||
id: `synthetic:${value}`,
|
||||
};
|
||||
}
|
||||
else {
|
||||
if (isDefault || !stream) {
|
||||
isDefault = true;
|
||||
stream = getDefaultMediaStream(v, msos);
|
||||
}
|
||||
if (isDefault || !stream) {
|
||||
isDefault = true;
|
||||
stream = getDefaultMediaStream(v, msos);
|
||||
}
|
||||
return {
|
||||
title: streamTypes[key].title,
|
||||
@@ -171,7 +193,6 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
const choices = [
|
||||
'Default',
|
||||
...msos.map(mso => mso.name),
|
||||
...storageSettings.values.synthenticStreams,
|
||||
];
|
||||
const defaultValue = getDefaultMediaStream(v, msos).name;
|
||||
|
||||
@@ -188,6 +209,16 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
onGet: async () => {
|
||||
let enabledStreams: StorageSetting;
|
||||
|
||||
const hideTranscode = device.mixins?.includes(getTranscodeMixinProviderId()) ? {
|
||||
hide: false,
|
||||
} : {};
|
||||
const hideTranscodes = {
|
||||
transcodeStreams: hideTranscode,
|
||||
missingCodecParameters: hideTranscode,
|
||||
videoDecoderArguments: hideTranscode,
|
||||
videoFilterArguments: hideTranscode,
|
||||
};
|
||||
|
||||
try {
|
||||
const msos = await device.mixinDevice.getVideoStreamOptions();
|
||||
|
||||
@@ -205,11 +236,13 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
lowResolutionStream: createStreamOptions(streamTypes.lowResolutionStream, msos),
|
||||
recordingStream: createStreamOptions(streamTypes.recordingStream, msos),
|
||||
remoteRecordingStream: createStreamOptions(streamTypes.remoteRecordingStream, msos),
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
enabledStreams,
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +251,7 @@ export function createStreamSettings(device: MixinDeviceBase<VideoCamera>) {
|
||||
}
|
||||
|
||||
return {
|
||||
...hideTranscodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
plugins/prebuffer-mixin/src/transcode-settings.ts
Normal file
53
plugins/prebuffer-mixin/src/transcode-settings.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import sdk, { MixinProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
||||
import { RebroadcastPlugin } from "./main";
|
||||
import { REBROADCAST_MIXIN_INTERFACE_TOKEN } from "./rebroadcast-mixin-token";
|
||||
const { deviceManager } = sdk;
|
||||
|
||||
export const TRANSCODE_MIXIN_PROVIDER_NATIVE_ID = 'transcode';
|
||||
|
||||
export function getTranscodeMixinProviderId() {
|
||||
if (!deviceManager.getNativeIds().includes(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID))
|
||||
return;
|
||||
const transcodeMixin = deviceManager.getDeviceState(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID);
|
||||
return transcodeMixin?.id;
|
||||
}
|
||||
|
||||
export class TranscodeMixinProvider extends ScryptedDeviceBase implements MixinProvider, Settings {
|
||||
constructor(public plugin: RebroadcastPlugin) {
|
||||
super(TRANSCODE_MIXIN_PROVIDER_NATIVE_ID);
|
||||
}
|
||||
|
||||
getSettings(): Promise<Setting[]> {
|
||||
return this.plugin.transcodeStorageSettings.getSettings();
|
||||
}
|
||||
|
||||
putSetting(key: string, value: SettingValue): Promise<void> {
|
||||
return this.plugin.transcodeStorageSettings.putSetting(key, value);
|
||||
}
|
||||
|
||||
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
|
||||
if (!interfaces.includes(REBROADCAST_MIXIN_INTERFACE_TOKEN))
|
||||
return;
|
||||
return [
|
||||
ScryptedInterface.Settings,
|
||||
];
|
||||
}
|
||||
|
||||
invalidateSettings(id: string) {
|
||||
process.nextTick(async () => {
|
||||
for (const [mixin, v] of this.plugin.currentMixins.entries()) {
|
||||
if (v.id === id)
|
||||
mixin?.onDeviceEvent(ScryptedInterface.Settings, undefined)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
this.invalidateSettings(mixinDeviceState.id);
|
||||
return mixinDevice;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
this.invalidateSettings(id);
|
||||
}
|
||||
}
|
||||
@@ -27,13 +27,12 @@ export interface DevInfo {
|
||||
wifi: number;
|
||||
}
|
||||
|
||||
async function getDeviceInfoInternal(host: string, parameters: Record<string, string>): Promise<DevInfo> {
|
||||
async function getDeviceInfo(host: string, username: string, password: string): Promise<DevInfo> {
|
||||
const url = new URL(`http://${host}/api.cgi`);
|
||||
const params = url.searchParams;
|
||||
params.set('cmd', 'GetDevInfo');
|
||||
for (const [key, value] of Object.entries(parameters)) {
|
||||
params.set(key, value);
|
||||
}
|
||||
params.set('user', username);
|
||||
params.set('password', password);
|
||||
|
||||
const response = await httpFetch({
|
||||
url,
|
||||
@@ -44,24 +43,13 @@ async function getDeviceInfoInternal(host: string, parameters: Record<string, st
|
||||
if (error)
|
||||
throw new Error('error during call to getDeviceInfo');
|
||||
|
||||
const ret: DevInfo = response.body?.[0]?.value?.DevInfo;
|
||||
if (!ret?.type && !ret?.model && !ret?.exactType)
|
||||
throw new Error('device info return unexpected data');
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getDeviceInfo(host: string, username: string, password: string): Promise<DevInfo> {
|
||||
const parameters = await getLoginParameters(host, username, password);
|
||||
return getDeviceInfoInternal(host, parameters.parameters);
|
||||
return response.body?.[0]?.value?.DevInfo;
|
||||
}
|
||||
|
||||
export async function getLoginParameters(host: string, username: string, password: string, forceToken?: boolean) {
|
||||
if (!forceToken) {
|
||||
try {
|
||||
await getDeviceInfoInternal(host, {
|
||||
user: username,
|
||||
password,
|
||||
});
|
||||
await getDeviceInfo(host, username, password);
|
||||
return {
|
||||
parameters: {
|
||||
user: username,
|
||||
|
||||
Submodule plugins/sample-cameraprovider updated: ce75c61948...51bbc2be20
2
plugins/snapshot/.vscode/settings.json
vendored
2
plugins/snapshot/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
703
plugins/snapshot/package-lock.json
generated
703
plugins/snapshot/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/snapshot",
|
||||
"version": "0.2.56",
|
||||
"version": "0.2.55",
|
||||
"description": "Snapshot Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
@@ -34,8 +34,8 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"sharp": "^0.33.5",
|
||||
"@types/node": "^20.10.6",
|
||||
"sharp": "^0.33.1",
|
||||
"whatwg-mimetype": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -117,7 +117,7 @@ export class VipsImage implements Image {
|
||||
}
|
||||
|
||||
async toImage(options: ImageOptions) {
|
||||
if (options?.format)
|
||||
if (options.format)
|
||||
throw new Error('format can only be used with toBuffer');
|
||||
const newVipsImage = await this.toVipsImage(options);
|
||||
return createVipsMediaObject(newVipsImage);
|
||||
|
||||
2
plugins/tapo/.vscode/settings.json
vendored
2
plugins/tapo/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "scrypted-nvr",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
@@ -2,17 +2,9 @@
|
||||
|
||||
This plugin adds two way audio support for Tapo cameras. This plugin does not import cameras into Scrypted. Use the ONVIF plugin to import Tapo cameras, and then use this plugin to add two way audio support.
|
||||
|
||||
## Tapo Setup
|
||||
# Setup
|
||||
|
||||
|
||||
1. Open the Tapo app on iOS/Android.
|
||||
2. Click `Me` in the bottom bar.
|
||||
3. Click `Tapo Lab`.
|
||||
4. Enable Third Party Compatibility.
|
||||
|
||||
## Scrypted Setup
|
||||
|
||||
1. Add the Tapo Camera using the ONVIF Plugin. The ONVIF password can be found in the camera's settings in the Tapo app: Settings -> Advanced Settings -> Camera Account -> Account Information.
|
||||
1. Add the Tapo Camera using the ONVIF Plugin.
|
||||
2. Enable ONVIF Two Way Audio on the camera.
|
||||
3. Enable the Tapo Two Way Audio extension.
|
||||
4. Enter your Tapo Cloud password into the Tapo Two Way Audio Settings. This is not the same as the ONVIF password.
|
||||
4. Enter your Tapo Cloud password into the Tapo Two Way Audio Settings.
|
||||
|
||||
44
plugins/tapo/package-lock.json
generated
44
plugins/tapo/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/tapo",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tapo",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.19",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
@@ -34,29 +34,23 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.100",
|
||||
"version": "0.3.45",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@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",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.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"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
@@ -68,9 +62,11 @@
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/tapo",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.19",
|
||||
"description": "Tapo Camera Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "build",
|
||||
"module": "Node16",
|
||||
"module": "commonjs",
|
||||
"target": "ES2021",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node16",
|
||||
|
||||
14
plugins/tensorflow-lite/.vscode/settings.json
vendored
14
plugins/tensorflow-lite/.vscode/settings.json
vendored
@@ -1,7 +1,19 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "koushik-ubuntuvm",
|
||||
"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": "koushik-windows",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
"python.analysis.extraPaths": [
|
||||
"./node_modules/@scrypted/sdk/types/scrypted_python"
|
||||
]
|
||||
|
||||
82
plugins/tensorflow-lite/package-lock.json
generated
82
plugins/tensorflow-lite/package-lock.json
generated
@@ -1,56 +1,51 @@
|
||||
{
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.74",
|
||||
"version": "0.1.68",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/tensorflow-lite",
|
||||
"version": "0.1.74",
|
||||
"version": "0.1.68",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.3.102",
|
||||
"version": "0.2.39",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
"rimraf": "^3.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-changelog": "bin/scrypted-changelog.js",
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-setup-project": "bin/scrypted-setup-project.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typedoc": "^0.26.11"
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"stringify-object": "^3.3.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
@@ -65,29 +60,24 @@
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@rollup/plugin-typescript": "^12.1.1",
|
||||
"@rollup/plugin-virtual": "^3.0.2",
|
||||
"@types/node": "^22.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"axios": "^1.7.8",
|
||||
"babel-loader": "^9.2.1",
|
||||
"babel-plugin-const-enum": "^1.2.0",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/stringify-object": "^4.0.0",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.15.9",
|
||||
"ncp": "^2.0.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"rollup": "^4.27.4",
|
||||
"tmp": "^0.2.3",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typedoc": "^0.26.11",
|
||||
"typescript": "^5.6.3",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
"rimraf": "^3.0.2",
|
||||
"stringify-object": "^3.3.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-node": "^10.4.0",
|
||||
"typedoc": "^0.23.21",
|
||||
"typescript": "^4.9.3",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.74"
|
||||
"version": "0.1.68"
|
||||
}
|
||||
|
||||
@@ -19,24 +19,19 @@ except Exception as e:
|
||||
pass
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import queue
|
||||
import re
|
||||
from typing import Any, Tuple
|
||||
|
||||
import scrypted_sdk
|
||||
import tflite_runtime.interpreter as tflite
|
||||
from .yolo_separate_outputs import *
|
||||
from scrypted_sdk.types import Setting, SettingValue
|
||||
|
||||
from common import yolo
|
||||
from predict import PredictPlugin
|
||||
|
||||
prepareExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="TFLite-Prepare")
|
||||
|
||||
availableModels = [
|
||||
"Default",
|
||||
"scrypted_yolov9s_relu_sep_320",
|
||||
"scrypted_yolov9t_relu_320",
|
||||
"scrypted_yolov9s_relu_320",
|
||||
"ssd_mobilenet_v2_coco_quant_postprocess",
|
||||
"tf2_ssd_mobilenet_v2_coco17_ptq",
|
||||
"ssdlite_mobiledet_coco_qat_postprocess",
|
||||
@@ -54,7 +49,6 @@ availableModels = [
|
||||
"efficientdet_lite3x_640_ptq",
|
||||
]
|
||||
|
||||
|
||||
def parse_label_contents(contents: str):
|
||||
lines = contents.splitlines()
|
||||
lines = [line for line in lines if line.strip()]
|
||||
@@ -85,7 +79,7 @@ class TensorFlowLitePlugin(
|
||||
edge_tpus = None
|
||||
pass
|
||||
|
||||
model_version = "v14"
|
||||
model_version = "v13"
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model not in availableModels:
|
||||
self.storage.setItem("model", "Default")
|
||||
@@ -100,12 +94,17 @@ class TensorFlowLitePlugin(
|
||||
nonlocal model
|
||||
|
||||
if defaultModel:
|
||||
model = "scrypted_yolov9s_relu_sep_320"
|
||||
model = "scrypted_yolov10n_320"
|
||||
# if edge_tpus and next(
|
||||
# (obj for obj in edge_tpus if obj["type"] == "usb"), None
|
||||
# ):
|
||||
# model = "ssdlite_mobiledet_coco_qat_postprocess"
|
||||
# else:
|
||||
# model = "efficientdet_lite0_320_ptq"
|
||||
self.yolo = "yolo" in model
|
||||
self.yolov9 = "yolov9" in model
|
||||
self.scrypted_model = "scrypted" in model
|
||||
self.scrypted_yolov10 = "scrypted_yolov10" in model
|
||||
self.scrypted_yolo_sep = "_sep" in model
|
||||
self.modelName = model
|
||||
|
||||
print(f"model: {model}")
|
||||
@@ -150,8 +149,7 @@ class TensorFlowLitePlugin(
|
||||
try:
|
||||
interpreter = make_interpreter(modelFile, ":%s" % idx)
|
||||
interpreter.allocate_tensors()
|
||||
self.image_input_details = interpreter.get_input_details()[0]
|
||||
_, height, width, channels = self.image_input_details[
|
||||
_, height, width, channels = interpreter.get_input_details()[0][
|
||||
"shape"
|
||||
]
|
||||
self.input_details = int(width), int(height), int(channels)
|
||||
@@ -173,8 +171,7 @@ class TensorFlowLitePlugin(
|
||||
modelFile = downloadModel()
|
||||
interpreter = tflite.Interpreter(model_path=modelFile)
|
||||
interpreter.allocate_tensors()
|
||||
self.image_input_details = interpreter.get_input_details()[0]
|
||||
_, height, width, channels = self.image_input_details["shape"]
|
||||
_, height, width, channels = interpreter.get_input_details()[0]["shape"]
|
||||
self.input_details = int(width), int(height), int(channels)
|
||||
available_interpreters.append(interpreter)
|
||||
self.interpreter_count = self.interpreter_count + 1
|
||||
@@ -185,7 +182,8 @@ class TensorFlowLitePlugin(
|
||||
thread_name = threading.current_thread().name
|
||||
interpreter = available_interpreters.pop()
|
||||
self.interpreters[thread_name] = interpreter
|
||||
print("Interpreter initialized on thread {}".format(thread_name))
|
||||
print('Interpreter initialized on thread {}'.format(thread_name))
|
||||
|
||||
|
||||
self.executor = concurrent.futures.ThreadPoolExecutor(
|
||||
initializer=executor_initializer,
|
||||
@@ -225,101 +223,61 @@ class TensorFlowLitePlugin(
|
||||
return self.input_details[0:2]
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def prepare():
|
||||
if not self.yolo:
|
||||
return input
|
||||
|
||||
im = np.stack([input])
|
||||
# this non-quantized code path is unused but here for reference.
|
||||
if self.image_input_details["dtype"] != np.int8 and self.image_input_details["dtype"] != np.int16:
|
||||
im = im.astype(np.float32) / 255.0
|
||||
return im
|
||||
|
||||
scale, zero_point = self.image_input_details["quantization"]
|
||||
if scale == 0.003986024297773838 and zero_point == -128:
|
||||
# fast path for quantization 1/255 = 0.003986024297773838
|
||||
im = im.view(np.int8)
|
||||
im -= 128
|
||||
else:
|
||||
im = im.astype(np.float32) / (255.0 * scale)
|
||||
im = (im + zero_point).astype(np.int8) # de-scale
|
||||
|
||||
return im
|
||||
|
||||
def predict(im):
|
||||
def predict():
|
||||
interpreter = self.interpreters[threading.current_thread().name]
|
||||
if not self.yolo:
|
||||
tflite_common.set_input(interpreter, im)
|
||||
if self.yolo:
|
||||
tensor_index = input_details(interpreter, "index")
|
||||
|
||||
im = np.stack([input])
|
||||
i = interpreter.get_input_details()[0]
|
||||
if i["dtype"] == np.int8:
|
||||
scale, zero_point = i["quantization"]
|
||||
if scale == 0.003986024297773838 and zero_point == -128:
|
||||
# fast path for quantization 1/255 = 0.003986024297773838
|
||||
im = im.view(np.int8)
|
||||
im -= 128
|
||||
else:
|
||||
im = im.astype(np.float32) / (255.0 * scale)
|
||||
im = (im + zero_point).astype(np.int8) # de-scale
|
||||
else:
|
||||
# this code path is unused.
|
||||
im = im.astype(np.float32) / 255.0
|
||||
interpreter.set_tensor(tensor_index, im)
|
||||
interpreter.invoke()
|
||||
output_details = interpreter.get_output_details()
|
||||
output = output_details[0]
|
||||
x = interpreter.get_tensor(output["index"])
|
||||
input_scale = self.get_input_details()[0]
|
||||
if x.dtype == np.int8:
|
||||
scale, zero_point = output["quantization"]
|
||||
threshold = yolo.defaultThreshold / scale + zero_point
|
||||
combined_scale = scale * input_scale
|
||||
if self.scrypted_yolov10:
|
||||
objs = yolo.parse_yolov10(
|
||||
x[0],
|
||||
threshold,
|
||||
scale=lambda v: (v - zero_point) * combined_scale,
|
||||
confidence_scale=lambda v: (v - zero_point) * scale,
|
||||
)
|
||||
else:
|
||||
objs = yolo.parse_yolov9(
|
||||
x[0],
|
||||
threshold,
|
||||
scale=lambda v: (v - zero_point) * combined_scale,
|
||||
confidence_scale=lambda v: (v - zero_point) * scale,
|
||||
)
|
||||
else:
|
||||
# this code path is unused.
|
||||
objs = yolo.parse_yolov9(x[0], scale=lambda v: v * input_scale)
|
||||
else:
|
||||
tflite_common.set_input(interpreter, input)
|
||||
interpreter.invoke()
|
||||
objs = detect.get_objects(
|
||||
interpreter, score_threshold=0.2, image_scale=(1, 1)
|
||||
)
|
||||
return objs
|
||||
|
||||
tensor_index = input_details(interpreter, "index")
|
||||
interpreter.set_tensor(tensor_index, im)
|
||||
interpreter.invoke()
|
||||
output_details = interpreter.get_output_details()
|
||||
output_tensors = [(interpreter.get_tensor(output["index"]), output) for output in output_details]
|
||||
|
||||
return output_tensors
|
||||
|
||||
def post_process(output_tensors):
|
||||
if not self.yolo:
|
||||
return output_tensors
|
||||
|
||||
# handle separate outputs for quantization accuracy
|
||||
if self.scrypted_yolo_sep:
|
||||
outputs = []
|
||||
for ot, output in output_tensors:
|
||||
o = ot.astype(np.float32)
|
||||
scale, zero_point = output["quantization"]
|
||||
o -= zero_point
|
||||
o *= scale
|
||||
outputs.append(o)
|
||||
|
||||
output = yolo_separate_outputs.decode_bbox(outputs, [input.width, input.height])
|
||||
if self.scrypted_yolov10:
|
||||
objs = yolo.parse_yolov10(output[0])
|
||||
else:
|
||||
objs = yolo.parse_yolov9(output[0])
|
||||
return objs
|
||||
|
||||
# this scale stuff can probably be optimized to dequantize ahead of time...
|
||||
x, output = output_tensors[0]
|
||||
input_scale = self.get_input_details()[0]
|
||||
|
||||
# this non-quantized code path is unused but here for reference.
|
||||
if x.dtype != np.int8 and x.dtype != np.int16:
|
||||
if self.scrypted_yolov10:
|
||||
objs = yolo.parse_yolov10(x[0], scale=lambda v: v * input_scale)
|
||||
else:
|
||||
objs = yolo.parse_yolov9(x[0], scale=lambda v: v * input_scale)
|
||||
return objs
|
||||
|
||||
# this scale stuff can probably be optimized to dequantize ahead of time...
|
||||
scale, zero_point = output["quantization"]
|
||||
combined_scale = scale * input_scale
|
||||
if self.scrypted_yolov10:
|
||||
objs = yolo.parse_yolov10(
|
||||
x[0],
|
||||
scale=lambda v: (v - zero_point) * combined_scale,
|
||||
confidence_scale=lambda v: (v - zero_point) * scale,
|
||||
threshold_scale=lambda v: (v - zero_point) * scale,
|
||||
)
|
||||
else:
|
||||
objs = yolo.parse_yolov9(
|
||||
x[0],
|
||||
scale=lambda v: (v - zero_point) * combined_scale,
|
||||
confidence_scale=lambda v: (v - zero_point) * scale,
|
||||
threshold_scale=lambda v: (v - zero_point) * scale,
|
||||
)
|
||||
return objs
|
||||
|
||||
|
||||
im = await asyncio.get_event_loop().run_in_executor(prepareExecutor, prepare)
|
||||
output_tensors = await asyncio.get_event_loop().run_in_executor(self.executor, lambda: predict(im))
|
||||
objs = await asyncio.get_event_loop().run_in_executor(prepareExecutor, lambda: post_process(output_tensors))
|
||||
objs = await asyncio.get_event_loop().run_in_executor(self.executor, predict)
|
||||
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
return ret
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import numpy as np
|
||||
from common.softmax import softmax
|
||||
class DFL:
|
||||
def __init__(self, c1=16):
|
||||
self.c1 = c1
|
||||
self.conv_weights = np.arange(c1).reshape(1, c1, 1, 1)
|
||||
|
||||
def forward(self, x):
|
||||
b, _, a = x.shape # batch, channels, anchors
|
||||
x = x.reshape(b, 4, self.c1, a).transpose(0, 2, 1, 3)
|
||||
x = softmax(x, axis=1)
|
||||
x = np.sum(self.conv_weights * x, axis=1)
|
||||
return x.reshape(b, 4, a)
|
||||
|
||||
def make_anchors(feats, strides, grid_cell_offset=0.5):
|
||||
anchor_points, stride_tensor = [], []
|
||||
assert feats is not None
|
||||
dtype = feats[0].dtype
|
||||
for i, stride in enumerate(strides):
|
||||
_, _, h, w = feats[i].shape
|
||||
sx = np.arange(w, dtype=dtype) + grid_cell_offset # shift x
|
||||
sy = np.arange(h, dtype=dtype) + grid_cell_offset # shift y
|
||||
sy, sx = np.meshgrid(sy, sx, indexing="ij")
|
||||
anchor_points.append(np.stack((sx, sy), axis=-1).reshape(-1, 2))
|
||||
stride_tensor.append(np.full((h * w, 1), stride, dtype=dtype))
|
||||
return np.concatenate(anchor_points), np.concatenate(stride_tensor)
|
||||
|
||||
def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
|
||||
lt, rb = np.split(distance, 2, axis=dim)
|
||||
|
||||
anchor_points = anchor_points.transpose(0, 2, 1)
|
||||
|
||||
x1y1 = anchor_points - lt
|
||||
x2y2 = anchor_points + rb
|
||||
if xywh:
|
||||
c_xy = (x1y1 + x2y2) / 2
|
||||
wh = x2y2 - x1y1
|
||||
return np.concatenate((c_xy, wh), axis=dim) # xywh bbox
|
||||
return np.concatenate((x1y1, x2y2), axis=dim) # xyxy bbox
|
||||
|
||||
def decode_bbox(preds, img_shape):
|
||||
num_classes = next((o.shape[2] for o in preds if o.shape[2] != 64), -1)
|
||||
assert num_classes != -1, 'cannot infer postprocessor inputs via output shape if there are 64 classes'
|
||||
pos = [
|
||||
i for i, _ in sorted(enumerate(preds),
|
||||
key=lambda x: (x[1].shape[2] if num_classes > 64 else -x[1].shape[2], -x[1].shape[1]))]
|
||||
x = np.transpose(
|
||||
np.concatenate([
|
||||
np.concatenate([preds[i] for i in pos[:len(pos) // 2]], axis=1),
|
||||
np.concatenate([preds[i] for i in pos[len(pos) // 2:]], axis=1)], axis=2), (0, 2, 1))
|
||||
reg_max = (x.shape[1] - num_classes) // 4
|
||||
dfl = DFL(reg_max) if reg_max > 1 else lambda x: x
|
||||
img_h, img_w = img_shape[-2], img_shape[-1]
|
||||
strides = [
|
||||
int(np.sqrt(img_shape[-2] * img_shape[-1] / preds[p].shape[1])) for p in pos if preds[p].shape[2] != 64]
|
||||
dims = [(img_h // s, img_w // s) for s in strides]
|
||||
fake_feats = [np.zeros((1, 1, h, w), dtype=preds[0].dtype) for h, w in dims]
|
||||
anchors, strides = make_anchors(fake_feats, strides, 0.5)
|
||||
|
||||
strides_tensor = strides.transpose(1, 0)
|
||||
strides_tensor = np.expand_dims(strides_tensor, 0)
|
||||
|
||||
dbox = dist2bbox(dfl.forward(x[:, :-num_classes, :]), anchors[None, ...], xywh=True, dim=1) * strides_tensor
|
||||
|
||||
return np.concatenate((dbox, 1 / (1 + np.exp(-x[:, -num_classes:, :]))), axis=1)
|
||||
355
plugins/unifi-protect/package-lock.json
generated
355
plugins/unifi-protect/package-lock.json
generated
@@ -9,15 +9,18 @@
|
||||
"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,5 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/unifi-protect",
|
||||
"type": "module",
|
||||
"version": "0.0.164",
|
||||
"description": "Unifi Protect Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,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),
|
||||
}
|
||||
@@ -113,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,
|
||||
}
|
||||
@@ -170,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.
|
||||
@@ -375,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();
|
||||
@@ -441,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")
|
||||
}
|
||||
@@ -458,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;
|
||||
@@ -480,7 +483,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
|
||||
text: title.substring(0, 30),
|
||||
type: 'CUSTOM_MESSAGE',
|
||||
};
|
||||
this.protect.api.updateCamera(this.findCamera(), {
|
||||
this.protect.api.updateDevice(this.findCamera(), {
|
||||
lcdMessage: payload,
|
||||
})
|
||||
|
||||
|
||||
@@ -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 { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
|
||||
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);
|
||||
@@ -249,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');
|
||||
@@ -271,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);
|
||||
@@ -284,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;
|
||||
@@ -347,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;
|
||||
@@ -392,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) {
|
||||
@@ -405,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,
|
||||
@@ -413,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,
|
||||
@@ -433,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,
|
||||
@@ -458,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,
|
||||
@@ -490,12 +497,12 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
|
||||
}
|
||||
|
||||
// handle package cameras as a sub device
|
||||
for (const camera of this.api.cameras) {
|
||||
for (const camera of this.api.bootstrap.cameras) {
|
||||
const devices: Device[] = [];
|
||||
|
||||
const providerNativeId = this.getNativeId(camera, true);
|
||||
|
||||
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
|
||||
if (camera.featureFlags.hasPackageCamera) {
|
||||
const nativeId = providerNativeId + '-packageCamera';
|
||||
const d: Device = {
|
||||
providerNativeId,
|
||||
@@ -569,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);
|
||||
|
||||
@@ -15,7 +15,7 @@ export class UnifiSensor extends ScryptedDeviceBase implements Thermometer, Humi
|
||||
|
||||
findSensor() {
|
||||
const id = this.protect.findId(this.nativeId);
|
||||
return this.protect.api.sensors.find(sensor => sensor.id === id);
|
||||
return this.protect.api.bootstrap.sensors.find(sensor => sensor.id === id);
|
||||
}
|
||||
|
||||
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
|
||||
export interface FeatureFlagsShim {
|
||||
hasPackageCamera: boolean;
|
||||
hasFingerprintSensor: boolean;
|
||||
}
|
||||
|
||||
export interface LastSeenShim {
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface PrivacyZone {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// export * from '@koush/unifi-protect'
|
||||
export * from '@koush/unifi-protect/src/index'
|
||||
export * from 'unifi-protect'
|
||||
// export * from '../../../external/unifi-protect/src/index'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user