mirror of
https://github.com/koush/scrypted.git
synced 2026-02-05 23:22:13 +00:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
711eb222ed | ||
|
|
19f8bfb74a | ||
|
|
08a8428d6e | ||
|
|
4feeeda904 | ||
|
|
753373a691 | ||
|
|
2f3529b822 | ||
|
|
2501d1460b | ||
|
|
e063637100 | ||
|
|
5ec0bf4bf3 | ||
|
|
0c05b59121 | ||
|
|
cbbfa0b525 | ||
|
|
28835b1ccc | ||
|
|
0585e7bbaf | ||
|
|
b2040ea2c8 | ||
|
|
2fd2151b4f | ||
|
|
4c7974519d | ||
|
|
d91c919558 | ||
|
|
7a297761bc | ||
|
|
c15e10e5cf | ||
|
|
3494106857 | ||
|
|
7d3dfb16f0 | ||
|
|
63fc223036 | ||
|
|
6736379858 | ||
|
|
7a811b2b22 | ||
|
|
dd5cb432c9 | ||
|
|
ab3a71ab49 | ||
|
|
b5c9382180 | ||
|
|
81682678ac | ||
|
|
dec184629e | ||
|
|
f33bb53138 | ||
|
|
2d3957e086 | ||
|
|
d16ed9e54f | ||
|
|
d7e8052498 | ||
|
|
48cd3830a5 | ||
|
|
ce138d1a17 | ||
|
|
7b4919fba9 | ||
|
|
0b3dee3a03 | ||
|
|
4cef09540b | ||
|
|
92583e568a | ||
|
|
67aaa08c31 | ||
|
|
2e9f618f6f | ||
|
|
bf4d39d6af | ||
|
|
c31e68f720 | ||
|
|
6d8b3c1ce7 | ||
|
|
106fef95b4 | ||
|
|
488d68ee1c | ||
|
|
f7e35fb1ee | ||
|
|
b1bf897bdb | ||
|
|
8eb533c220 | ||
|
|
f10cdfbced | ||
|
|
8f5e9e5a8c | ||
|
|
cc0283ef39 | ||
|
|
5c7b67c973 | ||
|
|
d1be0f1b4c | ||
|
|
55d58d1e44 | ||
|
|
d9dccf36a3 | ||
|
|
33477fdf80 | ||
|
|
e6ece3aa3e | ||
|
|
6a4126191b | ||
|
|
e9f999b911 | ||
|
|
1fef31a081 | ||
|
|
659f99c33d | ||
|
|
a9deff0046 | ||
|
|
7a56cefe2a | ||
|
|
a06c6e9568 | ||
|
|
56f127a203 | ||
|
|
2ffe67b2db | ||
|
|
44dc648398 | ||
|
|
7807cc4bc6 | ||
|
|
81fb690089 | ||
|
|
8b15617f6e | ||
|
|
fd8aa70352 | ||
|
|
be888d215d | ||
|
|
ce5f568a5d | ||
|
|
336220559f | ||
|
|
8014060a54 | ||
|
|
7f4c8997b9 | ||
|
|
9f73b92dbd | ||
|
|
381892fca6 | ||
|
|
a28df23032 | ||
|
|
dc5456d36f | ||
|
|
3a23e8ed26 | ||
|
|
e0db86cb41 | ||
|
|
37ccefebd1 | ||
|
|
0076c4827f | ||
|
|
c5c07d8169 | ||
|
|
2372acc796 | ||
|
|
6b9c3e4aa0 | ||
|
|
d5b652da8c | ||
|
|
2b9a0f082d | ||
|
|
b10b4d047e | ||
|
|
74cd23bd88 | ||
|
|
ef742bdb23 | ||
|
|
6f7fa54f24 | ||
|
|
d9a575cb5a | ||
|
|
29094afa4d | ||
|
|
62a92fe083 | ||
|
|
9b8bde556c | ||
|
|
326ef11760 | ||
|
|
92a0b4a863 | ||
|
|
9fd3641455 | ||
|
|
2918cf9ae1 | ||
|
|
6f004db859 | ||
|
|
367d741c5f | ||
|
|
8f83894e49 | ||
|
|
ea6e33d159 | ||
|
|
1b5565b5b2 | ||
|
|
19692d02c6 | ||
|
|
4179698c12 | ||
|
|
1eea3a87d0 | ||
|
|
ec89a77955 | ||
|
|
443158286e | ||
|
|
b168ca52c6 | ||
|
|
fe01d3a1ba | ||
|
|
18cad22627 | ||
|
|
c67c9a028c | ||
|
|
0cff8ad5ed | ||
|
|
0269959cf3 | ||
|
|
1b6de42eca | ||
|
|
39342d5d46 | ||
|
|
c4b5af46d0 | ||
|
|
a46235d095 | ||
|
|
848d490a66 | ||
|
|
87fbb95157 | ||
|
|
c036da9ae0 |
38
.github/workflows/docker-common.yml
vendored
38
.github/workflows/docker-common.yml
vendored
@@ -6,7 +6,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: self-hosted
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
NODE_VERSION: ["18"]
|
||||
@@ -19,27 +20,28 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.124
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.124
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.119
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.119
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://koush@192.168.2.124
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://koush@192.168.2.119
|
||||
platforms: linux/armhf
|
||||
# with:
|
||||
# platforms: linux/arm64,linux/armhf
|
||||
# append: |
|
||||
# - endpoint: ssh://koush@192.168.2.124
|
||||
# # platforms: linux/arm64
|
||||
# platforms: linux/arm64
|
||||
# # - endpoint: ssh://koush@192.168.2.119
|
||||
# # platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
|
||||
40
.github/workflows/docker.yml
vendored
40
.github/workflows/docker.yml
vendored
@@ -15,7 +15,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: self-hosted
|
||||
# runs-on: self-hosted
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin"]
|
||||
@@ -38,28 +39,29 @@ jobs:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.124
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.124
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up SSH
|
||||
uses: MrSquaare/ssh-setup-action@v2
|
||||
with:
|
||||
host: 192.168.2.119
|
||||
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
# - name: Set up SSH
|
||||
# uses: MrSquaare/ssh-setup-action@v2
|
||||
# with:
|
||||
# host: 192.168.2.119
|
||||
# private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
append: |
|
||||
- endpoint: ssh://koush@192.168.2.124
|
||||
platforms: linux/arm64
|
||||
- endpoint: ssh://koush@192.168.2.119
|
||||
platforms: linux/armhf
|
||||
|
||||
# with:
|
||||
# platforms: linux/arm64,linux/armhf
|
||||
# append: |
|
||||
# - endpoint: ssh://koush@192.168.2.124
|
||||
# # platforms: linux/arm64
|
||||
# platforms: linux/arm64
|
||||
# # - endpoint: ssh://koush@192.168.2.119
|
||||
# # platforms: linux/armhf
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -3,9 +3,9 @@ name: Test
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths: ["docker/**", ".github/workflows/test.yml"]
|
||||
paths: ["install/**", ".github/workflows/test.yml"]
|
||||
pull_request:
|
||||
paths: ["docker/**", ".github/workflows/test.yml"]
|
||||
paths: ["install/**", ".github/workflows/test.yml"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
.DS_Store
|
||||
__pycache__
|
||||
venv
|
||||
.venv
|
||||
|
||||
59
README.md
59
README.md
@@ -1,59 +1,20 @@
|
||||
# Scrypted
|
||||
|
||||
Scrypted is a high performance home video integration and automation platform.
|
||||
* Video load instantly, everywhere: [Demo](https://www.reddit.com/r/homebridge/comments/r34k6b/if_youre_using_homebridge_for_cameras_ditch_it/)
|
||||
* [HomeKit Secure Video Support](https://github.com/koush/scrypted/wiki/HomeKit-Secure-Video-Setup)
|
||||
* Google Home support: "Ok Google, Stream Backyard"
|
||||
* Alexa Support: Streaming to Alexa app on iOS/Android and Echo Show.
|
||||
Scrypted is a high performance home video integration platform and NVR with smart detections. [Instant, low latency, streaming](https://streamable.com/xbxn7z) to HomeKit, Google Home, and Alexa. Supports most cameras. [Learn more](https://docs.scrypted.app).
|
||||
|
||||
<img width="400" alt="Scrypted_Management_Console" src="https://user-images.githubusercontent.com/73924/185666320-ae972867-6c2c-488a-8413-fd8a215e9fee.png">
|
||||
<img src="https://github.com/koush/scrypted/assets/73924/57e1d556-cd3d-4448-81f9-a6c51b6513de">
|
||||
|
||||
# Installation
|
||||
## Installation and Documentation
|
||||
|
||||
Select the appropriate guide. After installation is finished, remember to visit [HomeKit Secure Video Setup](https://github.com/koush/scrypted/wiki/HomeKit-Secure-Video-Setup).
|
||||
Installation and camera onboarding instructions can be found in the [docs](https://docs.scrypted.app).
|
||||
|
||||
* [Raspberry Pi](https://github.com/koush/scrypted/wiki/Installation:-Raspberry-Pi)
|
||||
* Linux
|
||||
* [Docker Compose](https://github.com/koush/scrypted/wiki/Installation:-Docker-Compose-Linux) - This is the recommended method. Local installation may interfere with other server software.
|
||||
* [Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Linux) - Use Docker Compose. This is a reference documentation.
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Linux) - Use this if Docker scares you or whatever.
|
||||
* Mac
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Mac)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* Windows
|
||||
* [Local Installation](https://github.com/koush/scrypted/wiki/Installation:-Windows)
|
||||
* [WSL2 Installation](https://github.com/koush/scrypted/wiki/Installation:-WSL2-Windows)
|
||||
* [Home Assistant OS](https://github.com/koush/scrypted/wiki/Installation:-Home-Assistant-OS)
|
||||
<!-- * Docker Desktop is [not supported](https://github.com/koush/scrypted/wiki/Installation:-Docker-Desktop). -->
|
||||
* [ReadyNAS: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-ReadyNAS)
|
||||
* [Synology: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Synology-NAS)
|
||||
* [QNAP: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-QNAP-NAS)
|
||||
* [Unraid: Docker](https://github.com/koush/scrypted/wiki/Installation:-Docker-Unraid)
|
||||
|
||||
## Discord
|
||||
|
||||
Chat on Discord for support, tips, announcements, and bug reporting. There is an active and helpful community.
|
||||
|
||||
[Join Scrypted Discord](https://discord.gg/DcFzmBHYGq)
|
||||
|
||||
## Wiki
|
||||
|
||||
There are many topics covered in the [Scrypted Wiki](https://github.com/koush/scrypted/wiki) sidebar. Review them for documented support, tips, and guides before asking for assistance on GitHub or Discord.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
* Google Home
|
||||
* Apple HomeKit
|
||||
* Amazon Alexa
|
||||
|
||||
Supported accessories:
|
||||
* Camera and Core Plugins: https://github.com/koush/scrypted/tree/main/plugins
|
||||
* Community Plugins: https://github.com/orgs/scryptedapp/repositories
|
||||
## Community
|
||||
|
||||
Scrypted has active communities on [Discord](https://discord.gg/DcFzmBHYGq), [Reddit](https://reddit.com/r/scrypted), and [Github](https://github.com/koush/scrypted). Check them out if you have questions!
|
||||
|
||||
## Development
|
||||
|
||||
## Debug Scrypted Plugins in VSCode
|
||||
## Debug Scrypted Plugins in VS Code
|
||||
|
||||
```sh
|
||||
# this is an example for homekit.
|
||||
@@ -66,7 +27,7 @@ cd scrypted
|
||||
code plugins/homekit
|
||||
```
|
||||
|
||||
You can now launch (using the Start Debugging play button) the HomeKit Plugin in VSCode. Please be aware that you do *not* need to restart the Scrypted Server if you make changes to a plugin. Edit the plugin, launch, and the updated plugin will deploy on the running server.
|
||||
You can now launch (using the Start Debugging play button) the HomeKit Plugin in VS Code. Please be aware that you do *not* need to restart the Scrypted Server if you make changes to a plugin. Edit the plugin, launch, and the updated plugin will deploy on the running server.
|
||||
|
||||
If you do not want to set up VS Code, you can also run build and install the plugin directly from the command line:
|
||||
|
||||
@@ -80,7 +41,7 @@ npm run build && npm run scrypted-deploy 127.0.0.1
|
||||
Want to write your own plugin? Full documentation is available here: https://developer.scrypted.app
|
||||
|
||||
|
||||
## Debug the Scrypted Server in VSCode
|
||||
## Debug the Scrypted Server in VS Code
|
||||
|
||||
Debugging the server should not be necessary, as the server only provides the hosting and RPC mechanism for plugins. The following is for reference purpose. Most development can be done by debugging the relevant plugin.
|
||||
|
||||
@@ -94,4 +55,4 @@ cd scrypted
|
||||
code server
|
||||
```
|
||||
|
||||
You can now launch the Scrypted Server in VSCode.
|
||||
You can now launch the Scrypted Server in VS Code.
|
||||
|
||||
@@ -263,20 +263,23 @@ export async function startParserSession<T extends string>(ffmpegInput: FFmpegIn
|
||||
const rtsp = (options.parsers as any).rtsp as ReturnType<typeof createRtspParser>;
|
||||
rtsp.sdp.then(sdp => {
|
||||
const parsed = parseSdp(sdp);
|
||||
const audio = parsed.msections.find(msection=>msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection=>msection.type === 'video');
|
||||
const audio = parsed.msections.find(msection => msection.type === 'audio');
|
||||
const video = parsed.msections.find(msection => msection.type === 'video');
|
||||
inputVideoCodec = video?.codec;
|
||||
inputAudioCodec = audio?.codec;
|
||||
});
|
||||
|
||||
const sdp = rtsp.sdp.then(sdpString => [Buffer.from(sdpString)]);
|
||||
const sdp = new Deferred<Buffer[]>();
|
||||
rtsp.sdp.then(r => sdp.resolve([Buffer.from(r)]));
|
||||
killed.then(() => sdp.reject(new Error("ffmpeg killed before sdp could be parsed")));
|
||||
|
||||
start();
|
||||
|
||||
return {
|
||||
start() {
|
||||
deferredStart.resolve();
|
||||
},
|
||||
sdp,
|
||||
sdp: sdp.promise,
|
||||
get inputAudioCodec() {
|
||||
return inputAudioCodec;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import net from 'net';
|
||||
import { once } from 'events';
|
||||
import dgram, { SocketType } from 'dgram';
|
||||
import { once } from 'events';
|
||||
import net from 'net';
|
||||
|
||||
export async function closeQuiet(socket: dgram.Socket | net.Server) {
|
||||
if (!socket)
|
||||
@@ -37,6 +37,23 @@ export async function createBindZero(socketType?: SocketType) {
|
||||
return createBindUdp(0, socketType);
|
||||
}
|
||||
|
||||
export async function createSquentialBindZero(socketType?: SocketType) {
|
||||
let attempts = 0;
|
||||
while (true) {
|
||||
const rtpServer = await createBindZero(socketType);
|
||||
try {
|
||||
const rtcpServer = await createBindUdp(rtpServer.port + 1, socketType);
|
||||
return [rtpServer, rtcpServer];
|
||||
}
|
||||
catch (e) {
|
||||
attempts++;
|
||||
closeQuiet(rtpServer.server);
|
||||
}
|
||||
if (attempts === 10)
|
||||
throw new Error('unable to reserve sequential udp ports')
|
||||
}
|
||||
}
|
||||
|
||||
export async function reserveUdpPort() {
|
||||
const udp = await createBindZero();
|
||||
await new Promise(resolve => udp.server.close(() => resolve(undefined)));
|
||||
@@ -62,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export { listenZero, listenZeroSingleClient, ListenZeroSingleClientTimeoutError } from "@scrypted/server/src/listen-zero";
|
||||
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
|
||||
|
||||
@@ -6,14 +6,14 @@ import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
|
||||
import net from 'net';
|
||||
import { Duplex, Readable, Writable } from 'stream';
|
||||
import tls from 'tls';
|
||||
import { URL } from 'url';
|
||||
import { Deferred } from './deferred';
|
||||
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { closeQuiet, createBindZero, createSquentialBindZero, listenZeroSingleClient } from './listen-cluster';
|
||||
import { timeoutPromise } from './promise-utils';
|
||||
import { readLength, readLine } from './read-stream';
|
||||
import { MSection, parseSdp } from './sdp-utils';
|
||||
import { sleep } from './sleep';
|
||||
import { StreamChunk, StreamParser, StreamParserOptions } from './stream-parser';
|
||||
import { URL } from 'url';
|
||||
|
||||
const REQUIRED_WWW_AUTHENTICATE_KEYS = ['realm', 'nonce'];
|
||||
|
||||
@@ -195,48 +195,17 @@ export function createRtspParser(options?: StreamParserOptions): RtspStreamParse
|
||||
'-f', 'rtsp',
|
||||
],
|
||||
findSyncFrame(streamChunks: StreamChunk[]) {
|
||||
let foundIndex: number;
|
||||
let nonVideo: {
|
||||
[codec: string]: StreamChunk,
|
||||
} = {};
|
||||
|
||||
const createSyncFrame = () => {
|
||||
const ret = streamChunks.slice(foundIndex);
|
||||
// for (const nv of Object.values(nonVideo)) {
|
||||
// ret.unshift(nv);
|
||||
// }
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
nonVideo[streamChunk.type] = streamChunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS))
|
||||
foundIndex = prebufferIndex;
|
||||
}
|
||||
|
||||
if (foundIndex !== undefined)
|
||||
return createSyncFrame();
|
||||
|
||||
nonVideo = {};
|
||||
// some streams don't contain codec info, so find an idr frame instead.
|
||||
for (let prebufferIndex = 0; prebufferIndex < streamChunks.length; prebufferIndex++) {
|
||||
const streamChunk = streamChunks[prebufferIndex];
|
||||
if (streamChunk.type !== 'h264') {
|
||||
nonVideo[streamChunk.type] = streamChunk;
|
||||
continue;
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_SPS) || findH264NaluType(streamChunk, H264_NAL_TYPE_IDR)) {
|
||||
return streamChunks.slice(prebufferIndex);
|
||||
}
|
||||
if (findH264NaluType(streamChunk, H264_NAL_TYPE_IDR))
|
||||
foundIndex = prebufferIndex;
|
||||
}
|
||||
|
||||
if (foundIndex !== undefined)
|
||||
return createSyncFrame();
|
||||
|
||||
// oh well!
|
||||
},
|
||||
sdp: new Promise<string>(r => resolve = r),
|
||||
@@ -964,8 +933,7 @@ export class RtspServer {
|
||||
const match = transport.match(/.*?client_port=([0-9]+)-([0-9]+)/);
|
||||
const [_, rtp, rtcp] = match;
|
||||
|
||||
const rtpServer = await createBindZero();
|
||||
const rtcpServer = await createBindUdp(rtpServer.port + 1);
|
||||
const [rtpServer, rtcpServer] = await createSquentialBindZero();
|
||||
this.client.on('close', () => closeQuiet(rtpServer.server));
|
||||
this.client.on('close', () => closeQuiet(rtcpServer.server));
|
||||
this.setupTracks[msection.control] = {
|
||||
|
||||
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: 81f6570f59...4e95093f76
2
external/unifi-protect
vendored
2
external/unifi-protect
vendored
Submodule external/unifi-protect updated: 1f40c63e7f...3759ba334f
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 91be7cf469...9815d03344
@@ -1,6 +1,6 @@
|
||||
# Home Assistant Addon Configuration
|
||||
name: Scrypted
|
||||
version: "18-bullseye-full.s6-v0.23.0"
|
||||
version: "18-jammy-full.s6-v0.39.4"
|
||||
slug: scrypted
|
||||
description: Scrypted is a high performance home video integration and automation platform
|
||||
url: "https://github.com/koush/scrypted"
|
||||
|
||||
@@ -16,6 +16,7 @@ RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
@@ -59,14 +60,6 @@ RUN apt-get -y install \
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
@@ -92,10 +85,6 @@ RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
@@ -116,15 +105,30 @@ RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
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"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=full
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
|
||||
@@ -8,6 +8,7 @@ RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
@@ -37,7 +38,10 @@ ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=lite
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="lite"
|
||||
|
||||
@@ -12,13 +12,14 @@ RUN apt-get update && apt-get -y install \
|
||||
COPY fs /
|
||||
|
||||
# s6 process supervisor
|
||||
ARG S6_OVERLAY_VERSION=3.1.1.2
|
||||
ARG S6_OVERLAY_VERSION=3.1.5.0
|
||||
ENV S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0
|
||||
ENV S6_KEEP_ENV=1
|
||||
RUN case "$(uname -m)" in \
|
||||
x86_64) S6_ARCH='x86_64';; \
|
||||
armv7l) S6_ARCH='armhf';; \
|
||||
aarch64) S6_ARCH='aarch64';; \
|
||||
ARG TARGETARCH
|
||||
RUN case "${TARGETARCH}" in \
|
||||
amd64) S6_ARCH='x86_64';; \
|
||||
arm) S6_ARCH='armhf';; \
|
||||
arm64) S6_ARCH='aarch64';; \
|
||||
*) echo "Your system architecture isn't supported."; exit 1 ;; \
|
||||
esac \
|
||||
&& cd /tmp \
|
||||
|
||||
@@ -5,7 +5,7 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get -y update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get -y install curl software-properties-common apt-utils
|
||||
apt-get -y install curl software-properties-common apt-utils ffmpeg
|
||||
|
||||
# switch to nvm?
|
||||
ARG NODE_VERSION=18
|
||||
@@ -16,7 +16,10 @@ ENV SCRYPTED_CAN_RESTART="true"
|
||||
ENV SCRYPTED_VOLUME="/server/volume"
|
||||
ENV SCRYPTED_INSTALL_PATH="/server"
|
||||
|
||||
RUN test -f "/usr/bin/ffmpeg"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=thin
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="thin"
|
||||
|
||||
@@ -3,9 +3,10 @@ version: "3.5"
|
||||
# The Scrypted docker-compose.yml file typically resides at:
|
||||
# ~/.scrypted/docker-compose.yml
|
||||
|
||||
|
||||
# Scrypted NVR Storage (Optional Network Volume: Part 1 of 3)
|
||||
# Example volumes SMB (CIFS) and NFS.
|
||||
# Uncomment only one.
|
||||
|
||||
# volumes:
|
||||
# nvr:
|
||||
# driver_opts:
|
||||
@@ -20,38 +21,38 @@ version: "3.5"
|
||||
|
||||
services:
|
||||
scrypted:
|
||||
image: koush/scrypted
|
||||
environment:
|
||||
# Scrypted NVR Storage (Part 2 of 3)
|
||||
|
||||
# Uncomment the next line to configure the NVR plugin to store recordings
|
||||
# use the /nvr directory within the container. This can also be configured
|
||||
# within the plugin manually.
|
||||
# The drive or network share will ALSO need to be configured in the volumes
|
||||
# section below.
|
||||
# - SCRYPTED_NVR_VOLUME=/nvr
|
||||
|
||||
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
|
||||
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
|
||||
# nvidia support
|
||||
|
||||
# Uncomment next 3 lines for Nvidia GPU support.
|
||||
# - NVIDIA_VISIBLE_DEVICES=all
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all
|
||||
# runtime: nvidia
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
devices:
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
- /dev/dri:/dev/dri
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# coral PCI devices
|
||||
# - /dev/apex_0:/dev/apex_0
|
||||
# - /dev/apex_1:/dev/apex_1
|
||||
# Uncomment next line to run avahi-daemon inside the container
|
||||
# Don't use if dbus and avahi run on the host and are bind-mounted
|
||||
# (see below under "volumes")
|
||||
# - SCRYPTED_DOCKER_AVAHI=true
|
||||
# runtime: nvidia
|
||||
|
||||
volumes:
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
# modify and add the additional volume for Scrypted NVR
|
||||
# the following example would mount the /mnt/sda/video path on the host
|
||||
# Scrypted NVR Storage (Part 3 of 3)
|
||||
|
||||
# Modify to add the additional volume for Scrypted NVR.
|
||||
# The following example would mount the /mnt/sda/video path on the host
|
||||
# to the /nvr path inside the docker container.
|
||||
# - /mnt/sda/video:/nvr
|
||||
|
||||
# or use a network mount from one of the examples above
|
||||
# Or use a network mount from one of the CIFS/NFS examples at the top of this file.
|
||||
# - type: volume
|
||||
# source: nvr
|
||||
# target: /nvr
|
||||
@@ -60,8 +61,29 @@ services:
|
||||
|
||||
# uncomment the following lines to expose Avahi, an mDNS advertiser.
|
||||
# make sure Avahi is running on the host machine, otherwise this will not work.
|
||||
# not compatible with Avahi enabled via SCRYPTED_DOCKER_AVAHI=true
|
||||
# - /var/run/dbus:/var/run/dbus
|
||||
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
|
||||
|
||||
# Default volume for the Scrypted database. Typically should not be changed.
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
devices:
|
||||
# all usb devices, such as coral tpu
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
# hardware accelerated video decoding, opencl, etc.
|
||||
# - /dev/dri:/dev/dri
|
||||
# uncomment below as necessary.
|
||||
# zwave usb serial device
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# coral PCI devices
|
||||
# - /dev/apex_0:/dev/apex_0
|
||||
# - /dev/apex_1:/dev/apex_1
|
||||
|
||||
container_name: scrypted
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
image: koush/scrypted
|
||||
|
||||
# logging is noisy and will unnecessarily wear on flash storage.
|
||||
# scrypted has per device in memory logging that is preferred.
|
||||
logging:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$SCRYPTED_DOCKER_AVAHI" ]
|
||||
then
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, not starting avahi-daemon" >/dev/stderr
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
@@ -13,4 +13,4 @@ until [ -e /var/run/dbus/system_bus_socket ]; do
|
||||
sleep 1s
|
||||
done
|
||||
echo "Starting Avahi daemon..."
|
||||
exec avahi-daemon --no-chroot -f /etc/avahi/avahi-daemon.conf
|
||||
exec avahi-daemon --no-chroot -f /etc/avahi/avahi-daemon.conf
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, not starting dbus-daemon" >/dev/stderr
|
||||
while true
|
||||
do
|
||||
sleep 1000
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Starting dbus..."
|
||||
exec dbus-daemon --system --nofork
|
||||
exec dbus-daemon --system --nofork
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "${SCRYPTED_DOCKER_AVAHI}" != "true" ]]; then
|
||||
echo "SCRYPTED_DOCKER_AVAHI != true, won't manage dbus nor avahi-daemon" >/dev/stderr
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -qE " ((/var)?/run/dbus|(/var)?/run/avahi-daemon(/socket)?) " /proc/mounts; then
|
||||
echo "dbus and/or avahi-daemon volumes are bind mounted, won't touch them" >/dev/stderr
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# make run folders
|
||||
mkdir -p /var/run/dbus
|
||||
mkdir -p /var/run/avahi-daemon
|
||||
@@ -22,4 +32,4 @@ if [ ! -z "$DSM_HOSTNAME" ]; then
|
||||
sed -i "s/.*host-name.*/host-name=${DSM_HOSTNAME}/" /etc/avahi/avahi-daemon.conf
|
||||
else
|
||||
sed -i "s/.*host-name.*/#host-name=/" /etc/avahi/avahi-daemon.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -43,6 +43,10 @@ WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
|
||||
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
|
||||
echo "Created $DOCKER_COMPOSE_YML"
|
||||
curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
|
||||
if [ -d /dev/dri ]
|
||||
then
|
||||
sed -i 's/'#' - \/dev\/dri/- \/dev\/dri/g' $DOCKER_COMPOSE_YML
|
||||
fi
|
||||
|
||||
echo "Setting permissions on $SCRYPTED_HOME"
|
||||
chown -R $SERVICE_USER $SCRYPTED_HOME
|
||||
|
||||
@@ -15,15 +15,30 @@ RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
|
||||
apt-get -y dist-upgrade; \
|
||||
fi"
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
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"
|
||||
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
# changing this forces pip and npm to perform reinstalls.
|
||||
# if this base image changes, this version must be updated.
|
||||
ENV SCRYPTED_BASE_VERSION=20230608
|
||||
ENV SCRYPTED_DOCKER_FLAVOR=full
|
||||
ENV SCRYPTED_BASE_VERSION="20230608"
|
||||
ENV SCRYPTED_DOCKER_FLAVOR="full"
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.footer
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN apt-get update && apt-get -y install \
|
||||
curl software-properties-common apt-utils \
|
||||
build-essential \
|
||||
cmake \
|
||||
ffmpeg \
|
||||
gcc \
|
||||
libcairo2-dev \
|
||||
libgirepository1.0-dev \
|
||||
@@ -56,14 +57,6 @@ RUN apt-get -y install \
|
||||
RUN apt-get -y install \
|
||||
python3-gst-1.0
|
||||
|
||||
# python 3.9 from ppa.
|
||||
# 3.9 is the version with prebuilt support for tensorflow lite
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa && \
|
||||
apt-get -y install \
|
||||
python3.9 \
|
||||
python3.9-dev \
|
||||
python3.9-distutils
|
||||
|
||||
# armv7l does not have wheels for any of these
|
||||
# and compile times would forever, if it works at all.
|
||||
# furthermore, it's possible to run 32bit docker on 64bit arm,
|
||||
@@ -89,10 +82,6 @@ RUN python3 -m pip install --upgrade pip
|
||||
RUN python3 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
RUN python3.9 -m pip install --upgrade pip
|
||||
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
|
||||
RUN python3.9 -m pip install debugpy typing_extensions psutil
|
||||
|
||||
################################################################
|
||||
# End section generated from template/Dockerfile.full.header
|
||||
################################################################
|
||||
|
||||
8
packages/client/package-lock.json
generated
8
packages/client/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.1.54",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"@scrypted/types": "^0.2.94",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
@@ -21,9 +21,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.91",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.91.tgz",
|
||||
"integrity": "sha512-GfWil8cl2QwlTXk506ZXDALQfuv7zN48PtPlpmBMO/IYTQFtb+RB2zr+FwC9gdvRaZgs9NCCS2Fiig1OY7uxdQ=="
|
||||
"version": "0.2.94",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.94.tgz",
|
||||
"integrity": "sha512-615C6lLnJGk0qhp+Y72B3xeD2CS9p/h8JUmFDjKh4H4IjL6zlV10tZVAXWQt3Q5rmy1WAaS3nScR6NgxZ5woOA=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.91",
|
||||
"@scrypted/types": "^0.2.94",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
|
||||
1
packages/python-client/.gitignore
vendored
Normal file
1
packages/python-client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.venv
|
||||
16
packages/python-client/.vscode/launch.json
vendored
Normal file
16
packages/python-client/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Current File",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/test.py",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
]
|
||||
}
|
||||
1
packages/python-client/plugin_remote.py
Symbolic link
1
packages/python-client/plugin_remote.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/plugin_remote.py
|
||||
3
packages/python-client/requirements.txt
Normal file
3
packages/python-client/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
python-engineio[asyncio_client]
|
||||
aiohttp
|
||||
aiodns
|
||||
1
packages/python-client/rpc.py
Symbolic link
1
packages/python-client/rpc.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/rpc.py
|
||||
1
packages/python-client/rpc_reader.py
Symbolic link
1
packages/python-client/rpc_reader.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../server/python/rpc_reader.py
|
||||
1
packages/python-client/scrypted_python
Symbolic link
1
packages/python-client/scrypted_python
Symbolic link
@@ -0,0 +1 @@
|
||||
../../sdk/types/scrypted_python
|
||||
151
packages/python-client/test.py
Normal file
151
packages/python-client/test.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import nullcontext
|
||||
|
||||
import aiohttp
|
||||
import engineio
|
||||
|
||||
import plugin_remote
|
||||
import rpc_reader
|
||||
from plugin_remote import DeviceManager, MediaManager, SystemManager
|
||||
from scrypted_python.scrypted_sdk import ScryptedInterface, ScryptedStatic
|
||||
|
||||
|
||||
class EioRpcTransport(rpc_reader.RpcTransport):
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||
super().__init__()
|
||||
self.eio = engineio.AsyncClient(ssl_verify=False)
|
||||
self.loop = loop
|
||||
self.write_error: Exception = None
|
||||
self.read_queue = asyncio.Queue()
|
||||
self.write_queue = asyncio.Queue()
|
||||
|
||||
@self.eio.on("message")
|
||||
def on_message(data):
|
||||
self.read_queue.put_nowait(data)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(self.send_loop(), self.loop)
|
||||
|
||||
async def read(self):
|
||||
return await self.read_queue.get()
|
||||
|
||||
async def send_loop(self):
|
||||
while True:
|
||||
data = await self.write_queue.get()
|
||||
try:
|
||||
await self.eio.send(data)
|
||||
except Exception as e:
|
||||
self.write_error = e
|
||||
self.write_queue = None
|
||||
break
|
||||
|
||||
def writeBuffer(self, buffer, reject):
|
||||
async def send():
|
||||
try:
|
||||
if self.write_error:
|
||||
raise self.write_error
|
||||
self.write_queue.put_nowait(buffer)
|
||||
except Exception as e:
|
||||
reject(e)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(send(), self.loop)
|
||||
|
||||
def writeJSON(self, json, reject):
|
||||
return self.writeBuffer(json, reject)
|
||||
|
||||
|
||||
async def connect_scrypted_client(
|
||||
transport: EioRpcTransport,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
plugin_id: str = "@scrypted/core",
|
||||
session: aiohttp.ClientSession | None = None,
|
||||
) -> ScryptedStatic:
|
||||
login_url = f"{base_url}/login"
|
||||
login_body = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
if session:
|
||||
cm = nullcontext(session)
|
||||
else:
|
||||
cm = aiohttp.ClientSession()
|
||||
|
||||
async with cm as _session:
|
||||
async with _session.post(
|
||||
login_url, verify_ssl=False, json=login_body
|
||||
) as response:
|
||||
login_response = await response.json()
|
||||
|
||||
headers = {"Authorization": login_response["authorization"]}
|
||||
|
||||
await transport.eio.connect(
|
||||
base_url,
|
||||
headers=headers,
|
||||
engineio_path=f"/endpoint/{plugin_id}/engine.io/api/",
|
||||
)
|
||||
|
||||
ret = asyncio.Future[ScryptedStatic](loop=transport.loop)
|
||||
peer, peerReadLoop = await rpc_reader.prepare_peer_readloop(
|
||||
transport.loop, transport
|
||||
)
|
||||
peer.params["print"] = print
|
||||
|
||||
def callback(api, pluginId, hostInfo):
|
||||
remote = plugin_remote.PluginRemote(
|
||||
peer, api, pluginId, hostInfo, transport.loop
|
||||
)
|
||||
wrapped = remote.setSystemState
|
||||
|
||||
async def remoteSetSystemState(systemState):
|
||||
await wrapped(systemState)
|
||||
|
||||
async def resolve():
|
||||
sdk = ScryptedStatic()
|
||||
sdk.api = api
|
||||
sdk.remote = remote
|
||||
sdk.systemManager = SystemManager(api, remote.systemState)
|
||||
sdk.deviceManager = DeviceManager(
|
||||
remote.nativeIds, sdk.systemManager
|
||||
)
|
||||
sdk.mediaManager = MediaManager(await api.getMediaManager())
|
||||
ret.set_result(sdk)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(resolve(), transport.loop)
|
||||
|
||||
remote.setSystemState = remoteSetSystemState
|
||||
return remote
|
||||
|
||||
peer.params["getRemote"] = callback
|
||||
asyncio.run_coroutine_threadsafe(peerReadLoop(), transport.loop)
|
||||
|
||||
sdk = await ret
|
||||
return sdk
|
||||
|
||||
|
||||
async def main():
|
||||
transport = EioRpcTransport(asyncio.get_event_loop())
|
||||
sdk = await connect_scrypted_client(
|
||||
transport,
|
||||
"https://localhost:10443",
|
||||
os.environ["SCRYPTED_USERNAME"],
|
||||
os.environ["SCRYPTED_PASSWORD"],
|
||||
)
|
||||
|
||||
for id in sdk.systemManager.getSystemState():
|
||||
device = sdk.systemManager.getDeviceById(id)
|
||||
print(device.name)
|
||||
if ScryptedInterface.OnOff.value in device.interfaces:
|
||||
print(f"OnOff: device is {device.on}")
|
||||
|
||||
await transport.eio.disconnect()
|
||||
os._exit(0)
|
||||
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.run_coroutine_threadsafe(main(), loop)
|
||||
loop.run_forever()
|
||||
4
plugins/alexa/package-lock.json
generated
4
plugins/alexa/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"uuid": "^9.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
"prescrypted-setup-project": "scrypted-package-json",
|
||||
|
||||
@@ -6,11 +6,11 @@ import { supportedTypes } from ".";
|
||||
supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
|
||||
let capabilities: any[] = [];
|
||||
let category: DisplayCategory = 'DOORBELL';
|
||||
const displayCategories: DisplayCategory[] = ['DOORBELL'];
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
|
||||
capabilities = await getCameraCapabilities(device);
|
||||
category = 'CAMERA';
|
||||
displayCategories.push('CAMERA');
|
||||
}
|
||||
|
||||
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
|
||||
@@ -25,7 +25,7 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
|
||||
}
|
||||
|
||||
return {
|
||||
displayCategories: [category],
|
||||
displayCategories,
|
||||
capabilities
|
||||
};
|
||||
},
|
||||
|
||||
199
plugins/amcrest/package-lock.json
generated
199
plugins/amcrest/package-lock.json
generated
@@ -1,26 +1,25 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.122",
|
||||
"lockfileVersion": 2,
|
||||
"version": "0.0.123",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.122",
|
||||
"version": "0.0.123",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
"multiparty": "^4.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.11"
|
||||
"@types/node": "^18.16.18"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -35,8 +34,7 @@
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.87",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -71,9 +69,6 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
@@ -100,9 +95,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
"version": "18.16.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
|
||||
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw=="
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
@@ -120,15 +115,15 @@
|
||||
"node_modules/depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
|
||||
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"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",
|
||||
@@ -145,15 +140,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
|
||||
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
||||
"dependencies": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -165,11 +160,11 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/multiparty": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
|
||||
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz",
|
||||
"integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==",
|
||||
"dependencies": {
|
||||
"http-errors": "~1.8.0",
|
||||
"http-errors": "~1.8.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "2.1.5"
|
||||
},
|
||||
@@ -180,7 +175,7 @@
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -212,15 +207,15 @@
|
||||
"node_modules/statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
|
||||
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
@@ -236,147 +231,5 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"requires": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.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": "^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/multiparty": {
|
||||
"version": "0.0.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz",
|
||||
"integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz",
|
||||
"integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==",
|
||||
"requires": {
|
||||
"depd": "~1.1.2",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": ">= 1.5.0 < 2",
|
||||
"toidentifier": "1.0.0"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"multiparty": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.2.tgz",
|
||||
"integrity": "sha512-NtZLjlvsjcoGrzojtwQwn/Tm90aWJ6XXtPppYF4WmOk/6ncdwMMKggFY2NlRRN9yiCEIVxpOfPWahVEG2HAG8Q==",
|
||||
"requires": {
|
||||
"http-errors": "~1.8.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "2.1.5"
|
||||
}
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||
},
|
||||
"toidentifier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||
},
|
||||
"uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"requires": {
|
||||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/amcrest",
|
||||
"version": "0.0.122",
|
||||
"version": "0.0.123",
|
||||
"description": "Amcrest Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -39,9 +39,9 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/multiparty": "^0.0.33",
|
||||
"multiparty": "^4.2.2"
|
||||
"multiparty": "^4.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.11"
|
||||
"@types/node": "^18.16.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export class AmcrestCameraClient {
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
|
||||
@@ -8,6 +8,8 @@ The account you use for this plugin must have either SMS or email set as the def
|
||||
|
||||
If you experience any trouble logging in, clear the username and password boxes, reload the plugin, and try again.
|
||||
|
||||
If you are unable to see shared cameras in your separate Arlo account, ensure that both your primary and secondary accounts are upgraded according to this [forum post](https://web.archive.org/web/20230710141914/https://community.arlo.com/t5/Arlo-Secure/Invited-friend-cannot-see-devices-on-their-dashboard-Arlo-Pro-2/m-p/1889396#M1813). Verify the sharing worked by logging in via the Arlo web dashboard.
|
||||
|
||||
## General Setup Notes
|
||||
|
||||
* Ensure that your Arlo account's default 2FA option is set to either SMS or email.
|
||||
@@ -16,7 +18,7 @@ If you experience any trouble logging in, clear the username and password boxes,
|
||||
* It is highly recommended to enable the Rebroadcast plugin to allow multiple downstream plugins to pull the video feed within Scrypted.
|
||||
* If there is no audio on your camera, switch to the `FFmpeg (TCP)` parser under the `Cloud RTSP` settings.
|
||||
* Prebuffering should only be enabled if the camera is wired to a persistent power source, such as a wall outlet. Prebuffering will only work if your camera does not have a battery or `Plugged In to External Power` is selected.
|
||||
* The plugin supports pulling RTSP or DASH streams from Arlo Cloud. It is recommended to use RTSP for the lowest latency streams. DASH is inconsistent in reliability, and may return finicky codecs that require additional FFmpeg output arguments, e.g. `-vcodec h264`.
|
||||
* The plugin supports pulling RTSP or DASH streams from Arlo Cloud. It is recommended to use RTSP for the lowest latency streams. DASH is inconsistent in reliability, and may return finicky codecs that require additional FFmpeg output arguments, e.g. `-vcodec h264`. *Note that both RTSP and DASH will ultimately pull the same video stream feed from your camera, and they cannot both be used at the same time due to the single stream limitation.*
|
||||
|
||||
Note that streaming cameras uses extra Internet bandwidth, since video and audio packets will need to travel from the camera through your network, out to Arlo Cloud, and then back to your network and into Scrypted.
|
||||
|
||||
@@ -24,4 +26,16 @@ Note that streaming cameras uses extra Internet bandwidth, since video and audio
|
||||
|
||||
The Arlo Plugin supports using the IMAP protocol to check an email mailbox for Arlo 2FA codes. This requires you to specify an email 2FA option as the default in your Arlo account settings.
|
||||
|
||||
The plugin should work with any mailbox that supports IMAP, but so far has been tested with Gmail. To configure a Gmail mailbox, see [here](https://support.google.com/mail/answer/7126229?hl=en) to see the Gmail IMAP settings, and [here](https://support.google.com/accounts/answer/185833?hl=en) to create an App Password. Enter the App Password in place of your normal Gmail password.
|
||||
The plugin should work with any mailbox that supports IMAP, but so far has been tested with Gmail. To configure a Gmail mailbox, see [here](https://support.google.com/mail/answer/7126229?hl=en) to see the Gmail IMAP settings, and [here](https://support.google.com/accounts/answer/185833?hl=en) to create an App Password. Enter the App Password in place of your normal Gmail password.
|
||||
|
||||
The plugin searches for emails sent by Arlo's `do_not_reply@arlo.com` address when looking for 2FA codes. If you are using a service to forward emails to the mailbox registered with this plugin (e.g. a service like iCloud's Hide My Email), it is possible that Arlo's email sender address has been overwritten by the mail forwarder. Check the email registered with this plugin to see what address the mail forwarder uses to replace Arlo's sender address, and update that in the IMAP 2FA settings.
|
||||
|
||||
## Virtual Security System for Arlo Sirens
|
||||
|
||||
In external integrations like Homekit, sirens are exposed as simple on-off switches. This makes it easy to accidentally hit the switch when using the Home app. The Arlo Plugin creates a "virtual" security system device per siren to allow Scrypted to arm or disarm the siren switch to protect against accidental triggers. This fake security system device will be synced into Homekit as a separate accessory from the camera, with the siren itself merged into the security system accessory.
|
||||
|
||||
Note that the virtual security system is NOT tied to your Arlo account at all, and will not make any changes such as switching your device's motion alert armed/disarmed modes. For more information, please see the README on the virtual security system device in Scrypted.
|
||||
|
||||
## Video Clips
|
||||
|
||||
The Arlo Plugin will show video clips available in Arlo Cloud for cameras with cloud recording enabled. These clips are not downloaded onto your Scrypted server, but rather streamed on-demand. Deleting clips is not available in Scrypted and should be done through the Arlo app or the Arlo web dashboard.
|
||||
7
plugins/arlo/package-lock.json
generated
7
plugins/arlo/package-lock.json
generated
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.11",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.11",
|
||||
"license": "Apache",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.101",
|
||||
"version": "0.2.103",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.7.29",
|
||||
"version": "0.8.11",
|
||||
"description": "Arlo Plugin for Scrypted",
|
||||
"license": "Apache",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
"plugin",
|
||||
|
||||
@@ -75,14 +75,24 @@ USER_AGENTS = {
|
||||
"Gecko/20100101 Firefox/85.0",
|
||||
"linux":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
|
||||
|
||||
# extracted from cloudscraper as a working UA for cloudflare
|
||||
"android":
|
||||
"Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; PACM00 Build/O11019) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/8.8 Mobile Safari/537.36"
|
||||
}
|
||||
|
||||
# user agents for media players, e.g. the android app
|
||||
MEDIA_USER_AGENTS = {
|
||||
"android": "ijkplayer-android-4.5_28538"
|
||||
}
|
||||
|
||||
|
||||
class Arlo(object):
|
||||
BASE_URL = 'my.arlo.com'
|
||||
AUTH_URL = 'ocapi-app.arlo.com'
|
||||
BACKUP_AUTH_HOSTS = list(scrypted_arlo_go.BACKUP_AUTH_HOSTS())
|
||||
BACKUP_AUTH_HOSTS = ['NTIuMjEwLjMuMTIx', 'MzQuMjU1LjkyLjIxMg==', 'MzQuMjUxLjE3Ny45MA==', 'NTQuMjQ2LjE3MS4x']
|
||||
TRANSID_PREFIX = 'web'
|
||||
|
||||
random.shuffle(BACKUP_AUTH_HOSTS)
|
||||
@@ -91,7 +101,7 @@ class Arlo(object):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.event_stream = None
|
||||
self.request = Request()
|
||||
self.request = None
|
||||
|
||||
def to_timestamp(self, dt):
|
||||
if sys.version[0] == '2':
|
||||
@@ -140,6 +150,7 @@ class Arlo(object):
|
||||
self.user_id = user_id
|
||||
headers['Content-Type'] = 'application/json; charset=UTF-8'
|
||||
headers['User-Agent'] = USER_AGENTS['arlo']
|
||||
self.request = Request(mode="cloudscraper")
|
||||
self.request.session.headers.update(headers)
|
||||
self.BASE_URL = 'myapi.arlo.com'
|
||||
|
||||
@@ -150,7 +161,6 @@ class Arlo(object):
|
||||
'schemaVersion': '1',
|
||||
'Auth-Version': '2',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': USER_AGENTS['arlo'],
|
||||
'Origin': f'https://{self.BASE_URL}',
|
||||
'Referer': f'https://{self.BASE_URL}/',
|
||||
'Source': 'arloCamWeb',
|
||||
@@ -237,7 +247,7 @@ class Arlo(object):
|
||||
if finish_auth_body.get('data', {}).get('token') is None:
|
||||
raise Exception("Could not complete 2FA, maybe invalid token? If the error persists, please try reloading the plugin and logging in again.")
|
||||
|
||||
self.request = Request()
|
||||
self.request = Request(mode="cloudscraper")
|
||||
|
||||
# Update Authorization code with new code
|
||||
headers = {
|
||||
@@ -294,17 +304,23 @@ class Arlo(object):
|
||||
# filter out cameras without basestation, where they are their own basestations
|
||||
# this is so battery-powered devices do not drain due to pings
|
||||
# for wired devices, keep doorbells, sirens, and arloq in the list so they get pings
|
||||
proper_basestations = {}
|
||||
# we also add arlo baby devices (abc1000, abc1000a) since they are standalone-only
|
||||
# and seem to want pings
|
||||
devices_to_ping = {}
|
||||
for basestation in basestations.values():
|
||||
if basestation['deviceId'] == basestation.get('parentId') and \
|
||||
basestation['deviceType'] not in ['doorbell', 'siren', 'arloq', 'arloqs']:
|
||||
basestation['deviceType'] not in ['doorbell', 'siren', 'arloq', 'arloqs'] and \
|
||||
basestation['modelId'].lower() not in ['abc1000', 'abc1000a']:
|
||||
continue
|
||||
proper_basestations[basestation['deviceId']] = basestation
|
||||
# avd2001 is the battery doorbell, and we don't want to drain its battery, so disable pings
|
||||
if basestation['modelId'].lower().startswith('avd2001'):
|
||||
continue
|
||||
devices_to_ping[basestation['deviceId']] = basestation
|
||||
|
||||
logger.info(f"Will send heartbeat to the following devices: {list(proper_basestations.keys())}")
|
||||
logger.info(f"Will send heartbeat to the following devices: {list(devices_to_ping.keys())}")
|
||||
|
||||
# start heartbeat loop with only basestations
|
||||
asyncio.get_event_loop().create_task(heartbeat(self, list(proper_basestations.values())))
|
||||
# start heartbeat loop with only pingable devices
|
||||
asyncio.get_event_loop().create_task(heartbeat(self, list(devices_to_ping.values())))
|
||||
|
||||
# subscribe to all camera topics
|
||||
topics = [
|
||||
@@ -396,58 +412,135 @@ class Arlo(object):
|
||||
basestation_id = basestation.get('deviceId')
|
||||
return self.Notify(basestation, {"action":"set","resource":"subscriptions/"+self.user_id+"_web","publishResponse":False,"properties":{"devices":[basestation_id]}})
|
||||
|
||||
def SubscribeToMotionEvents(self, basestation, camera, callback):
|
||||
def SubscribeToErrorEvents(self, basestation, camera, callback):
|
||||
"""
|
||||
Use this method to subscribe to error events. You must provide a callback function which will get called once per error event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(code, message)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
# Note: It looks like sometimes a message is returned as an 'is' action
|
||||
# where a 'stateChangeReason' property contains the error message. This is
|
||||
# a bit of a hack but we will listen to both events with an 'error' key as
|
||||
# well as 'stateChangeReason' events.
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
if 'error' in event:
|
||||
error = event['error']
|
||||
elif 'properties' in event:
|
||||
error = event['properties'].get('stateChangeReason', {})
|
||||
else:
|
||||
return None
|
||||
message = error.get('message')
|
||||
code = error.get('code')
|
||||
stop = callback(code, message)
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, ['error', ('is', 'stateChangeReason')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToMotionEvents(self, basestation, camera, callback, logger) -> asyncio.Task:
|
||||
"""
|
||||
Use this method to subscribe to motion events. You must provide a callback function which will get called once per motion event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
return self._subscribe_to_motion_or_audio_events(basestation, camera, callback, logger, "motionDetected")
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
properties = event.get('properties', {})
|
||||
stop = None
|
||||
if 'motionDetected' in properties:
|
||||
stop = callback(properties['motionDetected'])
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToAudioEvents(self, basestation, camera, callback):
|
||||
def SubscribeToAudioEvents(self, basestation, camera, callback, logger):
|
||||
"""
|
||||
Use this method to subscribe to audio events. You must provide a callback function which will get called once per audio event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
|
||||
Returns the Task object that contains the subscription loop.
|
||||
"""
|
||||
return self._subscribe_to_motion_or_audio_events(basestation, camera, callback, logger, "audioDetected")
|
||||
|
||||
def _subscribe_to_motion_or_audio_events(self, basestation, camera, callback, logger, event_key) -> asyncio.Task:
|
||||
"""
|
||||
Helper class to implement force reset of events (when event end signal is dropped) and delay of end
|
||||
of event signals (when the sensor turns off and on quickly)
|
||||
|
||||
event_key is either motionDetected or audioDetected
|
||||
"""
|
||||
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
# if we somehow miss the *Detected = False event, this task
|
||||
# is used to force the caller to register the end of the event
|
||||
force_reset_event_task: asyncio.Task = None
|
||||
|
||||
# when we receive a normal *Detected = False event, this
|
||||
# task is used to delay the delivery in case the sensor
|
||||
# registers an event immediately afterwards
|
||||
delayed_event_end_task: asyncio.Task = None
|
||||
|
||||
async def reset_event(sleep_duration: float) -> None:
|
||||
nonlocal force_reset_event_task, delayed_event_end_task
|
||||
await asyncio.sleep(sleep_duration)
|
||||
|
||||
logger.debug(f"{event_key}: delivering False")
|
||||
callback(False)
|
||||
|
||||
force_reset_event_task = None
|
||||
delayed_event_end_task = None
|
||||
|
||||
def callbackwrapper(self, event):
|
||||
nonlocal force_reset_event_task, delayed_event_end_task
|
||||
properties = event.get('properties', {})
|
||||
|
||||
stop = None
|
||||
if 'audioDetected' in properties:
|
||||
stop = callback(properties['audioDetected'])
|
||||
if event_key in properties:
|
||||
event_detected = properties[event_key]
|
||||
delivery_delay = 10
|
||||
|
||||
logger.debug(f"{event_key}: {event_detected} {'will delay delivery by ' + str(delivery_delay) + 's' if not event_detected else ''}".rstrip())
|
||||
|
||||
if force_reset_event_task:
|
||||
logger.debug(f"{event_key}: cancelling previous force reset task")
|
||||
force_reset_event_task.cancel()
|
||||
force_reset_event_task = None
|
||||
if delayed_event_end_task:
|
||||
logger.debug(f"{event_key}: cancelling previous delay event task")
|
||||
delayed_event_end_task.cancel()
|
||||
delayed_event_end_task = None
|
||||
|
||||
if event_detected:
|
||||
stop = callback(event_detected)
|
||||
|
||||
# schedule a callback to reset the sensor
|
||||
# if we somehow miss the *Detected = False event
|
||||
force_reset_event_task = asyncio.get_event_loop().create_task(reset_event(60))
|
||||
else:
|
||||
delayed_event_end_task = asyncio.get_event_loop().create_task(reset_event(delivery_delay))
|
||||
|
||||
if not stop:
|
||||
return None
|
||||
return stop
|
||||
|
||||
return asyncio.get_event_loop().create_task(
|
||||
self.HandleEvents(basestation, resource, [('is', 'audioDetected')], callbackwrapper)
|
||||
self.HandleEvents(basestation, resource, [('is', event_key)], callbackwrapper)
|
||||
)
|
||||
|
||||
def SubscribeToBatteryEvents(self, basestation, camera, callback):
|
||||
@@ -455,7 +548,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to battery events. You must provide a callback function which will get called once per battery event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -482,7 +575,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to doorbell events. You must provide a callback function which will get called once per doorbell event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -518,7 +611,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to pushToTalk SDP answer events. You must provide a callback function which will get called once per SDP event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -546,7 +639,7 @@ class Arlo(object):
|
||||
Use this method to subscribe to pushToTalk ICE candidate answer events. You must provide a callback function which will get called once per candidate event.
|
||||
|
||||
The callback function should have the following signature:
|
||||
def callback(self, event)
|
||||
def callback(event)
|
||||
|
||||
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
|
||||
that has a big switch statement in it to handle all the various events Arlo produces.
|
||||
@@ -658,6 +751,16 @@ class Arlo(object):
|
||||
devices = self.request.get(f'https://{self.BASE_URL}/hmsweb/v2/users/devices')
|
||||
return devices
|
||||
|
||||
def GetDeviceCapabilities(self, device: dict) -> dict:
|
||||
return self._getDeviceCapabilitiesImpl(device['modelId'].lower(), device['interfaceVersion'])
|
||||
|
||||
@cached(cache=TTLCache(maxsize=64, ttl=60))
|
||||
def _getDeviceCapabilitiesImpl(self, model_id: str, interface_version: str) -> dict:
|
||||
return self.request.get(
|
||||
f'https://{self.BASE_URL}/resources/capabilities/{model_id}/{model_id}_{interface_version}.json',
|
||||
raw=True
|
||||
)
|
||||
|
||||
async def StartStream(self, basestation, camera, mode="rtsp"):
|
||||
"""
|
||||
This function returns the url of the rtsp video stream.
|
||||
@@ -665,7 +768,8 @@ class Arlo(object):
|
||||
It can be streamed with: ffmpeg -re -i 'rtsps://<url>' -acodec copy -vcodec copy test.mp4
|
||||
The request to /users/devices/startStream returns: { url:rtsp://<url>:443/vzmodulelive?egressToken=b<xx>&userAgent=iOS&cameraId=<camid>}
|
||||
|
||||
If mode is set to "dash", returns the url to the mpd file for DASH streaming.
|
||||
If mode is set to "dash", returns the url to the mpd file for DASH streaming. Note that DASH
|
||||
has very specific header requirements - see GetMPDHeaders()
|
||||
"""
|
||||
resource = f"cameras/{camera.get('deviceId')}"
|
||||
|
||||
@@ -698,6 +802,8 @@ class Arlo(object):
|
||||
|
||||
def callback(self, event):
|
||||
#return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
|
||||
if "error" in event:
|
||||
return None
|
||||
properties = event.get("properties", {})
|
||||
if properties.get("activityState") == "userStreamActive":
|
||||
if mode == "rtsp":
|
||||
@@ -720,11 +826,11 @@ class Arlo(object):
|
||||
|
||||
headers = {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Connection": "keep-alive",
|
||||
"DNT": "1",
|
||||
"Egress-Token": query['egressToken'][0],
|
||||
"Egress-Token": query['egressToken'][0], # this is very important
|
||||
"Origin": "https://my.arlo.com",
|
||||
"Referer": "https://my.arlo.com/",
|
||||
"User-Agent": USER_AGENTS["firefox"],
|
||||
@@ -735,6 +841,16 @@ class Arlo(object):
|
||||
resp = self.request.get(f'https://{self.BASE_URL}/hmsweb/users/devices/sipInfo')
|
||||
return resp
|
||||
|
||||
def GetSIPInfoV2(self, camera):
|
||||
resp = self.request.get(
|
||||
f'https://{self.BASE_URL}/hmsweb/users/devices/sipInfo/v2',
|
||||
headers={
|
||||
"xcloudId": camera.get('xCloudId'),
|
||||
"cameraId": camera.get('deviceId'),
|
||||
}
|
||||
)
|
||||
return resp
|
||||
|
||||
def StartPushToTalk(self, basestation, camera):
|
||||
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
|
||||
resp = self.request.get(url)
|
||||
@@ -792,6 +908,8 @@ class Arlo(object):
|
||||
)
|
||||
|
||||
def callback(self, event):
|
||||
if "error" in event:
|
||||
return None
|
||||
properties = event.get("properties", {})
|
||||
url = properties.get("presignedFullFrameSnapshotUrl")
|
||||
if url:
|
||||
@@ -917,6 +1035,32 @@ class Arlo(object):
|
||||
},
|
||||
})
|
||||
|
||||
def NightlightOn(self, basestation):
|
||||
resource = f"cameras/{basestation.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"nightLight": {
|
||||
"enabled": True
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def NightlightOff(self, basestation):
|
||||
resource = f"cameras/{basestation.get('deviceId')}"
|
||||
return self.Notify(basestation, {
|
||||
"action": "set",
|
||||
"resource": resource,
|
||||
"publishResponse": True,
|
||||
"properties": {
|
||||
"nightLight": {
|
||||
"enabled": False
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def GetLibrary(self, device, from_date: datetime, to_date: datetime):
|
||||
"""
|
||||
This call returns the following:
|
||||
|
||||
@@ -7,20 +7,25 @@ import scrypted_arlo_go
|
||||
from .logging import logger
|
||||
|
||||
|
||||
setdefaulttimeout(5)
|
||||
setdefaulttimeout(15)
|
||||
|
||||
|
||||
def pick_host(hosts, hostname_to_match, endpoint_to_test):
|
||||
session = requests.Session()
|
||||
session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
setdefaulttimeout(5)
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
c = ssl.get_server_certificate((host, 443))
|
||||
scrypted_arlo_go.VerifyCertHostname(c, hostname_to_match)
|
||||
r = session.post(f"https://{host}{endpoint_to_test}", headers={"Host": hostname_to_match})
|
||||
r.raise_for_status()
|
||||
return host
|
||||
except Exception as e:
|
||||
logger.warning(f"{host} is invalid: {e}")
|
||||
raise Exception("no valid hosts found!")
|
||||
try:
|
||||
session = requests.Session()
|
||||
session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
c = ssl.get_server_certificate((host, 443))
|
||||
scrypted_arlo_go.VerifyCertHostname(c, hostname_to_match)
|
||||
r = session.post(f"https://{host}{endpoint_to_test}", headers={"Host": hostname_to_match})
|
||||
r.raise_for_status()
|
||||
return host
|
||||
except Exception as e:
|
||||
logger.warning(f"{host} is invalid: {e}")
|
||||
raise Exception("no valid hosts found!")
|
||||
finally:
|
||||
setdefaulttimeout(15)
|
||||
|
||||
@@ -9,7 +9,7 @@ logger.setLevel(logging.INFO)
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
|
||||
# log formatting
|
||||
fmt = logging.Formatter("[Arlo] %(message)s")
|
||||
fmt = logging.Formatter("[Arlo]: %(message)s")
|
||||
ch.setFormatter(fmt)
|
||||
|
||||
# configure handler to logger
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
# limitations under the License.
|
||||
##
|
||||
|
||||
from functools import partialmethod
|
||||
import requests
|
||||
from requests.exceptions import HTTPError
|
||||
from requests_toolbelt.adapters import host_header_ssl
|
||||
@@ -21,6 +22,15 @@ import cloudscraper
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .logging import logger
|
||||
|
||||
|
||||
try:
|
||||
from curl_cffi import requests as curl_cffi_requests
|
||||
HAS_CURL_CFFI = True
|
||||
except:
|
||||
HAS_CURL_CFFI = False
|
||||
|
||||
#from requests_toolbelt.utils import dump
|
||||
#def print_raw_http(response):
|
||||
# data = dump.dump_all(response, request_prefix=b'', response_prefix=b'')
|
||||
@@ -29,13 +39,21 @@ import uuid
|
||||
class Request(object):
|
||||
"""HTTP helper class"""
|
||||
|
||||
def __init__(self, timeout=5, mode="cloudscraper"):
|
||||
if mode == "cloudscraper":
|
||||
def __init__(self, timeout=5, mode="curl" if HAS_CURL_CFFI else "cloudscraper"):
|
||||
if mode == "curl":
|
||||
logger.debug("HTTP helper using curl_cffi")
|
||||
self.session = curl_cffi_requests.Session(impersonate="chrome110")
|
||||
elif mode == "cloudscraper":
|
||||
logger.debug("HTTP helper using cloudscraper")
|
||||
from .arlo_async import USER_AGENTS
|
||||
self.session = cloudscraper.CloudScraper(browser={"custom": USER_AGENTS["arlo"]})
|
||||
self.session = cloudscraper.CloudScraper(browser={"custom": USER_AGENTS["android"]})
|
||||
elif mode == "ip":
|
||||
logger.debug("HTTP helper using requests with HostHeaderSSLAdapter")
|
||||
self.session = requests.Session()
|
||||
self.session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
|
||||
else:
|
||||
logger.debug("HTTP helper using requests")
|
||||
self.session = requests.Session()
|
||||
self.timeout = timeout
|
||||
|
||||
def gen_event_id(self):
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import sseclient
|
||||
import threading
|
||||
|
||||
from .stream_async import Stream
|
||||
from .stream_async import Stream
|
||||
from .logging import logger
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class EventStream(Stream):
|
||||
continue
|
||||
|
||||
try:
|
||||
response = json.loads(event.data)
|
||||
response = json.loads(event.data.strip())
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
@@ -36,6 +36,7 @@ class EventStream(Stream):
|
||||
if self.event_stream_stop_event.is_set() or \
|
||||
self.shutting_down_stream is event_stream:
|
||||
logger.info(f"SSE {id(event_stream)} disconnected")
|
||||
self.shutting_down_stream = None
|
||||
return None
|
||||
elif response.get('status') == 'connected':
|
||||
if not self.connected:
|
||||
@@ -59,10 +60,10 @@ class EventStream(Stream):
|
||||
self.shutting_down_stream = self.event_stream
|
||||
self.event_stream = None
|
||||
await self.start()
|
||||
# give it an extra sleep to ensure any previous connections have disconnected properly
|
||||
# this is so we can mark reconnecting to False properly
|
||||
await asyncio.sleep(1)
|
||||
self.shutting_down_stream = None
|
||||
while self.shutting_down_stream is not None:
|
||||
# ensure any previous connections have disconnected properly
|
||||
# this is so we can mark reconnecting to False properly
|
||||
await asyncio.sleep(1)
|
||||
self.reconnecting = False
|
||||
|
||||
def subscribe(self, topics):
|
||||
|
||||
@@ -177,22 +177,25 @@ class Stream:
|
||||
|
||||
now = time.time()
|
||||
event = StreamEvent(response, now, now + self.expire)
|
||||
self._queue_impl(key, event)
|
||||
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
# specialized setup for error responses
|
||||
if 'error' in response:
|
||||
key = f"{resource}/error"
|
||||
self._queue_impl(key, event)
|
||||
|
||||
# for optimized lookups, notify listeners of individual properties
|
||||
properties = response.get('properties', {})
|
||||
for property in properties.keys():
|
||||
key = f"{resource}/{action}/{property}"
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
self._queue_impl(key, event)
|
||||
|
||||
def _queue_impl(self, key, event):
|
||||
if key not in self.queues:
|
||||
q = self.queues[key] = asyncio.Queue()
|
||||
else:
|
||||
q = self.queues[key]
|
||||
q.put_nowait(event)
|
||||
|
||||
def requeue(self, event, resource, action, property=None):
|
||||
if not property:
|
||||
|
||||
@@ -18,6 +18,7 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
|
||||
nativeId: str = None
|
||||
arlo_device: dict = None
|
||||
arlo_basestation: dict = None
|
||||
arlo_capabilities: dict = None
|
||||
provider: ArloProvider = None
|
||||
stop_subscriptions: bool = False
|
||||
|
||||
@@ -32,6 +33,12 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
|
||||
self.provider = provider
|
||||
self.logger.setLevel(self.provider.get_current_log_level())
|
||||
|
||||
try:
|
||||
self.arlo_capabilities = self.provider.arlo.GetDeviceCapabilities(self.arlo_device)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not load device capabilities: {e}")
|
||||
self.arlo_capabilities = {}
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.stop_subscriptions = True
|
||||
self.cancel_pending_tasks()
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from scrypted_sdk import ScryptedDeviceBase
|
||||
from scrypted_sdk.types import Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType
|
||||
from scrypted_sdk.types import Device, DeviceProvider, Setting, SettingValue, Settings, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .base import ArloDeviceBase
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloBasestation(ArloDeviceBase, DeviceProvider):
|
||||
class ArloBasestation(ArloDeviceBase, DeviceProvider, Settings):
|
||||
MODELS_WITH_SIRENS = [
|
||||
"vmb4000",
|
||||
"vmb4500"
|
||||
@@ -29,7 +29,10 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider):
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS])
|
||||
|
||||
def get_applicable_interfaces(self) -> List[str]:
|
||||
return [ScryptedInterface.DeviceProvider.value]
|
||||
return [
|
||||
ScryptedInterface.DeviceProvider.value,
|
||||
ScryptedInterface.Settings.value,
|
||||
]
|
||||
|
||||
def get_device_type(self) -> str:
|
||||
return ScryptedDeviceType.DeviceProvider.value
|
||||
@@ -68,4 +71,20 @@ class ArloBasestation(ArloDeviceBase, DeviceProvider):
|
||||
vss_id = f'{self.arlo_device["deviceId"]}.vss'
|
||||
if not self.vss:
|
||||
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
return self.vss
|
||||
return self.vss
|
||||
|
||||
async def getSettings(self) -> List[Setting]:
|
||||
return [
|
||||
{
|
||||
"group": "General",
|
||||
"key": "print_debug",
|
||||
"title": "Debug Info",
|
||||
"description": "Prints information about this device to console.",
|
||||
"type": "button",
|
||||
}
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: SettingValue) -> None:
|
||||
if key == "print_debug":
|
||||
self.logger.info(f"Device Capabilities: {self.arlo_capabilities}")
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
@@ -5,7 +5,9 @@ import aiohttp
|
||||
from async_timeout import timeout as async_timeout
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import threading
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
import scrypted_arlo_go
|
||||
@@ -14,9 +16,8 @@ import scrypted_sdk
|
||||
from scrypted_sdk.types import Setting, Settings, SettingValue, Device, Camera, VideoCamera, RequestMediaStreamOptions, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, Charger, ChargeState, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
|
||||
|
||||
from .arlo.arlo_async import USER_AGENTS
|
||||
from .experimental import EXPERIMENTAL
|
||||
from .base import ArloDeviceBase
|
||||
from .spotlight import ArloSpotlight, ArloFloodlight
|
||||
from .spotlight import ArloSpotlight, ArloFloodlight, ArloNightlight
|
||||
from .vss import ArloSirenVirtualSecuritySystem
|
||||
from .child_process import HeartbeatChildProcess
|
||||
from .util import BackgroundTaskMixin, async_print_exception_guard
|
||||
@@ -26,13 +27,30 @@ if TYPE_CHECKING:
|
||||
from .provider import ArloProvider
|
||||
|
||||
|
||||
class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
def __init__(self, camera: ArloCamera) -> None:
|
||||
super().__init__()
|
||||
self.camera = camera
|
||||
self.logger = camera.logger
|
||||
self.provider = camera.provider
|
||||
self.arlo_device = camera.arlo_device
|
||||
self.arlo_basestation = camera.arlo_basestation
|
||||
|
||||
async def initialize_push_to_talk(self, media: MediaObject) -> None:
|
||||
raise NotImplementedError("not implemented")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
raise NotImplementedError("not implemented")
|
||||
|
||||
|
||||
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, AudioSensor, Battery, Charger):
|
||||
MODELS_WITH_SPOTLIGHTS = [
|
||||
"vmc4040p",
|
||||
"vmc2030",
|
||||
"vmc2032",
|
||||
"vmc4040p",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc4060p",
|
||||
"vmc5040",
|
||||
"vml2030",
|
||||
"vml4030",
|
||||
@@ -40,61 +58,85 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
|
||||
MODELS_WITH_FLOODLIGHTS = ["fb1001"]
|
||||
|
||||
MODELS_WITH_NIGHTLIGHTS = [
|
||||
"abc1000",
|
||||
"abc1000a",
|
||||
]
|
||||
|
||||
MODELS_WITH_SIRENS = [
|
||||
"vmc4040p",
|
||||
"fb1001",
|
||||
"vmc2030",
|
||||
"vmc2020",
|
||||
"vmc2030",
|
||||
"vmc2032",
|
||||
"vmc4030",
|
||||
"vmc4030p",
|
||||
"vmc4040p",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc4060p",
|
||||
"vmc5040",
|
||||
"vml2030",
|
||||
"vmc4030",
|
||||
"vml4030",
|
||||
"vmc4030p",
|
||||
]
|
||||
|
||||
MODELS_WITH_AUDIO_SENSORS = [
|
||||
"vmc4040p",
|
||||
"abc1000",
|
||||
"abc1000a",
|
||||
"fb1001",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc5040",
|
||||
"vmc3040",
|
||||
"vmc3040s",
|
||||
"vmc4030",
|
||||
"vml4030",
|
||||
"vmc4030p",
|
||||
"vmc4040p",
|
||||
"vmc4041p",
|
||||
"vmc4050p",
|
||||
"vmc5040",
|
||||
"vml4030",
|
||||
]
|
||||
|
||||
MODELS_WITHOUT_BATTERY = [
|
||||
"avd1001",
|
||||
"vmc2040",
|
||||
"vmc3040",
|
||||
"vmc3040s",
|
||||
]
|
||||
|
||||
timeout: int = 30
|
||||
intercom_session = None
|
||||
goSM = None
|
||||
intercom_session: ArloCameraIntercomSession = None
|
||||
light: ArloSpotlight = None
|
||||
vss: ArloSirenVirtualSecuritySystem = None
|
||||
picture_lock: asyncio.Lock = None
|
||||
|
||||
# eco mode bookkeeping
|
||||
picture_lock: asyncio.Lock = None
|
||||
last_picture: bytes = None
|
||||
last_picture_time: datetime = datetime(1970, 1, 1)
|
||||
|
||||
# socket logger
|
||||
logger_loop: asyncio.AbstractEventLoop = None
|
||||
logger_server: asyncio.AbstractServer = None
|
||||
logger_server_port: int = 0
|
||||
|
||||
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
|
||||
self.picture_lock = asyncio.Lock()
|
||||
|
||||
self.start_error_subscription()
|
||||
self.start_motion_subscription()
|
||||
self.start_audio_subscription()
|
||||
self.start_battery_subscription()
|
||||
self.create_task(self.delayed_init())
|
||||
|
||||
def __del__(self) -> None:
|
||||
super().__del__()
|
||||
def logger_exit_callback():
|
||||
self.logger_server.close()
|
||||
self.logger_loop.stop()
|
||||
self.logger_loop.close()
|
||||
self.logger_loop.call_soon_threadsafe(logger_exit_callback)
|
||||
|
||||
async def delayed_init(self) -> None:
|
||||
await self.create_tcp_logger_server()
|
||||
|
||||
if not self.has_battery:
|
||||
return
|
||||
|
||||
@@ -112,13 +154,59 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
await asyncio.sleep(0.1)
|
||||
iterations += 1
|
||||
|
||||
@async_print_exception_guard
|
||||
async def create_tcp_logger_server(self) -> None:
|
||||
self.logger_loop = asyncio.new_event_loop()
|
||||
|
||||
def thread_main():
|
||||
asyncio.set_event_loop(self.logger_loop)
|
||||
self.logger_loop.run_forever()
|
||||
|
||||
threading.Thread(target=thread_main).start()
|
||||
|
||||
# this is a bit convoluted since we need the async functions to run in the
|
||||
# logger loop thread instead of in the current thread
|
||||
def setup_callback():
|
||||
async def callback(reader, writer):
|
||||
try:
|
||||
while not reader.at_eof():
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
break
|
||||
line = str(line, 'utf-8')
|
||||
line = line.rstrip()
|
||||
self.logger.info(line)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
self.logger.exception("Logger server callback raised an exception")
|
||||
|
||||
async def setup():
|
||||
self.logger_server = await asyncio.start_server(callback, host='localhost', port=0, family=socket.AF_INET, flags=socket.SOCK_STREAM)
|
||||
self.logger_server_port = self.logger_server.sockets[0].getsockname()[1]
|
||||
self.logger.info(f"Started logging server at localhost:{self.logger_server_port}")
|
||||
|
||||
self.logger_loop.create_task(setup())
|
||||
|
||||
self.logger_loop.call_soon_threadsafe(setup_callback)
|
||||
|
||||
|
||||
def start_error_subscription(self) -> None:
|
||||
def callback(code, message):
|
||||
self.logger.error(f"Arlo returned error code {code} with message: {message}")
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToErrorEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def start_motion_subscription(self) -> None:
|
||||
def callback(motionDetected):
|
||||
self.motionDetected = motionDetected
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToMotionEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
self.provider.arlo.SubscribeToMotionEvents(self.arlo_basestation, self.arlo_device, callback, self.logger)
|
||||
)
|
||||
|
||||
def start_audio_subscription(self) -> None:
|
||||
@@ -130,7 +218,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
return self.stop_subscriptions
|
||||
|
||||
self.register_task(
|
||||
self.provider.arlo.SubscribeToAudioEvents(self.arlo_basestation, self.arlo_device, callback)
|
||||
self.provider.arlo.SubscribeToAudioEvents(self.arlo_basestation, self.arlo_device, callback, self.logger)
|
||||
)
|
||||
|
||||
def start_battery_subscription(self) -> None:
|
||||
@@ -153,7 +241,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
ScryptedInterface.Settings.value,
|
||||
])
|
||||
|
||||
if EXPERIMENTAL or not self.uses_sip_push_to_talk:
|
||||
if self.has_push_to_talk:
|
||||
results.add(ScryptedInterface.Intercom.value)
|
||||
|
||||
if self.has_battery:
|
||||
@@ -176,8 +264,8 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
|
||||
def get_builtin_child_device_manifests(self) -> List[Device]:
|
||||
results = []
|
||||
if self.has_spotlight or self.has_floodlight:
|
||||
light = self.get_or_create_spotlight_or_floodlight()
|
||||
if self.has_spotlight or self.has_floodlight or self.has_nightlight:
|
||||
light = self.get_or_create_light()
|
||||
results.append({
|
||||
"info": {
|
||||
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
|
||||
@@ -186,7 +274,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
"serialNumber": self.arlo_device["deviceId"],
|
||||
},
|
||||
"nativeId": light.nativeId,
|
||||
"name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}',
|
||||
"name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight" if self.has_floodlight else "Nightlight"}',
|
||||
"interfaces": light.get_applicable_interfaces(),
|
||||
"type": light.get_device_type(),
|
||||
"providerNativeId": self.nativeId,
|
||||
@@ -225,7 +313,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
return False
|
||||
|
||||
@property
|
||||
def snapshot_throttle_interval(self) -> bool:
|
||||
def snapshot_throttle_interval(self) -> int:
|
||||
interval = self.storage.getItem("snapshot_throttle_interval")
|
||||
if interval is None:
|
||||
interval = 60
|
||||
@@ -244,6 +332,10 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
def has_floodlight(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_FLOODLIGHTS])
|
||||
|
||||
@property
|
||||
def has_nightlight(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_NIGHTLIGHTS])
|
||||
|
||||
@property
|
||||
def has_siren(self) -> bool:
|
||||
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS])
|
||||
@@ -256,9 +348,13 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
def has_battery(self) -> bool:
|
||||
return not any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITHOUT_BATTERY])
|
||||
|
||||
@property
|
||||
def has_push_to_talk(self) -> bool:
|
||||
return bool(self.arlo_capabilities.get("Capabilities", {}).get("PushToTalk", {}).get("fullDuplex"))
|
||||
|
||||
@property
|
||||
def uses_sip_push_to_talk(self) -> bool:
|
||||
return self.arlo_device["deviceId"] == self.arlo_device["parentId"]
|
||||
return "sip" in self.arlo_capabilities.get("Capabilities", {}).get("PushToTalk", {}).get("signal", [])
|
||||
|
||||
async def getSettings(self) -> List[Setting]:
|
||||
result = []
|
||||
@@ -300,6 +396,15 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
"type": "number",
|
||||
}
|
||||
)
|
||||
result.append(
|
||||
{
|
||||
"group": "General",
|
||||
"key": "print_debug",
|
||||
"title": "Debug Info",
|
||||
"description": "Prints information about this device to console.",
|
||||
"type": "button",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@async_print_exception_guard
|
||||
@@ -313,6 +418,8 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
await self.provider.discover_devices()
|
||||
elif key in ["eco_mode"]:
|
||||
self.storage.setItem(key, value == "true" or value == True)
|
||||
elif key == "print_debug":
|
||||
self.logger.info(f"Device Capabilities: {self.arlo_capabilities}")
|
||||
else:
|
||||
self.storage.setItem(key, value)
|
||||
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
|
||||
@@ -353,7 +460,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
self.logger.debug(f"Got snapshot URL for at {pic_url}")
|
||||
|
||||
if pic_url is None:
|
||||
raise Exception("Error taking snapshot")
|
||||
raise Exception("Error taking snapshot: no url returned")
|
||||
|
||||
async with async_timeout(self.timeout):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -440,68 +547,18 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
return await scrypted_sdk.mediaManager.createFFmpegMediaObject(ffmpeg_input)
|
||||
|
||||
@async_print_exception_guard
|
||||
async def startIntercom(self, media) -> None:
|
||||
async def startIntercom(self, media: MediaObject) -> None:
|
||||
self.logger.info("Starting intercom")
|
||||
|
||||
if self.uses_sip_push_to_talk:
|
||||
sip_info = self.provider.arlo.GetSIPInfo()
|
||||
sip_call_info = sip_info["sipCallInfo"]
|
||||
|
||||
ice_servers = [{"url": "stun:stun.l.google.com:19302"}]
|
||||
self.logger.debug(f"Will use ice servers: {[ice['url'] for ice in ice_servers]}")
|
||||
|
||||
ice_servers = scrypted_arlo_go.Slice_webrtc_ICEServer([
|
||||
scrypted_arlo_go.NewWebRTCICEServer(
|
||||
scrypted_arlo_go.go.Slice_string([ice['url']]),
|
||||
ice.get('username', ''),
|
||||
ice.get('credential', '')
|
||||
)
|
||||
for ice in ice_servers
|
||||
])
|
||||
sip_cfg = scrypted_arlo_go.SIPInfo(
|
||||
DeviceID=self.nativeId,
|
||||
CallerURI=f"sip:{sip_call_info['id']}@{sip_call_info['domain']}:{sip_call_info['port']}",
|
||||
CalleeURI=sip_call_info['calleeUri'],
|
||||
Password=sip_call_info['password'],
|
||||
UserAgent="SIP.js/0.20.1",
|
||||
WebsocketURI="wss://livestream-z2-prod.arlo.com:7443",
|
||||
WebsocketOrigin="https://my.arlo.com",
|
||||
WebsocketHeaders=scrypted_arlo_go.HeadersMap({"User-Agent": USER_AGENTS["arlo"]}),
|
||||
)
|
||||
|
||||
self.goSM = scrypted_arlo_go.NewSIPWebRTCManager("Arlo SIP "+self.nativeId, ice_servers, sip_cfg)
|
||||
|
||||
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
|
||||
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
|
||||
audio_port = self.goSM.InitializeAudioRTPListener(scrypted_arlo_go.WebRTCMimeTypeOpus)
|
||||
|
||||
ffmpeg_path = await scrypted_sdk.mediaManager.getFFmpegPath()
|
||||
ffmpeg_args = [
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-analyzeduration", "0",
|
||||
"-fflags", "-nobuffer",
|
||||
"-probesize", "500000",
|
||||
*ffmpeg_params["inputArguments"],
|
||||
"-vn",
|
||||
"-acodec", "libopus",
|
||||
"-f", "rtp",
|
||||
"-flush_packets", "1",
|
||||
f"rtp://localhost:{audio_port}?pkt_size={scrypted_arlo_go.UDP_PACKET_SIZE()}",
|
||||
]
|
||||
self.logger.debug(f"Starting ffmpeg at {ffmpeg_path} with '{' '.join(ffmpeg_args)}'")
|
||||
|
||||
self.intercom_ffmpeg_subprocess = HeartbeatChildProcess("Arlo Subprocess "+self.logger_name, ffmpeg_path, *ffmpeg_args)
|
||||
self.intercom_ffmpeg_subprocess.start()
|
||||
|
||||
self.goSM.Start()
|
||||
# signaling happens over sip
|
||||
self.intercom_session = ArloCameraSIPIntercomSession(self)
|
||||
else:
|
||||
# we need to do signaling through arlo cloud apis
|
||||
self.intercom_session = ArloCameraIntercomSession(self)
|
||||
await self.intercom_session.initialize_push_to_talk(media)
|
||||
self.intercom_session = ArloCameraWebRTCIntercomSession(self)
|
||||
await self.intercom_session.initialize_push_to_talk(media)
|
||||
|
||||
self.logger.info("Intercom ready")
|
||||
self.logger.info("Intercom initialized")
|
||||
|
||||
@async_print_exception_guard
|
||||
async def stopIntercom(self) -> None:
|
||||
@@ -509,9 +566,6 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
if self.intercom_session is not None:
|
||||
await self.intercom_session.shutdown()
|
||||
self.intercom_session = None
|
||||
if self.goSM is not None:
|
||||
self.goSM.Close()
|
||||
self.goSM = None
|
||||
|
||||
async def getVideoClip(self, videoId: str) -> MediaObject:
|
||||
self.logger.info(f"Getting video clip {videoId}")
|
||||
@@ -574,17 +628,17 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
|
||||
@async_print_exception_guard
|
||||
async def removeVideoClips(self, videoClipIds: List[str]) -> None:
|
||||
# Arlo does support deleting, but let's be safe and disable that
|
||||
raise Exception("deleting Arlo video clips is not implemented by this plugin")
|
||||
# Arlo Cloud does support deleting, but let's be safe and not expose that here
|
||||
raise Exception("deleting Arlo video clips is not implemented by this plugin - please delete clips through the Arlo app")
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
|
||||
if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight):
|
||||
return self.get_or_create_spotlight_or_floodlight()
|
||||
if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight) or (nativeId.endswith("nightlight") and self.has_nightlight):
|
||||
return self.get_or_create_light()
|
||||
if nativeId.endswith("vss") and self.has_siren:
|
||||
return self.get_or_create_vss()
|
||||
return None
|
||||
|
||||
def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight:
|
||||
def get_or_create_light(self) -> ArloSpotlight:
|
||||
if self.has_spotlight:
|
||||
light_id = f'{self.arlo_device["deviceId"]}.spotlight'
|
||||
if not self.light:
|
||||
@@ -593,6 +647,10 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
light_id = f'{self.arlo_device["deviceId"]}.floodlight'
|
||||
if not self.light:
|
||||
self.light = ArloFloodlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
|
||||
elif self.has_nightlight:
|
||||
light_id = f'{self.arlo_device["deviceId"]}.nightlight'
|
||||
if not self.light:
|
||||
self.light = ArloNightlight(light_id, self.arlo_device, self.provider, self)
|
||||
return self.light
|
||||
|
||||
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
|
||||
@@ -603,29 +661,24 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
|
||||
return self.vss
|
||||
|
||||
|
||||
class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
def __init__(self, camera):
|
||||
super().__init__()
|
||||
self.camera = camera
|
||||
self.logger = camera.logger
|
||||
self.provider = camera.provider
|
||||
self.arlo_device = camera.arlo_device
|
||||
self.arlo_basestation = camera.arlo_basestation
|
||||
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
class ArloCameraWebRTCIntercomSession(ArloCameraIntercomSession):
|
||||
def __init__(self, camera: ArloCamera) -> None:
|
||||
super().__init__(camera)
|
||||
|
||||
self.arlo_pc = None
|
||||
self.arlo_sdp_answered = False
|
||||
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
|
||||
self.stop_subscriptions = False
|
||||
self.start_sdp_answer_subscription()
|
||||
self.start_candidate_answer_subscription()
|
||||
|
||||
def __del__(self):
|
||||
def __del__(self) -> None:
|
||||
self.stop_subscriptions = True
|
||||
self.cancel_pending_tasks()
|
||||
|
||||
def start_sdp_answer_subscription(self):
|
||||
def start_sdp_answer_subscription(self) -> None:
|
||||
def callback(sdp):
|
||||
if self.arlo_pc and not self.arlo_sdp_answered:
|
||||
if "a=mid:" not in sdp:
|
||||
@@ -643,7 +696,7 @@ class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
self.provider.arlo.SubscribeToSDPAnswers(self.arlo_basestation, self.arlo_device, callback)
|
||||
)
|
||||
|
||||
def start_candidate_answer_subscription(self):
|
||||
def start_candidate_answer_subscription(self) -> None:
|
||||
def callback(candidate):
|
||||
if self.arlo_pc:
|
||||
prefix = "a=candidate:"
|
||||
@@ -661,7 +714,7 @@ class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
)
|
||||
|
||||
@async_print_exception_guard
|
||||
async def initialize_push_to_talk(self, media):
|
||||
async def initialize_push_to_talk(self, media: MediaObject) -> None:
|
||||
self.logger.info("Initializing push to talk")
|
||||
|
||||
session_id, ice_servers = self.provider.arlo.StartPushToTalk(self.arlo_basestation, self.arlo_device)
|
||||
@@ -675,7 +728,8 @@ class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
)
|
||||
for ice in ice_servers
|
||||
])
|
||||
self.arlo_pc = scrypted_arlo_go.NewWebRTCManager("Arlo WebRTC "+self.camera.logger_name, ice_servers)
|
||||
|
||||
self.arlo_pc = scrypted_arlo_go.NewWebRTCManager(self.camera.logger_server_port, ice_servers)
|
||||
|
||||
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
|
||||
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
|
||||
@@ -690,15 +744,23 @@ class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
"-fflags", "-nobuffer",
|
||||
"-probesize", "500000",
|
||||
*ffmpeg_params["inputArguments"],
|
||||
"-vn",
|
||||
"-acodec", "libopus",
|
||||
"-flags", "+global_header",
|
||||
"-vbr", "off",
|
||||
"-ar", "48k",
|
||||
"-b:a", "32k",
|
||||
"-bufsize", "96k",
|
||||
"-ac", "2",
|
||||
"-application", "lowdelay",
|
||||
"-dn", "-sn", "-vn",
|
||||
"-frame_duration", "20",
|
||||
"-f", "rtp",
|
||||
"-flush_packets", "1",
|
||||
f"rtp://localhost:{audio_port}?pkt_size={scrypted_arlo_go.UDP_PACKET_SIZE()}",
|
||||
]
|
||||
self.logger.debug(f"Starting ffmpeg at {ffmpeg_path} with '{' '.join(ffmpeg_args)}'")
|
||||
|
||||
self.intercom_ffmpeg_subprocess = HeartbeatChildProcess("Arlo Subprocess "+self.camera.logger_name, ffmpeg_path, *ffmpeg_args)
|
||||
self.intercom_ffmpeg_subprocess = HeartbeatChildProcess("FFmpeg", self.camera.logger_server_port, ffmpeg_path, *ffmpeg_args)
|
||||
self.intercom_ffmpeg_subprocess.start()
|
||||
|
||||
self.sdp_answered = False
|
||||
@@ -714,22 +776,129 @@ class ArloCameraIntercomSession(BackgroundTaskMixin):
|
||||
session_id, offer_sdp
|
||||
)
|
||||
|
||||
candidates = self.arlo_pc.WaitAndGetICECandidates()
|
||||
self.logger.debug(f"Gathered {len(candidates)} candidates")
|
||||
for candidate in candidates:
|
||||
candidate = scrypted_arlo_go.WebRTCICECandidateInit(
|
||||
scrypted_arlo_go.WebRTCICECandidate(handle=candidate).ToJSON()
|
||||
).Candidate
|
||||
self.logger.debug(f"Sending candidate to Arlo: {candidate}")
|
||||
self.provider.arlo.NotifyPushToTalkCandidate(
|
||||
self.arlo_basestation, self.arlo_device,
|
||||
session_id, candidate,
|
||||
)
|
||||
def trickle_candidates():
|
||||
count = 0
|
||||
try:
|
||||
while True:
|
||||
candidate = self.arlo_pc.GetNextICECandidate()
|
||||
candidate = scrypted_arlo_go.WebRTCICECandidateInit(
|
||||
scrypted_arlo_go.WebRTCICECandidate(handle=candidate.handle).ToJSON()
|
||||
).Candidate
|
||||
self.logger.debug(f"Sending candidate to Arlo: {candidate}")
|
||||
self.provider.arlo.NotifyPushToTalkCandidate(
|
||||
self.arlo_basestation, self.arlo_device,
|
||||
session_id, candidate,
|
||||
)
|
||||
count += 1
|
||||
except RuntimeError as e:
|
||||
if str(e) == "no more candidates":
|
||||
self.logger.debug(f"End of candidates, found {count} candidate(s)")
|
||||
else:
|
||||
self.logger.exception("Exception while processing trickle candidates")
|
||||
except Exception:
|
||||
self.logger.exception("Exception while processing trickle candidates")
|
||||
|
||||
async def shutdown(self):
|
||||
# we can trickle candidates asynchronously so the caller to startIntercom
|
||||
# knows we are ready to receive packets
|
||||
threading.Thread(target=trickle_candidates).start()
|
||||
|
||||
@async_print_exception_guard
|
||||
async def shutdown(self) -> None:
|
||||
if self.intercom_ffmpeg_subprocess is not None:
|
||||
self.intercom_ffmpeg_subprocess.stop()
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
if self.arlo_pc is not None:
|
||||
self.arlo_pc.Close()
|
||||
self.arlo_pc = None
|
||||
self.arlo_pc = None
|
||||
|
||||
|
||||
class ArloCameraSIPIntercomSession(ArloCameraIntercomSession):
|
||||
def __init__(self, camera: ArloCamera) -> None:
|
||||
super().__init__(camera)
|
||||
|
||||
self.arlo_sip = None
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
|
||||
@async_print_exception_guard
|
||||
async def initialize_push_to_talk(self, media: MediaObject) -> None:
|
||||
self.logger.info("Initializing push to talk")
|
||||
|
||||
sip_info = self.provider.arlo.GetSIPInfo()
|
||||
sip_call_info = sip_info["sipCallInfo"]
|
||||
|
||||
# though GetSIPInfo returns ice servers, there doesn't seem to be any indication
|
||||
# that they are used on the arlo web dashboard, so just use what Chrome inserts
|
||||
ice_servers = [{"url": "stun:stun.l.google.com:19302"}]
|
||||
self.logger.debug(f"Will use ice servers: {[ice['url'] for ice in ice_servers]}")
|
||||
|
||||
ice_servers = scrypted_arlo_go.Slice_webrtc_ICEServer([
|
||||
scrypted_arlo_go.NewWebRTCICEServer(
|
||||
scrypted_arlo_go.go.Slice_string([ice['url']]),
|
||||
ice.get('username', ''),
|
||||
ice.get('credential', '')
|
||||
)
|
||||
for ice in ice_servers
|
||||
])
|
||||
sip_cfg = scrypted_arlo_go.SIPInfo(
|
||||
DeviceID=self.camera.nativeId,
|
||||
CallerURI=f"sip:{sip_call_info['id']}@{sip_call_info['domain']}:{sip_call_info['port']}",
|
||||
CalleeURI=sip_call_info['calleeUri'],
|
||||
Password=sip_call_info['password'],
|
||||
UserAgent="SIP.js/0.20.1",
|
||||
WebsocketURI="wss://livestream-z2-prod.arlo.com:7443",
|
||||
WebsocketOrigin="https://my.arlo.com",
|
||||
WebsocketHeaders=scrypted_arlo_go.HeadersMap({"User-Agent": USER_AGENTS["arlo"]}),
|
||||
)
|
||||
|
||||
self.arlo_sip = scrypted_arlo_go.NewSIPWebRTCManager(self.camera.logger_server_port, ice_servers, sip_cfg)
|
||||
|
||||
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
|
||||
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
|
||||
audio_port = self.arlo_sip.InitializeAudioRTPListener(scrypted_arlo_go.WebRTCMimeTypeOpus)
|
||||
|
||||
ffmpeg_path = await scrypted_sdk.mediaManager.getFFmpegPath()
|
||||
ffmpeg_args = [
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-analyzeduration", "0",
|
||||
"-fflags", "-nobuffer",
|
||||
"-probesize", "500000",
|
||||
*ffmpeg_params["inputArguments"],
|
||||
"-acodec", "libopus",
|
||||
"-flags", "+global_header",
|
||||
"-vbr", "off",
|
||||
"-ar", "48k",
|
||||
"-b:a", "32k",
|
||||
"-bufsize", "96k",
|
||||
"-ac", "2",
|
||||
"-application", "lowdelay",
|
||||
"-dn", "-sn", "-vn",
|
||||
"-frame_duration", "20",
|
||||
"-f", "rtp",
|
||||
"-flush_packets", "1",
|
||||
f"rtp://localhost:{audio_port}?pkt_size={scrypted_arlo_go.UDP_PACKET_SIZE()}",
|
||||
]
|
||||
self.logger.debug(f"Starting ffmpeg at {ffmpeg_path} with '{' '.join(ffmpeg_args)}'")
|
||||
|
||||
self.intercom_ffmpeg_subprocess = HeartbeatChildProcess("FFmpeg", self.camera.logger_server_port, ffmpeg_path, *ffmpeg_args)
|
||||
self.intercom_ffmpeg_subprocess.start()
|
||||
|
||||
def sip_start():
|
||||
try:
|
||||
self.arlo_sip.Start()
|
||||
except Exception:
|
||||
self.logger.exception("Exception starting sip call")
|
||||
|
||||
# do remaining setup asynchronously so the caller to startIntercom
|
||||
# can start sending packets
|
||||
threading.Thread(target=sip_start).start()
|
||||
|
||||
@async_print_exception_guard
|
||||
async def shutdown(self) -> None:
|
||||
if self.intercom_ffmpeg_subprocess is not None:
|
||||
self.intercom_ffmpeg_subprocess.stop()
|
||||
self.intercom_ffmpeg_subprocess = None
|
||||
if self.arlo_sip is not None:
|
||||
self.arlo_sip.Close()
|
||||
self.arlo_sip = None
|
||||
@@ -3,42 +3,74 @@ import subprocess
|
||||
import time
|
||||
import threading
|
||||
|
||||
import scrypted_arlo_go
|
||||
|
||||
|
||||
HEARTBEAT_INTERVAL = 5
|
||||
|
||||
|
||||
def multiprocess_main(name, child_conn, exe, args):
|
||||
print(f"[{name}] Child process starting")
|
||||
sp = subprocess.Popen([exe, *args])
|
||||
def multiprocess_main(name, logger_port, child_conn, exe, args):
|
||||
logger = scrypted_arlo_go.NewTCPLogger(logger_port, "HeartbeatChildProcess")
|
||||
|
||||
logger.Send(f"{name} starting\n")
|
||||
sp = subprocess.Popen([exe, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
# pull stdout and stderr from the subprocess and forward it over to
|
||||
# our tcp logger
|
||||
def logging_thread(stdstream):
|
||||
while True:
|
||||
line = stdstream.readline()
|
||||
if not line:
|
||||
break
|
||||
line = str(line, 'utf-8')
|
||||
logger.Send(line)
|
||||
stdout_t = threading.Thread(target=logging_thread, args=(sp.stdout,))
|
||||
stderr_t = threading.Thread(target=logging_thread, args=(sp.stderr,))
|
||||
stdout_t.start()
|
||||
stderr_t.start()
|
||||
|
||||
while True:
|
||||
has_data = child_conn.poll(HEARTBEAT_INTERVAL * 3)
|
||||
if not has_data:
|
||||
break
|
||||
|
||||
# check if the subprocess is still alive, if not then exit
|
||||
if sp.poll() is not None:
|
||||
break
|
||||
|
||||
keep_alive = child_conn.recv()
|
||||
if not keep_alive:
|
||||
break
|
||||
|
||||
logger.Send(f"{name} exiting\n")
|
||||
|
||||
sp.terminate()
|
||||
sp.wait()
|
||||
print(f"[{name}] Child process exiting")
|
||||
|
||||
stdout_t.join()
|
||||
stderr_t.join()
|
||||
|
||||
logger.Send(f"{name} exited\n")
|
||||
logger.Close()
|
||||
|
||||
|
||||
class HeartbeatChildProcess:
|
||||
"""Class to manage running a child process that gets cleaned up if the parent exits.
|
||||
|
||||
|
||||
When spawining subprocesses in Python, if the parent is forcibly killed (as is the case
|
||||
when Scrypted restarts plugins), subprocesses get orphaned. This approach uses parent-child
|
||||
heartbeats for the child to ensure that the parent process is still alive, and to cleanly
|
||||
exit the child if the parent has terminated.
|
||||
"""
|
||||
|
||||
def __init__(self, name, exe, *args):
|
||||
def __init__(self, name, logger_port, exe, *args):
|
||||
self.name = name
|
||||
self.logger_port = logger_port
|
||||
self.exe = exe
|
||||
self.args = args
|
||||
|
||||
self.parent_conn, self.child_conn = multiprocessing.Pipe()
|
||||
self.process = multiprocessing.Process(target=multiprocess_main, args=(name, self.child_conn, exe, args))
|
||||
self.process = multiprocessing.Process(target=multiprocess_main, args=(name, logger_port, self.child_conn, exe, args))
|
||||
self.process.daemon = True
|
||||
self._stop = False
|
||||
|
||||
@@ -55,4 +87,7 @@ class HeartbeatChildProcess:
|
||||
def heartbeat(self):
|
||||
while not self._stop:
|
||||
time.sleep(HEARTBEAT_INTERVAL)
|
||||
if not self.process.is_alive():
|
||||
self.stop()
|
||||
break
|
||||
self.parent_conn.send(True)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
EXPERIMENTAL = False
|
||||
import os
|
||||
|
||||
EXPERIMENTAL = os.environ.get("SCRYPTED_ARLO_EXPERIMENTAL", "0") not in ["", "0"]
|
||||
@@ -23,7 +23,7 @@ def createScryptedLogger(scrypted_device, name):
|
||||
sh = ScryptedDeviceLoggingWrapper(scrypted_device)
|
||||
|
||||
# log formatting
|
||||
fmt = logging.Formatter("[Arlo %(name)s] %(message)s")
|
||||
fmt = logging.Formatter("[Arlo %(name)s]: %(message)s")
|
||||
sh.setFormatter(fmt)
|
||||
|
||||
# configure handler to logger
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import asyncio
|
||||
from bs4 import BeautifulSoup
|
||||
import email
|
||||
import functools
|
||||
import imaplib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
import traceback
|
||||
from typing import List
|
||||
|
||||
import scrypted_sdk
|
||||
@@ -43,7 +44,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
|
||||
def __init__(self, nativeId: str = None) -> None:
|
||||
super().__init__(nativeId=nativeId)
|
||||
self.logger_name = "provider"
|
||||
self.logger_name = "Provider"
|
||||
|
||||
self.arlo_cameras = {}
|
||||
self.arlo_basestations = {}
|
||||
@@ -140,6 +141,14 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
def imap_mfa_password(self) -> str:
|
||||
return self.storage.getItem("imap_mfa_password")
|
||||
|
||||
@property
|
||||
def imap_mfa_sender(self) -> str:
|
||||
sender = self.storage.getItem("imap_mfa_sender")
|
||||
if sender is None or sender == "":
|
||||
sender = "do_not_reply@arlo.com"
|
||||
self.storage.setItem("imap_mfa_sender", sender)
|
||||
return sender
|
||||
|
||||
@property
|
||||
def imap_mfa_interval(self) -> int:
|
||||
interval = self.storage.getItem("imap_mfa_interval")
|
||||
@@ -186,12 +195,12 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
self._arlo_mfa_complete_auth = self._arlo.LoginMFA()
|
||||
self.logger.info(f"Initialized Arlo client, waiting for MFA code")
|
||||
return None
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
except Exception:
|
||||
self.logger.exception("Error initializing Arlo client")
|
||||
self._arlo = None
|
||||
self._arlo_mfa_complete_auth = None
|
||||
self._arlo_mfa_code = None
|
||||
return None
|
||||
raise
|
||||
|
||||
async def do_arlo_setup(self) -> None:
|
||||
try:
|
||||
@@ -201,15 +210,15 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
])
|
||||
|
||||
self.arlo.event_stream.set_refresh_interval(self.refresh_interval)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
traceback.print_exc()
|
||||
self.logger.error(f"Error logging in, will retry with fresh login")
|
||||
except requests.exceptions.HTTPError:
|
||||
self.logger.exception("Error logging in")
|
||||
self.logger.error("Will retry with fresh login")
|
||||
self._arlo = None
|
||||
self._arlo_mfa_code = None
|
||||
self.storage.setItem("arlo_auth_headers", None)
|
||||
_ = self.arlo
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
except Exception:
|
||||
self.logger.exception("Error logging in")
|
||||
|
||||
def invalidate_arlo_client(self) -> None:
|
||||
if self._arlo is not None:
|
||||
@@ -235,7 +244,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
self.print(f"Setting plugin transport to {self.arlo_transport}")
|
||||
change_stream_class(self.arlo_transport)
|
||||
|
||||
def initialize_imap(self) -> None:
|
||||
def initialize_imap(self, try_count=1) -> None:
|
||||
if not self.imap_mfa_host or not self.imap_mfa_port or \
|
||||
not self.imap_mfa_username or not self.imap_mfa_password or \
|
||||
not self.imap_mfa_interval:
|
||||
@@ -243,7 +252,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
|
||||
self.exit_imap()
|
||||
try:
|
||||
self.logger.info("Trying connect to IMAP")
|
||||
self.logger.info(f"Trying connect to IMAP (attempt {try_count})")
|
||||
self.imap = imaplib.IMAP4_SSL(self.imap_mfa_host, port=self.imap_mfa_port)
|
||||
|
||||
res, _ = self.imap.login(self.imap_mfa_username, self.imap_mfa_password)
|
||||
@@ -257,9 +266,14 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
res, self.imap_skip_emails = self.imap.search(None, "FROM", "do_not_reply@arlo.com")
|
||||
if res.lower() != "ok":
|
||||
raise Exception(f"IMAP failed to fetch old Arlo emails: {res}")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.exit_imap()
|
||||
except Exception:
|
||||
self.logger.exception("IMAP initialization error")
|
||||
|
||||
if try_count >= 10:
|
||||
self.logger.error("Tried to connect to IMAP too many times. Will request a plugin restart.")
|
||||
self.create_task(scrypted_sdk.deviceManager.requestRestart())
|
||||
|
||||
asyncio.get_event_loop().call_later(try_count*try_count, functools.partial(self.initialize_imap, try_count=try_count+1))
|
||||
else:
|
||||
self.logger.info("Connected to IMAP")
|
||||
self.imap_signal = asyncio.Queue()
|
||||
@@ -291,22 +305,39 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
self.storage.setItem("arlo_user_id", "")
|
||||
|
||||
# initialize login and prompt for MFA
|
||||
_ = self.arlo
|
||||
try:
|
||||
_ = self.arlo
|
||||
except Exception:
|
||||
self.logger.exception("Unrecoverable login error")
|
||||
self.logger.error("Will request a plugin restart")
|
||||
await scrypted_sdk.deviceManager.requestRestart()
|
||||
return
|
||||
|
||||
# do imap lookup
|
||||
# adapted from https://github.com/twrecked/pyaarlo/blob/77c202b6f789c7104a024f855a12a3df4fc8df38/pyaarlo/tfa.py
|
||||
try:
|
||||
try_count = 0
|
||||
while True:
|
||||
self.logger.info("Checking IMAP for MFA codes")
|
||||
try_count += 1
|
||||
|
||||
sleep_duration = 1
|
||||
if try_count > 5:
|
||||
sleep_duration = 2
|
||||
elif try_count > 10:
|
||||
sleep_duration = 5
|
||||
elif try_count > 20:
|
||||
sleep_duration = 10
|
||||
|
||||
self.logger.info(f"Checking IMAP for MFA codes (attempt {try_count})")
|
||||
|
||||
self.imap.check()
|
||||
res, emails = self.imap.search(None, "FROM", "do_not_reply@arlo.com")
|
||||
res, emails = self.imap.search(None, "FROM", self.imap_mfa_sender)
|
||||
if res.lower() != "ok":
|
||||
raise Exception("IMAP error: {res}")
|
||||
|
||||
if emails == self.imap_skip_emails:
|
||||
self.logger.info("No new emails found, will sleep and retry")
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(sleep_duration)
|
||||
continue
|
||||
|
||||
skip_emails = self.imap_skip_emails[0].split()
|
||||
@@ -323,8 +354,9 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
if part.get_content_type() != "text/html":
|
||||
continue
|
||||
try:
|
||||
for line in part.get_payload(decode=True).splitlines():
|
||||
code = re.match(r"^\W+(\d{6})\W*$", line.decode())
|
||||
soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
|
||||
for line in soup.get_text().splitlines():
|
||||
code = re.match(r"^\W*(\d{6})\W*$", line)
|
||||
if code is not None:
|
||||
return code.group(1)
|
||||
except:
|
||||
@@ -345,20 +377,30 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
break
|
||||
|
||||
self.logger.info("No MFA code found, will sleep and retry")
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
self.logger.error("Will retry on next IMAP interval")
|
||||
await asyncio.sleep(sleep_duration)
|
||||
except Exception:
|
||||
self.logger.exception("Error while checking for MFA codes")
|
||||
|
||||
self._arlo = old_arlo
|
||||
self.storage.setItem("arlo_auth_headers", old_headers)
|
||||
self.storage.setItem("arlo_user_id", old_user_id)
|
||||
self._arlo_mfa_code = None
|
||||
self._arlo_mfa_complete_auth = None
|
||||
|
||||
self.logger.error("Will reload IMAP connection")
|
||||
asyncio.get_event_loop().call_soon(self.initialize_imap)
|
||||
else:
|
||||
# finish login
|
||||
if old_arlo:
|
||||
old_arlo.Unsubscribe()
|
||||
_ = self.arlo
|
||||
|
||||
try:
|
||||
_ = self.arlo
|
||||
except Exception:
|
||||
self.logger.exception("Unrecoverable login error")
|
||||
self.logger.error("Will request a plugin restart")
|
||||
await scrypted_sdk.deviceManager.requestRestart()
|
||||
return
|
||||
|
||||
# continue by sleeping/waiting for a signal
|
||||
interval = self.imap_mfa_interval * 24 * 60 * 60 # convert interval days to seconds
|
||||
@@ -444,6 +486,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
"type": "password",
|
||||
"value": self.imap_mfa_password,
|
||||
},
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_sender",
|
||||
"title": "IMAP Email Sender",
|
||||
"value": self.imap_mfa_sender,
|
||||
"description": "The sender email address to search for when loading 2FA codes. See plugin README for more details.",
|
||||
},
|
||||
{
|
||||
"group": "IMAP 2FA",
|
||||
"key": "imap_mfa_interval",
|
||||
@@ -659,22 +708,30 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
|
||||
for provider_id in provider_to_device_map.keys():
|
||||
if provider_id is None:
|
||||
continue
|
||||
|
||||
if len(provider_to_device_map[provider_id]) > 0:
|
||||
self.logger.debug(f"Sending {provider_id} and children to scrypted server")
|
||||
else:
|
||||
self.logger.debug(f"Sending {provider_id} to scrypted server")
|
||||
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged({
|
||||
"devices": provider_to_device_map[provider_id],
|
||||
"providerNativeId": provider_id,
|
||||
})
|
||||
|
||||
# ensure devices at the root match all that was discovered
|
||||
self.logger.debug("Sending top level devices to scrypted server")
|
||||
await scrypted_sdk.deviceManager.onDevicesChanged({
|
||||
"devices": provider_to_device_map[None]
|
||||
})
|
||||
self.logger.debug("Done discovering devices")
|
||||
|
||||
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
|
||||
async with self.device_discovery_lock:
|
||||
return await self.getDevice_impl(nativeId)
|
||||
|
||||
async def getDevice_impl(self, nativeId: str) -> ArloDeviceBase:
|
||||
ret = self.scrypted_devices.get(nativeId, None)
|
||||
ret = self.scrypted_devices.get(nativeId)
|
||||
if ret is None:
|
||||
ret = self.create_device(nativeId)
|
||||
if ret is not None:
|
||||
|
||||
@@ -51,4 +51,22 @@ class ArloFloodlight(ArloSpotlight):
|
||||
async def turnOff(self) -> None:
|
||||
self.logger.info("Turning off")
|
||||
self.provider.arlo.FloodlightOff(self.arlo_basestation, self.arlo_device)
|
||||
self.on = False
|
||||
|
||||
|
||||
class ArloNightlight(ArloSpotlight):
|
||||
|
||||
def __init__(self, nativeId: str, arlo_device: dict, provider: ArloProvider, camera: ArloCamera) -> None:
|
||||
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_device, provider=provider, camera=camera)
|
||||
|
||||
@async_print_exception_guard
|
||||
async def turnOn(self) -> None:
|
||||
self.logger.info("Turning on")
|
||||
self.provider.arlo.NightlightOn(self.arlo_device)
|
||||
self.on = True
|
||||
|
||||
@async_print_exception_guard
|
||||
async def turnOff(self) -> None:
|
||||
self.logger.info("Turning off")
|
||||
self.provider.arlo.NightlightOff(self.arlo_device)
|
||||
self.on = False
|
||||
@@ -34,6 +34,11 @@ def async_print_exception_guard(fn):
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
# hack to detect if the applied function is actually a method
|
||||
# on a scrypted object
|
||||
if len(args) > 0 and hasattr(args[0], "logger"):
|
||||
getattr(args[0], "logger").exception(f"{fn.__qualname__} raised an exception")
|
||||
else:
|
||||
traceback.print_exc()
|
||||
raise
|
||||
return wrapped
|
||||
@@ -3,9 +3,11 @@ sseclient==0.0.22
|
||||
aiohttp==3.8.4
|
||||
requests==2.28.2
|
||||
cachetools==5.3.0
|
||||
scrypted-arlo-go==0.1.3
|
||||
scrypted-arlo-go==0.4.0
|
||||
cloudscraper==1.2.71
|
||||
curl-cffi==0.5.7; platform_machine != 'armv7l'
|
||||
async-timeout==4.0.2
|
||||
beautifulsoup4==4.12.2
|
||||
--extra-index-url=https://www.piwheels.org/simple/
|
||||
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/
|
||||
--prefer-binary
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.129",
|
||||
"version": "0.1.130",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.129",
|
||||
"version": "0.1.130",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.129",
|
||||
"version": "0.1.130",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<v-card-text>
|
||||
<v-card-title style="justify-content: center;" class="headline text-uppercase">Scrypted
|
||||
</v-card-title>
|
||||
<v-card-subtitle v-if="$store.state.hasLogin === false" style="justify-content: center;" class="text-uppercase">Create Account
|
||||
<v-card-subtitle v-if="$store.state.hasLogin === false" style="display: flex; justify-content: center;" class="text-uppercase">Create Account
|
||||
</v-card-subtitle>
|
||||
<v-container grid-list-md>
|
||||
<v-layout wrap>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<v-btn v-on="on" small>
|
||||
<v-icon x-small>fa fa-calendar-alt</v-icon>
|
||||
|
||||
{{ new Date(date).getFullYear() }}-{{ new Date(date).getMonth() }}-{{ new Date(date).getDate() }}
|
||||
{{ new Date(date).getFullYear() }}-{{ new Date(date).getMonth() + 1 }}-{{ new Date(date).getDate() }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
|
||||
4
plugins/coreml/package-lock.json
generated
4
plugins/coreml/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/coreml",
|
||||
"version": "0.1.15",
|
||||
"version": "0.1.21",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.15"
|
||||
"version": "0.1.21"
|
||||
}
|
||||
|
||||
@@ -1,45 +1,125 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import scrypted_sdk
|
||||
from typing import Any, Tuple
|
||||
from predict import PredictPlugin, Prediction, Rectangle
|
||||
import coremltools as ct
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
from typing import Any, Tuple
|
||||
|
||||
import coremltools as ct
|
||||
import numpy as np
|
||||
import scrypted_sdk
|
||||
from PIL import Image
|
||||
from scrypted_sdk import Setting, SettingValue
|
||||
|
||||
import yolo
|
||||
from predict import Prediction, PredictPlugin, Rectangle
|
||||
|
||||
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "CoreML-Predict")
|
||||
|
||||
|
||||
def parse_label_contents(contents: str):
|
||||
lines = contents.splitlines()
|
||||
ret = {}
|
||||
for row_number, content in enumerate(lines):
|
||||
pair = re.split(r'[:\s]+', content.strip(), maxsplit=1)
|
||||
pair = re.split(r"[:\s]+", content.strip(), maxsplit=1)
|
||||
if len(pair) == 2 and pair[0].strip().isdigit():
|
||||
ret[int(pair[0])] = pair[1].strip()
|
||||
else:
|
||||
ret[row_number] = content.strip()
|
||||
return ret
|
||||
|
||||
|
||||
class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt', 'coco_labels.txt')
|
||||
modelFile = self.downloadFile('https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel', 'MobileNetV2_SSDLite.mlmodel')
|
||||
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
if model == "Default":
|
||||
# model = "ssdlite_mobilenet_v2"
|
||||
if "arm" in platform.processor():
|
||||
model = "yolov8n"
|
||||
else:
|
||||
model = "ssdlite_mobilenet_v2"
|
||||
self.yolo = "yolo" in model
|
||||
self.yolov8 = "yolov8" in model
|
||||
model_version = "v2"
|
||||
|
||||
print(f"model: {model}")
|
||||
|
||||
if not self.yolo:
|
||||
# todo convert these to mlpackage
|
||||
labelsFile = self.downloadFile(
|
||||
f"https://github.com/koush/coreml-models/raw/main/{model}/coco_labels.txt",
|
||||
"coco_labels.txt",
|
||||
)
|
||||
modelFile = self.downloadFile(
|
||||
f"https://github.com/koush/coreml-models/raw/main/{model}/{model}.mlmodel",
|
||||
f"{model}.mlmodel",
|
||||
)
|
||||
else:
|
||||
if self.yolov8:
|
||||
modelFile = self.downloadFile(
|
||||
f"https://github.com/koush/coreml-models/raw/main/{model}/{model}.mlmodel",
|
||||
f"{model}.mlmodel",
|
||||
)
|
||||
else:
|
||||
files = [
|
||||
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/FeatureDescriptions.json",
|
||||
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/Metadata.json",
|
||||
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
||||
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{model}.mlmodel",
|
||||
f"{model}/{model}.mlpackage/Manifest.json",
|
||||
]
|
||||
|
||||
for f in files:
|
||||
p = self.downloadFile(
|
||||
f"https://github.com/koush/coreml-models/raw/main/{f}",
|
||||
f"{model_version}/{f}",
|
||||
)
|
||||
modelFile = os.path.dirname(p)
|
||||
|
||||
labelsFile = self.downloadFile(
|
||||
f"https://github.com/koush/coreml-models/raw/main/{model}/coco_80cl.txt",
|
||||
f"{model_version}/{model}/coco_80cl.txt",
|
||||
)
|
||||
|
||||
self.model = ct.models.MLModel(modelFile)
|
||||
|
||||
|
||||
self.modelspec = self.model.get_spec()
|
||||
self.inputdesc = self.modelspec.description.input[0]
|
||||
self.inputheight = self.inputdesc.type.imageType.height
|
||||
self.inputwidth = self.inputdesc.type.imageType.width
|
||||
|
||||
labels_contents = open(labelsFile, 'r').read()
|
||||
labels_contents = open(labelsFile, "r").read()
|
||||
self.labels = parse_label_contents(labels_contents)
|
||||
# csv in mobilenet model
|
||||
# self.modelspec.description.metadata.userDefined['classes']
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.minThreshold = .2
|
||||
self.minThreshold = 0.2
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
model = self.storage.getItem("model") or "Default"
|
||||
return [
|
||||
{
|
||||
"key": "model",
|
||||
"title": "Model",
|
||||
"description": "The detection model used to find objects.",
|
||||
"choices": [
|
||||
"Default",
|
||||
"ssdlite_mobilenet_v2",
|
||||
"yolov4-tiny",
|
||||
"yolov8n",
|
||||
],
|
||||
"value": model,
|
||||
},
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: SettingValue):
|
||||
self.storage.setItem(key, value)
|
||||
await self.onDeviceEvent(scrypted_sdk.ScryptedInterface.Settings.value, None)
|
||||
await scrypted_sdk.deviceManager.requestRestart()
|
||||
|
||||
# width, height, channels
|
||||
def get_input_details(self) -> Tuple[int, int, int]:
|
||||
@@ -49,17 +129,71 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
return (self.inputwidth, self.inputheight)
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
# run in executor if this is the plugin loop
|
||||
if asyncio.get_event_loop() is self.loop:
|
||||
out_dict = await asyncio.get_event_loop().run_in_executor(predictExecutor, lambda: self.model.predict({'image': input, 'confidenceThreshold': self.minThreshold }))
|
||||
else:
|
||||
out_dict = self.model.predict({'image': input, 'confidenceThreshold': self.minThreshold })
|
||||
|
||||
coordinatesList = out_dict['coordinates'].astype(float)
|
||||
|
||||
objs = []
|
||||
|
||||
for index, confidenceList in enumerate(out_dict['confidence'].astype(float)):
|
||||
# run in executor if this is the plugin loop
|
||||
if self.yolo:
|
||||
input_name = "image" if self.yolov8 else "input_1"
|
||||
if asyncio.get_event_loop() is self.loop:
|
||||
out_dict = await asyncio.get_event_loop().run_in_executor(
|
||||
predictExecutor, lambda: self.model.predict({input_name: input})
|
||||
)
|
||||
else:
|
||||
out_dict = self.model.predict({input_name: input})
|
||||
|
||||
if self.yolov8:
|
||||
out_blob = out_dict["var_914"]
|
||||
var_914 = out_dict["var_914"]
|
||||
results = var_914[0]
|
||||
objs = yolo.parse_yolov8(results)
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
return ret
|
||||
|
||||
out_blob = out_dict["Identity"]
|
||||
|
||||
objects = yolo.parse_yolo_region(
|
||||
out_blob,
|
||||
(input.width, input.height),
|
||||
(81, 82, 135, 169, 344, 319),
|
||||
# (23,27, 37,58, 81,82),
|
||||
False,
|
||||
)
|
||||
|
||||
for r in objects:
|
||||
obj = Prediction(
|
||||
r["classId"].astype(float),
|
||||
r["confidence"].astype(float),
|
||||
Rectangle(
|
||||
r["xmin"].astype(float),
|
||||
r["ymin"].astype(float),
|
||||
r["xmax"].astype(float),
|
||||
r["ymax"].astype(float),
|
||||
),
|
||||
)
|
||||
objs.append(obj)
|
||||
|
||||
# what about output[1]?
|
||||
# 26 26
|
||||
# objects = yolo.parse_yolo_region(out_blob, (input.width, input.height), (23,27, 37,58, 81,82))
|
||||
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
return ret
|
||||
|
||||
if asyncio.get_event_loop() is self.loop:
|
||||
out_dict = await asyncio.get_event_loop().run_in_executor(
|
||||
predictExecutor,
|
||||
lambda: self.model.predict(
|
||||
{"image": input, "confidenceThreshold": self.minThreshold}
|
||||
),
|
||||
)
|
||||
else:
|
||||
out_dict = self.model.predict(
|
||||
{"image": input, "confidenceThreshold": self.minThreshold}
|
||||
)
|
||||
|
||||
coordinatesList = out_dict["coordinates"].astype(float)
|
||||
|
||||
for index, confidenceList in enumerate(out_dict["confidence"].astype(float)):
|
||||
values = confidenceList
|
||||
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
|
||||
maxConfidence = confidenceList[maxConfidenceIndex]
|
||||
@@ -80,12 +214,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
|
||||
l = x - w2
|
||||
t = y - h2
|
||||
|
||||
obj = Prediction(maxConfidenceIndex, maxConfidence, Rectangle(
|
||||
l,
|
||||
t,
|
||||
l + w,
|
||||
t + h
|
||||
))
|
||||
obj = Prediction(
|
||||
maxConfidenceIndex, maxConfidence, Rectangle(l, t, l + w, t + h)
|
||||
)
|
||||
objs.append(obj)
|
||||
|
||||
ret = self.create_detection_result(objs, src_size, cvss)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#
|
||||
coremltools
|
||||
|
||||
# pillow for anything not intel linux, pillow-simd is available on x64 linux
|
||||
|
||||
1
plugins/coreml/src/yolo
Symbolic link
1
plugins/coreml/src/yolo
Symbolic link
@@ -0,0 +1 @@
|
||||
../../openvino/src/yolo
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "koushik-ubuntu",
|
||||
}
|
||||
62
plugins/google-device-access/package-lock.json
generated
62
plugins/google-device-access/package-lock.json
generated
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "@scrypted/google-device-access",
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.97",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/google-device-access",
|
||||
"version": "0.0.96",
|
||||
"version": "0.0.97",
|
||||
"dependencies": {
|
||||
"@googleapis/smartdevicemanagement": "^1.0.0",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^1.3.4",
|
||||
"axios": "^1.4.0",
|
||||
"client-oauth2": "^4.3.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.14.1"
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.4.1"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.69",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -101,18 +101,18 @@
|
||||
"integrity": "sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw=="
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
||||
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"version": "4.14.195",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
|
||||
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
@@ -122,9 +122,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
|
||||
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==",
|
||||
"version": "20.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz",
|
||||
"integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
@@ -157,9 +157,9 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -841,18 +841,18 @@
|
||||
"integrity": "sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw=="
|
||||
},
|
||||
"@types/debug": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
|
||||
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
|
||||
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"version": "4.14.195",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
|
||||
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ms": {
|
||||
@@ -862,9 +862,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz",
|
||||
"integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==",
|
||||
"version": "20.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.1.tgz",
|
||||
"integrity": "sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/tough-cookie": {
|
||||
@@ -891,9 +891,9 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
||||
@@ -40,14 +40,14 @@
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@googleapis/smartdevicemanagement": "^1.0.0",
|
||||
"axios": "^1.3.4",
|
||||
"axios": "^1.4.0",
|
||||
"client-oauth2": "^4.3.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.14.1"
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/node": "^20.4.1"
|
||||
},
|
||||
"version": "0.0.96"
|
||||
"version": "0.0.97"
|
||||
}
|
||||
|
||||
@@ -161,7 +161,9 @@ class NestCamera extends ScryptedDeviceBase implements Readme, Camera, VideoCame
|
||||
},
|
||||
|
||||
setRemoteDescription: async (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup) => {
|
||||
const offerSdp = description.sdp.replace('a=ice-options:trickle\r\n', '');
|
||||
const offerSdp = description.sdp.replace('a=ice-options:trickle\r\n', '')
|
||||
// hack, webrtc plugin is not resecting recvonly for some reason
|
||||
.replaceAll('sendrecv', 'recvonly');
|
||||
|
||||
const result = await this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, {
|
||||
command: "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream",
|
||||
|
||||
163
plugins/hikvision/package-lock.json
generated
163
plugins/hikvision/package-lock.json
generated
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.127",
|
||||
"version": "0.0.128",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.127",
|
||||
"version": "0.0.128",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"axios": "^0.23.0",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"axios": "^1.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"xml2js": "^0.4.23"
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.11"
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.87",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -107,24 +107,50 @@
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"node_modules/@types/xml2js": {
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
|
||||
"integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==",
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz",
|
||||
"integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
@@ -146,20 +172,57 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz",
|
||||
"integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
@@ -238,45 +301,93 @@
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||
},
|
||||
"@types/xml2js": {
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
|
||||
"integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==",
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz",
|
||||
"integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
|
||||
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz",
|
||||
"integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/hikvision",
|
||||
"version": "0.0.127",
|
||||
"version": "0.0.128",
|
||||
"description": "Hikvision Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -38,10 +38,10 @@
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"axios": "^0.23.0",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
"axios": "^1.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"xml2js": "^0.4.23"
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.11"
|
||||
|
||||
@@ -117,6 +117,7 @@ export class HikvisionCameraAPI {
|
||||
method: "GET",
|
||||
responseType: 'arraybuffer',
|
||||
url: url,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
|
||||
2
plugins/homekit/.vscode/settings.json
vendored
2
plugins/homekit/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "koushik-ubuntu"
|
||||
"scrypted.debugHost": "127.0.0.1"
|
||||
}
|
||||
@@ -27,7 +27,7 @@ If recordings dont work, it's generally because of a few reasons, **follow the s
|
||||
|
||||
3) If HomeKit requested the video, but nothing showed up in the timeline:
|
||||
* HomeKit may have decided the motion wasn't worth recording. Set your HomeKit recording options to all motion when testing.
|
||||
* The recordings are in a bad format that can't be used by HomeKit. See below for optimal HomeKit Codec Settings. Enabling Transcode Debug Mode in the HomeKit settings for that camera may fix this for testing purposes, but long term usage is not recommended as it reduces quality and increases CPU load.
|
||||
* The recordings are in a bad format that can't be used by HomeKit. See below for optimal HomeKit Codec Settings. Enabling `Debug Mode` (select `Transcode Video` and `Transcode Audio`) in the HomeKit settings for that camera may fix this for testing purposes, but long term usage is not recommended as it reduces quality and increases CPU load.
|
||||
* Try rebooting your Home Hubs (HomePods and AppleTVs). Make sure they are fully up to date.
|
||||
|
||||
### HomeKit Discovery and Pairing Issues
|
||||
|
||||
4
plugins/homekit/package-lock.json
generated
4
plugins/homekit/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.29",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.29",
|
||||
"dependencies": {
|
||||
"@koush/werift-src": "file:../../external/werift",
|
||||
"check-disk-space": "^3.3.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/homekit",
|
||||
"version": "1.2.27",
|
||||
"version": "1.2.29",
|
||||
"description": "HomeKit Plugin for Scrypted",
|
||||
"scripts": {
|
||||
"scrypted-setup-project": "scrypted-setup-project",
|
||||
|
||||
@@ -64,15 +64,14 @@ export class H264Repacketizer {
|
||||
extraPackets = 0;
|
||||
fuaMax: number;
|
||||
pendingFuA: RtpPacket[];
|
||||
// log whether a stapa sps/pps has been seen.
|
||||
// resets on every idr frame, to trigger codec information
|
||||
// to be resent.
|
||||
seenStapASps = false;
|
||||
// the stapa packet that will be sent before an idr frame.
|
||||
stapa: RtpPacket;
|
||||
fuaMin: number;
|
||||
|
||||
constructor(public console: Console, public maxPacketSize: number, public codecInfo: {
|
||||
sps: Buffer,
|
||||
pps: Buffer,
|
||||
sei?: Buffer,
|
||||
}, public jitterBuffer = new JitterBuffer(console, 4)) {
|
||||
// 12 is the rtp/srtp header size.
|
||||
this.fuaMax = maxPacketSize - FU_A_HEADER_SIZE;
|
||||
@@ -98,6 +97,11 @@ export class H264Repacketizer {
|
||||
this.codecInfo.pps = pps;
|
||||
}
|
||||
|
||||
updateSei(sei: Buffer) {
|
||||
this.ensureCodecInfo();
|
||||
this.codecInfo.sei = sei;
|
||||
}
|
||||
|
||||
shouldFilter(nalType: number) {
|
||||
// currently nothing is filtered, but it seems that some SEI packets cause issues
|
||||
// and should be ignored, while others show up in the stap-a sps/pps packet
|
||||
@@ -202,6 +206,14 @@ export class H264Repacketizer {
|
||||
return datas.shift();
|
||||
}
|
||||
|
||||
// a single nalu stapa is unnecessary, return the nalu itself.
|
||||
// this can happen when trying to packetize multiple nalus into a stapa
|
||||
// and the last nalu does not fit into the first stapa, and ends up in
|
||||
// a new stapa.
|
||||
if (counter === 1) {
|
||||
return payload[1];
|
||||
}
|
||||
|
||||
payload.unshift(Buffer.from([stapHeader]));
|
||||
return Buffer.concat(payload);
|
||||
}
|
||||
@@ -266,7 +278,7 @@ export class H264Repacketizer {
|
||||
}
|
||||
else {
|
||||
if (splitNaluType === NAL_TYPE_IDR)
|
||||
this.maybeSendSpsPps(first, ret);
|
||||
this.maybeSendStapACodecInfo(first, ret);
|
||||
|
||||
this.fragment(first, ret, {
|
||||
payload: split,
|
||||
@@ -319,11 +331,20 @@ export class H264Repacketizer {
|
||||
});
|
||||
}
|
||||
|
||||
maybeSendSpsPps(packet: RtpPacket, ret: RtpPacket[]) {
|
||||
maybeSendStapACodecInfo(packet: RtpPacket, ret: RtpPacket[]) {
|
||||
if (this.stapa) {
|
||||
// stapa with codec information was sent recently, no need to send codec info.
|
||||
this.stapa = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.codecInfo?.sps || !this.codecInfo?.pps)
|
||||
return;
|
||||
|
||||
const aggregates = this.packetizeStapA([this.codecInfo.sps, this.codecInfo.pps]);
|
||||
const agg = [this.codecInfo.sps, this.codecInfo.pps];
|
||||
if (this.codecInfo?.sei)
|
||||
agg.push(this.codecInfo.sei);
|
||||
const aggregates = this.packetizeStapA(agg);
|
||||
if (aggregates.length !== 1) {
|
||||
this.console.error('expected only 1 packet for sps/pps stapa');
|
||||
return;
|
||||
@@ -406,9 +427,7 @@ export class H264Repacketizer {
|
||||
// the stream may not contain codec information in stapa or may be sending it
|
||||
// in separate sps/pps packets which is not supported by homekit.
|
||||
if (originalNalType === NAL_TYPE_IDR) {
|
||||
if (!this.seenStapASps)
|
||||
this.maybeSendSpsPps(packet, ret);
|
||||
this.seenStapASps = false;
|
||||
this.maybeSendStapACodecInfo(packet, ret);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -451,26 +470,43 @@ export class H264Repacketizer {
|
||||
else if (nalType === NAL_TYPE_STAP_A) {
|
||||
this.flushPendingFuA(ret);
|
||||
|
||||
// break the aggregated packet up and send it.
|
||||
const depacketized = depacketizeStapA(packet.payload)
|
||||
.filter(payload => {
|
||||
let hasSps = false;
|
||||
let hasPps = false;
|
||||
|
||||
// break the aggregated packet up to update codec information.
|
||||
depacketizeStapA(packet.payload)
|
||||
.forEach(payload => {
|
||||
const nalType = payload[0] & 0x1F;
|
||||
this.seenStapASps = this.seenStapASps || (nalType === NAL_TYPE_SPS);
|
||||
if (this.shouldFilter(nalType)) {
|
||||
return false;
|
||||
}
|
||||
if (nalType === NAL_TYPE_SPS)
|
||||
if (nalType === NAL_TYPE_SPS) {
|
||||
hasSps = true;
|
||||
this.updateSps(payload);
|
||||
if (nalType === NAL_TYPE_PPS)
|
||||
}
|
||||
else if (nalType === NAL_TYPE_PPS) {
|
||||
hasPps = true;
|
||||
this.updatePps(payload);
|
||||
return true;
|
||||
}
|
||||
else if (nalType === NAL_TYPE_SEI) {
|
||||
this.updateSei(payload);
|
||||
}
|
||||
else if (nalType === NAL_TYPE_DELIMITER) {
|
||||
// this is uncommon but has been seen. seems to be a no-op nalu.
|
||||
}
|
||||
else if (nalType === NAL_TYPE_NON_IDR) {
|
||||
// this is uncommon but has been seen. oddly, on reolink this non-idr was sent
|
||||
// after the codec information. so codec information can be changed between
|
||||
// idr and non-idr? maybe it is not applied until next idr?
|
||||
}
|
||||
else {
|
||||
this.console.warn('Skipped a stapa type. Please report this to @koush on Discord.', nalType)
|
||||
}
|
||||
});
|
||||
if (depacketized.length === 0) {
|
||||
this.extraPackets--;
|
||||
return;
|
||||
}
|
||||
const aggregates = this.packetizeStapA(depacketized);
|
||||
this.createRtpPackets(packet, aggregates, ret);
|
||||
|
||||
// log that a stapa with codec info was sent
|
||||
if (hasSps && hasPps)
|
||||
this.stapa = packet;
|
||||
|
||||
const stapa = this.packetizeStapA(depacketizeStapA(packet.payload));
|
||||
this.createRtpPackets(packet, stapa, ret);
|
||||
}
|
||||
else if (nalType >= 1 && nalType < 24) {
|
||||
this.flushPendingFuA(ret);
|
||||
@@ -491,6 +527,11 @@ export class H264Repacketizer {
|
||||
this.updatePps(packet.payload);
|
||||
return;
|
||||
}
|
||||
else if (nalType === NAL_TYPE_SEI) {
|
||||
this.extraPackets--;
|
||||
this.updateSei(packet.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.shouldFilter(nalType)) {
|
||||
this.extraPackets--;
|
||||
@@ -500,9 +541,7 @@ export class H264Repacketizer {
|
||||
if (nalType === NAL_TYPE_IDR) {
|
||||
// if this is an idr frame, but no sps has been sent, dummy one up.
|
||||
// the stream may not contain sps.
|
||||
if (!this.seenStapASps)
|
||||
this.maybeSendSpsPps(packet, ret);
|
||||
this.seenStapASps = false;
|
||||
this.maybeSendStapACodecInfo(packet, ret);
|
||||
}
|
||||
|
||||
this.fragment(packet, ret);
|
||||
|
||||
@@ -95,7 +95,7 @@ export class JitterBuffer {
|
||||
|
||||
// missed/late bunch of packets
|
||||
if (packetDistance > this.jitterSize) {
|
||||
this.console.log('jitter buffer skipped packets:', packetDistance);
|
||||
// this.console.log('jitter buffer skipped packets:', packetDistance);
|
||||
const { lastSequenceNumber } = this;
|
||||
this.lastSequenceNumber = sequenceNumber - this.jitterSize;
|
||||
// use the previous sequence number to flush any packets that are too old compared
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import sdk, { Fan, AirQuality, AirQualitySensor, CO2Sensor, NOXSensor, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedInterface, VOCSensor, FanMode, OnOff, DeviceProvider, ScryptedDeviceType } from "@scrypted/sdk";
|
||||
import sdk, { AirQuality, AirQualitySensor, CO2Sensor, DeviceProvider, Fan, FanMode, NOXSensor, OnOff, PM10Sensor, PM25Sensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VOCSensor } from "@scrypted/sdk";
|
||||
import { bindCharacteristic } from "../common";
|
||||
import { Accessory, Characteristic, CharacteristicEventTypes, Service, uuid } from '../hap';
|
||||
import type { HomeKitPlugin } from "../main";
|
||||
@@ -6,10 +6,30 @@ import { getService as getOnOffService } from "./onoff-base";
|
||||
|
||||
const { deviceManager, systemManager } = sdk;
|
||||
|
||||
export function getSafeMdnsName(device: ScryptedDevice) {
|
||||
// Valid domains can include 0-9, a-z (case insensitive), dash, and period.
|
||||
// However, period must filtered because this is an mdns subdomain.
|
||||
|
||||
// The underlying mdns advertisers also support spaces (allegedly, since it seems to work already, but I have not looked closely).
|
||||
|
||||
let newName = '';
|
||||
let name = device.name || 'Scrypted';
|
||||
for (const c of name) {
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '-' || c === ' ') {
|
||||
newName += c
|
||||
}
|
||||
}
|
||||
|
||||
if (!newName)
|
||||
newName = 'Scrypted';
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
export function makeAccessory(device: ScryptedDevice, homekitPlugin: HomeKitPlugin, suffix?: string): Accessory {
|
||||
const mixinStorage = deviceManager.getMixinStorage(device.id, homekitPlugin.nativeId);
|
||||
const resetId = mixinStorage.getItem('resetAccessory') || '';
|
||||
return new Accessory(device.name, uuid.generate(resetId + device.id + (suffix ? '-' + suffix : '')));
|
||||
return new Accessory(getSafeMdnsName(device), uuid.generate(resetId + device.id + (suffix ? '-' + suffix : '')));
|
||||
}
|
||||
|
||||
export function getChildDevices(device: ScryptedDevice & DeviceProvider): ScryptedDevice[] {
|
||||
|
||||
4
plugins/objectdetector/package-lock.json
generated
4
plugins/objectdetector/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.141",
|
||||
"version": "0.0.160",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.141",
|
||||
"version": "0.0.160",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/objectdetector",
|
||||
"version": "0.0.141",
|
||||
"version": "0.0.160",
|
||||
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Deferred } from '@scrypted/common/src/deferred';
|
||||
import { sleep } from '@scrypted/common/src/sleep';
|
||||
import sdk, { Camera, DeviceProvider, DeviceState, EventListenerRegister, Image, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionGeneratorResult, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoFrame, VideoFrameGenerator } from '@scrypted/sdk';
|
||||
import sdk, { Camera, DeviceProvider, DeviceState, EventListenerRegister, Image, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoFrame, VideoFrameGenerator } from '@scrypted/sdk';
|
||||
import { StorageSettings } from '@scrypted/sdk/storage-settings';
|
||||
import crypto from 'crypto';
|
||||
import os from 'os';
|
||||
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
|
||||
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes-no-sharp';
|
||||
import { getMaxConcurrentObjectDetectionSessions } from './performance-profile';
|
||||
import { serverSupportsMixinEventMasking } from './server-version';
|
||||
import { getAllDevices, safeParseJson } from './util';
|
||||
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes-no-sharp';
|
||||
import os from 'os';
|
||||
|
||||
const polygonOverlap = require('polygon-overlap');
|
||||
const insidePolygon = require('point-inside-polygon');
|
||||
@@ -16,8 +17,6 @@ const insidePolygon = require('point-inside-polygon');
|
||||
const { systemManager } = sdk;
|
||||
|
||||
const defaultDetectionDuration = 20;
|
||||
const defaultDetectionInterval = 60;
|
||||
const defaultDetectionTimeout = 60;
|
||||
const defaultMotionDuration = 30;
|
||||
|
||||
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
|
||||
@@ -76,43 +75,30 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
this.maybeStartMotionDetection();
|
||||
}
|
||||
},
|
||||
detectionDuration: {
|
||||
detectionDurationDEPRECATED: {
|
||||
hide: true,
|
||||
title: 'Detection Duration',
|
||||
subgroup: 'Advanced',
|
||||
description: 'The duration in seconds to analyze video when motion occurs.',
|
||||
type: 'number',
|
||||
defaultValue: defaultDetectionDuration,
|
||||
},
|
||||
detectionTimeout: {
|
||||
title: 'Detection Timeout',
|
||||
subgroup: 'Advanced',
|
||||
description: 'Timeout in seconds before removing an object that is no longer detected.',
|
||||
type: 'number',
|
||||
defaultValue: defaultDetectionTimeout,
|
||||
},
|
||||
motionDuration: {
|
||||
title: 'Motion Duration',
|
||||
description: 'The duration in seconds to wait to reset the motion sensor.',
|
||||
type: 'number',
|
||||
defaultValue: defaultMotionDuration,
|
||||
},
|
||||
motionAsObjects: {
|
||||
title: 'Motion Detection Objects',
|
||||
description: 'Report motion detections as objects (useful for debugging).',
|
||||
type: 'boolean',
|
||||
},
|
||||
detectionInterval: {
|
||||
type: 'number',
|
||||
defaultValue: defaultDetectionInterval,
|
||||
hide: true,
|
||||
},
|
||||
});
|
||||
motionTimeout: NodeJS.Timeout;
|
||||
detectionIntervalTimeout: NodeJS.Timeout;
|
||||
zones = this.getZones();
|
||||
zoneInfos = this.getZoneInfos();
|
||||
detectionIntervalTimeout: NodeJS.Timeout;
|
||||
analyzeStop = 0;
|
||||
detectionStartTime: number;
|
||||
analyzeStop: number;
|
||||
detectorSignal = new Deferred<void>().resolve();
|
||||
released = false;
|
||||
|
||||
get detectorRunning() {
|
||||
return !this.detectorSignal.finished;
|
||||
}
|
||||
@@ -131,24 +117,16 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
|
||||
this.bindObjectDetection();
|
||||
this.register();
|
||||
this.resetDetectionTimeout();
|
||||
}
|
||||
|
||||
clearDetectionTimeout() {
|
||||
clearTimeout(this.detectionIntervalTimeout);
|
||||
this.detectionIntervalTimeout = undefined;
|
||||
}
|
||||
|
||||
resetDetectionTimeout() {
|
||||
this.clearDetectionTimeout();
|
||||
this.detectionIntervalTimeout = setInterval(async () => {
|
||||
if (this.hasMotionType) {
|
||||
// force a motion detection restart if it quit
|
||||
if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_REPLACE)
|
||||
this.startPipelineAnalysis();
|
||||
if (this.released)
|
||||
return;
|
||||
}
|
||||
}, this.storageSettings.values.detectionInterval * 1000);
|
||||
if (!this.hasMotionType)
|
||||
return;
|
||||
if (this.motionSensorSupplementation !== BUILTIN_MOTION_SENSOR_REPLACE)
|
||||
return;
|
||||
this.startPipelineAnalysis();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
clearMotionTimeout() {
|
||||
@@ -159,6 +137,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
resetMotionTimeout() {
|
||||
this.clearMotionTimeout();
|
||||
this.motionTimeout = setTimeout(() => {
|
||||
this.console.log('Motion timed out.');
|
||||
this.motionDetected = false;
|
||||
// if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_ASSIST) {
|
||||
// this.console.log(`${this.objectDetection.name} timed out confirming motion, stopping video detection.`)
|
||||
@@ -178,7 +157,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
if (this.hasMotionType)
|
||||
ret['motionAsObjects'] = this.storageSettings.values.motionAsObjects;
|
||||
ret['motionAsObjects'] = true;
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -255,12 +234,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
startPipelineAnalysis() {
|
||||
if (!this.detectorSignal.finished)
|
||||
if (!this.detectorSignal.finished || this.released)
|
||||
return;
|
||||
|
||||
const signal = this.detectorSignal = new Deferred();
|
||||
this.detectionStartTime = Date.now();
|
||||
if (!this.hasMotionType)
|
||||
this.plugin.objectDetectionStarted(this.console);
|
||||
this.plugin.objectDetectionStarted(this.name, this.console);
|
||||
|
||||
const options = {
|
||||
snapshotPipeline: this.plugin.shouldUseSnapshotPipeline(),
|
||||
@@ -286,35 +266,44 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
suppress?: boolean,
|
||||
}) {
|
||||
while (!signal.finished) {
|
||||
if (options.suppress) {
|
||||
this.console.log('Resuming motion processing after active motion timeout.');
|
||||
}
|
||||
const shouldSleep = await this.runPipelineAnalysis(signal, options);
|
||||
options.suppress = true;
|
||||
if (!shouldSleep || signal.finished)
|
||||
return;
|
||||
this.console.log('Suspending motion processing during active motion timeout.');
|
||||
this.resetMotionTimeout();
|
||||
// sleep until a moment before motion duration to start peeking again
|
||||
// to have an opporunity to reset the motion timeout.
|
||||
await sleep(this.storageSettings.values.motionDuration * 1000 - 4000);
|
||||
}
|
||||
}
|
||||
|
||||
async createFrameGenerator(signal: Deferred<void>, options: {
|
||||
snapshotPipeline: boolean,
|
||||
suppress?: boolean,
|
||||
}, updatePipelineStatus: (status: string) => void): Promise<AsyncGenerator<VideoFrame, any, unknown>> {
|
||||
|
||||
getCurrentFrameGenerator(snapshotPipeline: boolean) {
|
||||
let frameGenerator: string = this.frameGenerator;
|
||||
if (!this.hasMotionType && options.snapshotPipeline) {
|
||||
if (!this.hasMotionType && snapshotPipeline) {
|
||||
frameGenerator = 'Snapshot';
|
||||
this.console.warn(`Due to limited performance, Snapshot mode is being used with ${this.plugin.statsSnapshotConcurrent} actively detecting cameras.`);
|
||||
}
|
||||
return frameGenerator;
|
||||
}
|
||||
|
||||
async createFrameGenerator(signal: Deferred<void>,
|
||||
frameGenerator: string,
|
||||
options: {
|
||||
snapshotPipeline: boolean,
|
||||
suppress?: boolean,
|
||||
}, updatePipelineStatus: (status: string) => void): Promise<AsyncGenerator<VideoFrame, any, unknown>> {
|
||||
if (frameGenerator === 'Snapshot' && !this.hasMotionType) {
|
||||
|
||||
options.snapshotPipeline = true;
|
||||
this.console.log('Snapshot', '+', this.objectDetection.name);
|
||||
const self = this;
|
||||
return (async function* gen() {
|
||||
try {
|
||||
const flush = async () => {};
|
||||
const flush = async () => { };
|
||||
while (!signal.finished) {
|
||||
const now = Date.now();
|
||||
const sleeper = async () => {
|
||||
@@ -369,16 +358,24 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
// ask rebroadcast to mute audio, not needed.
|
||||
audio: null,
|
||||
});
|
||||
updatePipelineStatus('generateVideoFrames');
|
||||
|
||||
return await videoFrameGenerator.generateVideoFrames(stream, {
|
||||
queue: 0,
|
||||
fps: this.hasMotionType ? 4 : undefined,
|
||||
resize: this.model?.inputSize ? {
|
||||
width: this.model.inputSize[0],
|
||||
height: this.model.inputSize[1],
|
||||
} : undefined,
|
||||
format: this.model?.inputFormat,
|
||||
});
|
||||
try {
|
||||
return await videoFrameGenerator.generateVideoFrames(stream, {
|
||||
queue: 0,
|
||||
fps: this.hasMotionType ? 4 : undefined,
|
||||
// this seems to be unused now?
|
||||
resize: this.model?.inputSize ? {
|
||||
width: this.model.inputSize[0],
|
||||
height: this.model.inputSize[1],
|
||||
} : undefined,
|
||||
// this seems to be unused now?
|
||||
format: this.model?.inputFormat,
|
||||
});
|
||||
}
|
||||
finally {
|
||||
updatePipelineStatus('waiting first result');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,7 +384,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
suppress?: boolean,
|
||||
}) {
|
||||
const start = Date.now();
|
||||
this.analyzeStop = start + this.getDetectionDuration();
|
||||
|
||||
let lastStatusTime = Date.now();
|
||||
let lastStatus = 'starting';
|
||||
@@ -429,23 +425,42 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
}
|
||||
|
||||
let longObjectDetectionWarning = false;
|
||||
|
||||
const frameGenerator = this.getCurrentFrameGenerator(options.snapshotPipeline);
|
||||
for await (const detected of
|
||||
await sdk.connectRPCObject(
|
||||
await this.objectDetection.generateObjectDetections(
|
||||
await this.createFrameGenerator(signal, options, updatePipelineStatus), {
|
||||
settings: this.getCurrentSettings(),
|
||||
await this.createFrameGenerator(signal,
|
||||
frameGenerator,
|
||||
options,
|
||||
updatePipelineStatus), {
|
||||
settings: {
|
||||
...this.getCurrentSettings(),
|
||||
analyzeMode: !!this.analyzeStop,
|
||||
frameGenerator,
|
||||
},
|
||||
sourceId: this.id,
|
||||
zones,
|
||||
}))) {
|
||||
if (signal.finished) {
|
||||
break;
|
||||
}
|
||||
if (!this.hasMotionType && Date.now() > this.analyzeStop) {
|
||||
|
||||
// stop when analyze period ends.
|
||||
if (!this.hasMotionType && this.analyzeStop && Date.now() > this.analyzeStop) {
|
||||
this.analyzeStop = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
// apply the zones to the detections and get a shallow copy list of detections after
|
||||
// exclusion zones have applied
|
||||
const originalDetections = detected.detected.detections;
|
||||
const zonedDetections = this.applyZones(detected.detected);
|
||||
detected.detected.detections = zonedDetections;
|
||||
|
||||
@@ -454,6 +469,11 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
if (!this.hasMotionType) {
|
||||
this.plugin.trackDetection();
|
||||
|
||||
// const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
|
||||
// const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
|
||||
// if (numZonedDetections !== numOriginalDetections)
|
||||
// this.console.log('Zone filtered detections:', numZonedDetections - numOriginalDetections);
|
||||
|
||||
for (const d of detected.detected.detections) {
|
||||
currentDetections.add(d.className);
|
||||
}
|
||||
@@ -482,19 +502,22 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
this.setDetection(detected.detected, mo);
|
||||
// this.console.log('image saved', detected.detected.detections);
|
||||
}
|
||||
this.reportObjectDetections(detected.detected);
|
||||
const motionFound = this.reportObjectDetections(detected.detected);
|
||||
if (this.hasMotionType) {
|
||||
if (this.motionDetected) {
|
||||
// if motion is detected, stop processing and exit loop allowing it to sleep.
|
||||
clearInterval(interval);
|
||||
return true;
|
||||
// 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 || Date.now() > this.analyzeStop) {
|
||||
this.analyzeStop = undefined;
|
||||
clearInterval(interval);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
updatePipelineStatus('waiting result');
|
||||
// this.handleDetectionEvent(detected.detected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]) {
|
||||
@@ -510,12 +533,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
return box;
|
||||
}
|
||||
|
||||
getDetectionDuration() {
|
||||
// when motion type, the detection interval is a keepalive reset.
|
||||
// the duration needs to simply be an arbitrarily longer time.
|
||||
return this.hasMotionType ? this.storageSettings.values.detectionInterval * 1000 * 5 : this.storageSettings.values.detectionDuration * 1000;
|
||||
}
|
||||
|
||||
applyZones(detection: ObjectsDetected) {
|
||||
// determine zones of the objects, if configured.
|
||||
if (!detection.detections)
|
||||
@@ -584,12 +601,12 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
reportObjectDetections(detection: ObjectsDetected) {
|
||||
let motionFound = false;
|
||||
if (this.hasMotionType) {
|
||||
const found = detection.detections?.find(d => d.className === 'motion');
|
||||
if (found) {
|
||||
motionFound = !!detection.detections?.find(d => d.className === 'motion');
|
||||
if (motionFound) {
|
||||
if (!this.motionDetected)
|
||||
this.motionDetected = true;
|
||||
this.resetMotionTimeout();
|
||||
|
||||
// if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_ASSIST) {
|
||||
// if (!this.motionDetected) {
|
||||
@@ -611,8 +628,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.hasMotionType || this.storageSettings.values.motionAsObjects)
|
||||
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
|
||||
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
|
||||
return motionFound;
|
||||
}
|
||||
|
||||
setDetection(detection: ObjectsDetected, detectionInput: MediaObject) {
|
||||
@@ -707,10 +724,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
|
||||
this.storageSettings.settings.detectionDuration.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.detectionTimeout.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.detectionDurationDEPRECATED.hide = this.hasMotionType;
|
||||
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
|
||||
this.storageSettings.settings.motionAsObjects.hide = !this.hasMotionType;
|
||||
|
||||
settings.push(...await this.storageSettings.getSettings());
|
||||
|
||||
@@ -773,16 +788,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.hasMotionType) {
|
||||
settings.push(
|
||||
{
|
||||
title: 'Analyze',
|
||||
description: 'Analyzes the video stream for 1 minute. Results will be shown in the Console.',
|
||||
key: 'analyzeButton',
|
||||
type: 'button',
|
||||
},
|
||||
);
|
||||
}
|
||||
settings.push(
|
||||
{
|
||||
title: 'Analyze',
|
||||
description: 'Analyzes the video stream for 1 minute. Results will be shown in the Console.',
|
||||
key: 'analyzeButton',
|
||||
type: 'button',
|
||||
},
|
||||
);
|
||||
|
||||
return settings;
|
||||
}
|
||||
@@ -861,9 +874,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|
||||
}
|
||||
|
||||
async release() {
|
||||
this.released = true;
|
||||
super.release();
|
||||
this.clearDetectionTimeout();
|
||||
this.clearMotionTimeout();
|
||||
clearInterval(this.detectionIntervalTimeout);
|
||||
this.motionListener?.removeListener();
|
||||
this.motionMixinListener?.removeListener();
|
||||
this.endObjectDetection();
|
||||
@@ -926,10 +940,18 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
|
||||
return ret;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any) {
|
||||
async releaseMixin(id: string, mixinDevice: ObjectDetectionMixin) {
|
||||
this.currentMixins.delete(mixinDevice);
|
||||
return mixinDevice.release();
|
||||
}
|
||||
|
||||
release(): void {
|
||||
super.release();
|
||||
for (const m of this.currentMixins) {
|
||||
m.release();
|
||||
}
|
||||
this.currentMixins.clear();
|
||||
}
|
||||
}
|
||||
|
||||
interface ObjectDetectionStatistics {
|
||||
@@ -944,15 +966,28 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
statsSnapshotDetections: number;
|
||||
statsSnapshotConcurrent = 0;
|
||||
storageSettings = new StorageSettings(this, {
|
||||
maxConcurrentDetections: {
|
||||
title: 'Max Concurrent Detections',
|
||||
description: `The max number concurrent cameras that will perform object detection while their motion sensor is triggered. Older sessions will be terminated when the limit is reached. The default value is ${getMaxConcurrentObjectDetectionSessions()}.`,
|
||||
defaultValue: 'Default',
|
||||
combobox: true,
|
||||
choices: [
|
||||
'Default',
|
||||
...[2, 3, 4, 5, 6, 7, 8, 9, 10].map(i => i.toString()),
|
||||
],
|
||||
mapPut: (o, v) => {
|
||||
return parseInt(v) || 'Default';
|
||||
}
|
||||
},
|
||||
activeMotionDetections: {
|
||||
title: 'Active Motion Detection Sessions',
|
||||
multiple: true,
|
||||
readonly: true,
|
||||
onGet: async () => {
|
||||
const motion = [...this.currentMixins.values()]
|
||||
const motionDetections = [...this.currentMixins.values()]
|
||||
.map(d => [...d.currentMixins.values()].filter(dd => dd.hasMotionType)).flat();
|
||||
const choices = motion.map(dd => dd.name);
|
||||
const value = motion.filter(c => c.detectorRunning).map(dd => dd.name);
|
||||
const choices = motionDetections.map(dd => dd.name);
|
||||
const value = motionDetections.filter(c => c.detectorRunning).map(dd => dd.name);
|
||||
return {
|
||||
choices,
|
||||
value,
|
||||
@@ -970,10 +1005,10 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
multiple: true,
|
||||
readonly: true,
|
||||
onGet: async () => {
|
||||
const motion = [...this.currentMixins.values()]
|
||||
const objectDetections = [...this.currentMixins.values()]
|
||||
.map(d => [...d.currentMixins.values()].filter(dd => !dd.hasMotionType)).flat();
|
||||
const choices = motion.map(dd => dd.name);
|
||||
const value = motion.filter(c => c.detectorRunning).map(dd => dd.name);
|
||||
const choices = objectDetections.map(dd => dd.name);
|
||||
const value = objectDetections.filter(c => c.detectorRunning).map(dd => dd.name);
|
||||
return {
|
||||
choices,
|
||||
value,
|
||||
@@ -991,6 +1026,13 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
shouldUseSnapshotPipeline() {
|
||||
this.pruneOldStatistics();
|
||||
|
||||
// deprecated in favor of object detection session eviction.
|
||||
return false;
|
||||
|
||||
// never use snapshot mode if its a single camera.
|
||||
if (this.statsSnapshotConcurrent < 2)
|
||||
return false;
|
||||
|
||||
// find any concurrent cameras with as many or more that had passable results
|
||||
for (const [k, v] of this.objectDetectionStatistics.entries()) {
|
||||
if (v.dps > 2 && k >= this.statsSnapshotConcurrent)
|
||||
@@ -1019,10 +1061,27 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
this.statsSnapshotDetections++;
|
||||
}
|
||||
|
||||
objectDetectionStarted(console: Console) {
|
||||
objectDetectionStarted(name: string, console: Console) {
|
||||
this.resetStats(console);
|
||||
|
||||
this.statsSnapshotConcurrent++;
|
||||
|
||||
let maxConcurrent = this.storageSettings.values.maxConcurrentDetections || 'Default';
|
||||
maxConcurrent = Math.max(parseInt(maxConcurrent)) || getMaxConcurrentObjectDetectionSessions();
|
||||
|
||||
const objectDetections = [...this.currentMixins.values()]
|
||||
.map(d => [...d.currentMixins.values()].filter(dd => !dd.hasMotionType)).flat()
|
||||
.filter(c => c.detectorRunning)
|
||||
.sort((a, b) => a.detectionStartTime - b.detectionStartTime);
|
||||
|
||||
while (objectDetections.length > maxConcurrent) {
|
||||
const old = objectDetections.pop();
|
||||
// allow exceeding the concurrency limit if user interaction triggered analyze.
|
||||
if (old.analyzeStop)
|
||||
continue;
|
||||
old.console.log(`Ending object detection to process activity on ${name}.`);
|
||||
old.endObjectDetection();
|
||||
}
|
||||
}
|
||||
|
||||
objectDetectionEnded(console: Console, snapshotPipeline: boolean) {
|
||||
@@ -1106,10 +1165,11 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings,
|
||||
return ret;
|
||||
}
|
||||
|
||||
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
|
||||
async releaseMixin(id: string, mixinDevice: ObjectDetectorMixin): Promise<void> {
|
||||
// what does this mean to make a mixin provider no longer available?
|
||||
// just ignore it until reboot?
|
||||
this.currentMixins.delete(mixinDevice);
|
||||
return mixinDevice.release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
plugins/objectdetector/src/performance-profile.ts
Normal file
32
plugins/objectdetector/src/performance-profile.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import os from 'os';
|
||||
|
||||
let totalGigahertz = 0;
|
||||
|
||||
export function getMaxConcurrentObjectDetectionSessions() {
|
||||
const cpus = os.cpus();
|
||||
|
||||
// apple silicon cpu.speed is incorrect, and can handle quite a bit due to
|
||||
// gpu decode and neural core usage.
|
||||
// .5 detect per cpu is a conservative guess. so an m1 ultra would handle 10
|
||||
// simultaneous camera detections.
|
||||
// apple silicon also reports cpu speed as 24 mhz, so the following code would
|
||||
// fail anyways.
|
||||
if (process.platform === 'darwin' && process.arch === 'arm64')
|
||||
return cpus.length * .5;
|
||||
|
||||
let speed = 0;
|
||||
for (const cpu of cpus) {
|
||||
// can cpu speed be zero? is that a thing?
|
||||
speed += cpu.speed || 600;
|
||||
}
|
||||
|
||||
totalGigahertz = Math.max(speed, totalGigahertz);
|
||||
|
||||
// a wyse 5070 self reports in description as 1.5ghz and has 4 cores and can comfortably handle
|
||||
// two 2k detections at the same time.
|
||||
// the speed reported while detecting caps at 2500, presumably due to burst?
|
||||
// the total mhz would be 10000 in this case.
|
||||
// observed idle per cpu speed is 800.
|
||||
// not sure how hyperthreading plays into this.
|
||||
return Math.max(2, totalGigahertz / 4000);
|
||||
}
|
||||
264
plugins/onvif/package-lock.json
generated
264
plugins/onvif/package-lock.json
generated
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"name": "@scrypted/onvif",
|
||||
"version": "0.0.121",
|
||||
"lockfileVersion": 2,
|
||||
"version": "0.0.123",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/onvif",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.123",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": "^0.8.5",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"base-64": "^1.0.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"http-auth-utils": "^3.0.5",
|
||||
"md5": "^2.3.0",
|
||||
"onvif": "^0.6.7",
|
||||
"onvif": "^0.6.8",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/md5": "^2.3.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/xml2js": "^0.4.9"
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/node": "^18.16.18",
|
||||
"@types/xml2js": "^0.4.11"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
@@ -39,33 +39,9 @@
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../external/onvif": {
|
||||
"version": "0.6.5",
|
||||
"extraneous": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coveralls": "^3.1.1",
|
||||
"dot": "^1.1.3",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"ip": "^1.1.5",
|
||||
"keypress": "^0.2.1",
|
||||
"mocha": "^9.1.3",
|
||||
"mocha-lcov-reporter": "^1.3.0",
|
||||
"nimble": "^0.0.2",
|
||||
"nyc": "^15.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.87",
|
||||
"version": "0.2.103",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -100,9 +76,6 @@
|
||||
"typedoc": "^0.23.21"
|
||||
}
|
||||
},
|
||||
"../sdk": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
@@ -121,24 +94,21 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/md5": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.1.tgz",
|
||||
"integrity": "sha512-OK3oe+ALIoPSo262lnhAYwpqFNXbiwH2a+0+Z5YBnkQEwWD8fk5+PIeRhYA48PzvX9I4SGNpWy+9bLj8qz92RQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
|
||||
"integrity": "sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"version": "18.16.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
|
||||
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/xml2js": {
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
|
||||
"integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==",
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz",
|
||||
"integrity": "sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -165,7 +135,7 @@
|
||||
"node_modules/charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
|
||||
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -173,15 +143,15 @@
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
|
||||
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||
"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",
|
||||
@@ -198,14 +168,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-auth-utils": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.2.tgz",
|
||||
"integrity": "sha512-cQ8957aiUX0lgV1620uIGKGJc0sEuD/QK4ueZ0hb60MGbO0f6ahcuIgPjamAD98D/AUGizKVm+dNvUVHs0f4Ow==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.5.tgz",
|
||||
"integrity": "sha512-A592YHM51dmcru5vePB1yo8zjr0iHJrSo67x8bdjXrazP1Zzvx6zu/Sece+g2gxgkHJsRnmbi1xShKQkIie+YA==",
|
||||
"dependencies": {
|
||||
"yerror": "^6.0.0"
|
||||
"yerror": "^6.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.19.0"
|
||||
"node": ">=16.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-buffer": {
|
||||
@@ -229,9 +199,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/onvif": {
|
||||
"version": "0.6.7",
|
||||
"resolved": "https://registry.npmjs.org/onvif/-/onvif-0.6.7.tgz",
|
||||
"integrity": "sha512-7ButP0QQcSvHaz2Jj04Hb5UWGQ+XTx1kTar2Hwe1KTbPhwC/3QoQcY06/Nnx6y94M4RDaKIf5iQY98PXJX5H4g==",
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/onvif/-/onvif-0.6.8.tgz",
|
||||
"integrity": "sha512-GkrBlgusJCAGRBxfLBmykJpfKbPY16mChERORqt5J7aFt7y48KyqoynS+w7D3nZcjWPKR7WyHiJV9XN4e+Foiw==",
|
||||
"dependencies": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"xml2js": "^0.4.23"
|
||||
@@ -266,176 +236,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yerror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.0.1.tgz",
|
||||
"integrity": "sha512-0Bxo+NyeucjxhmPB5z3lmI/N/cOu8L1Q8JVta6/I5G6J/JhCSSPwk8qt9N4yOFSjwkvhDwzUSQglfBIAllvi1Q==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.2.1.tgz",
|
||||
"integrity": "sha512-WPZgybhCBzsMSSqGYBnj20NZo4FiFKQG0+i/21cYGVd4B7eYtvYDOpjk/0e8UM1eVHJ+4Ja6bZ7TAjHH6mk+ew==",
|
||||
"engines": {
|
||||
"node": ">=12.19.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@koush/axios-digest-auth": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
|
||||
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
|
||||
"requires": {
|
||||
"auth-header": "^1.0.0",
|
||||
"axios": "^0.21.4"
|
||||
}
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"@types/node": "^16.9.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/preset-typescript": "^7.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": "^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/md5": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.1.tgz",
|
||||
"integrity": "sha512-OK3oe+ALIoPSo262lnhAYwpqFNXbiwH2a+0+Z5YBnkQEwWD8fk5+PIeRhYA48PzvX9I4SGNpWy+9bLj8qz92RQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/xml2js": {
|
||||
"version": "0.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz",
|
||||
"integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"auth-header": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
|
||||
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"base-64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
|
||||
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
|
||||
},
|
||||
"charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||
},
|
||||
"crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
|
||||
},
|
||||
"http-auth-utils": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.2.tgz",
|
||||
"integrity": "sha512-cQ8957aiUX0lgV1620uIGKGJc0sEuD/QK4ueZ0hb60MGbO0f6ahcuIgPjamAD98D/AUGizKVm+dNvUVHs0f4Ow==",
|
||||
"requires": {
|
||||
"yerror": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
|
||||
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
|
||||
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
|
||||
"requires": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"onvif": {
|
||||
"version": "0.6.7",
|
||||
"resolved": "https://registry.npmjs.org/onvif/-/onvif-0.6.7.tgz",
|
||||
"integrity": "sha512-7ButP0QQcSvHaz2Jj04Hb5UWGQ+XTx1kTar2Hwe1KTbPhwC/3QoQcY06/Nnx6y94M4RDaKIf5iQY98PXJX5H4g==",
|
||||
"requires": {
|
||||
"lodash.get": "^4.4.2",
|
||||
"xml2js": "^0.4.23"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
"yerror": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.0.1.tgz",
|
||||
"integrity": "sha512-0Bxo+NyeucjxhmPB5z3lmI/N/cOu8L1Q8JVta6/I5G6J/JhCSSPwk8qt9N4yOFSjwkvhDwzUSQglfBIAllvi1Q=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/onvif",
|
||||
"version": "0.0.121",
|
||||
"version": "0.0.123",
|
||||
"description": "ONVIF Camera Plugin for Scrypted",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
@@ -40,14 +40,14 @@
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"base-64": "^1.0.0",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"http-auth-utils": "^4.0.0",
|
||||
"md5": "^2.3.0",
|
||||
"onvif": "^0.6.7",
|
||||
"xml2js": "^0.4.23"
|
||||
"onvif": "^0.6.8",
|
||||
"xml2js": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/md5": "^2.3.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/xml2js": "^0.4.9"
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/node": "^20.3.2",
|
||||
"@types/xml2js": "^0.4.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } fro
|
||||
import { connectCameraAPI, OnvifCameraAPI, OnvifEvent } from "./onvif-api";
|
||||
import { OnvifIntercom } from "./onvif-intercom";
|
||||
import { OnvifPTZMixinProvider } from "./onvif-ptz";
|
||||
import { listenEvents } from "./onvif-events";
|
||||
|
||||
const { mediaManager, systemManager, deviceManager } = sdk;
|
||||
|
||||
@@ -241,17 +242,7 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
|
||||
|
||||
|
||||
async listenEvents() {
|
||||
let motionTimeout: NodeJS.Timeout;
|
||||
let binaryTimeout: NodeJS.Timeout;
|
||||
|
||||
const client = await this.createClient();
|
||||
try {
|
||||
await client.supportsEvents();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
await client.createSubscription();
|
||||
|
||||
try {
|
||||
const eventTypes = await client.getEventTypes();
|
||||
if (eventTypes?.length && this.storage.getItem('onvifDetector') !== 'true') {
|
||||
@@ -261,61 +252,8 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
this.console.log('listening events');
|
||||
const events = client.listenEvents();
|
||||
events.on('event', (event, className) => {
|
||||
if (event === OnvifEvent.MotionBuggy) {
|
||||
this.motionDetected = true;
|
||||
clearTimeout(motionTimeout);
|
||||
motionTimeout = setTimeout(() => this.motionDetected = false, 30000);
|
||||
return;
|
||||
}
|
||||
if (event === OnvifEvent.BinaryRingEvent) {
|
||||
this.binaryState = true;
|
||||
clearTimeout(binaryTimeout);
|
||||
binaryTimeout = setTimeout(() => this.binaryState = false, 30000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === OnvifEvent.MotionStart)
|
||||
this.motionDetected = true;
|
||||
else if (event === OnvifEvent.MotionStop)
|
||||
this.motionDetected = false;
|
||||
else if (event === OnvifEvent.AudioStart)
|
||||
this.audioDetected = true;
|
||||
else if (event === OnvifEvent.AudioStop)
|
||||
this.audioDetected = false;
|
||||
else if (event === OnvifEvent.BinaryStart)
|
||||
this.binaryState = true;
|
||||
else if (event === OnvifEvent.BinaryStop)
|
||||
this.binaryState = false;
|
||||
else if (event === OnvifEvent.Detection) {
|
||||
const d: ObjectsDetected = {
|
||||
timestamp: Date.now(),
|
||||
detections: [
|
||||
{
|
||||
score: undefined,
|
||||
className,
|
||||
}
|
||||
]
|
||||
}
|
||||
this.onDeviceEvent(ScryptedInterface.ObjectDetector, d);
|
||||
}
|
||||
});
|
||||
|
||||
const ret: Destroyable = {
|
||||
destroy() {
|
||||
client.unsubscribe();
|
||||
},
|
||||
on(eventName: string | symbol, listener: (...args: any[]) => void) {
|
||||
return events.on(eventName, listener);
|
||||
},
|
||||
emit(eventName: string | symbol, ...args: any[]) {
|
||||
return events.emit(eventName, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
return ret;
|
||||
return listenEvents(this, client);
|
||||
}
|
||||
|
||||
createClient() {
|
||||
|
||||
@@ -284,6 +284,7 @@ export class OnvifCameraAPI {
|
||||
url: snapshotUri,
|
||||
responseType: 'arraybuffer',
|
||||
httpsAgent,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
return Buffer.from(response.data);
|
||||
|
||||
76
plugins/onvif/src/onvif-events.ts
Normal file
76
plugins/onvif/src/onvif-events.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { ObjectsDetected, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface } from "@scrypted/sdk";
|
||||
import { OnvifCameraAPI, OnvifEvent } from "./onvif-api";
|
||||
import { Destroyable } from "../../rtsp/src/rtsp";
|
||||
|
||||
export async function listenEvents(thisDevice: ScryptedDeviceBase, client: OnvifCameraAPI) {
|
||||
let motionTimeout: NodeJS.Timeout;
|
||||
let binaryTimeout: NodeJS.Timeout;
|
||||
|
||||
try {
|
||||
await client.supportsEvents();
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
await client.createSubscription();
|
||||
|
||||
thisDevice.console.log('listening events');
|
||||
const events = client.listenEvents();
|
||||
events.on('event', (event, className) => {
|
||||
if (event === OnvifEvent.MotionBuggy) {
|
||||
thisDevice.motionDetected = true;
|
||||
clearTimeout(motionTimeout);
|
||||
motionTimeout = setTimeout(() => thisDevice.motionDetected = false, 30000);
|
||||
return;
|
||||
}
|
||||
if (event === OnvifEvent.BinaryRingEvent) {
|
||||
thisDevice.binaryState = true;
|
||||
clearTimeout(binaryTimeout);
|
||||
binaryTimeout = setTimeout(() => thisDevice.binaryState = false, 30000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event === OnvifEvent.MotionStart)
|
||||
thisDevice.motionDetected = true;
|
||||
else if (event === OnvifEvent.MotionStop)
|
||||
thisDevice.motionDetected = false;
|
||||
else if (event === OnvifEvent.AudioStart)
|
||||
thisDevice.audioDetected = true;
|
||||
else if (event === OnvifEvent.AudioStop)
|
||||
thisDevice.audioDetected = false;
|
||||
else if (event === OnvifEvent.BinaryStart)
|
||||
thisDevice.binaryState = true;
|
||||
else if (event === OnvifEvent.BinaryStop)
|
||||
thisDevice.binaryState = false;
|
||||
else if (event === OnvifEvent.Detection) {
|
||||
const d: ObjectsDetected = {
|
||||
timestamp: Date.now(),
|
||||
detections: [
|
||||
{
|
||||
score: undefined,
|
||||
className,
|
||||
}
|
||||
]
|
||||
}
|
||||
thisDevice.onDeviceEvent(ScryptedInterface.ObjectDetector, d);
|
||||
}
|
||||
});
|
||||
|
||||
const ret: Destroyable = {
|
||||
destroy() {
|
||||
try {
|
||||
client.unsubscribe();
|
||||
}
|
||||
catch (e) {
|
||||
console.warn('Error unsubscribing', e);
|
||||
}
|
||||
},
|
||||
on(eventName: string | symbol, listener: (...args: any[]) => void) {
|
||||
return events.on(eventName, listener);
|
||||
},
|
||||
emit(eventName: string | symbol, ...args: any[]) {
|
||||
return events.emit(eventName, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
4
plugins/opencv/package-lock.json
generated
4
plugins/opencv/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.85",
|
||||
"version": "0.0.86",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/opencv",
|
||||
"version": "0.0.85",
|
||||
"version": "0.0.86",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.0.85"
|
||||
"version": "0.0.86"
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ class OpenCVPlugin(DetectPlugin):
|
||||
detection_result['detections'] = detections
|
||||
detection_result['inputDimensions'] = src_size
|
||||
|
||||
if session.previous_frame is None:
|
||||
if session.previous_frame is None or session.previous_frame.shape != session.curFrame.shape:
|
||||
session.previous_frame = session.curFrame
|
||||
session.curFrame = None
|
||||
return detection_result
|
||||
|
||||
8
plugins/openvino/.vscode/settings.json
vendored
8
plugins/openvino/.vscode/settings.json
vendored
@@ -1,16 +1,16 @@
|
||||
|
||||
{
|
||||
// docker installation
|
||||
"scrypted.debugHost": "koushik-ubuntu",
|
||||
"scrypted.serverRoot": "/server",
|
||||
// "scrypted.debugHost": "koushik-ubuntu",
|
||||
// "scrypted.serverRoot": "/server",
|
||||
|
||||
// pi local installation
|
||||
// "scrypted.debugHost": "192.168.2.119",
|
||||
// "scrypted.serverRoot": "/home/pi/.scrypted",
|
||||
|
||||
// local checkout
|
||||
// "scrypted.debugHost": "127.0.0.1",
|
||||
// "scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.serverRoot": "/Users/koush/.scrypted",
|
||||
// "scrypted.debugHost": "koushik-windows",
|
||||
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
|
||||
|
||||
|
||||
4
plugins/openvino/package-lock.json
generated
4
plugins/openvino/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.22",
|
||||
"version": "0.1.34",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/openvino",
|
||||
"version": "0.1.22",
|
||||
"version": "0.1.34",
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk"
|
||||
},
|
||||
"version": "0.1.22"
|
||||
"version": "0.1.34"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ from scrypted_sdk.types import Setting
|
||||
|
||||
from predict import PredictPlugin, Prediction, Rectangle
|
||||
import numpy as np
|
||||
|
||||
import yolo
|
||||
|
||||
def parse_label_contents(contents: str):
|
||||
lines = contents.splitlines()
|
||||
@@ -27,81 +27,211 @@ def parse_label_contents(contents: str):
|
||||
ret[row_number] = content.strip()
|
||||
return ret
|
||||
|
||||
def param_to_string(parameters) -> str:
|
||||
"""Convert a list / tuple of parameters returned from IE to a string."""
|
||||
if isinstance(parameters, (list, tuple)):
|
||||
return ', '.join([str(x) for x in parameters])
|
||||
else:
|
||||
return str(parameters)
|
||||
|
||||
def dump_device_properties(core):
|
||||
print('Available devices:')
|
||||
for device in core.available_devices:
|
||||
print(f'{device} :')
|
||||
print('\tSUPPORTED_PROPERTIES:')
|
||||
for property_key in core.get_property(device, 'SUPPORTED_PROPERTIES'):
|
||||
if property_key not in ('SUPPORTED_METRICS', 'SUPPORTED_CONFIG_KEYS', 'SUPPORTED_PROPERTIES'):
|
||||
try:
|
||||
property_val = core.get_property(device, property_key)
|
||||
except TypeError:
|
||||
property_val = 'UNSUPPORTED TYPE'
|
||||
print(f'\t\t{property_key}: {param_to_string(property_val)}')
|
||||
print('')
|
||||
|
||||
class OpenVINOPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings):
|
||||
def __init__(self, nativeId: str | None = None):
|
||||
super().__init__(nativeId=nativeId)
|
||||
|
||||
self.core = ov.Core()
|
||||
dump_device_properties(self.core)
|
||||
available_devices = self.core.available_devices
|
||||
print('available devices: %s' % available_devices)
|
||||
|
||||
xmlFile = self.downloadFile('https://raw.githubusercontent.com/koush/openvino-models/main/ssd_mobilenet_v1_coco/FP16/ssd_mobilenet_v1_coco.xml', 'ssd_mobilenet_v1_coco.xml')
|
||||
mappingFile = self.downloadFile('https://raw.githubusercontent.com/koush/openvino-models/main/ssd_mobilenet_v1_coco/FP16/ssd_mobilenet_v1_coco.mapping', 'ssd_mobilenet_v1_coco.mapping')
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/openvino-models/main/ssd_mobilenet_v1_coco/FP16/ssd_mobilenet_v1_coco.bin', 'ssd_mobilenet_v1_coco.bin')
|
||||
mode = self.storage.getItem('mode')
|
||||
if mode == 'Default':
|
||||
mode = 'AUTO'
|
||||
mode = mode or 'AUTO'
|
||||
|
||||
precision = self.storage.getItem('precision') or 'Default'
|
||||
if precision == 'Default':
|
||||
using_mode = mode
|
||||
if using_mode == 'AUTO':
|
||||
if 'GPU' in available_devices:
|
||||
using_mode = 'GPU'
|
||||
if using_mode == 'GPU':
|
||||
precision = 'FP16'
|
||||
else:
|
||||
precision = 'FP32'
|
||||
|
||||
|
||||
model = self.storage.getItem('model') or 'Default'
|
||||
if model == 'Default':
|
||||
model = 'ssd_mobilenet_v1_coco'
|
||||
self.yolo = 'yolo' in model
|
||||
self.yolov8 = "yolov8" in model
|
||||
self.sigmoid = model == 'yolo-v4-tiny-tf'
|
||||
|
||||
print(f'model/mode/precision: {model}/{mode}/{precision}')
|
||||
|
||||
if self.yolov8:
|
||||
self.model_dim = 640
|
||||
elif self.yolo:
|
||||
self.model_dim = 416
|
||||
else:
|
||||
self.model_dim = 300
|
||||
|
||||
model_version = 'v4'
|
||||
xmlFile = self.downloadFile(f'https://raw.githubusercontent.com/koush/openvino-models/main/{model}/{precision}/{model}.xml', f'{model_version}/{precision}/{model}.xml')
|
||||
binFile = self.downloadFile(f'https://raw.githubusercontent.com/koush/openvino-models/main/{model}/{precision}/{model}.bin', f'{model_version}/{precision}/{model}.bin')
|
||||
if self.yolo:
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/openvino-models/main/coco_80cl.txt', 'coco_80cl.txt')
|
||||
else:
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/openvino-models/main/coco_labels.txt', 'coco_labels.txt')
|
||||
|
||||
print(xmlFile, binFile, labelsFile)
|
||||
|
||||
mode = self.storage.getItem('mode') or 'AUTO'
|
||||
try:
|
||||
self.compiled_model = self.core.compile_model(xmlFile, mode)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("Reverting to AUTO mode.")
|
||||
print("Reverting all settings.")
|
||||
self.storage.removeItem('mode')
|
||||
asyncio.run_coroutine_threadsafe(scrypted_sdk.deviceManager.requestRestart(), asyncio.get_event_loop())
|
||||
self.storage.removeItem('model')
|
||||
self.storage.removeItem('precision')
|
||||
self.requestRestart()
|
||||
|
||||
labelsFile = self.downloadFile('https://raw.githubusercontent.com/google-coral/test_data/master/coco_labels.txt', 'coco_labels.txt')
|
||||
labels_contents = open(labelsFile, 'r').read()
|
||||
self.labels = parse_label_contents(labels_contents)
|
||||
|
||||
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix="openvino", )
|
||||
|
||||
async def getSettings(self) -> list[Setting]:
|
||||
mode = self.storage.getItem('mode') or 'AUTO'
|
||||
mode = self.storage.getItem('mode') or 'Default'
|
||||
model = self.storage.getItem('model') or 'Default'
|
||||
precision = self.storage.getItem('precision') or 'Default'
|
||||
return [
|
||||
{
|
||||
'key': 'model',
|
||||
'title': 'Model',
|
||||
'description': 'The detection model used to find objects.',
|
||||
'choices': [
|
||||
'Default',
|
||||
'ssd_mobilenet_v1_coco',
|
||||
'ssdlite_mobilenet_v2',
|
||||
'yolo-v3-tiny-tf',
|
||||
'yolo-v4-tiny-tf',
|
||||
'yolov8n',
|
||||
],
|
||||
'value': model,
|
||||
},
|
||||
{
|
||||
'key': 'mode',
|
||||
'title': 'Mode',
|
||||
'description': 'AUTO, CPU, or GPU mode to use for detections. Requires plugin reload. Use CPU if the system has unreliable GPU drivers.',
|
||||
'choices': [
|
||||
'Default',
|
||||
'AUTO',
|
||||
'CPU',
|
||||
'GPU',
|
||||
],
|
||||
'value': mode,
|
||||
},
|
||||
{
|
||||
'key': 'precision',
|
||||
'title': 'Precision',
|
||||
'description': 'The model floating point precision. FP16 is recommended for GPU. FP32 is recommended for CPU.',
|
||||
'choices': [
|
||||
'Default',
|
||||
'FP16',
|
||||
'FP32',
|
||||
],
|
||||
'value': precision,
|
||||
}
|
||||
]
|
||||
|
||||
async def putSetting(self, key: str, value: SettingValue):
|
||||
self.storage.setItem(key, value)
|
||||
await self.onDeviceEvent(scrypted_sdk.ScryptedInterface.Settings.value, None)
|
||||
await scrypted_sdk.deviceManager.requestRestart()
|
||||
self.requestRestart()
|
||||
|
||||
# width, height, channels
|
||||
def get_input_details(self) -> Tuple[int, int, int]:
|
||||
return [300, 300, 3]
|
||||
return [self.model_dim, self.model_dim, 3]
|
||||
|
||||
def get_input_size(self) -> Tuple[int, int]:
|
||||
return [300, 300]
|
||||
return [self.model_dim, self.model_dim]
|
||||
|
||||
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
|
||||
def predict():
|
||||
infer_request = self.compiled_model.create_infer_request()
|
||||
input_tensor = ov.Tensor(array=np.expand_dims(np.array(input), axis=0), shared_memory=True)
|
||||
if self.yolov8:
|
||||
im = np.stack([input])
|
||||
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
|
||||
im = im.astype(np.float32) / 255.0
|
||||
im = np.ascontiguousarray(im) # contiguous
|
||||
im = ov.Tensor(array=im, shared_memory=True)
|
||||
input_tensor = im
|
||||
elif self.yolo:
|
||||
input_tensor = ov.Tensor(array=np.expand_dims(np.array(input), axis=0).astype(np.float32), shared_memory=True)
|
||||
else:
|
||||
input_tensor = ov.Tensor(array=np.expand_dims(np.array(input), axis=0), shared_memory=True)
|
||||
# Set input tensor for model with one input
|
||||
infer_request.set_input_tensor(input_tensor)
|
||||
infer_request.start_async()
|
||||
infer_request.wait()
|
||||
output = infer_request.get_output_tensor()
|
||||
|
||||
objs = []
|
||||
|
||||
if self.yolov8:
|
||||
objs = yolo.parse_yolov8(infer_request.outputs[0].data[0])
|
||||
return objs
|
||||
|
||||
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].astype(float):
|
||||
valid, index, confidence, l, t, r, b = values
|
||||
if valid == -1:
|
||||
break
|
||||
|
||||
def torelative(value: float):
|
||||
return value * 300
|
||||
return value * self.model_dim
|
||||
|
||||
l = torelative(l)
|
||||
t = torelative(t)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
openvino==2022.3.0
|
||||
openvino==2023.0.1
|
||||
|
||||
# pillow for anything not intel linux, pillow-simd is available on x64 linux
|
||||
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'
|
||||
|
||||
159
plugins/openvino/src/yolo/__init__.py
Normal file
159
plugins/openvino/src/yolo/__init__.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import sys
|
||||
from math import exp
|
||||
import numpy as np
|
||||
|
||||
from predict import Prediction, Rectangle
|
||||
|
||||
def parse_yolov8(results, scale = 1):
|
||||
objs = []
|
||||
keep = np.argwhere(results[4:] > .2)
|
||||
for indices in keep:
|
||||
class_id = indices[0]
|
||||
index = indices[1]
|
||||
confidence = results[class_id + 4, index]
|
||||
x = results[0][index].astype(float) * scale
|
||||
y = results[1][index].astype(float) * scale
|
||||
w = results[2][index].astype(float) * scale
|
||||
h = results[3][index].astype(float) * scale
|
||||
obj = Prediction(
|
||||
int(class_id),
|
||||
confidence.astype(float),
|
||||
Rectangle(
|
||||
x - w / 2,
|
||||
y - h / 2,
|
||||
x + w / 2,
|
||||
y + h / 2,
|
||||
),
|
||||
)
|
||||
objs.append(obj)
|
||||
|
||||
return objs
|
||||
|
||||
def sig(x):
|
||||
return 1/(1 + np.exp(-x))
|
||||
|
||||
def intersection_over_union(box_1, box_2):
|
||||
width_of_overlap_area = min(box_1['xmax'], box_2['xmax']) - max(box_1['xmin'], box_2['xmin'])
|
||||
height_of_overlap_area = min(box_1['ymax'], box_2['ymax']) - max(box_1['ymin'], box_2['ymin'])
|
||||
if width_of_overlap_area < 0 or height_of_overlap_area < 0:
|
||||
area_of_overlap = 0
|
||||
else:
|
||||
area_of_overlap = width_of_overlap_area * height_of_overlap_area
|
||||
box_1_area = (box_1['ymax'] - box_1['ymin']) * (box_1['xmax'] - box_1['xmin'])
|
||||
box_2_area = (box_2['ymax'] - box_2['ymin']) * (box_2['xmax'] - box_2['xmin'])
|
||||
area_of_union = box_1_area + box_2_area - area_of_overlap
|
||||
if area_of_union == 0:
|
||||
return 0
|
||||
return area_of_overlap / area_of_union
|
||||
|
||||
def scale_bbox(x, y, h, w, class_id, confidence, h_scale, w_scale):
|
||||
"""scale = np.array([min(w_scale/h_scale, 1), min(h_scale/w_scale, 1)])
|
||||
offset = 0.5*(np.ones(2) - scale)
|
||||
x, y = (np.array([x, y]) - offset) / scale
|
||||
width, height = np.array([w, h]) / scale"""
|
||||
#print(f"x{x}, y{y}, w{w}, h{h}")
|
||||
xmin = int((x - w / 2) * w_scale)
|
||||
ymin = int((y - h / 2) * h_scale)
|
||||
xmax = int(xmin + w * w_scale)
|
||||
ymax = int(ymin + h * h_scale)
|
||||
|
||||
print(f"x{xmin}, y{ymin}, xm{xmax}, ym{ymax}")
|
||||
return dict(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, class_id=class_id, confidence=confidence)
|
||||
|
||||
|
||||
def parse_yolo_region(blob, original_im_shape, anchors, sigmoid = True):
|
||||
# ------------------------------------------ Validating output parameters ------------------------------------------
|
||||
_, c1, c2, c3 = blob.shape # [26, 26] and [13, 13]
|
||||
if c1 == 255:
|
||||
out_blob_h, out_blob_w = c2, c3
|
||||
i_oth = 1
|
||||
i_r = 2
|
||||
i_c = 3
|
||||
else:
|
||||
i_oth = 3
|
||||
i_r = 1
|
||||
i_c = 2
|
||||
out_blob_h, out_blob_w = c1, c2
|
||||
|
||||
assert out_blob_w == out_blob_h, "Invalid size of output blob. It sould be in NCHW layout and height should " \
|
||||
"be equal to width. Current height = {}, current width = {}" \
|
||||
"".format(out_blob_h, out_blob_w)
|
||||
|
||||
# ------------------------------------------ Extracting layer parameters -------------------------------------------
|
||||
#print(f"predictions shape{blob.shape}")
|
||||
orig_im_h, orig_im_w = original_im_shape # 416
|
||||
objects = list()
|
||||
|
||||
cell_w = orig_im_w / out_blob_w
|
||||
cell_h = orig_im_h / out_blob_h
|
||||
|
||||
for oth in range(0, blob.shape[i_oth], 85): # 255
|
||||
for row in range(blob.shape[i_r]): # 13
|
||||
for col in range(blob.shape[i_c]): # 13
|
||||
#print(f"l {l}")
|
||||
if i_oth == 3:
|
||||
info_per_anchor = blob[0, row, col, oth:oth+85] #print("prob"+str(prob))
|
||||
else:
|
||||
info_per_anchor = blob[0, oth:oth+85, row, col] #print("prob"+str(prob))
|
||||
|
||||
confidences = info_per_anchor[5:]
|
||||
if sigmoid:
|
||||
confidences = [sig(raw) for raw in confidences]
|
||||
class_id = np.argmax(confidences)
|
||||
|
||||
rel_cell_x, rel_cell_y, width, height, box_confidence = info_per_anchor[:5]
|
||||
if sigmoid:
|
||||
box_confidence = sig(box_confidence)
|
||||
if box_confidence < .2:
|
||||
continue
|
||||
|
||||
confidence = confidences[class_id]
|
||||
if confidence < .2:
|
||||
continue
|
||||
|
||||
if sigmoid:
|
||||
rel_cell_x = sig(rel_cell_x)
|
||||
rel_cell_y = sig(rel_cell_y)
|
||||
|
||||
x = (col + rel_cell_x) * cell_w
|
||||
y = (row + rel_cell_y) * cell_h
|
||||
|
||||
n = int(oth/85)
|
||||
|
||||
try:
|
||||
width = exp(width)
|
||||
height = exp(height)
|
||||
except OverflowError:
|
||||
continue
|
||||
|
||||
width = width * anchors[2 * n]
|
||||
height = height * anchors[2 * n + 1]
|
||||
|
||||
xmin = x - width / 2
|
||||
xmax = x + width / 2
|
||||
ymin = y - height / 2
|
||||
ymax = y + height /2
|
||||
objects.append(
|
||||
{
|
||||
'xmin': xmin.astype(float),
|
||||
'xmax': xmax.astype(float),
|
||||
'ymin': ymin.astype(float),
|
||||
'ymax': ymax.astype(float),
|
||||
'confidence': confidence.astype(float),
|
||||
'classId': class_id.astype(float),
|
||||
}
|
||||
)
|
||||
|
||||
# Filtering overlapping boxes with respect to the --iou_threshold CLI parameter
|
||||
objects = sorted(objects, key=lambda obj : obj['confidence'], reverse=True)
|
||||
for i in range(len(objects)):
|
||||
if objects[i]['confidence'] == 0:
|
||||
continue
|
||||
for j in range(i + 1, len(objects)):
|
||||
if objects[i]['classId'] != objects[j]['classId']:
|
||||
continue
|
||||
if intersection_over_union(objects[i], objects[j]) > .2:
|
||||
objects[j]['confidence'] = 0
|
||||
|
||||
objects = list(filter(lambda o: o['confidence'] > 0, objects))
|
||||
return objects
|
||||
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.9.92",
|
||||
"version": "0.9.97",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/prebuffer-mixin",
|
||||
"version": "0.9.92",
|
||||
"version": "0.9.97",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user