Compare commits

..

125 Commits

Author SHA1 Message Date
Koushik Dutta
711eb222ed postbeta 2023-07-26 17:35:07 -07:00
Koushik Dutta
19f8bfb74a unifi-protect: older controller fix for doorbells 2023-07-24 15:07:10 -07:00
Koushik Dutta
08a8428d6e openvino: update dep 2023-07-23 18:50:21 -07:00
Koushik Dutta
4feeeda904 openvino: restart if detection times out 2023-07-23 18:48:07 -07:00
Koushik Dutta
753373a691 server: validate local address 2023-07-22 18:54:23 -07:00
Koushik Dutta
2f3529b822 rebroadcast: simplify prebuffer sync frame search, remove dead code... 2023-07-21 18:16:13 -07:00
Koushik Dutta
2501d1460b sdk: fix frame generator signature 2023-07-21 18:12:51 -07:00
Koushik Dutta
e063637100 rebroadcast: Fix prebuffer sync frame search lol 2023-07-21 18:12:37 -07:00
Koushik Dutta
5ec0bf4bf3 Merge branch 'main' of github.com:koush/scrypted 2023-07-20 19:53:47 -07:00
Koushik Dutta
0c05b59121 tensorflow-lite: temp hack to fix wide angle detection 2023-07-20 19:53:43 -07:00
Koushik Dutta
cbbfa0b525 homekit: readme 2023-07-20 13:25:24 -07:00
Koushik Dutta
28835b1ccc unifi-protect: use new isDoorbell flag 2023-07-18 12:24:54 -07:00
Koushik Dutta
0585e7bbaf unifi: log characteristics 2023-07-18 11:42:53 -07:00
Koushik Dutta
b2040ea2c8 videoanalysis: fix motion suspend/timeout/resume interaction 2023-07-18 10:05:36 -07:00
Koushik Dutta
2fd2151b4f Merge branch 'main' of github.com:koush/scrypted 2023-07-18 09:30:19 -07:00
Koushik Dutta
4c7974519d homekit/webrtc: fix broken stapa handling on unifi 2023-07-18 09:30:15 -07:00
Brett Jia
d91c919558 arlo: upstreaming changes for versions 0.8.5 - 0.8.11 (#956)
* cancel motion and audio events after 60s

* retry on imap errors

* bump 0.8.5 for beta

* better detection of sse shutdown to avoid thrashing

* restart plugin on unrecoverable login error

* bump 0.8.6 for beta

* more error handling + bump curl-cffi

* bump 0.8.7 for release

* delay motion and audio event end triggers by 10s

* transfer sip ffmpeg params to stream signaling code

* bump 0.8.8 for beta

* allow customizing imap sender address

* bump 0.8.9 for beta

* bump 0.8.10 for release

* docs, imap backoff, use bs4 to parse 2fa email

* bump 0.8.11 for release
2023-07-16 14:46:48 -07:00
Koushik Dutta
7a297761bc homekit: fix mdns names? 2023-07-15 09:35:32 -07:00
Koushik Dutta
c15e10e5cf rtp: disable jitter buffer spam 2023-07-15 09:12:01 -07:00
Koushik Dutta
3494106857 python-client: fix message queues 2023-07-15 08:52:24 -07:00
Koushik Dutta
7d3dfb16f0 predict: rev model downloads for label normalization 2023-07-14 12:13:18 -07:00
Koushik Dutta
63fc223036 docker: move deadsnakes ppa to docker only. tf no longer supported local install. 2023-07-12 21:57:26 -07:00
Koushik Dutta
6736379858 ring: support reload login between auth and code 2023-07-11 19:02:43 -07:00
Koushik Dutta
7a811b2b22 ring: publish auth fixes 2023-07-11 16:59:54 -07:00
Koushik Dutta
dd5cb432c9 google-device-access: hack comment 2023-07-11 14:49:47 -07:00
Koushik Dutta
ab3a71ab49 Merge branch 'main' of github.com:koush/scrypted 2023-07-11 14:46:13 -07:00
Koushik Dutta
b5c9382180 google-device-access: fix webrtc negotiation via hack 2023-07-11 14:46:07 -07:00
Koushik Dutta
81682678ac Update README.md 2023-07-11 11:18:44 -07:00
Koushik Dutta
dec184629e Update README.md (#946)
* Update README.md

* Update README.md
2023-07-11 11:15:35 -07:00
dignabbit
f33bb53138 docker: improve management of avahi (#940)
Co-authored-by: Dignabbit <test@example.com>
2023-07-11 11:15:13 -07:00
Koushik Dutta
2d3957e086 Update README.md 2023-07-10 23:57:49 -07:00
Koushik Dutta
d16ed9e54f Update README.md 2023-07-10 23:57:01 -07:00
Koushik Dutta
d7e8052498 ring: remove push receiver token shim 2023-07-10 11:27:02 -07:00
Koushik Dutta
48cd3830a5 webrtc: pass through single packet stapa 2023-07-10 07:59:05 -07:00
Koushik Dutta
ce138d1a17 videoanalysis: log when object detection is zone filtered. 2023-07-08 11:29:15 -07:00
Koushik Dutta
7b4919fba9 Merge branch 'main' of github.com:koush/scrypted 2023-07-08 09:17:07 -07:00
Koushik Dutta
0b3dee3a03 ring: support custom controlCenterDisplayName 2023-07-08 09:16:54 -07:00
Raman Gupta
4cef09540b sdk: fix typing (#938) 2023-07-08 09:11:55 -07:00
Koushik Dutta
92583e568a ring: fix erroneous polling 2023-07-07 14:39:36 -07:00
Koushik Dutta
67aaa08c31 Merge branch 'main' of github.com:koush/scrypted 2023-07-06 08:12:59 -07:00
Koushik Dutta
2e9f618f6f snapshot: fix default behavior when snapshot url is provided on cameras without a Camera interface 2023-07-06 08:12:54 -07:00
Raman Gupta
bf4d39d6af sdk: Improve python generation (#931)
* Improve python generation

* tweak

* tweak

* Move classes to other
2023-07-06 08:04:19 -07:00
Raman Gupta
c31e68f720 Update connect_scrypted_client (#932) 2023-07-05 17:38:43 -07:00
Koushik Dutta
6d8b3c1ce7 ring: publish with more auth fixes 2023-07-05 12:47:05 -07:00
Koushik Dutta
106fef95b4 webrtc: notify track startup failure 2023-07-04 23:52:19 -07:00
Koushik Dutta
488d68ee1c python-client: initial implementation 2023-07-04 14:02:51 -07:00
Koushik Dutta
f7e35fb1ee Merge branch 'main' of github.com:koush/scrypted 2023-07-04 09:07:09 -07:00
Koushik Dutta
b1bf897bdb ring: notification fixes 2023-07-04 09:07:05 -07:00
Brett Jia
8eb533c220 arlo: update to 0.8.4 beta (#923)
* basestation debugging output

* faster 2 way startup

* fix type annotation

* separate thread for logging server + bump scrypted-arlo-go

* update backup auth hosts

* bump 0.8.1 for release

* further optimize 2 way startup latency

* bump 0.8.2 for release

* skip pings on battery doorbell

* bump 0.8.3 for beta

* more docs

* try fix cloudflare 403 with curl-cffi

* bump 0.8.4 for beta
2023-07-02 17:14:29 -07:00
Koushik Dutta
f10cdfbced opencv: handle frame size changes 2023-07-02 14:08:00 -07:00
Koushik Dutta
8f5e9e5a8c rebroadcast: keep trying to restart rtsp server 2023-07-02 12:53:19 -07:00
Koushik Dutta
cc0283ef39 videoanalysis: add pipeline hang logging 2023-07-02 08:47:10 -07:00
Koushik Dutta
5c7b67c973 videoanalysis: restart motion detection on stopped streams 2023-07-02 08:38:43 -07:00
Koushik Dutta
d1be0f1b4c Merge branch 'main' of github.com:koush/scrypted 2023-06-30 19:11:41 -07:00
Koushik Dutta
55d58d1e44 reolink: use new client per event listener 2023-06-30 19:11:37 -07:00
Koushik Dutta
d9dccf36a3 mail: readme 2023-06-30 17:58:23 -07:00
Koushik Dutta
33477fdf80 reolink/onvif: fix listener destroy throw error 2023-06-30 11:49:20 -07:00
Koushik Dutta
e6ece3aa3e videoanalysis: add anayze mode hint 2023-06-30 11:42:40 -07:00
Koushik Dutta
6a4126191b videoanalysis: settings tweaks 2023-06-30 11:07:06 -07:00
Koushik Dutta
e9f999b911 docker: simplify nvr storage instructions 2023-06-30 10:29:56 -07:00
Koushik Dutta
1fef31a081 docker: fix reversed logic 2023-06-29 21:14:52 -07:00
Koushik Dutta
659f99c33d docker: fix install on linux when /dev/dri is missing 2023-06-29 21:13:47 -07:00
Koushik Dutta
a9deff0046 webrtc: allow mac/ios types 2023-06-29 19:38:23 -07:00
Koushik Dutta
7a56cefe2a reolink: add support for reolink doorbells, deprecating onvif plugin usage 2023-06-29 09:44:40 -07:00
Koushik Dutta
a06c6e9568 webrtc: fix erroneous window laptop transcode. fix spurious NAL delimiter logging. 2023-06-28 20:33:24 -07:00
Koushik Dutta
56f127a203 webrtc: stapa/sei fix. stream start failure fix/logging. 2023-06-28 11:24:26 -07:00
Koushik Dutta
2ffe67b2db videoanalysis: fix cpu calc 2023-06-27 23:23:02 -07:00
Koushik Dutta
44dc648398 videoanalysis: uncap detection duration. disable snapshot fallback. use a max concurrent detection calcuation 2023-06-27 23:09:41 -07:00
Koushik Dutta
7807cc4bc6 Merge branch 'main' of github.com:koush/scrypted 2023-06-27 20:00:38 -07:00
Koushik Dutta
81fb690089 reolink: docs 2023-06-27 20:00:32 -07:00
Brett Jia
8b15617f6e arlo: various enhancements + upstreaming changes (#913)
* reorder models and add VMC4060P

* add VMC4060P

* use new UA for cloudscraper + bump scrypted-arlo-go

* bump 0.7.30 for release

* improve readme

* tcp logger server to collect individual camera output + add arlo baby to hw lists

* send exception guard logs to device logger

* bump scrypted-arlo-go with new logging interface

* log device-specific errors returned from arlo

* bump 0.7.31 for beta

* more error listeners and some comments

* experimental arlo baby fix

* bump 0.7.32 for beta

* arlo baby nightlight

* bump 0.7.33 for beta

* nightlight device name fix

* bump 0.7.34 for beta

* fix nightlight constructor

* bump 0.7.35 for beta

* bump 0.7.36 for release

* functional sip webrtc 2way

* refactored 2way code + various tweaks throughout

* document sip v2 endpoint

* update backup auth host

* bump 0.7.37 for release

* add media user agent

* sip refactoring bugfixes

* bump 0.8.0 for release
2023-06-26 20:01:53 -07:00
Koushik Dutta
fd8aa70352 rebroadcast: improve prebuffer session logging 2023-06-25 18:13:20 -07:00
Koushik Dutta
be888d215d alexa: fix doorbells 2023-06-25 10:28:46 -07:00
Koushik Dutta
ce5f568a5d server: fix non admin cli login. 2023-06-24 10:49:58 -07:00
Koushik Dutta
336220559f videoanalysis: fix potential leak 2023-06-22 23:32:15 -07:00
Koushik Dutta
8014060a54 rebroadcast :publish 2023-06-22 17:59:15 -07:00
Brett Jia
7f4c8997b9 snapshot: tell ffmpeg pipe input format (#902)
* snapshot: tell ffmpeg pipe input format

* use image2pipe
2023-06-21 16:09:42 -07:00
Koushik Dutta
9f73b92dbd Merge branch 'main' of github.com:koush/scrypted 2023-06-20 20:32:31 -07:00
Koushik Dutta
381892fca6 webrtc: fix dtls cookie race condition 2023-06-20 20:32:26 -07:00
Koushik Dutta
a28df23032 snapshot: add request timeout 2023-06-18 11:14:55 -07:00
Koushik Dutta
dc5456d36f tensorflow-lite: fix yolov8 uint8 to int8 color conversion 2023-06-17 23:50:46 -07:00
Koushik Dutta
3a23e8ed26 coreml: fix mobilenet url 2023-06-17 23:17:56 -07:00
Koushik Dutta
e0db86cb41 cameras: timeout snapshots to free socket 2023-06-17 23:09:23 -07:00
Koushik Dutta
37ccefebd1 tensorflow-lite: readme 2023-06-17 12:17:05 -07:00
Koushik Dutta
0076c4827f tensorflow-lite: yolov8 is not compatible with usb edgetpu 2023-06-17 12:14:49 -07:00
Koushik Dutta
c5c07d8169 tensorflow-lite: fall back to mobilenet if edgepu startup fails 2023-06-16 17:18:46 -07:00
Koushik Dutta
2372acc796 rebroadcast: cleanup sdp rejection 2023-06-16 15:38:48 -07:00
Brett Jia
6b9c3e4aa0 rebroadcast: recover after ffmpeg exits before printing sdp (#890)
* rebroadcast: recover after ffmpeg exits before printing sdp

* Revert "rebroadcast: recover after ffmpeg exits before printing sdp"

This reverts commit aee2124937.

* reject sdp promise on ffmpeg exit
2023-06-16 15:33:47 -07:00
Koushik Dutta
d5b652da8c ring: save push credentials, polling now disabled by default. publish beta 2023-06-16 12:08:22 -07:00
Koushik Dutta
2b9a0f082d predict: refactor, add support for yolov8 on tflite 2023-06-16 12:08:04 -07:00
Koushik Dutta
b10b4d047e openvino: fix labels 2023-06-15 14:09:41 -07:00
Koushik Dutta
74cd23bd88 openvino: functional yolov8 2023-06-15 11:46:37 -07:00
Koushik Dutta
ef742bdb23 coreml/openvino: yolov8 support 2023-06-15 00:54:53 -07:00
Koushik Dutta
6f7fa54f24 coreml: yolov8 default on apple silicon 2023-06-14 20:56:45 -07:00
Koushik Dutta
d9a575cb5a coreml: add yolov8 2023-06-14 18:43:29 -07:00
Koushik Dutta
29094afa4d server: fix typo 2023-06-14 12:35:17 -07:00
Koushik Dutta
62a92fe083 coreml/openvino: improve yolov4, add yolov3 to openvino 2023-06-12 21:39:33 -07:00
Koushik Dutta
9b8bde556c coreml: add yolov4-tiny model 2023-06-12 17:59:05 -07:00
Koushik Dutta
326ef11760 openvino: cleanup 2023-06-12 17:45:38 -07:00
Koushik Dutta
92a0b4a863 client: update sdk 2023-06-12 13:01:50 -07:00
Koushik Dutta
9fd3641455 openvino: support more models 2023-06-12 13:01:42 -07:00
Koushik Dutta
2918cf9ae1 core: ui fixes 2023-06-12 09:40:26 -07:00
Koushik Dutta
6f004db859 openvino: test other models 2023-06-11 23:29:37 -07:00
Koushik Dutta
367d741c5f openvino: fix models to accept rgb instead of bgr 2023-06-11 23:06:00 -07:00
Koushik Dutta
8f83894e49 python-codecs: implement hang protection 2023-06-11 14:24:53 -07:00
Koushik Dutta
ea6e33d159 videoanalysis: prevent snapshot throttling when its only a single camera 2023-06-11 09:13:05 -07:00
Koushik Dutta
1b5565b5b2 openvino: choose better defaults for precision 2023-06-11 08:48:55 -07:00
Koushik Dutta
19692d02c6 docker: update s6 2023-06-10 23:50:44 -07:00
Koushik Dutta
4179698c12 gh: switch back to gh builders 2023-06-10 20:38:50 -07:00
Koushik Dutta
1eea3a87d0 gh: switch back to gh builders 2023-06-10 20:38:07 -07:00
Koushik Dutta
ec89a77955 gh: switch back to gh builders 2023-06-10 20:33:30 -07:00
Koushik Dutta
443158286e gh: remove pi builder 2023-06-10 20:30:54 -07:00
Koushik Dutta
b168ca52c6 gh: remove pi builder 2023-06-10 20:27:46 -07:00
Koushik Dutta
fe01d3a1ba gh: remove pi builder 2023-06-10 20:26:12 -07:00
Koushik Dutta
18cad22627 ha: publish 2023-06-10 19:31:10 -07:00
Koushik Dutta
c67c9a028c ha: publish 2023-06-10 19:30:23 -07:00
Koushik Dutta
0cff8ad5ed docker: fix ffmpeg path 2023-06-10 18:59:11 -07:00
Koushik Dutta
0269959cf3 docker: use apt ffmpeg 2023-06-10 17:22:16 -07:00
Koushik Dutta
1b6de42eca ha: update 2023-06-10 16:49:25 -07:00
Koushik Dutta
39342d5d46 docker: Fix arch detection on pi builders 2023-06-10 16:28:58 -07:00
Koushik Dutta
c4b5af46d0 docker: Fix arch detection on pi builders 2023-06-10 16:27:30 -07:00
Koushik Dutta
a46235d095 python-codecs: publish 2023-06-10 15:25:56 -07:00
Koushik Dutta
848d490a66 Merge branch 'main' of github.com:koush/scrypted 2023-06-10 15:11:12 -07:00
Koushik Dutta
87fbb95157 postrelease 2023-06-10 15:10:25 -07:00
Koushik Dutta
c036da9ae0 Update test.yml 2023-06-09 16:27:09 -07:00
160 changed files with 4091 additions and 2357 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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
View File

@@ -1,2 +1,4 @@
.DS_Store
__pycache__
venv
.venv

View File

@@ -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.

View File

@@ -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;
},

View File

@@ -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";

View File

@@ -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] = {

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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 \

View File

@@ -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"

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
################################################################

View File

@@ -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",

View File

@@ -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
View File

@@ -0,0 +1 @@
.venv

View 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
}
]
}

View File

@@ -0,0 +1 @@
../../server/python/plugin_remote.py

View File

@@ -0,0 +1,3 @@
python-engineio[asyncio_client]
aiohttp
aiodns

View File

@@ -0,0 +1 @@
../../server/python/rpc.py

View File

@@ -0,0 +1 @@
../../server/python/rpc_reader.py

View File

@@ -0,0 +1 @@
../../sdk/types/scrypted_python

View 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()

View File

@@ -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"

View File

@@ -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",

View File

@@ -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
};
},

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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);

View File

@@ -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.

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -1 +1,3 @@
EXPERIMENTAL = False
import os
EXPERIMENTAL = os.environ.get("SCRYPTED_ARLO_EXPERIMENTAL", "0") not in ["", "0"]

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>

View File

@@ -40,7 +40,7 @@
<v-btn v-on="on" small>
<v-icon x-small>fa fa-calendar-alt</v-icon>
&nbsp;
{{ 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>

View File

@@ -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"
}

View File

@@ -40,5 +40,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.15"
"version": "0.1.21"
}

View File

@@ -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)

View File

@@ -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
View File

@@ -0,0 +1 @@
../../openvino/src/yolo

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "127.0.0.1",
"scrypted.debugHost": "koushik-ubuntu",
}

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -117,6 +117,7 @@ export class HikvisionCameraAPI {
method: "GET",
responseType: 'arraybuffer',
url: url,
timeout: 60000,
});
return Buffer.from(response.data);

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "koushik-ubuntu"
"scrypted.debugHost": "127.0.0.1"
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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);

View File

@@ -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

View File

@@ -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[] {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();
}
}

View 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);
}

View File

@@ -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=="
}
}
}

View File

@@ -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"
}
}

View File

@@ -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() {

View File

@@ -284,6 +284,7 @@ export class OnvifCameraAPI {
url: snapshotUri,
responseType: 'arraybuffer',
httpsAgent,
timeout: 60000,
});
return Buffer.from(response.data);

View 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;
}

View File

@@ -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"
}

View File

@@ -36,5 +36,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.85"
"version": "0.0.86"
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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"
}

View File

@@ -40,5 +40,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.22"
"version": "0.1.34"
}

View File

@@ -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)

View File

@@ -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'

View 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

View File

@@ -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