Compare commits

..

158 Commits

Author SHA1 Message Date
Koushik Dutta
5eab99866f server: Force ipv4 for npm usage 2023-09-19 13:39:18 -07:00
Koushik Dutta
e10a4f3c58 client: abnormal login results of any type on the alternate urls should fail 2023-09-19 11:30:13 -07:00
Koushik Dutta
2585b1832e docker: add node 20 base 2023-09-19 10:49:23 -07:00
Koushik Dutta
5e8e0d7773 client: validate results 2023-09-19 10:30:19 -07:00
Koushik Dutta
7c17b478d7 cloud: add cors options 2023-09-19 10:29:44 -07:00
Koushik Dutta
9f5dd55c73 h264: ignore nal delimiter 2023-09-19 10:11:44 -07:00
Koushik Dutta
b6f400382d client: support local checks 2023-09-19 09:49:33 -07:00
Koushik Dutta
024b2166b8 snapshot: publish 2023-09-18 08:25:11 -07:00
Koushik Dutta
b49771840e amcrest: httpsAgent usage fixes 2023-09-17 20:34:48 -07:00
Koushik Dutta
4001fc996f amcrest: publish 2023-09-17 17:59:56 -07:00
Koushik Dutta
0d97010ca8 amcrest: fix audiocodec detection nre 2023-09-17 17:59:22 -07:00
Koushik Dutta
e243d99d12 sdk: unprivatize settings method 2023-09-15 14:59:01 -07:00
Koushik Dutta
86a91dfbe4 webrtc: update from upstream 2023-09-15 09:09:46 -07:00
Koushik Dutta
c86ae752e8 videoanalysis: fixup spurious motion triggering object detection on a lot of cams 2023-09-15 09:02:32 -07:00
Koushik Dutta
b7ca477b98 cloud: show tunnel url 2023-09-14 08:30:57 -07:00
Koushik Dutta
c37f8926b8 onvif: fix 2 way audio logging 2023-09-14 08:15:37 -07:00
Koushik Dutta
4b181a8ac9 videoanalysis: fix migration bug by reenabling mixins 2023-09-14 08:15:18 -07:00
Koushik Dutta
b8439aaec3 server: add axios post shim 2023-09-13 16:17:16 -07:00
Koushik Dutta
77d0c33657 videoanalysis: move object detectors behind developer mode flag to prevent footgunning 2023-09-13 10:45:12 -07:00
Koushik Dutta
0b6d61a801 sdk: fix python generation 2023-09-09 20:45:31 -07:00
Koushik Dutta
71a2d27cbd detect: add ObjectDetection filtering interfaces to prevent footgunning 2023-09-09 20:40:56 -07:00
Koushik Dutta
f8f79f5cc2 sdk: add ObjectDetectionPreview 2023-09-09 20:34:57 -07:00
Koushik Dutta
988f297e32 sdk: add ObjectDetectionGenerator 2023-09-09 20:33:09 -07:00
Koushik Dutta
6e109d89e0 Merge branch 'main' of github.com:koush/scrypted 2023-09-09 13:45:24 -07:00
Koushik Dutta
6ada4854bc python-codecs: reduce jpeg quality for better file sizes 2023-09-09 13:45:20 -07:00
Koushik Dutta
bc5e89668f ha: publish 2023-09-09 10:04:27 -07:00
Brett Jia
4c11def52b core: use webpack bundled map marker (#1049)
* core: use webpack bundled map marker

* document source of marker icon workaround

* disable touch zoom
2023-09-09 09:57:15 -07:00
Koushik Dutta
8890d307f4 docker: add builder secrets 2023-09-09 09:39:03 -07:00
Koushik Dutta
9f8f562dcc docker: fixup template path 2023-09-08 21:34:13 -07:00
Koushik Dutta
2ce798c8c2 server: postrelease 2023-09-08 20:12:08 -07:00
Koushik Dutta
4271ef321f postrelease 2023-09-08 20:11:59 -07:00
Koushik Dutta
f976903a29 server: update deps 2023-09-08 20:11:15 -07:00
Nick Berardi
4ca63aadd5 alexa: display camera on doorbell press (#1066) 2023-09-08 13:56:49 -07:00
Koushik Dutta
6c932aec89 snapshot: refactor to remove ffmpeg usage 2023-09-07 09:34:23 -07:00
Koushik Dutta
d7030c3dcf videoanalysis: ignore webcodec if not running under electron 2023-09-06 08:43:59 -07:00
Koushik Dutta
172ebf06de server: add pending result method tracker 2023-09-06 07:50:53 -07:00
Koushik Dutta
5f28c5a291 postbeta 2023-09-06 07:50:32 -07:00
Koushik Dutta
4c9ba5073e cloud: cleanup 2023-09-05 22:51:49 -07:00
Koushik Dutta
11d67f36be cloud: show port in Advanced in Disabled/Cloudflare Tunnel mode 2023-09-05 20:00:43 -07:00
Koushik Dutta
d38357ded9 webrtc: better 6to4 detection 2023-09-05 10:31:34 -07:00
Koushik Dutta
f22e2ccfe7 webrtc: fast path for ipv6 relay candidates 2023-09-05 09:47:22 -07:00
Koushik Dutta
e2b2f68477 server: postbeta 2023-09-05 08:29:24 -07:00
Koushik Dutta
57e87fbe8d postbeta 2023-09-05 08:29:14 -07:00
Koushik Dutta
31b05162fc beta 2023-09-04 17:56:14 -07:00
Koushik Dutta
c63efa0fca cloud: fixup settings.json 2023-09-04 17:56:10 -07:00
Koushik Dutta
ce5255aa45 postbeta 2023-09-04 17:02:25 -07:00
Koushik Dutta
4692be1586 server: v6/v4 mixup fix 2023-09-04 17:02:17 -07:00
Koushik Dutta
632d971dd5 server: remove axios 2023-09-04 16:56:49 -07:00
Koushik Dutta
2f17c85e99 postbeta 2023-09-04 16:56:36 -07:00
Koushik Dutta
9c6cdc9ac3 postbeta 2023-09-04 16:46:13 -07:00
Koushik Dutta
7007456bdd server: fix ipv6 addresses 2023-09-04 16:46:05 -07:00
Koushik Dutta
73fc738c0b cloud: additional bin path fixes 2023-09-03 17:45:55 -07:00
Koushik Dutta
abd1227fab cloud: fix cloudflared bin install 2023-09-03 17:39:29 -07:00
Brett Jia
7d2226df75 arlo: upstreaming changes (#1059)
* eager stream urls

* bump 0.8.16 for beta

* use curl-cffi everywhere, use alternative to piwheels, configurable eager streams

* bump 0.8.17 for beta

* bump 0.8.18 for release

* update backup hosts

* bump 0.8.19 for release

* resurrect pyav and aiortc

* bump 0.8.20 for beta

* unify scrypted-arlo-go and aiortc, disable aiortc

* update backup hosts

* use native sse client

* bump 0.8.21 for beta

* fix native sseclient restart loop

* update backup hosts

* bump 0.8.22 for beta

* handle disconnects with python-level restart

* bump 0.8.23 for beta

* move sse restart to native code

* bump 0.8.24 for beta

* bump 0.8.25 for release

* update backup hosts

* bump 0.8.26 for release
2023-09-03 15:41:04 -07:00
Koushik Dutta
8f50415920 cloud: need to learn to code 2023-09-02 18:27:17 -07:00
Koushik Dutta
20ed523b30 cloud: fix EACCES 2023-09-02 17:25:29 -07:00
Koushik Dutta
effadb1437 cloud: add/shim macos arm64 cloudflared builds 2023-09-02 14:50:57 -07:00
Koushik Dutta
07c7c91c63 coreml: public beta that uses use coremltools beta 2023-09-01 14:41:46 -07:00
Koushik Dutta
878ddbdf1c python-codecs: fix windows leak 2023-08-31 18:10:10 -07:00
Koushik Dutta
d95e9c78ea cameras: update ip address in info after adding 2023-08-30 09:05:17 -07:00
Koushik Dutta
49dc1d8f36 python-codecs: add support for gstreamer jpeg output, publish beta 2023-08-29 20:54:17 -07:00
Koushik Dutta
425e17a88b tensorflow-lite: fix windows 2023-08-27 22:10:19 -07:00
Koushik Dutta
9bca6b0a94 ha: add network share support 2023-08-27 10:28:14 -07:00
Koushik Dutta
3a62d9cd31 cli: add ffplay filter args 2023-08-26 21:31:31 -07:00
Koushik Dutta
8f6bedd9d8 sdk: publish 2023-08-26 20:38:19 -07:00
Koushik Dutta
1c2a9d767f rebroadcast: Fix up output arguments handling and rtsp rebroadcast 2023-08-26 19:55:27 -07:00
Koushik Dutta
7ecee4298c sdk: update 2023-08-26 16:51:01 -07:00
Koushik Dutta
4f1aad895f sdk: update 2023-08-26 16:50:04 -07:00
Koushik Dutta
94667d2136 core: promote snapshot to core plugin, publish 2023-08-26 13:43:38 -07:00
Koushik Dutta
7d13055eae Merge branch 'main' of github.com:koush/scrypted 2023-08-26 13:38:00 -07:00
Koushik Dutta
f90140dbd7 core: add doc links 2023-08-26 13:37:41 -07:00
Brett Jia
8b3a66b6ba core: replace google maps with leaflet/OSM (#1046)
* core: replace google maps with leaflet/OSM

* core: publish
2023-08-26 10:40:40 -07:00
Brett Jia
8c03852cfb chromecast: publish (#1045) 2023-08-25 19:16:51 -07:00
slyoldfox
d795cd527d bticino: 0.0.11 - kick off audio and video streams sooner without waiting for the SIP call to be established (#1044)
Co-authored-by: Marc Vanbrabant <marc@foreach.be>
2023-08-25 08:36:26 -07:00
Koushik Dutta
a24d986717 tflite: update yolov8n 320 model 2023-08-24 19:48:32 -07:00
Koushik Dutta
60ec304e68 predict: report hardware acceleration optiosn 2023-08-24 12:25:36 -07:00
Koushik Dutta
6a9d498ff8 snapshot: relax error messages 2023-08-24 09:23:53 -07:00
Koushik Dutta
c60821043b cameras: remove pam-diff dependency 2023-08-24 08:49:35 -07:00
Koushik Dutta
e5a63dd992 coreml: remove python-codecs dependency, mac should use the desktop app. 2023-08-23 21:20:29 -07:00
Koushik Dutta
f77ea922f2 predict: readd python codecs dependency 2023-08-23 21:19:48 -07:00
Koushik Dutta
1e8deeb638 Merge branch 'main' of github.com:koush/scrypted 2023-08-23 18:50:28 -07:00
Koushik Dutta
a28ecb71e1 videoanalysis: add better explanation for pipeline ffmpeg pipeline failure 2023-08-23 18:50:21 -07:00
Brett Jia
4067455396 core: publish (#1038) 2023-08-23 10:50:16 -07:00
Brett Jia
9b828a6045 core: docker installs delay update prompt until image is ready (#1034)
* core: docker installs delay update prompt until image is ready

* update settings page with new check
2023-08-23 09:58:11 -07:00
Koushik Dutta
efce576c68 server: beta 2023-08-21 13:38:09 -07:00
Koushik Dutta
66b314f2aa postbeta 2023-08-21 13:36:52 -07:00
Koushik Dutta
d6ebc1fa85 postbeta 2023-08-21 13:35:45 -07:00
Koushik Dutta
8d756a26bd server: Fix hang caused by null-ish headers 2023-08-21 13:33:24 -07:00
Koushik Dutta
81c28b86d3 reolink: update readme 2023-08-20 11:02:29 -07:00
Koushik Dutta
73f5e03774 core: publish 2023-08-18 08:42:28 -07:00
Koushik Dutta
cd078afcf9 client: fix webrtc usage 2023-08-17 18:31:50 -07:00
Koushik Dutta
6e393514cf cloud: add No TLS Verify to cloudflare readme section 2023-08-17 10:38:01 -07:00
Koushik Dutta
4b62bceede cloud: add cloudflare tunnel token option 2023-08-17 10:19:09 -07:00
Koushik Dutta
fbbbdd8ab5 cloud: publish 2023-08-16 19:34:02 -07:00
Koushik Dutta
a0e28c0a28 core: publish 2023-08-16 19:07:08 -07:00
Koushik Dutta
ff28238422 cloud: publish beta 2023-08-16 19:05:44 -07:00
Koushik Dutta
4e9744360a client: add support for cloudflared 2023-08-16 18:59:36 -07:00
Koushik Dutta
7336fac8c4 cloud: minor code cleanups and remove duckdns 2023-08-16 18:02:45 -07:00
Koushik Dutta
6771d17829 cloud: restructure 2023-08-16 14:50:52 -07:00
Koushik Dutta
62f1ca66f6 core: make iframe logins less confusing. show hostname on login screen. 2023-08-16 10:57:31 -07:00
Koushik Dutta
13cc562e68 cloud: duckdns prototype 2023-08-15 21:40:23 -07:00
Koushik Dutta
aff1e86d6f Revert "cloud: revert duckdns + letsencrypt"
This reverts commit 7d022548b9.
2023-08-15 21:29:30 -07:00
Koushik Dutta
c1f1e96109 predict: cleanups 2023-08-15 21:29:18 -07:00
Koushik Dutta
a36b3066fe python-codecs: fix corrupt frames 2023-08-15 21:27:56 -07:00
Koushik Dutta
cadf10b505 Merge branch 'main' of github.com:koush/scrypted 2023-08-15 11:16:26 -07:00
Koushik Dutta
ed541629b2 core: change type to prevent mqtt from enabling 2023-08-15 11:16:09 -07:00
Koushik Dutta
7d022548b9 cloud: revert duckdns + letsencrypt 2023-08-12 23:14:13 -07:00
Koushik Dutta
9aa9bae3a3 cloud: fixup hostname logic 2023-08-12 23:09:41 -07:00
Koushik Dutta
7f29b05980 cloud: supports letsencrypt via duckdns 2023-08-12 22:54:25 -07:00
Koushik Dutta
b89573e910 cloud: cleanup deps 2023-08-12 20:28:34 -07:00
Koushik Dutta
18426bcdc1 cloud: restructure 2023-08-12 20:05:56 -07:00
Koushik Dutta
f562dd5362 cloud: fix unhandled rejection 2023-08-12 19:59:00 -07:00
Koushik Dutta
1f1218a594 cloud: increase connection pool 2023-08-12 19:55:59 -07:00
Koushik Dutta
1aca97c2ae common: updates 2023-08-12 19:38:17 -07:00
Koushik Dutta
bd41410367 common: add async queue 2023-08-12 14:10:51 -07:00
Koushik Dutta
291d734a05 videoanalysis: restart object detection if crashed or evicted 2023-08-12 12:55:13 -07:00
Koushik Dutta
feec534b86 python-codecs: publish 2023-08-11 09:29:27 -07:00
Koushik Dutta
9ae7e6c0b5 h264-repacketizer: add types 2023-08-11 09:29:16 -07:00
Koushik Dutta
a6f11d6d0c cloud: improve head of line issues 2023-08-11 09:28:59 -07:00
Koushik Dutta
a15af8005b opencv: avoid broken version 4.8.0.76 2023-08-10 07:39:38 -07:00
Koushik Dutta
c13a3f252a core: publish 2023-08-10 07:35:30 -07:00
Koushik Dutta
0eaf9ef2d9 Merge branch 'main' of github.com:koush/scrypted 2023-08-10 07:35:16 -07:00
Nick Berardi
b9fc69347a alexa: added helpful error messages regarding token expiration (#1007) 2023-08-09 17:24:54 -07:00
Koushik Dutta
f6e8a363ab webrtc: fix webrtc connection timeout leak 2023-08-07 09:02:43 -07:00
Brett Jia
a6d163ec5a core: aggregate streams support horizontal padding (#1002) 2023-08-06 15:23:04 -07:00
Brett Jia
2d62944ac1 python-codecs: make annotations compatible to pre-3.10 (#1000) 2023-08-06 09:54:08 -07:00
Koushik Dutta
b564553998 python-codecs: rollback sdk bug 2023-08-06 09:53:26 -07:00
Koushik Dutta
6e4fdb6e99 videoanalysis: fix object detection eviction bug 2023-08-04 13:47:19 -07:00
Koushik Dutta
ca00983ecd client: better webrtc api connection usage 2023-08-03 20:33:24 -07:00
Koushik Dutta
36b8b9eeed common: formatting 2023-08-03 19:45:19 -07:00
Koushik Dutta
fbd6937627 webrtc/core: streamline p2p connection 2023-08-03 19:18:51 -07:00
Koushik Dutta
7c66826657 docker: dont pass usb through by default 2023-08-02 08:41:46 -07:00
Koushik Dutta
62c4a8b240 detection plugins: remove image splitting logic, let upstream handle that. switch to yolov8_320 as default. 2023-07-31 14:12:56 -07:00
Koushik Dutta
af860d840a mac: fix cli script 2023-07-31 13:54:43 -07:00
Koushik Dutta
42eb4fc80b python-codecs: dont letterbox resize requests. 2023-07-31 00:56:03 -07:00
Koushik Dutta
5c965936e9 Merge branch 'main' of github.com:koush/scrypted 2023-07-30 23:53:47 -07:00
Koushik Dutta
fe5cc59872 core: fix object detection svg layout 2023-07-30 23:53:42 -07:00
Brett Jia
5d965ebfa7 arlo: upstreaming changes to 0.8.15 (#982)
* wip hidden devices

* hidden devices impl

* bump 0.8.12 for beta

* update backup auth hosts

* bump 0.8.13 for release

* use curl-cffi everywhere

* bump 0.8.14 for beta

* Revert "use curl-cffi everywhere"

This reverts commit 80422a8037.

* update auth hosts

* bump 0.8.15 for release
2023-07-29 10:02:12 -07:00
Koushik Dutta
b462249d93 Merge branch 'main' of github.com:koush/scrypted 2023-07-28 21:19:46 -07:00
Koushik Dutta
29d8abed45 tensorflow-lite: more models 2023-07-28 21:19:41 -07:00
Koushik Dutta
65cb13b0d1 tensorflow-lite: add more models 2023-07-28 20:24:51 -07:00
Koushik Dutta
522f8e9cba Update config.yaml 2023-07-28 13:36:15 -07:00
Koushik Dutta
16199463ec python-codecs: publsih 2023-07-28 00:02:17 -07:00
Koushik Dutta
220c010232 python-codecs: fix zygote pop usage. implement firstFrameOnly pipeline blocking. 2023-07-28 00:01:01 -07:00
Koushik Dutta
02238f99b2 python-codecs: use zygote to speed up inference startup 2023-07-27 23:52:52 -07:00
Koushik Dutta
1e53234cd6 Merge branch 'main' of github.com:koush/scrypted 2023-07-27 14:54:52 -07:00
Koushik Dutta
824b7327a1 cloud: update deps 2023-07-27 14:54:41 -07:00
Koushik Dutta
81d4a3f249 Update docker.yml 2023-07-27 13:48:43 -07:00
Koushik Dutta
db1bd07b71 docker: increment base 2023-07-27 13:32:24 -07:00
Koushik Dutta
35026f6b5b Merge branch 'main' of github.com:koush/scrypted 2023-07-27 13:25:37 -07:00
Koushik Dutta
9160efc2f7 docker: allow pip to install to system 2023-07-27 13:25:33 -07:00
Koushik Dutta
6bc1e6a742 Update docker-common.yml 2023-07-27 13:16:45 -07:00
Koushik Dutta
475e4a60d7 Update docker-common.yml 2023-07-27 13:15:31 -07:00
Koushik Dutta
1f2edf1a12 Update docker-common.yml 2023-07-27 13:05:38 -07:00
Koushik Dutta
b3db0aa78f Update docker-common.yml 2023-07-27 13:05:21 -07:00
Koushik Dutta
0766d67a75 docker: move coral to full image only 2023-07-27 12:52:46 -07:00
Koushik Dutta
d2ac428916 server: publish 2023-07-26 17:35:36 -07:00
Koushik Dutta
945fb16bd6 postrelease 2023-07-26 17:35:19 -07:00
179 changed files with 17255 additions and 26362 deletions

View File

@@ -6,11 +6,11 @@ on:
jobs:
build:
name: Push Docker image to Docker Hub
# runs-on: self-hosted
runs-on: ubuntu-latest
runs-on: self-hosted
# runs-on: ubuntu-latest
strategy:
matrix:
NODE_VERSION: ["18"]
NODE_VERSION: ["18", "20"]
BASE: ["jammy"]
FLAVOR: ["full", "lite", "thin"]
steps:
@@ -20,28 +20,27 @@ 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: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
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: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# with:
# platforms: linux/arm64,linux/armhf
# append: |
# - endpoint: ssh://koush@192.168.2.124
# # platforms: linux/arm64
# platforms: linux/arm64
# # - endpoint: ssh://koush@192.168.2.119
# # platforms: linux/armhf
with:
platforms: linux/arm64,linux/armhf
append: |
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
platforms: linux/arm64
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM7 }}
platforms: linux/armhf
- name: Login to Docker Hub
uses: docker/login-action@v2

View File

@@ -15,11 +15,11 @@ on:
jobs:
build:
name: Push Docker image to Docker Hub
# runs-on: self-hosted
runs-on: ubuntu-latest
runs-on: self-hosted
# runs-on: ubuntu-latest
strategy:
matrix:
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin"]
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin", "20-jammy-full", "20-jammy-lite", "20-jammy-thin"]
SUPERVISOR: ["", ".s6"]
steps:
- name: Check out the repo
@@ -39,29 +39,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: ${{ secrets.DOCKER_SSH_HOST_ARM64 }}
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: ${{ secrets.DOCKER_SSH_HOST_ARM7 }}
private-key: ${{ secrets.DOCKER_SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# with:
# platforms: linux/arm64,linux/armhf
# append: |
# - endpoint: ssh://koush@192.168.2.124
# # platforms: linux/arm64
# platforms: linux/arm64
# # - endpoint: ssh://koush@192.168.2.119
# # platforms: linux/armhf
with:
platforms: linux/arm64,linux/armhf
append: |
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM64 }}
platforms: linux/arm64
- endpoint: ssh://${{ secrets.DOCKER_SSH_USER }}@${{ secrets.DOCKER_SSH_HOST_ARM7 }}
platforms: linux/armhf
- name: Login to Docker Hub
uses: docker/login-action@v2
with:

2
.gitmodules vendored
View File

@@ -33,5 +33,5 @@
path = plugins/sample-cameraprovider
url = ../../koush/scrypted-sample-cameraprovider
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/node-nat-upnp
path = plugins/cloud/external/node-nat-upnp
url = ../../koush/node-nat-upnp.git

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/common",
"version": "1.0.1",
"version": "1.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/common",
"version": "1.0.1",
"version": "1.0.2",
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",

View File

@@ -1,5 +1,6 @@
{
"name": "@scrypted/common",
"private": true,
"version": "1.0.1",
"description": "",
"main": "index.js",

171
common/src/async-queue.ts Normal file
View File

@@ -0,0 +1,171 @@
import { Deferred } from "./deferred";
class EndError extends Error {
}
export function createAsyncQueue<T>() {
let ended: Error | undefined;
const waiting: Deferred<T>[] = [];
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
const dequeue = async () => {
if (queued.length) {
const { item, dequeued: enqueue } = queued.shift()!;
enqueue?.resolve();
return item;
}
if (ended)
throw ended;
const deferred = new Deferred<T>();
waiting.push(deferred);
return deferred.promise;
}
const submit = (item: T, dequeued?: Deferred<void>, signal?: AbortSignal) => {
if (ended)
return false;
if (waiting.length) {
const deferred = waiting.shift();
dequeued?.resolve();
deferred.resolve(item);
return true;
}
const qi = {
item,
dequeued,
};
queued!.push(qi);
signal?.addEventListener('abort', () => {
const index = queued.indexOf(qi);
if (index === -1)
return;
queued.splice(index, 1);
dequeued?.reject(new Error('abort'));
});
return true;
}
function queue() {
return (async function* () {
while (true) {
try {
const item = await dequeue();
yield item;
}
catch (e) {
if (e instanceof EndError)
return;
throw e;
}
}
})();
}
function clear(error?: Error) {
const ret: T[] = [];
const items = queued.splice(0, queued.length);
for (const item of items) {
if (error)
item.dequeued?.reject(error)
else
item.dequeued?.resolve(undefined);
ret.push(item.item);
}
return ret;
}
return {
clear() {
return clear();
},
queued,
async pipe(callback: (i: T) => void) {
for await (const i of queue()) {
callback(i as any);
}
},
submit(item: T, signal?: AbortSignal) {
return submit(item, undefined, signal);
},
end(e?: Error) {
if (ended)
return false;
// catch to prevent unhandled rejection.
ended = e || new EndError()
clear(e);
return true;
},
async enqueue(item: T, signal?: AbortSignal) {
const dequeued = new Deferred<void>();
if (!submit(item, dequeued, signal))
return false;
await dequeued.promise;
return true;
},
dequeue,
get queue() {
return queue();
}
}
}
// async function testSlowEnqueue() {
// const asyncQueue = createAsyncQueue<number>();
// asyncQueue.submit(-1);
// asyncQueue.submit(-1);
// asyncQueue.submit(-1);
// asyncQueue.submit(-1);
// (async () => {
// console.log('go');
// for (let i = 0; i < 10; i++) {
// asyncQueue.submit(i);
// await sleep(100);
// }
// asyncQueue.end(new Error('fail'));
// })();
// const runQueue = async (str?: string) => {
// for await (const n of asyncQueue.queue) {
// console.log(str, n);
// }
// }
// runQueue('start');
// setTimeout(runQueue, 400);
// }
// async function testSlowDequeue() {
// const asyncQueue = createAsyncQueue<number>();
// const runQueue = async (str?: string) => {
// for await (const n of asyncQueue.queue) {
// await sleep(100);
// }
// }
// runQueue()
// .catch(e => console.error('queue threw', e));
// console.log('go');
// for (let i = 0; i < 10; i++) {
// console.log(await asyncQueue.enqueue(i));
// console.log(i);
// }
// asyncQueue.end(new Error('fail'));
// console.log(await asyncQueue.enqueue(555));
// }
// testSlowDequeue();

View File

@@ -3,14 +3,13 @@ import sdk from "@scrypted/sdk";
const { systemManager } = sdk;
const autoIncludeToken = 'v4';
export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
hasEnabledMixin: { [id: string]: string } = {};
pluginsComponent: Promise<any>;
unshiftMixin = false;
constructor(nativeId?: string) {
constructor(nativeId?: string, public autoIncludeToken = 'v4') {
super(nativeId);
try {
@@ -30,10 +29,12 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
this.maybeEnableMixin(eventSource);
});
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
this.maybeEnableMixin(device);
}
process.nextTick(() => {
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
this.maybeEnableMixin(device);
}
});
}
async shouldEnableMixin(device: ScryptedDevice) {
@@ -44,7 +45,7 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
if (!device || device.mixins?.includes(this.id))
return;
if (this.hasEnabledMixin[device.id] === autoIncludeToken)
if (this.hasEnabledMixin[device.id] === this.autoIncludeToken)
return;
const match = await this.canMixin(device.type, device.interfaces);
@@ -66,9 +67,9 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
}
setHasEnabledMixin(id: string) {
if (this.hasEnabledMixin[id] === autoIncludeToken)
if (this.hasEnabledMixin[id] === this.autoIncludeToken)
return;
this.hasEnabledMixin[id] = autoIncludeToken;
this.hasEnabledMixin[id] = this.autoIncludeToken;
this.storage.setItem('hasEnabledMixin', JSON.stringify(this.hasEnabledMixin));
}

View File

@@ -51,14 +51,8 @@ function silence() {
return ret;
}
export class BrowserSignalingSession implements RTCSignalingSession {
private pc: RTCPeerConnection;
pcDeferred = new Deferred<RTCPeerConnection>();
dcDeferred = new Deferred<RTCDataChannel>();
microphone: RTCRtpSender;
micEnabled = false;
onPeerConnection: (pc: RTCPeerConnection) => Promise<void>;
options: RTCSignalingOptions = {
function createOptions() {
const options: RTCSignalingOptions = {
userAgent: getUserAgent(),
capabilities: {
audio: RTCRtpReceiver.getCapabilities?.('audio') || {
@@ -76,6 +70,18 @@ export class BrowserSignalingSession implements RTCSignalingSession {
height: screen.height,
},
};
return options;
}
export class BrowserSignalingSession implements RTCSignalingSession {
private pc: RTCPeerConnection;
pcDeferred = new Deferred<RTCPeerConnection>();
dcDeferred = new Deferred<RTCDataChannel>();
microphone: RTCRtpSender;
micEnabled = false;
onPeerConnection: (pc: RTCPeerConnection) => Promise<void>;
__proxy_props = { options: createOptions() };
options = createOptions();
constructor() {
}
@@ -284,6 +290,10 @@ function createCandidateQueue(console: Console, type: string, session: RTCSignal
}
}
export async function legacyGetSignalingSessionOptions(session: RTCSignalingSession) {
return typeof session.options === 'object' ? session.options : await session.getOptions();
}
export async function connectRTCSignalingClients(
console: Console,
offerClient: RTCSignalingSession,
@@ -291,8 +301,8 @@ export async function connectRTCSignalingClients(
answerClient: RTCSignalingSession,
answerSetup: Partial<RTCAVSignalingSetup>
) {
const offerOptions = await offerClient.getOptions();
const answerOptions = await answerClient.getOptions();
const offerOptions = await legacyGetSignalingSessionOptions(offerClient);
const answerOptions = await legacyGetSignalingSessionOptions(answerClient);
const disableTrickle = offerOptions?.disableTrickle || answerOptions?.disableTrickle;
if (offerOptions?.offer && answerOptions?.offer)

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "18-jammy-full.s6-v0.39.4"
version: "18-jammy-full.s6-v0.50.0"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"
@@ -35,6 +35,7 @@ backup_exclude:
map:
- config:rw
- media:rw
- share:rw
devices:
- /dev/mem
- /dev/dri/renderD128

View File

@@ -36,12 +36,6 @@ RUN apt-get -y install \
python3-setuptools \
python3-wheel
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN apt-get -y install \
@@ -74,13 +68,12 @@ RUN apt-get -y install \
python3-pil \
python3-skimage
# python pip
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
# pyvips is broken on x86 due to mismatch ffi
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
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
@@ -113,10 +106,19 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
python3.9-dev \
python3.9-distutils
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3.9 -m pip install --upgrade pip
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3.9 -m pip install debugpy typing_extensions psutil
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
@@ -127,7 +129,7 @@ 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_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="full"
################################################################

View File

@@ -43,5 +43,5 @@ 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_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="lite"

View File

@@ -21,5 +21,5 @@ 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_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="thin"

View File

@@ -50,7 +50,7 @@ services:
# 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
# - /mnt/media/video:/nvr
# Or use a network mount from one of the CIFS/NFS examples at the top of this file.
# - type: volume
@@ -67,17 +67,25 @@ services:
# Default volume for the Scrypted database. Typically should not be changed.
- ~/.scrypted/volume:/server/volume
devices:
devices: [
# uncomment the common systems devices to pass
# them through to docker.
# all usb devices, such as coral tpu
- /dev/bus/usb:/dev/bus/usb
# "/dev/bus/usb:/dev/bus/usb",
# hardware accelerated video decoding, opencl, etc.
# - /dev/dri:/dev/dri
# "/dev/dri:/dev/dri",
# uncomment below as necessary.
# zwave usb serial device
# - /dev/ttyACM0:/dev/ttyACM0
# "/dev/ttyACM0:/dev/ttyACM0",
# coral PCI devices
# - /dev/apex_0:/dev/apex_0
# - /dev/apex_1:/dev/apex_1
# "/dev/apex_0:/dev/apex_0",
# "/dev/apex_1:/dev/apex_1",
]
container_name: scrypted
restart: unless-stopped

View File

@@ -23,10 +23,19 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
python3.9-dev \
python3.9-distutils
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3.9 -m pip install --upgrade pip
RUN python3.9 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3.9 -m pip install debugpy typing_extensions psutil
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
@@ -37,7 +46,7 @@ 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_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="full"
################################################################

View File

@@ -33,12 +33,6 @@ RUN apt-get -y install \
python3-setuptools \
python3-wheel
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" | tee /etc/apt/sources.list.d/coral-edgetpu.list
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN apt-get -y install \
@@ -71,13 +65,12 @@ RUN apt-get -y install \
python3-pil \
python3-skimage
# python pip
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
# pyvips is broken on x86 due to mismatch ffi
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
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

View File

@@ -58,6 +58,9 @@ brew unpin gst-python
### END HACK WORKAROUND
# seems to be necessary for python-codecs' pycairo dependency or something?
RUN_IGNORE gobject-introspection libffi pkg-config
# gstreamer plugins
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav
# gst python bindings

View File

@@ -19,11 +19,11 @@
"-r",
"ts-node/register"
],
"preLaunchTask": "npm: build",
"args": [
"ffplay",
"Kitchen",
"getRecordingStream",
"{\"startTime\":1677699495709}"
"Baby Camera@192.168.2.109",
"getVideoStream",
],
"sourceMaps": true,
"resolveSourceMapLocations": [
@@ -35,4 +35,4 @@
],
},
]
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "scrypted",
"version": "1.0.67",
"version": "1.0.69",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.0.67",
"version": "1.0.69",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.1.43",

View File

@@ -1,6 +1,6 @@
{
"name": "scrypted",
"version": "1.0.67",
"version": "1.0.69",
"description": "",
"main": "./dist/main.js",
"bin": {

View File

@@ -172,8 +172,11 @@ async function main() {
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
}
}
console.log('ffplay', ...ffmpegInput.inputArguments);
child_process.spawn('ffplay', ffmpegInput.inputArguments, {
const args = [...ffmpegInput.inputArguments];
if (ffmpegInput.h264FilterArguments)
args.push(...ffmpegInput.h264FilterArguments);
console.log('ffplay', ...args);
child_process.spawn('ffplay', args, {
stdio: 'inherit',
});
sdk.disconnect();

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/client",
"version": "1.1.54",
"version": "1.1.55",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.54",
"version": "1.1.55",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.94",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.54",
"version": "1.1.55",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {

View File

@@ -1,5 +1,5 @@
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import axios, { AxiosRequestConfig } from 'axios';
import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios';
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
import { Deferred } from "../../../common/src/deferred";
@@ -8,7 +8,6 @@ import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceCon
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
import type { IOSocket } from '../../../server/src/io';
import { MediaObject } from '../../../server/src/plugin/mediaobject';
import type { MediaObjectRemote } from '../../../server/src/plugin/plugin-api';
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
import { RpcPeer } from '../../../server/src/rpc';
import { createRpcDuplexSerializer, createRpcSerializer } from '../../../server/src/rpc-serializer';
@@ -48,9 +47,8 @@ export interface ScryptedClientStatic extends ScryptedStatic {
browserSignalingSession?: BrowserSignalingSession;
address?: string;
connectionType: ScryptedClientConnectionType;
authorization?: string;
queryToken?: { [parameter: string]: string };
rpcPeer: RpcPeer,
rpcPeer: RpcPeer;
loginResult: ScryptedClientLoginResult;
}
export interface ScryptedConnectionOptions {
@@ -59,6 +57,7 @@ export interface ScryptedConnectionOptions {
webrtc?: boolean;
baseUrl?: string;
axiosConfig?: AxiosRequestConfig;
previousLoginResult?: ScryptedClientLoginResult;
}
export interface ScryptedLoginOptions extends ScryptedConnectionOptions {
@@ -138,6 +137,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
// should maybe move this into the cloud server itself.
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
const directAddress = response.headers['x-scrypted-direct-address'];
const cloudAddress = response.headers['x-scrypted-cloud-address'];
return {
error: response.data.error as string,
@@ -147,20 +147,27 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
addresses,
scryptedCloud,
directAddress,
cloudAddress,
};
}
export async function checkScryptedClientLogin(options?: ScryptedConnectionOptions) {
let { baseUrl } = options || {};
const url = combineBaseUrl(baseUrl, 'login');
const headers: AxiosRequestHeaders = {};
if (options?.previousLoginResult?.authorization)
headers.Authorization = options?.previousLoginResult?.authorization;
const response = await axios.get(url, {
withCredentials: true,
headers,
...options?.axiosConfig,
});
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
const directAddress = response.headers['x-scrypted-direct-address'];
const cloudAddress = response.headers['x-scrypted-cloud-address'];
return {
baseUrl,
hostname: response.data.hostname as string,
redirect: response.data.redirect as string,
username: response.data.username as string,
@@ -173,9 +180,19 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
addresses: response.data.addresses as string[],
scryptedCloud,
directAddress,
cloudAddress,
};
}
export interface ScryptedClientLoginResult {
authorization: string;
queryToken: { [parameter: string]: string };
localAddresses: string[];
scryptedCloud: boolean;
directAddress: string;
cloudAddress: string;
}
export class ScryptedClientLoginError extends Error {
constructor(public result: Awaited<ReturnType<typeof checkScryptedClientLogin>>) {
super(result.error);
@@ -211,35 +228,78 @@ export async function redirectScryptedLogout(baseUrl?: string) {
export async function connectScryptedClient(options: ScryptedClientOptions): Promise<ScryptedClientStatic> {
const start = Date.now();
let { baseUrl, pluginId, clientName, username, password } = options;
let authorization: string;
let queryToken: any;
const extraHeaders: { [header: string]: string } = {};
let localAddresses: string[];
let scryptedCloud: boolean;
let directAddress: string;
let cloudAddress: string;
console.log('@scrypted/client', packageJson.version);
const extraHeaders: { [header: string]: string } = {};
if (username && password) {
const loginResult = await loginScryptedClient(options as ScryptedLoginOptions);
if (loginResult.authorization)
extraHeaders['Authorization'] = loginResult.authorization;
localAddresses = loginResult.addresses;
scryptedCloud = loginResult.scryptedCloud;
directAddress = loginResult.directAddress;
cloudAddress = loginResult.cloudAddress;
authorization = loginResult.authorization;
queryToken = loginResult.queryToken;
console.log('login result', Date.now() - start, loginResult);
}
else {
const loginCheck = await checkScryptedClientLogin({
const urlsToCheck = new Set<string>();
for (const u of [
...options?.previousLoginResult?.localAddresses || [],
options?.previousLoginResult?.directAddress,
options?.previousLoginResult?.cloudAddress,
]) {
if (u)
urlsToCheck.add(u);
}
// the alternate urls must have a valid response.
const loginCheckPromises = [...urlsToCheck].map(async baseUrl => {
const loginCheck = await checkScryptedClientLogin({
baseUrl,
previousLoginResult: options?.previousLoginResult,
});
if (loginCheck.error || loginCheck.redirect)
throw new Error('login error');
if (!loginCheck.authorization || !loginCheck.username || !loginCheck.queryToken) {
console.error(loginCheck);
throw new Error('malformed login result');
}
return loginCheck;
});
const baseUrlCheck = checkScryptedClientLogin({
baseUrl,
});
loginCheckPromises.push(baseUrlCheck);
let loginCheck: Awaited<ReturnType<typeof checkScryptedClientLogin>>;
try {
loginCheck = await Promise.any(loginCheckPromises);
}
catch (e) {
loginCheck = await baseUrlCheck;
}
if (loginCheck.error || loginCheck.redirect)
throw new ScryptedClientLoginError(loginCheck);
localAddresses = loginCheck.addresses;
scryptedCloud = loginCheck.scryptedCloud;
directAddress = loginCheck.directAddress;
cloudAddress = loginCheck.cloudAddress;
username = loginCheck.username;
authorization = loginCheck.authorization;
queryToken = loginCheck.queryToken;
@@ -280,8 +340,27 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
addresses.push(directAddress);
}
if (((scryptedCloud && options.direct === undefined) || options.direct) && cloudAddress) {
addresses.push(cloudAddress);
}
const tryAddresses = !!addresses.length;
const tryWebrtc = !!globalThis.RTCPeerConnection && (scryptedCloud && options.webrtc === undefined) || options.webrtc;
const webrtcLastFailedKey = 'webrtcLastFailed';
const canUseWebrtc = !!globalThis.RTCPeerConnection;
let tryWebrtc = canUseWebrtc && options.webrtc;
// try webrtc by default on scrypted cloud.
// but webrtc takes a while to fail, so backoff if it fails to prevent continual slow starts.
if (scryptedCloud && canUseWebrtc && globalThis.localStorage && options.webrtc === undefined) {
tryWebrtc = true;
const webrtcLastFailed = parseFloat(localStorage.getItem(webrtcLastFailedKey));
// if webrtc has failed in the past day, dont attempt to use it.
const now = Date.now();
if (webrtcLastFailed < now && webrtcLastFailed > now - 1 * 24 * 60 * 60 * 1000) {
tryWebrtc = false;
console.warn('WebRTC API connection recently failed. Skipping.')
}
}
console.log({
tryLocalAddressess: tryAddresses,
tryWebrtc,
@@ -458,7 +537,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
const p2pPromises = [...promises];
promises.push((async () => {
const waitDuration = tryWebrtc ? 3000 : (tryAddresses ? 1000 : 0);
const waitDuration = tryWebrtc ? 10000 : (tryAddresses ? 1000 : 0);
console.log('waiting', waitDuration);
if (waitDuration) {
// give the peer to peers a second, but then try connecting directly.
@@ -485,6 +564,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
const any = Promise.any(promises);
let { ready, connectionType, address, rpcPeer } = await any;
if (tryWebrtc && connectionType !== 'webrtc')
localStorage.setItem(webrtcLastFailedKey, Date.now().toString());
console.log('connected', connectionType, address)
socket = ready;
@@ -602,9 +684,15 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
pluginHostAPI: undefined,
rtcConnectionManagement,
browserSignalingSession,
authorization,
queryToken,
rpcPeer,
loginResult: {
directAddress,
localAddresses,
scryptedCloud,
queryToken,
authorization,
cloudAddress,
}
}
socket.on('close', () => {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/rpc",
"version": "0.0.2",
"version": "0.0.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/rpc",
"version": "0.0.2",
"version": "0.0.4",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.11.18",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/deferred",
"version": "0.0.2",
"version": "0.0.4",
"description": "",
"main": "dist/index.js",
"scripts": {

View File

@@ -0,0 +1 @@
../../../common/src/async-queue.ts

View File

@@ -0,0 +1 @@
../../../common/src/deferred.ts

View File

@@ -1 +0,0 @@
../../../common/src/deferred.ts

View File

@@ -0,0 +1,2 @@
export * from './deferred';
export * from './async-queue';

View File

@@ -0,0 +1,23 @@
{
// 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": "ts-node",
"type": "node",
"request": "launch",
"args": [
"${workspaceFolder}/test/test.ts"
],
"runtimeArgs": [
"-r",
"ts-node/register"
],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
}
]
}

View File

@@ -1,16 +1,17 @@
{
"name": "@scrypted/h264-packetizer",
"name": "@scrypted/h264-repacketizer",
"version": "0.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/h264-packetizer",
"name": "@scrypted/h264-repacketizer",
"version": "0.0.7",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.11.18",
"rimraf": "^4.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
}
},
@@ -43,12 +44,121 @@
"../sdk/types": {
"extraneous": true
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
},
"node_modules/acorn": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/rimraf": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
@@ -64,6 +174,49 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
@@ -76,26 +229,165 @@
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"engines": {
"node": ">=6"
}
}
},
"dependencies": {
"@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
"dev": true
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
},
"@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
},
"@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
},
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
},
"acorn": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"rimraf": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.1.1.tgz",
"integrity": "sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==",
"dev": true
},
"ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"requires": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
}
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@@ -14,6 +14,7 @@
"devDependencies": {
"@types/node": "^18.11.18",
"rimraf": "^4.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
}
}

View File

@@ -0,0 +1,93 @@
import { H264Repacketizer, depacketizeStapA } from '../src/index';
import { H264_NAL_TYPE_IDR, H264_NAL_TYPE_PPS, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_A, RtspServer, getNaluTypesInNalu } from '../../../common/src/rtsp-server';
import fs from 'fs';
import { getNvrSessionStream } from '../../../../nvr/nvr-plugin/src/session-stream';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { RtpPacket } from '../../../external/werift/packages/rtp/src/rtp/rtp';
function parse(parameters: string) {
const spspps = parameters.split(',');
// empty sprop-parameter-sets is apparently a thing:
// a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=
if (spspps?.length !== 2) {
return {
sps: undefined,
pps: undefined,
};
}
const [sps, pps] = spspps;
return {
sps: Buffer.from(sps, 'base64'),
pps: Buffer.from(pps, 'base64'),
}
}
async function main() {
const spspps = parse('Z2QAM6wVFKAoALWQ,aO48sA==');
// Z2QAM6wVFKAoALWQ
// Z00AMpY1QEABg03BQEFQAAADABAAAAMDKEA=
const repacketizer = new H264Repacketizer(console, 1300, undefined);
const stream = fs.createReadStream('/Users/koush/Downloads/rtsp/1692537093973.rtsp', {
start: 0,
highWaterMark: 800000,
});
let rtspParser = new RtspServer(stream as any, '');
rtspParser.setupTracks = {
'0': {
codec: '0',
protocol: 'tcp',
control: '',
destination: 0,
},
'2': {
codec: '2',
protocol: 'tcp',
control: '',
destination: 2,
},
}
for await (const rtspSample of rtspParser.handleRecord()) {
if (rtspSample.type !== '0')
continue;
const rtp = RtpPacket.deSerialize(rtspSample.packet);
const nalus = getNaluTypesInNalu(rtp.payload);
if (nalus.has(H264_NAL_TYPE_SEI)) {
console.warn('SEI', rtp.payload)
}
if (nalus.has(H264_NAL_TYPE_SPS)) {
console.warn('SPS', rtp.payload, spspps.sps)
}
if (nalus.has(H264_NAL_TYPE_PPS)) {
console.warn('PPS', rtp.payload, spspps.sps)
}
if (nalus.has(H264_NAL_TYPE_STAP_A)) {
const parts = depacketizeStapA(rtp.payload);
console.log('stapa', parts);
for (const part of parts) {
}
}
if (nalus.has(H264_NAL_TYPE_IDR)) {
const h264Packetizer = new H264Repacketizer(console, 65535, spspps as any);
// offset the stapa packet by -1 so the sequence numbers can be reused.
h264Packetizer.extraPackets = -1;
const stapas: RtpPacket[] = [];
const idr = RtpPacket.deSerialize(rtspSample.packet);
h264Packetizer.maybeSendStapACodecInfo(idr, stapas);
if (stapas.length === 1) {
const stapa = stapas[0].serialize();
// console.log(stapa);
}
}
}
}
main();

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2019",
"target": "ES2020",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
@@ -10,6 +10,7 @@
"declaration": true,
"resolveJsonModule": true,
},
"exclude": ["**/node_modules"],
"include": [
"src/**/*"
],

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/alexa",
"version": "0.2.6",
"version": "0.2.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.2.6",
"version": "0.2.7",
"dependencies": {
"axios": "^1.3.4",
"uuid": "^9.0.0"
@@ -18,7 +18,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.101",
"version": "0.2.104",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.2.6",
"version": "0.2.8",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -392,14 +392,21 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
})
}
private setReauthenticateAlert() {
const msg: string = "Please reauthenticate by following the directions below.";
this.log.a(msg);
}
getAccessToken(): Promise<string> {
if (this.accessToken)
return this.accessToken;
this.log.clearAlerts();
const { tokenInfo } = this.storageSettings.values;
if (tokenInfo === undefined) {
this.log.e("Please reauthenticate by following the directions below.");
this.setReauthenticateAlert();
throw new Error("'tokenInfo' is undefined");
}
@@ -432,19 +439,19 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
case 'invalid_grant':
case 'unauthorized_client':
self.console.error(error?.response?.data);
self.log.e(error?.response?.data?.error_description);
self.log.a(error?.response?.data?.error_description);
self.storageSettings.values.tokenInfo = undefined;
self.accessToken = undefined;
break;
case 'authorization_pending':
self.console.warn(error?.response?.data);
self.log.w(error?.response?.data?.error_description);
self.log.a(error?.response?.data?.error_description);
break;
case 'expired_token':
self.console.warn(error?.response?.data);
self.log.w(error?.response?.data?.error_description);
self.log.a(error?.response?.data?.error_description);
self.accessToken = undefined;
break;
@@ -488,6 +495,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
});
if (accessToken !== undefined) {
this.log.clearAlerts();
try {
response.send({
"event": {

View File

@@ -120,7 +120,7 @@ export async function getCameraCapabilities(device: ScryptedDevice): Promise<Dis
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
"isFullDuplexAudioSupported": true,
}
} as DiscoveryCapability
];

View File

@@ -7,10 +7,19 @@ import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedE
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: AlexaHttpResponse, public directive: any) {
this.options = this.createOptions();
this.__proxy_props = { options: this.createOptions() };
}
__proxy_props: { options: RTCSignalingOptions; };
options: RTCSignalingOptions;
async getOptions(): Promise<RTCSignalingOptions> {
return {
return this.options;
}
private createOptions() {
const options: RTCSignalingOptions = {
proxy: true,
offer: {
type: 'offer',
@@ -24,7 +33,9 @@ export class AlexaSignalingSession implements RTCSignalingSession {
width: 1280,
height: 720
}
}
};
return options;
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {

View File

@@ -6,7 +6,7 @@ import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Doorbell, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
let capabilities: any[] = [];
const displayCategories: DisplayCategory[] = ['DOORBELL'];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
capabilities = await getCameraCapabilities(device);
@@ -24,6 +24,9 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
);
}
// Important: If your device is a video doorbell, make sure that you list CAMERA before DOORBELL in the displayCategories list.
displayCategories.push('DOORBELL');
return {
displayCategories,
capabilities
@@ -38,6 +41,9 @@ supportedTypes.set(ScryptedDeviceType.Doorbell, {
if (response)
return response;
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === false)
return {};
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
return {
event: {

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "Node16",

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.123",
"version": "0.0.127",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.123",
"version": "0.0.127",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.123",
"version": "0.0.127",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -95,6 +95,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
for (const element of deviceParameters) {
try {
const response = await this.getClient().digestAuth.request({
httpsAgent: amcrestHttpsAgent,
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`
});
@@ -147,6 +148,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
return;
const response = await this.getClient().digestAuth.request({
httpsAgent: amcrestHttpsAgent,
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`
});
this.console.log('reconfigure result', response.data);
@@ -190,14 +192,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|| event === AmcrestEvent.PhoneCallDetectStart
|| event === AmcrestEvent.AlarmIPCStart
|| event === AmcrestEvent.DahuaTalkInvite) {
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds)
{
if (payload.includes(callerId))
{
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds) {
if (payload.includes(callerId)) {
this.binaryState = true;
}
} else
{
} else {
this.binaryState = true;
}
}
@@ -259,25 +258,23 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (!twoWayAudio)
twoWayAudio = isDoorbell ? 'Amcrest' : 'None';
if (doorbellType == DAHUA_DOORBELL_TYPE)
{
if (doorbellType == DAHUA_DOORBELL_TYPE) {
ret.push(
{
title: 'Multiple Call Buttons',
key: 'multipleCallIds',
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
type: 'boolean',
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
}
{
title: 'Multiple Call Buttons',
key: 'multipleCallIds',
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
type: 'boolean',
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
}
);
}
const multipleCallIds = this.storage.getItem('multipleCallIds');
if (multipleCallIds)
{
if (multipleCallIds) {
ret.push(
{
title: 'Caller ID',
@@ -288,7 +285,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
)
}
ret.push(
{
@@ -309,11 +306,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
);
return ret;
}
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
@@ -401,11 +398,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
?.replace('.', '')?.toLowerCase()?.trim();
if (audioCodec?.includes('aac'))
audioCodec = 'aac';
else if (audioCodec.includes('g711a'))
else if (audioCodec?.includes('g711a'))
audioCodec = 'pcm_alaw';
else if (audioCodec.includes('g711u'))
else if (audioCodec?.includes('g711u'))
audioCodec = 'pcm_ulaw';
else if (audioCodec.includes('g711'))
else if (audioCodec?.includes('g711'))
audioCodec = 'pcm';
if (vso.audio)
@@ -490,7 +487,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
this.videoStreamOptions = undefined;
super.putSetting(key, value);
this.updateDevice();
this.updateDeviceInfo();
}
@@ -639,6 +636,7 @@ class AmcrestProvider extends RtspProvider {
device.setHttpPortOverride(settings.httpPort?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
device.updateDeviceInfo();
return nativeId;
}

View File

@@ -10,6 +10,8 @@ If you experience any trouble logging in, clear the username and password boxes,
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.
**If you add or remove cameras from your main Arlo account, or share/un-share/re-share cameras with the Arlo account used with this plugin, ensure that you reload this plugin to get the updated camera state from Arlo Cloud.**
## General Setup Notes
* Ensure that your Arlo account's default 2FA option is set to either SMS or email.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/arlo",
"version": "0.8.11",
"version": "0.8.26",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.8.11",
"version": "0.8.26",
"license": "Apache",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
@@ -14,7 +14,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.103",
"version": "0.2.104",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.8.11",
"version": "0.8.26",
"description": "Arlo Plugin for Scrypted",
"license": "Apache",
"keywords": [

View File

@@ -92,7 +92,8 @@ MEDIA_USER_AGENTS = {
class Arlo(object):
BASE_URL = 'my.arlo.com'
AUTH_URL = 'ocapi-app.arlo.com'
BACKUP_AUTH_HOSTS = ['NTIuMjEwLjMuMTIx', 'MzQuMjU1LjkyLjIxMg==', 'MzQuMjUxLjE3Ny45MA==', 'NTQuMjQ2LjE3MS4x']
BACKUP_AUTH_HOSTS = ["NTIuMzEuMTU3LjE4MQ==","MzQuMjQ4LjE1My42OQ==","My4yNDguMTI4Ljc3","MzQuMjQ2LjE0LjI5"]
#BACKUP_AUTH_HOSTS = BACKUP_AUTH_HOSTS[2:3]
TRANSID_PREFIX = 'web'
random.shuffle(BACKUP_AUTH_HOSTS)
@@ -102,6 +103,7 @@ class Arlo(object):
self.password = password
self.event_stream = None
self.request = None
self.logged_in = False
def to_timestamp(self, dt):
if sys.version[0] == '2':
@@ -153,6 +155,7 @@ class Arlo(object):
self.request = Request(mode="cloudscraper")
self.request.session.headers.update(headers)
self.BASE_URL = 'myapi.arlo.com'
self.logged_in = True
def LoginMFA(self):
device_id = str(uuid.uuid4())
@@ -173,6 +176,7 @@ class Arlo(object):
self.request = Request()
try:
#raise Exception("testing backup hosts")
auth_host = self.AUTH_URL
self.request.options(f'https://{auth_host}/api/auth', headers=headers)
logger.info("Using primary authentication host")
@@ -258,6 +262,7 @@ class Arlo(object):
}
self.request.session.headers.update(headers)
self.BASE_URL = 'myapi.arlo.com'
self.logged_in = True
return complete_auth
@@ -761,7 +766,7 @@ class Arlo(object):
raw=True
)
async def StartStream(self, basestation, camera, mode="rtsp"):
async def StartStream(self, basestation, camera, mode="rtsp", eager=True):
"""
This function returns the url of the rtsp video stream.
This stream needs to be called within 30 seconds or else it becomes invalid.
@@ -770,6 +775,9 @@ class Arlo(object):
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()
If 'eager' is True, will return the stream url without waiting for Arlo to report that
the stream has started.
"""
resource = f"cameras/{camera.get('deviceId')}"
@@ -799,6 +807,14 @@ class Arlo(object):
},
headers={"xcloudId":camera.get('xCloudId'), 'User-Agent': ua}
)
if mode == "rtsp":
nl.stream_url_dict['url'] = nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
else:
nl.stream_url_dict['url'] = nl.stream_url_dict['url'].replace(":80", "")
if eager:
trigger(self)
return nl.stream_url_dict['url']
def callback(self, event):
#return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
@@ -806,10 +822,7 @@ class Arlo(object):
return None
properties = event.get("properties", {})
if properties.get("activityState") == "userStreamActive":
if mode == "rtsp":
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
else:
return nl.stream_url_dict['url'].replace(":80", "")
return nl.stream_url_dict['url']
return None
return await self.TriggerAndHandleEvent(

View File

@@ -19,17 +19,13 @@ import requests
from requests.exceptions import HTTPError
from requests_toolbelt.adapters import host_header_ssl
import cloudscraper
from curl_cffi import requests as curl_cffi_requests
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):
@@ -39,7 +35,7 @@ except:
class Request(object):
"""HTTP helper class"""
def __init__(self, timeout=5, mode="curl" if HAS_CURL_CFFI else "cloudscraper"):
def __init__(self, timeout=5, mode="curl"):
if mode == "curl":
logger.debug("HTTP helper using curl_cffi")
self.session = curl_cffi_requests.Session(impersonate="chrome110")

View File

@@ -1,8 +1,9 @@
import asyncio
import json
import sseclient
import threading
import scrypted_arlo_go
from .stream_async import Stream
from .logging import logger
@@ -18,35 +19,45 @@ class EventStream(Stream):
def thread_main(self):
event_stream = self.event_stream
for event in event_stream:
logger.debug(f"Received event: {event}")
if event is None:
logger.info(f"SSE {id(event_stream)} appears to be broken")
while True:
try:
event = event_stream.Next()
except:
logger.info(f"SSE {event_stream.UUID} exited")
if self.shutting_down_stream is event_stream:
self.shutting_down_stream = None
return None
if event.data.strip() == "":
logger.debug(f"Received event: {event}")
if event.strip() == "":
continue
try:
response = json.loads(event.data.strip())
response = json.loads(event.strip())
except json.JSONDecodeError:
continue
if response.get('action') == 'logout':
if self.event_stream_stop_event.is_set() or \
self.shutting_down_stream is event_stream:
logger.info(f"SSE {id(event_stream)} disconnected")
logger.info(f"SSE {event_stream.UUID} disconnected")
self.shutting_down_stream = None
event_stream.Close()
return None
elif response.get('status') == 'connected':
if not self.connected:
logger.info(f"SSE {id(event_stream)} connected")
logger.info(f"SSE {event_stream.UUID} connected")
self.initializing = False
self.connected = True
else:
self.event_loop.call_soon_threadsafe(self._queue_response, response)
self.event_stream = sseclient.SSEClient('https://myapi.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization'), session=self.arlo.request.session)
self.event_stream = scrypted_arlo_go.NewSSEClient(
'https://myapi.arlo.com/hmsweb/client/subscribe?token='+self.arlo.request.session.headers.get('Authorization'),
scrypted_arlo_go.HeadersMap(self.arlo.request.session.headers)
)
self.event_stream.Start()
self.event_stream_thread = threading.Thread(name="EventStream", target=thread_main, args=(self, ))
self.event_stream_thread.setDaemon(True)
self.event_stream_thread.start()
@@ -58,6 +69,7 @@ class EventStream(Stream):
self.reconnecting = True
self.connected = False
self.shutting_down_stream = self.event_stream
self.shutting_down_stream.Close()
self.event_stream = None
await self.start()
while self.shutting_down_stream is not None:

View File

@@ -57,6 +57,9 @@ class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTa
if self.arlo_device.get("parentId") and self.arlo_device["parentId"] != self.arlo_device["deviceId"]:
parent = self.arlo_device["parentId"]
if parent in self.provider.hidden_device_ids:
parent = None
return {
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),

View File

@@ -1,5 +1,8 @@
from __future__ import annotations
from aioice import Candidate
from aiortc import RTCSessionDescription, RTCIceGatherer, RTCIceServer
from aiortc.rtcicetransport import candidate_to_aioice, candidate_from_aioice
import asyncio
import aiohttp
from async_timeout import timeout as async_timeout
@@ -15,12 +18,14 @@ import scrypted_arlo_go
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 .experimental import EXPERIMENTAL
from .arlo.arlo_async import USER_AGENTS
from .base import ArloDeviceBase
from .spotlight import ArloSpotlight, ArloFloodlight, ArloNightlight
from .vss import ArloSirenVirtualSecuritySystem
from .child_process import HeartbeatChildProcess
from .util import BackgroundTaskMixin, async_print_exception_guard
from .rtcpeerconnection import BackgroundRTCPeerConnection
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
@@ -101,6 +106,11 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
"vmc3040s",
]
PTT_IMPL_CHOICES = [
"scrypted-arlo-go",
"aiortc",
]
timeout: int = 30
intercom_session: ArloCameraIntercomSession = None
light: ArloSpotlight = None
@@ -312,6 +322,21 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
else:
return False
@property
def disable_eager_streams(self) -> bool:
if self.storage:
return True if self.storage.getItem("disable_eager_streams") else False
else:
return False
@property
def ptt_impl(self) -> str:
impl = self.storage.getItem("ptt_impl")
if impl is None:
impl = ArloCamera.PTT_IMPL_CHOICES[0]
#self.storage.setItem("ptt_impl", impl)
return impl
@property
def snapshot_throttle_interval(self) -> int:
interval = self.storage.getItem("snapshot_throttle_interval")
@@ -371,7 +396,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
"type": "boolean",
},
)
result.append(
result.extend([
{
"group": "General",
"key": "eco_mode",
@@ -380,8 +405,26 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
"description": "Configures Scrypted to limit the number of requests made to this camera. " + \
"Additional eco mode settings will appear when this is turned on.",
"type": "boolean",
}
)
},
{
"group": "General",
"key": "disable_eager_streams",
"title": "Disable Eager Streams",
"value": self.disable_eager_streams,
"description": "If eager streams are disabled, Scrypted will wait for Arlo Cloud to report that " + \
"the camera stream has started before passing the stream URL to downstream consumers.",
"type": "boolean",
},
])
if self.has_push_to_talk and EXPERIMENTAL:
result.append({
"group": "General",
"key": "ptt_impl",
"title": "Two Way Audio Implementation",
"value": self.ptt_impl,
"description": "Implementation used to perform two-way audio negotiation.",
"choices": ArloCamera.PTT_IMPL_CHOICES,
})
if self.eco_mode:
result.append(
{
@@ -416,7 +459,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
if key in ["wired_to_power"]:
self.storage.setItem(key, value == "true" or value == True)
await self.provider.discover_devices()
elif key in ["eco_mode"]:
elif key in ["eco_mode", "disable_eager_streams"]:
self.storage.setItem(key, value == "true" or value == True)
elif key == "print_debug":
self.logger.info(f"Device Capabilities: {self.arlo_capabilities}")
@@ -457,7 +500,7 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
return await scrypted_sdk.mediaManager.createMediaObject(self.last_picture, "image/jpeg")
pic_url = await asyncio.wait_for(self.provider.arlo.TriggerFullFrameSnapshot(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
self.logger.debug(f"Got snapshot URL for at {pic_url}")
self.logger.debug(f"Got snapshot URL at {pic_url}")
if pic_url is None:
raise Exception("Error taking snapshot: no url returned")
@@ -511,15 +554,15 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
async def _getVideoStreamURL(self, container: str) -> str:
self.logger.info(f"Requesting {container} stream")
url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device, mode=container), timeout=self.timeout)
url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device, mode=container, eager=not self.disable_eager_streams), timeout=self.timeout)
self.logger.debug(f"Got {container} stream URL at {url}")
return url
@async_print_exception_guard
async def getVideoStream(self, options: RequestMediaStreamOptions = None) -> MediaObject:
async def getVideoStream(self, options: RequestMediaStreamOptions = {}) -> MediaObject:
self.logger.debug("Entered getVideoStream")
mso = await self.getVideoStreamOptions(id=options["id"])
mso = await self.getVideoStreamOptions(id=options.get("id", "default"))
mso['refreshAt'] = round(time.time() * 1000) + 30 * 60 * 1000
container = mso["container"]
@@ -555,7 +598,10 @@ class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider,
self.intercom_session = ArloCameraSIPIntercomSession(self)
else:
# we need to do signaling through arlo cloud apis
self.intercom_session = ArloCameraWebRTCIntercomSession(self)
if self.ptt_impl == "scrypted-arlo-go":
self.intercom_session = ArloCameraWebRTCIntercomSession(self)
else:
self.intercom_session = ArloCameraPyAVIntercomSession(self)
await self.intercom_session.initialize_push_to_talk(media)
self.logger.info("Intercom initialized")
@@ -901,4 +947,103 @@ class ArloCameraSIPIntercomSession(ArloCameraIntercomSession):
self.intercom_ffmpeg_subprocess = None
if self.arlo_sip is not None:
self.arlo_sip.Close()
self.arlo_sip = None
self.arlo_sip = None
class ArloCameraPyAVIntercomSession(ArloCameraWebRTCIntercomSession):
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:
# arlo appears to not return a mux id in the response, which
# doesn't play nicely with our webrtc peers. let's add it
sdp += "a=mid:0\r\n"
self.logger.info(f"Arlo response sdp:\n{sdp}")
sdp = RTCSessionDescription(sdp=sdp, type="answer")
self.create_task(self.arlo_pc.setRemoteDescription(sdp))
self.arlo_sdp_answered = True
return self.stop_subscriptions
self.register_task(
self.provider.arlo.SubscribeToSDPAnswers(self.arlo_basestation, self.arlo_device, callback)
)
def start_candidate_answer_subscription(self) -> None:
def callback(candidate):
if self.arlo_pc:
prefix = "a=candidate:"
if candidate.startswith(prefix):
candidate = candidate[len(prefix):]
candidate = candidate.strip()
self.logger.info(f"Arlo response candidate: {candidate}")
candidate = candidate_from_aioice(Candidate.from_sdp(candidate))
if candidate.sdpMid is None:
# arlo appears to not return a mux id in the response, which
# doesn't play nicely with aiortc. let's add it
candidate.sdpMid = 0
self.create_task(self.arlo_pc.addIceCandidate(candidate))
return self.stop_subscriptions
self.register_task(
self.provider.arlo.SubscribeToCandidateAnswers(self.arlo_basestation, self.arlo_device, callback)
)
@async_print_exception_guard
async def initialize_push_to_talk(self, media: MediaObject) -> None:
self.logger.info("Initializing push to talk")
ffmpeg_params = json.loads(await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput.value))
self.logger.debug(f"Received ffmpeg params: {ffmpeg_params}")
session_id, ice_servers = self.provider.arlo.StartPushToTalk(self.arlo_basestation, self.arlo_device)
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")
ice_servers = [
RTCIceServer(urls=ice["url"], credential=ice.get("credential"), username=ice.get("username"))
for ice in ice_servers
]
ice_gatherer = RTCIceGatherer(ice_servers)
await ice_gatherer.gather()
local_candidates = [
f"candidate:{Candidate.to_sdp(candidate_to_aioice(candidate))}"
for candidate in ice_gatherer.getLocalCandidates()
]
log_candidates = '\n'.join(local_candidates)
self.logger.info(f"Local candidates:\n{log_candidates}")
# MediaPlayer/PyAV will block until the intercom stream starts, and it seems that scrypted waits
# for startIntercom to exit before sending data. So, let's do the remaining setup in a coroutine
# so this function can return early.
# This is required even if we use BackgroundRTCPeerConnection, since setting up MediaPlayer may
# block the background thread's event loop and prevent other async functions from running.
async def async_setup():
pc = self.arlo_pc = BackgroundRTCPeerConnection(self.logger)
self.sdp_answered = False
pc.add_rtsp_audio(ffmpeg_params["url"])
offer = await pc.createOffer()
self.logger.info(f"Arlo offer sdp:\n{offer.sdp}")
await pc.setLocalDescription(offer)
self.provider.arlo.NotifyPushToTalkSDP(
self.arlo_basestation, self.arlo_device,
session_id, offer.sdp
)
for candidate in local_candidates:
self.provider.arlo.NotifyPushToTalkCandidate(
self.arlo_basestation, self.arlo_device,
session_id, candidate
)
self.create_task(async_setup())
@async_print_exception_guard
async def shutdown(self) -> None:
if self.arlo_pc is not None:
await self.arlo_pc.close()
self.arlo_pc = None

View File

@@ -27,9 +27,10 @@ from .base import ArloDeviceBase
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
arlo_cameras = None
arlo_basestations = None
all_device_ids: set = set()
_arlo_mfa_code = None
scrypted_devices = None
_arlo = None
_arlo: Arlo = None
_arlo_mfa_complete_auth = None
device_discovery_lock: asyncio.Lock = None
@@ -157,6 +158,23 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
self.storage.setItem("imap_mfa_interval", interval)
return int(interval)
@property
def hidden_devices(self) -> List[str]:
hidden = self.storage.getItem("hidden_devices")
if hidden is None:
hidden = []
self.storage.setItem("hidden_devices", hidden)
return hidden
@property
def hidden_device_ids(self) -> List[str]:
ids = []
for id in self.hidden_devices:
m = re.match(r".*\((.*)\)$", id)
if m is not None:
ids.append(m.group(1))
return ids
@property
def arlo(self) -> Arlo:
if self._arlo is not None:
@@ -530,6 +548,16 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
"value": self.plugin_verbosity == "Verbose",
"type": "boolean",
},
{
"group": "General",
"key": "hidden_devices",
"title": "Hidden Devices",
"description": "Select the Arlo devices to hide in this plugin. Hidden devices will be removed from Scrypted and will "
"not be re-added when the plugin reloads.",
"value": self.hidden_devices,
"multiple": True,
"choices": [id for id in self.all_device_ids],
},
])
return results
@@ -573,6 +601,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
elif key.startswith("imap_mfa"):
self.initialize_imap()
skip_arlo_client = True
elif key == "hidden_devices":
if self._arlo is not None and self._arlo.logged_in:
self._arlo.Unsubscribe()
await self.do_arlo_setup()
skip_arlo_client = True
else:
# force arlo client to be invalidated and reloaded
self.invalidate_arlo_client()
@@ -618,12 +651,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
return await self.discover_devices_impl()
async def discover_devices_impl(self) -> None:
if not self.arlo:
if not self._arlo or not self._arlo.logged_in:
raise Exception("Arlo client not connected, cannot discover devices")
self.logger.info("Discovering devices...")
self.arlo_cameras = {}
self.arlo_basestations = {}
self.all_device_ids = set()
self.scrypted_devices = {}
camera_devices = []
@@ -632,13 +666,20 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
basestations = self.arlo.GetDevices(['basestation', 'siren'])
for basestation in basestations:
nativeId = basestation["deviceId"]
self.all_device_ids.add(f"{basestation['deviceName']} ({nativeId})")
self.logger.debug(f"Adding {nativeId}")
if nativeId in self.arlo_basestations:
self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added")
continue
self.arlo_basestations[nativeId] = basestation
if nativeId in self.hidden_device_ids:
self.logger.info(f"Skipping manifest for basestation {nativeId} ({basestation['modelId']}) as it is hidden")
continue
device = await self.getDevice_impl(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
@@ -657,11 +698,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
self.logger.info(f"Discovered {len(basestations)} basestations")
self.logger.info(f"Discovered {len(self.arlo_basestations)} basestations")
cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"])
for camera in cameras:
nativeId = camera["deviceId"]
self.all_device_ids.add(f"{camera['deviceName']} ({nativeId})")
self.logger.debug(f"Adding {nativeId}")
if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations:
@@ -671,6 +714,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
if nativeId in self.arlo_cameras:
self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added")
continue
if nativeId in self.hidden_device_ids:
self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because it is hidden")
continue
self.arlo_cameras[nativeId] = camera
if camera["deviceId"] == camera["parentId"]:
@@ -683,7 +731,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
manifest = device.get_device_manifest()
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']} parent {camera['parentId']}): {scrypted_interfaces}")
if camera["deviceId"] == camera["parentId"]:
if camera["deviceId"] == camera["parentId"] or camera["parentId"] in self.hidden_device_ids:
provider_to_device_map.setdefault(None, []).append(manifest)
else:
provider_to_device_map.setdefault(camera["parentId"], []).append(manifest)
@@ -701,7 +749,9 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
if len(cameras) != len(camera_devices):
self.logger.info(f"Discovered {len(cameras)} cameras, but only {len(camera_devices)} are usable")
self.logger.info(f"Are all cameras shared with admin permissions?")
self.logger.info("This could be because some cameras are hidden.")
self.logger.info("If a camera is not hidden but is still missing, ensure all cameras shared with "
"admin permissions in the Arlo app.")
else:
self.logger.info(f"Discovered {len(cameras)} cameras")
@@ -726,7 +776,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceL
})
self.logger.debug("Done discovering devices")
# force a settings refresh so the hidden devices list can be updated
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
self.logger.debug(f"Scrypted requested to load device {nativeId}")
async with self.device_discovery_lock:
return await self.getDevice_impl(nativeId)

View File

@@ -0,0 +1,107 @@
from aiortc import RTCPeerConnection
from aiortc.contrib.media import MediaPlayer
import asyncio
import threading
import queue
class BackgroundRTCPeerConnection:
"""Proxy class to use RTCPeerConnection in a background thread.
The purpose of this proxy is to ensure that RTCPeerConnection operations
do not block the main asyncio thread. From testing, it seems that the
close() function blocks until the source RTSP server exits, which we
have no control over. Additionally, since asyncio coroutines are tied
to the event loop they were constructed from, it is not possible to only
run close() in a separate thread. Therefore, each instance of RTCPeerConnection
is launched within its own ephemeral thread, which cleans itself up once
close() completes.
"""
def __init__(self, logger):
self.main_loop = asyncio.get_event_loop()
self.background_loop = asyncio.new_event_loop()
self.logger = logger
self.thread_started = queue.Queue(1)
self.thread = threading.Thread(target=self.__background_main)
self.thread.start()
self.thread_started.get()
def __background_main(self):
self.logger.info(f"Background RTC loop {self.thread.name} starting")
self.pc = RTCPeerConnection()
asyncio.set_event_loop(self.background_loop)
self.thread_started.put(True)
self.background_loop.run_forever()
self.logger.info(f"Background RTC loop {self.thread.name} exiting")
async def __run_background(self, coroutine, await_result=True, stop_loop=False):
fut = self.main_loop.create_future()
def background_callback():
# callback to run on main_loop.
def to_main(result, is_error):
if is_error:
fut.set_exception(result)
else:
fut.set_result(result)
# callback to run on background_loop., after the coroutine completes
def callback(task):
is_error = False
if task.exception():
result = task.exception()
is_error = True
else:
result = task.result()
# send results to the main loop
self.main_loop.call_soon_threadsafe(to_main, result, is_error)
# stopping the loop here ensures that the coroutine completed
# and doesn't raise any "task not awaited" exceptions
if stop_loop:
self.background_loop.stop()
task = self.background_loop.create_task(coroutine)
task.add_done_callback(callback)
# start the callback in the background loop
self.background_loop.call_soon_threadsafe(background_callback)
if not await_result:
return None
return await fut
async def createOffer(self):
return await self.__run_background(self.pc.createOffer())
async def setLocalDescription(self, sdp):
return await self.__run_background(self.pc.setLocalDescription(sdp))
async def setRemoteDescription(self, sdp):
return await self.__run_background(self.pc.setRemoteDescription(sdp))
async def addIceCandidate(self, candidate):
return await self.__run_background(self.pc.addIceCandidate(candidate))
async def close(self):
await self.__run_background(self.pc.close(), await_result=False, stop_loop=True)
def add_rtsp_audio(self, rtsp_url):
"""Adds an audio track to the RTCPeerConnection given a source RTSP url.
This constructs a MediaPlayer in the background thread's asyncio loop,
since MediaPlayer also utilizes coroutines and asyncio.
Note that this may block the background thread's event loop if the RTSP
server is not yet ready.
"""
def add_rtsp_audio_background():
media_player = MediaPlayer(rtsp_url, format="rtsp")
self.pc.addTrack(media_player.audio)
self.background_loop.call_soon_threadsafe(add_rtsp_audio_background)

View File

@@ -1,13 +1,14 @@
paho-mqtt==1.6.1
sseclient==0.0.22
aiohttp==3.8.4
requests==2.28.2
cachetools==5.3.0
scrypted-arlo-go==0.4.0
scrypted-arlo-go==0.5.2
cloudscraper==1.2.71
curl-cffi==0.5.7; platform_machine != 'armv7l'
curl-cffi==0.5.7
async-timeout==4.0.2
beautifulsoup4==4.12.2
--extra-index-url=https://www.piwheels.org/simple/
aiortc==1.5.0
av==9.2.0
--extra-index-url=https://bjia56.github.io/armv7l-wheels/
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/
--prefer-binary

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/bticino",
"version": "0.0.9",
"version": "0.0.11",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/bticino",
"version": "0.0.9",
"version": "0.0.11",
"dependencies": {
"@slyoldfox/sip": "^0.0.6-1",
"sdp": "^3.0.3",
@@ -40,7 +40,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.85",
"version": "0.2.103",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -905,9 +905,9 @@
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
},
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"bin": {
"semver": "bin/semver"
}
@@ -1832,9 +1832,9 @@
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw=="
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
},
"shebang-command": {
"version": "2.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/bticino",
"version": "0.0.9",
"version": "0.0.11",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -28,7 +28,6 @@
],
"pluginDependencies": [
"@scrypted/prebuffer-mixin",
"@scrypted/pam-diff",
"@scrypted/snapshot"
]
},

View File

@@ -27,7 +27,7 @@ const { mediaManager } = sdk;
export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
private session: SipCallSession
private remoteRtpDescription: RtpDescription
private remoteRtpDescription: Promise<RtpDescription>
private audioOutForwarder: dgram.Socket
private audioOutProcess: ChildProcess
private currentMedia: FFmpegInput | MediaStreamUrl
@@ -158,9 +158,10 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
const audioOutForwarder = await createBindZero()
this.audioOutForwarder = audioOutForwarder.server
let address = (await this.remoteRtpDescription).address
audioOutForwarder.server.on('message', message => {
if( this.session )
this.session.audioSplitter.send(message, 40004, this.remoteRtpDescription.address)
this.session.audioSplitter.send(message, 40004, address)
return null
});
@@ -244,7 +245,12 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
client.setKeepAlive(true, 10000)
let sip: SipCallSession
try {
await this.controllerApi.updateStreamEndpoint()
if( !this.incomingCallRequest ) {
// If this is a "view" call, update the stream endpoint to send it only to "us"
// In case of an incoming doorbell event, the C300X is already streaming video to all registered endpoints
await this.controllerApi.updateStreamEndpoint()
}
let rtsp: RtspServer;
const cleanup = () => {
client.destroy();
@@ -271,7 +277,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
sip.onCallEnded.subscribe(cleanup)
// Call the C300X
this.remoteRtpDescription = await sip.callOrAcceptInvite(
this.remoteRtpDescription = sip.callOrAcceptInvite(
( audio ) => {
return [
//TODO: Payload types are hardcoded

View File

@@ -1,13 +1,12 @@
{
"name": "@scrypted/chromecast",
"version": "0.1.55",
"version": "0.1.56",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/chromecast",
"version": "0.1.55",
"hasInstallScript": true,
"version": "0.1.56",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -40,38 +39,39 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.199",
"version": "0.2.103",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
"typedoc": "^0.23.21"
}
},
"../sdk": {
@@ -386,23 +386,24 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@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": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"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.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/chromecast",
"version": "0.1.55",
"version": "0.1.56",
"description": "Send video, audio, and text to speech notifications to Chromecast and Google Home devices",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -2,3 +2,4 @@
out/
node_modules/
dist/
external

View File

@@ -6,4 +6,4 @@ fs
src
.vscode
dist/*.js
node-nat-upnp
external

View File

@@ -3,8 +3,35 @@
1. Log into Scrypted Cloud using the login button.
2. This Scrypted server is now available at https://home.scrypted.app.
See below for additional recommendations.
## Optional but Recommended
## Port Forwarding
1. Set up Port Forwarding with UPNP or Router Forwarding.
2. Use the Advanced Tab to verify Port Forwarding is correctly configured.
1. Open the Firewall and Port Forwarding Settings on the network's router.
2. Use the ports shown in Settings to configure a Port Forwarding rule on the router.
Use the `Test Port Forward` buttin in `Advanced` Settings tab to verify the configuration is correct.
## Custom Domains
Custom Domains can be used with the Cloud Plugin.
Set up a reverse proxy to the https Forward Port shown in settings.
## Cloudflare Tunnels
Scrypted Cloud automatically creates a login free tunnel for remote access.
The following steps are only necessary if you want to associate the tunnel with your existing Cloudflare account to manage it remotely.
1. Create the Tunnel in the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com).
2. Copy the token shown for the tunnel shown in the `install [token]` command. E.g. `cloudflared service install eyJhI344aA...`.
3. Paste the token into the Cloud Plugin Advanced Settings.
4. Add a `Public Hostname` to the tunnel.
* Choose a (sub)domain.
* Service `Type` is `HTTPS` and `URL` is `localhost:port`. Replace the port with `Forward Port` from Cloud Plugin Settings.
* Expand `Additional Application Settings` -> `TLS` menus and enable `No TLS Verify`.
5. Reload Cloud Plugin.
6. Verify Cloudflare successfully connected by observing the `Console` Logs.

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@
"scrypted": {
"name": "Scrypted Cloud",
"type": "API",
"realfs": true,
"interfaces": [
"SystemSettings",
"BufferConverter",
@@ -40,20 +41,18 @@
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"axios": "^1.4.0",
"bpmux": "^8.2.1",
"cloudflared": "^0.4.0",
"exponential-backoff": "^3.1.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
"nat-upnp": "file:./external/node-nat-upnp"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/http-proxy": "^1.17.11",
"@types/ip": "^1.1.0",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
"@types/node": "^20.4.5"
},
"version": "0.1.14"
"version": "0.1.41"
}

View File

@@ -0,0 +1,58 @@
import path from 'path';
// "optionalDependencies": {
// "@greenlock/manager": "^3.1.0",
// "@koush/greenlock": "^4.0.9",
// "acme-dns-01-duckdns": "^3.0.1",
// "greenlock-store-fs": "^3.2.2"
// },
export async function registerDuckDns(duckDnsHostname: string, duckDnsToken: string): Promise<{
cert: string;
chain: string;
privkey: string;
}> {
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
const greenlockD = path.join(pluginVolume, 'greenlock.d');
const Greenlock = require('@koush/greenlock');
const greenlock = Greenlock.create({
packageRoot: process.env.NODE_PATH,
configDir: greenlockD,
packageAgent: 'Scrypted/1.0',
maintainerEmail: 'koushd@gmail.com',
notify: function (event, details) {
if ('error' === event) {
// `details` is an error object in this case
console.error(details);
}
}
});
await greenlock.manager
.defaults({
challenges: {
'dns-01': {
module: 'acme-dns-01-duckdns',
token: duckDnsToken,
},
},
agreeToTerms: true,
subscriberEmail: 'koushd@gmail.com',
});
const altnames = [duckDnsHostname];
const r = await greenlock
.add({
subject: altnames[0],
altnames: altnames
});
const result = await greenlock
.get({ servername: duckDnsHostname });
const { pems } = result;
return pems;
}

View File

@@ -11,12 +11,19 @@ import upnp from 'nat-upnp';
import net from 'net';
import os from 'os';
import path from 'path';
import qs from 'query-string';
import { Duplex } from 'stream';
import { Duplex, Readable } from 'stream';
import tls from 'tls';
import Url from 'url';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { PushManager } from './push';
import { readLine } from '../../../common/src/read-stream';
import { qsparse, qsstringify } from "./qs";
import * as cloudflared from 'cloudflared';
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
import { backOff } from "exponential-backoff";
import ip from 'ip';
import { Deferred } from "@scrypted/common/src/deferred";
// import { registerDuckDns } from "./greenlock";
const { deviceManager, endpointManager, systemManager } = sdk;
@@ -34,9 +41,9 @@ class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
}
async convert(data: Buffer | string, fromMimeType: string): Promise<Buffer> {
if (this.cloud.storageSettings.values.forwardingMode === 'Custom Domain' && this.cloud.storageSettings.values.hostname) {
return Buffer.from(`https://${this.cloud.getHostname()}${await this.cloud.getCloudMessagePath()}/${data}`);
}
const validDomain = this.cloud.getSSLHostname();
if (validDomain)
return Buffer.from(`https://${validDomain}${await this.cloud.getCloudMessagePath()}/${data}`);
const url = `http://127.0.0.1/push/${data}`;
return this.cloud.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.cloud.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
@@ -44,6 +51,8 @@ class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
}
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter, DeviceProvider, HttpRequestHandler {
cloudflareTunnel: string;
cloudflared: Awaited<ReturnType<typeof cloudflared.tunnel>>;
manager = new PushManager(DEFAULT_SENDER_ID);
server: http.Server;
secureServer: https.Server;
@@ -83,23 +92,46 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
placeholder: 'my-server.dyndns.com',
onPut: () => this.scheduleRefreshPortForward(),
},
securePort: {
title: 'Local HTTPS Port',
description: 'The Scrypted Cloud plugin listens on this port for for cloud connections. The router must use UPNP, port forwarding, or a reverse proxy to send requests to this port.',
type: 'number',
onPut: (ov, nv) => {
if (ov && ov !== nv)
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
duckDnsToken: {
hide: true,
title: 'Duck DNS Token',
placeholder: 'xxxxx123456',
onPut: () => {
this.storageSettings.values.duckDnsCertValid = false;
this.log.a('Reload the Scrypted Cloud Plugin to apply the Duck DNS change.');
}
},
duckDnsHostname: {
hide: true,
title: 'Duck DNS Hostname',
placeholder: 'my-scrypted.duckdns.org',
onPut: () => {
this.storageSettings.values.duckDnsCertValid = false;
this.log.a('Reload the Scrypted Cloud Plugin to apply the Duck DNS change.');
}
},
duckDnsCertValid: {
type: 'boolean',
hide: true,
},
upnpPort: {
title: 'External HTTPS Port',
title: 'From Port',
description: "The external network port on router used by port forwarding.",
type: 'number',
onPut: (ov, nv) => {
if (ov !== nv)
this.scheduleRefreshPortForward();
},
},
securePort: {
title: 'Forward Port',
description: 'The internal https port used by the Scrypted Cloud plugin. Connections must be forwarded to this port on this server\'s internal IP address.',
type: 'number',
onPut: (ov, nv) => {
if (ov && ov !== nv)
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
}
},
upnpStatus: {
title: 'UPNP Status',
description: 'The status of the UPNP NAT reservation.',
@@ -119,11 +151,28 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
hide: true,
json: true,
},
cloudflaredTunnelToken: {
group: 'Advanced',
title: 'Cloudflare Tunnel Token',
description: 'Optional: Enter the Cloudflare token from the Cloudflare Dashbaord to track and manage the tunnel remotely.',
onPut: () => {
this.cloudflared?.child.kill();
},
},
cloudflaredTunnelUrl: {
group: 'Advanced',
title: 'Cloudflare Tunnel URL',
description: 'Cloudflare Tunnel URL is a randomized cloud connection, unless a Cloudflare Tunnel Token is provided.',
readonly: true,
mapGet: () => this.cloudflareTunnel || 'Unavailable',
},
register: {
group: 'Advanced',
title: 'Register',
type: 'button',
onPut: () => this.manager.registrationId.then(r => this.sendRegistrationId(r)),
onPut: () => {
this.manager.registrationId.then(r => this.sendRegistrationId(r))
},
description: 'Register server with Scrypted Cloud.',
},
testPortForward: {
@@ -133,12 +182,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
onPut: () => this.testPortForward(),
description: 'Test the port forward connection from Scrypted Cloud.',
},
additionalCorsOrigins: {
title: "Additional CORS Origins",
description: "Debugging purposes only. DO NOT EDIT.",
group: 'CORS',
multiple: true,
combobox: true,
defaultValue: [],
}
});
upnpInterval: NodeJS.Timeout;
upnpClient = upnp.createClient();
upnpStatus = 'Starting';
securePort: number;
randomBytes = crypto.randomBytes(16).toString('base64');
reverseConnections = new Set<Duplex>();
constructor() {
super();
@@ -171,7 +229,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.storageSettings.settings.securePort.onGet = async () => {
return {
hide: this.storageSettings.values.forwardingMode === 'Disabled',
group: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Advanced' : undefined,
title: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare Port' : 'Forward Port',
}
};
@@ -181,6 +240,20 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
};
// this.storageSettings.settings.duckDnsToken.onGet = async () => {
// return {
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
// || this.storageSettings.values.forwardingMode === 'Disabled',
// }
// };
// this.storageSettings.settings.duckDnsHostname.onGet = async () => {
// return {
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
// || this.storageSettings.values.forwardingMode === 'Disabled',
// }
// };
this.log.clearAlerts();
this.storageSettings.settings.securePort.onPut = (ov, nv) => {
@@ -229,17 +302,52 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.storageSettings.values.upnpPort = upnpPort;
// scrypted cloud will replace localhost with requesting ip.
const ip = this.storageSettings.values.forwardingMode === 'Custom Domain'
? this.storageSettings.values.hostname?.toString()
: (await axios(`https://${SCRYPTED_SERVER}/_punch/ip`)).data.ip;
let ip: string;
if (this.storageSettings.values.forwardingMode === 'Custom Domain') {
ip = this.storageSettings.values.hostname?.toString();
if (!ip)
throw new Error('Hostname is required for port Custom Domain setup.');
}
else if (this.storageSettings.values.duckDnsHostname && this.storageSettings.values.duckDnsToken) {
try {
const url = new URL('https://www.duckdns.org/update');
url.searchParams.set('domains', this.storageSettings.values.duckDnsHostname);
url.searchParams.set('token', this.storageSettings.values.duckDnsToken);
await axios(url.toString());
}
catch (e) {
this.console.error('Duck DNS Erorr', e);
throw new Error('Duck DNS Error. See Console Logs.');
}
if (!ip)
throw new Error('Hostname is required for port Custom Domain setup.');
try {
throw new Error('not implemented');
// const pems = await registerDuckDns(this.storageSettings.values.duckDnsHostname, this.storageSettings.values.duckDnsToken);
// this.storageSettings.values.duckDnsCertValid = true;
// const certificate = this.storageSettings.values.certificate;
// const chain = pems.cert.trim() + '\n' + pems.chain.trim();
// if (certificate.certificate !== chain || certificate.serviceKey !== pems.privkey) {
// certificate.certificate = chain;
// certificate.serviceKey = pems.privkey;
// this.storageSettings.values.certificate = certificate;
// deviceManager.requestRestart();
// }
}
catch (e) {
this.console.error("Let's Encrypt Error", e);
throw new Error("Let's Encrypt Error. See Console Logs.");
}
ip = this.storageSettings.values.duckDnsHostname;
}
else {
ip = (await axios(`https://${SCRYPTED_SERVER}/_punch/ip`)).data.ip;
}
if (this.storageSettings.values.forwardingMode === 'Custom Domain')
upnpPort = 443;
this.console.log(`Mapped port https://127.0.0.1:${this.securePort} to https://${ip}:${upnpPort}`);
this.console.log(`Scrypted Cloud mapped https://${ip}:${upnpPort} to https://127.0.0.1:${this.securePort}`);
// the ip is not sent, but should be checked to see if it changed.
if (this.storageSettings.values.lastPersistedUpnpPort !== upnpPort || ip !== this.storageSettings.values.lastPersistedIp) {
@@ -341,21 +449,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
async whitelist(localUrl: string, ttl: number, baseUrl: string): Promise<Buffer> {
const local = Url.parse(localUrl);
const local = new URL(localUrl);
if (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname) {
return Buffer.from(`${baseUrl}${local.path}`);
if (this.getSSLHostname()) {
return Buffer.from(`${baseUrl}${local.pathname}`);
}
if (this.whitelisted.has(local.path)) {
return Buffer.from(this.whitelisted.get(local.path));
if (this.whitelisted.has(local.pathname)) {
return Buffer.from(this.whitelisted.get(local.pathname));
}
const { token_info } = this.storageSettings.values;
if (!token_info)
throw new Error('@scrypted/cloud is not logged in.');
const q = qs.stringify({
scope: local.path,
const q = qsstringify({
scope: local.pathname,
ttl,
})
const scope = await axios(`https://${this.getHostname()}/_punch/scope?${q}`, {
@@ -365,13 +473,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
})
const { userToken, userTokenSignature } = scope.data;
const tokens = qs.stringify({
const tokens = qsstringify({
user_token: userToken,
user_token_signature: userTokenSignature
})
const url = `${baseUrl}${local.path}?${tokens}`;
this.whitelisted.set(local.path, url);
const url = `${baseUrl}${local.pathname}?${tokens}`;
this.whitelisted.set(local.pathname, url);
return Buffer.from(url);
}
@@ -383,6 +491,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
`https://${SCRYPTED_SERVER}`,
// chromecast receiver. move this into google home and chromecast plugins?
'https://koush.github.io',
...this.storageSettings.values.additionalCorsOrigins,
],
});
}
@@ -393,7 +502,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
getAuthority() {
const upnp_port = this.storageSettings.values.forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
const hostname = this.storageSettings.values.forwardingMode === 'Custom Domain' ? this.storageSettings.values.hostname : undefined;
const hostname = this.storageSettings.values.forwardingMode === 'Custom Domain'
? this.storageSettings.values.hostname
: this.storageSettings.values.duckDnsToken && this.storageSettings.values.duckDnsHostname;
if (upnp_port === 443 && !hostname) {
const error = this.storageSettings.values.forwardingMode === 'Custom Domain'
@@ -413,7 +524,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
const { upnp_port, hostname } = this.getAuthority();
const registration_secret = this.storageSettings.values.registrationSecret || crypto.randomBytes(8).toString('base64');
const q = qs.stringify({
const q = qsstringify({
upnp_port,
registration_id,
sender_id: DEFAULT_SENDER_ID,
@@ -479,10 +590,14 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
getSSLHostname() {
const validDomain = (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|| (this.storageSettings.values.duckDnsCertValid && this.storageSettings.values.duckDnsHostname && this.storageSettings.values.upnpPort && `${this.storageSettings.values.duckDnsHostname}:${this.storageSettings.values.upnpPort}`);
return validDomain;
}
getHostname() {
if (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
return this.storageSettings.values.hostname;
return SCRYPTED_SERVER;
return this.getSSLHostname() || SCRYPTED_SERVER;
}
async convert(data: Buffer, fromMimeType: string): Promise<Buffer> {
@@ -518,7 +633,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
async getOauthUrl(): Promise<string> {
const args = qs.stringify({
const args = qsstringify({
hostname: os.hostname(),
registration_id: await this.manager.registrationId,
sender_id: DEFAULT_SENDER_ID,
@@ -553,9 +668,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
this.console.log(req.socket?.remoteAddress, req.url);
const url = Url.parse(req.url);
if (url.path.startsWith('/web/oauth/callback') && url.query) {
const query = qs.parse(url.query);
const url = new URL(req.url, 'https://localhost');
if (url.pathname.startsWith('/web/oauth/callback') && url.search) {
const query = qsparse(url.searchParams);
if (!query.callback_url && query.token_info && query.user_info) {
this.storageSettings.values.token_info = query.token_info;
this.storageSettings.values.lastPersistedRegistrationId = await this.manager.registrationId;
@@ -569,16 +684,19 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
return;
}
}
else if (url.path === '/web/') {
if (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
res.setHeader('Location', `https://${this.storageSettings.values.hostname}/endpoint/@scrypted/core/public/`);
else
else if (url.pathname === '/web/') {
const validDomain = this.getSSLHostname();
if (validDomain) {
res.setHeader('Location', `https://${validDomain}/endpoint/@scrypted/core/public/`);
}
else {
res.setHeader('Location', '/endpoint/@scrypted/core/public/');
}
res.writeHead(302);
res.end();
return;
}
else if (url.path === '/web/component/home/endpoint') {
else if (url.pathname === '/web/component/home/endpoint') {
this.proxy.web(req, res, {
target: googleHomeTarget.toString(),
ignorePath: true,
@@ -586,7 +704,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
});
return;
}
else if (url.path === '/web/component/alexa/endpoint') {
else if (url.pathname === '/web/component/alexa/endpoint') {
this.proxy.web(req, res, {
target: alexaTarget.toString(),
ignorePath: true,
@@ -598,9 +716,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.proxy.web(req, res, { headers }, (err) => console.error(err));
}
const wsHandler = (req: http.IncomingMessage, socket: Duplex, head: Buffer) => this.proxy.ws(req, socket, head, { target: wsTarget.toString(), ws: true, secure: false, headers }, (err) => console.error(err));
const wsHandler = (req: http.IncomingMessage, socket: Duplex, head: Buffer) => {
this.console.log(req.socket?.remoteAddress, req.url);
this.proxy.ws(req, socket, head, { target: wsTarget.toString(), ws: true, secure: false, headers }, (err) => console.error(err))
};
this.server = http.createServer(handler);
this.server.keepAliveTimeout = 0;
this.server.on('upgrade', wsHandler);
// this can be localhost because this is a server initiated loopback proxy through bpmux
this.server.listen(0, '127.0.0.1');
@@ -620,7 +742,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.upnpInterval = setInterval(() => this.refreshPortForward(), 30 * 60 * 1000);
this.refreshPortForward();
const agent = new http.Agent({ maxSockets: Number.MAX_VALUE, keepAlive: true });
this.proxy = HttpProxy.createProxy({
agent,
target: httpTarget,
secure: false,
});
@@ -628,7 +752,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.proxy.on('proxyRes', (res, req) => {
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
res.headers['X-Scrypted-Cloud-Address'] = this.cloudflareTunnel;
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address, X-Scrypted-Cloud-Address';
});
let backoff = 0;
@@ -653,14 +778,21 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
backoff = Date.now();
const random = Math.random().toString(36).substring(2);
this.console.log('scrypted server requested a connection:', random);
const registrationId = await this.manager.registrationId;
this.ensureReverseConnections(registrationId);
const client = tls.connect(4001, SCRYPTED_SERVER, {
rejectUnauthorized: false,
});
client.on('close', () => this.console.log('scrypted server connection ended:', random));
const registrationId = await this.manager.registrationId;
client.write(registrationId + '\n');
const mux: any = new bpmux.BPMux(client as any);
mux.on('handshake', async (socket: Duplex) => {
this.ensureReverseConnections(registrationId);
this.console.warn('mux connection required');
let local: any;
await new Promise(resolve => process.nextTick(resolve));
@@ -675,9 +807,166 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
});
}
});
this.startCloudflared();
}
async startCloudflared() {
while (true) {
try {
this.console.log('starting cloudflared');
this.cloudflared = await backOff(async () => {
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
const version = 2;
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`, `${process.platform}-${process.arch}`);
const bin = path.join(cloudflareD, cloudflared.bin);
if (!fs.existsSync(bin)) {
for (let i = 0; i <= version; i++) {
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`);
rmSync(cloudflareD, {
force: true,
recursive: true,
});
}
if (process.platform === 'darwin' && process.arch === 'arm64') {
const bin = path.join(cloudflareD, cloudflared.bin);
mkdirSync(path.dirname(bin), {
recursive: true,
});
const tmp = `${bin}.tmp`;
const stream = await axios('https://github.com/scryptedapp/cloudflared/releases/download/2023.8.2/cloudflared-darwin-arm64', {
responseType: 'stream',
});
const write = stream.data.pipe(fs.createWriteStream(tmp));
await once(write, 'close');
renameSync(tmp, bin);
fs.chmodSync(bin, 0o0755)
}
else {
await cloudflared.install(bin);
}
}
process.chdir(cloudflareD);
const secureUrl = `https://127.0.0.1:${this.securePort}`;
const args: any = {};
if (this.storageSettings.values.cloudflaredTunnelToken) {
args['run'] = null;
args['--token'] = this.storageSettings.values.cloudflaredTunnelToken;
}
else {
args['--no-tls-verify'] = null;
args['--url'] = secureUrl;
}
const deferred = new Deferred<string>();
const cloudflareTunnel = cloudflared.tunnel(args);
cloudflareTunnel.child.stdout.on('data', data => this.console.log(data.toString()));
cloudflareTunnel.child.stderr.on('data', data => {
const string: string = data.toString();
this.console.error(string);
const lines = string.split('\n');
for (const line of lines) {
if (line.includes('hostname'))
this.console.log(line);
const match = /config=(".*?}")/gm.exec(line)
if (match) {
const json = match[1];
this.console.log(json);
try {
// the config is already json stringified and needs to be double parsed.
// '2023-09-02T21:18:10Z INF Updated to new configuration config="{\"ingress\":[{\"hostname\":\"tunneltest.example.com\", \"originRequest\":{\"noTLSVerify\":true}, \"service\":\"https://localhost:52960\"}, {\"service\":\"http_status:404\"}], \"warp-routing\":{\"enabled\":false}}" version=6'
const parsed = JSON.parse(JSON.parse(json));
const hostname = parsed.ingress?.[0]?.hostname;
if (!hostname)
deferred.resolve(undefined)
else
deferred.resolve(`https://${hostname}`)
}
catch (e) {
this.console.error("Error parsing config", e);
}
}
}
});
cloudflareTunnel.child.on('exit', () => deferred.resolve(undefined));
try {
this.cloudflareTunnel = await Promise.any([deferred.promise, cloudflareTunnel.url]);
if (!this.cloudflareTunnel)
throw new Error('cloudflared exited, the provided cloudflare tunnel token may be invalid.')
}
catch (e) {
this.console.error('cloudflared error', e);
throw e;
}
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${secureUrl}`);
return cloudflareTunnel;
}, {
startingDelay: 60000,
timeMultiple: 1.2,
numOfAttempts: 1000,
maxDelay: 300000,
});
await once(this.cloudflared.child, 'exit');
throw new Error('cloudflared exited.');
}
catch (e) {
this.console.error('cloudflared error', e);
throw e;
}
finally {
this.cloudflared = undefined;
this.cloudflareTunnel = undefined;
}
}
}
ensureReverseConnections(registrationId: string) {
while (this.reverseConnections.size < 10) {
this.createReverseConnection(registrationId);
}
}
async createReverseConnection(registrationId: string) {
const client = tls.connect(4001, SCRYPTED_SERVER, {
rejectUnauthorized: false,
});
this.reverseConnections.add(client);
const random = Math.random().toString(36).substring(2);
let claimed = false;
client.on('close', () => {
this.console.log('scrypted server reverse connection ended:', random);
this.reverseConnections.delete(client);
if (claimed)
this.ensureReverseConnections(registrationId);
});
client.write(`reverse:${registrationId}\n`);
try {
const read = await readLine(client);
}
catch (e) {
return;
}
claimed = true;
let local: any;
await new Promise(resolve => process.nextTick(resolve));
const port = (this.server.address() as any).port;
local = net.connect({
port,
host: '127.0.0.1',
});
await new Promise(resolve => process.nextTick(resolve));
client.pipe(local).pipe(client);
}
async oauthCallback(req: http.IncomingMessage, res: http.ServerResponse) {
const reqUrl = new URL(req.url, 'https://localhost');

16
plugins/cloud/src/qs.ts Normal file
View File

@@ -0,0 +1,16 @@
export function qsstringify(dict: any) {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(dict)) {
params.set(k, v?.toString());
}
return params.toString();
}
export function qsparse(search: URLSearchParams) {
const ret: any = {};
for (const [k, v] of search.entries()) {
ret[k] = v;
}
return ret;
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.1.130",
"version": "0.1.142",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.1.130",
"version": "0.1.142",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -87,34 +87,35 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.21",
"version": "0.2.103",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"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",
"tmp": "^0.2.1",
"typescript": "^4.9.3",
"webpack": "^5.74.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.9",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
@@ -249,12 +250,12 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^18.11.9",
"@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": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
@@ -262,10 +263,11 @@
"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.3",
"webpack": "^5.74.0",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.1.130",
"version": "0.1.142",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",
@@ -24,7 +24,7 @@
],
"scrypted": {
"name": "Scrypted Core",
"type": "DeviceProvider",
"type": "Builtin",
"interfaces": [
"@scrypted/launcher-ignore",
"HttpRequestHandler",
@@ -34,6 +34,7 @@
"Settings"
],
"pluginDependencies": [
"@scrypted/snapshot",
"@scrypted/webrtc"
]
},

View File

@@ -80,7 +80,8 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
for (let i = 0; i < inputs.length; i++) {
ffmpegInput.inputArguments.push(...inputs[i].inputArguments);
filter.push(`[${i}:v] scale=-1:${h},pad=${w}:ih:(ow-iw)/2 [pos${i}];`)
// https://superuser.com/a/891478
filter.push(`[${i}:v] scale=(iw*sar)*min(${w}/(iw*sar)\\,${h}/ih):ih*min(${w}/(iw*sar)\\,${h}/ih),pad=${w}:${h}:(${w}-iw*min(${w}/iw\\,${h}/ih))/2:(${h}-ih*min(${w}/iw\\,${h}/ih))/2 [pos${i}];`)
}
for (let i = inputs.length; i < dim * dim; i++) {
ffmpegInput.inputArguments.push(

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,9 @@
"private": true,
"scripts": {
"dev": "concurrently --names 'server,client' --prefix-colors 'gray,white.bold' --prefix '{time} ({name})\t' --timestamp-format 'HH:mm:ss.SSS' --kill-others npm:serve-server npm:serve",
"serve": "vue-cli-service serve --open",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve --open",
"serve-server": "cd ../../../server && npm run serve",
"build": "vue-cli-service build --dest ../fs/dist",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build --dest ../fs/dist",
"lint": "vue-cli-service lint"
},
"dependencies": {
@@ -26,6 +26,7 @@
"draggabilly": "^2.3.0",
"engine.io-client": "^5.2.0",
"feather-icons": "^4.28.0",
"leaflet": "^1.9.4",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"monaco-editor": "^0.27.0",
@@ -49,7 +50,7 @@
"vue-script2": "^2.1.0",
"vue-slider-component": "^3.2.11",
"vue-swatches": "^1.0.4",
"vue2-google-maps": "^0.10.7",
"vue2-leaflet": "^2.7.1",
"vuetify": "^2.6.13",
"vuex": "^3.6.2",
"webpack-dev-server": "^4.9.2",
@@ -58,6 +59,8 @@
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-proposal-optional-catch-binding": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.13.8",
"@babel/plugin-transform-modules-commonjs": "^7.13.8",
"@babel/plugin-transform-typescript": "^7.13.0",

View File

@@ -10,10 +10,18 @@
<v-card width="300px" class="elevation-24">
<v-card-title style="justify-content: center;" class="headline text-uppercase">Scrypted
</v-card-title>
<v-card-subtitle style="text-align: center;">{{ $store.state.version }}</v-card-subtitle>
<v-card-subtitle v-if="$store.state.loginHostname"
style="text-align: center; font-weight: 300; font-size: .75rem !important; font-family: Quicksand, sans-serif!important;"
class="text-subtitle-2 text-uppercase">
{{ $store.state.version }}
<br />
Logged into: {{ $store.state.loginHostname
}}
</v-card-subtitle>
<v-card-subtitle v-else style="text-align: center;">{{ $store.state.version }}</v-card-subtitle>
<v-list class="transparent">
<v-list-item v-for="application in applications" :key="application.name"
:to="application.to" :href="application.href">
<v-list-item v-for="application in applications" :key="application.name" :to="application.to"
:href="application.href">
<v-icon small>{{ application.icon }}</v-icon>
<v-list-item-title style="text-align: center;">{{ application.name }}
</v-list-item-title>
@@ -27,11 +35,11 @@
<v-card-actions>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://twitter.com/scryptedapp/">
<v-icon small>fab fa-twitter</v-icon>
<v-btn v-on="on" icon href="https://discord.gg/DcFzmBHYGq">
<v-icon small>fab fa-discord</v-icon>
</v-btn>
</template>
<span>Twitter</span>
<span>Discord</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
@@ -43,19 +51,11 @@
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://github.com/koush/scrypted">
<v-icon small>fab fa-github</v-icon>
<v-btn v-on="on" icon href="https://docs.scrypted.app">
<v-icon small>fa fa-file-text</v-icon>
</v-btn>
</template>
<span>Github</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://discord.gg/DcFzmBHYGq">
<v-icon small>fab fa-discord</v-icon>
</v-btn>
</template>
<span>Discord</span>
<span>Documentation</span>
</v-tooltip>
<v-spacer></v-spacer>
<v-tooltip bottom>

View File

@@ -7,6 +7,9 @@
</v-card-title>
<v-card-subtitle v-if="$store.state.hasLogin === false" style="display: flex; justify-content: center;" class="text-uppercase">Create Account
</v-card-subtitle>
<v-card-subtitle v-if="$store.state.loginHostname"
style="text-align: center; font-weight: 300; font-size: .75rem !important; font-family: Quicksand, sans-serif!important;"
class="text-subtitle-2 text-uppercase">Log into: {{ $store.state.loginHostname }}</v-card-subtitle>
<v-container grid-list-md>
<v-layout wrap>
<v-flex xs12>

View File

@@ -6,14 +6,6 @@
</v-card-title>
<v-card-text>Connection interrupted.</v-card-text>
<v-card-actions>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://github.com/koush/scrypted">
<v-icon small>fab fa-github</v-icon>
</v-btn>
</template>
<span>Github</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://discord.gg/DcFzmBHYGq">
@@ -22,6 +14,22 @@
</template>
<span>Discord</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://www.reddit.com/r/Scrypted/">
<v-icon small>fab fa-reddit</v-icon>
</v-btn>
</template>
<span>Reddit</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon href="https://docs.scrypted.app">
<v-icon small>fa fa-file-text</v-icon>
</v-btn>
</template>
<span>Documentation</span>
</v-tooltip>
<v-spacer></v-spacer>
<v-btn text @click="reconnect">Reconnect</v-btn>
</v-card-actions>

View File

@@ -37,6 +37,7 @@ Vue.use(Vue => {
baseUrl: getCurrentBaseUrl(),
});
store.commit("setLoginHostname", undefined);
store.commit("setHasLogin", undefined);
store.commit("setIsLoggedIn", undefined);
store.commit("setUsername", undefined);
@@ -53,6 +54,7 @@ Vue.use(Vue => {
});
return;
}
store.commit("setLoginHostname", response.hostname);
if (!response.expiration) {
store.commit("setHasLogin", response.hasLogin);
throw new Error("Login failed.");

View File

@@ -25,7 +25,8 @@
</v-list-item-content>
</v-list-item>
<div dense nav v-for="category in categories" :key="category">
<v-divider></v-divider>
<template v-for="category in categories" >
<v-subheader>{{ category }}</v-subheader>
<v-list-item v-for="item in filterComponents(category)" :key="item.id" link :to="getComponentViewPath(item.id)"
@@ -39,7 +40,7 @@
</v-list-item-content>
</v-list-item>
<v-divider></v-divider>
</div>
</template>
<v-subheader>Social</v-subheader>
<v-list-item link href="https://discord.gg/DcFzmBHYGq" active-class="purple white--text tile">
<v-list-item-icon>
@@ -72,6 +73,16 @@
</v-list-item>
<v-divider></v-divider>
<v-subheader>Other</v-subheader>
<v-list-item link href="https://docs.scrypted.app" active-class="purple white--text tile">
<v-list-item-icon>
<v-icon small>fa fa-file-text</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Documentation</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item active-class="deep-purple accent-4 white--text">
<v-list-item-icon>
<v-icon small>fa-code-branch</v-icon>
@@ -87,7 +98,7 @@
<script>
import { getComponentViewPath } from "./helpers";
import { checkUpdate } from "./plugin/plugin";
import { checkServerUpdate } from "./plugin/plugin";
export default {
props: {
@@ -160,11 +171,9 @@ export default {
// in which case fall back and determine what the install type is.
const info = await this.$scrypted.systemManager.getComponent("info");
const version = await info.getVersion();
const scryptedEnv = await info.getScryptedEnv();
this.currentVersion = version;
const { updateAvailable } = await checkUpdate(
"@scrypted/server",
version
);
const { updateAvailable } = await checkServerUpdate(version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
this.updateAvailable = updateAvailable;
}
@@ -206,6 +215,4 @@ export default {
},
};
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -101,7 +101,7 @@
</v-layout>
</template>
<script>
import { checkUpdate } from "../plugin/plugin";
import { checkServerUpdate } from "../plugin/plugin";
import Settings from "../../interfaces/Settings.vue"
import {createSystemSettingsDevice} from './system-settings';
@@ -140,10 +140,8 @@ export default {
catch (e) {
// old scrypted servers dont support this call, or it may be unimplemented
// in which case fall back and determine what the install type is.
const { updateAvailable } = await checkUpdate(
"@scrypted/server",
version
);
const scryptedEnv = await info.getScryptedEnv();
const { updateAvailable } = await checkServerUpdate(version, scryptedEnv.SCRYPTED_INSTALL_ENVIRONMENT);
this.updateAvailable = updateAvailable;
}
},

View File

@@ -12,6 +12,7 @@ const pluginSnapshot = require("!!raw-loader!./plugin-snapshot.ts").default.spli
export interface PluginUpdateCheck {
updateAvailable?: string;
updatePublished?: Date;
versions: any;
}
@@ -29,11 +30,17 @@ export async function checkUpdate(npmPackage: string, npmPackageVersion: string)
const { data } = response;
const versions = Object.values(data.versions).sort((a: any, b: any) => semver.compare(a.version, b.version)).reverse();
let updateAvailable: any;
let updatePublished: any;
let latest: any;
if (data["dist-tags"]) {
latest = data["dist-tags"].latest;
if (npmPackageVersion && semver.gt(latest, npmPackageVersion)) {
updateAvailable = latest;
try {
updatePublished = new Date(data["time"][latest]);
} catch {
updatePublished = null;
}
}
}
for (const [k, v] of Object.entries(data['dist-tags'])) {
@@ -54,10 +61,36 @@ export async function checkUpdate(npmPackage: string, npmPackageVersion: string)
}
return {
updateAvailable,
updatePublished,
versions,
};
}
export async function checkServerUpdate(version: string, installEnvironment: string): Promise<PluginUpdateCheck> {
const { updateAvailable, updatePublished, versions } = await checkUpdate(
"@scrypted/server",
version
);
if (installEnvironment == "docker" && updatePublished) {
console.log(`New scrypted server version published ${updatePublished}`);
// check if there is a new docker image available, using 'latest' tag
// this is done so newer server versions in npm are not immediately
// displayed until a docker image has been published
let response: AxiosResponse<any> = await axios.get("https://corsproxy.io?https://hub.docker.com/v2/namespaces/koush/repositories/scrypted/tags/latest");
const { data } = response;
const imagePublished = new Date(data.last_updated);
console.log(`Latest docker image published ${imagePublished}`);
if (imagePublished < updatePublished) {
// docker image is not yet published
return { updateAvailable: null, updatePublished: null, versions: null }
}
}
return { updateAvailable, updatePublished, versions };
}
export async function installNpm(systemManager: SystemManager, npmPackage: string, version?: string): Promise<string> {
const plugins = await systemManager.getComponent('plugins');
await plugins.installNpm(npmPackage, version);

View File

@@ -21,7 +21,7 @@
-webkit-transform-style: preserve-3d;
" playsinline autoplay></video>
<svg :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
<svg width="100%" height="100%" preserveAspectRatio="none" ref="svg" style="
top: 0;
left: 0;
position: absolute;
@@ -141,15 +141,23 @@ export default {
let contents = "";
const toPercent= (v, d) => {
return `${v / d * 100}%`;
}
for (const detection of this.lastDetection.detections || []) {
if (!detection.boundingBox) continue;
const svgScale = this.svgWidth / 1080;
const sw = 2 * svgScale;
const s = "red";
const x = detection.boundingBox[0];
const y = detection.boundingBox[1];
const w = detection.boundingBox[2];
const h = detection.boundingBox[3];
let x = detection.boundingBox[0];
let y = detection.boundingBox[1];
let w = detection.boundingBox[2];
let h = detection.boundingBox[3];
x = toPercent(x, this.lastDetection?.inputDimensions?.[0] || 1920);
y = toPercent(y, this.lastDetection?.inputDimensions?.[1] || 1080);
w = toPercent(w, this.lastDetection?.inputDimensions?.[0] || 1920);
h = toPercent(h, this.lastDetection?.inputDimensions?.[1] || 1080);
let t = ``;
let toffset = 0;
if (detection.score && detection.className !== 'motion') {
@@ -159,11 +167,11 @@ export default {
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
const fs = 20 * svgScale;
const fs = 20;
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="2" fill="none" />
<text x="${x}" y="${y}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
<text x="${x}" y="${y}" font-size="${fs}" fill="white">${t}</text>
`;
contents += box;
}

View File

@@ -1,13 +1,37 @@
<template>
<v-btn text color="primary" @click="onClick">Login</v-btn>
<div>
<v-dialog v-model="loginDialog" max-width="300px">
<v-card>
<v-card-title>Login Required</v-card-title>
<v-card-text>Scrypted Management Console is currently inside a browser iframe. For web security, a new tab will be
opened, and the
browser may prompt to log into this server again.
<br />
<br />
<b>Home Assistant Addon installations must create a new Administrator user</b> within the Scrypted Users sidebar menu to log in from outside of Home Assistant.
</v-card-text>
<v-card-actions>
<v-spacer>
</v-spacer>
<v-btn icon @click="loginDialog = false">Cancel</v-btn>
<v-btn icon @click="onClickContinue">OK</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-btn text color="primary" @click="onClick">Login</v-btn>
</div>
</template>
<script>
import qs from 'query-string';
import RPCInterface from "./RPCInterface.vue";
import { getCurrentBaseUrl } from '../../../../../packages/client/src';
export default {
mixins: [RPCInterface],
data() {
return {
loginDialog: false,
};
},
methods: {
onChange() { },
isIFrame() {
@@ -17,15 +41,18 @@ export default {
return true;
}
},
onClickContinue: async function () {
const endpointManager = this.$scrypted.endpointManager;
const ep = await endpointManager.getPublicLocalEndpoint();
const u = new URL(ep);
u.hash = window.location.hash;
u.pathname = '/endpoint/@scrypted/core/public/';
window.open(u.toString(), '_blank');
},
onClick: async function () {
// must escape iframe for login.
if (this.isIFrame()) {
const endpointManager = this.$scrypted.endpointManager;
const ep = await endpointManager.getPublicLocalEndpoint();
const u = new URL(ep);
u.hash = window.location.hash;
u.pathname = '/endpoint/@scrypted/core/public/';
window.open(u.toString(), '_blank');
this.loginDialog = true;
return;
}

View File

@@ -1,31 +1,55 @@
<template>
<GmapMap
<l-map
:center="center"
:zoom="zoom"
ref="mapRef"
style="height: 400px"
style="height: 400px;"
:options="{
mapTypeControl: false,
fullscreenControl: false,
}"
zoomControl: false,
attributionControl: false,
dragging: false,
doubleClickZoom: false,
boxZoom: false,
scrollWheelZoom: false,
touchZoom: false,
}"
>
<GmapMarker v-if="position" :position="position" :label="lazyValue.name" />
</GmapMap>
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="position"></l-marker>
<l-control-attribution position="bottomright" :prefix="prefix"></l-control-attribution>
</l-map>
</template>
<script>
import { latLng, Icon } from "leaflet";
import { LMap, LTileLayer, LMarker, LControlAttribution } from "vue2-leaflet";
import 'leaflet/dist/leaflet.css';
import RPCInterface from "../RPCInterface.vue";
// https://vue2-leaflet.netlify.app/quickstart/#marker-icons-are-missing
delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
export default {
mixins: [RPCInterface],
components: {
LMap,
LTileLayer,
LMarker,
LControlAttribution,
},
data () {
return {
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
prefix: '<a target="blank" href="https://leafletjs.com/">Leaflet</a>',
attribution: '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
};
},
computed: {
center() {
if (!this.position) {
return {
lat: 0,
lng: 0
};
}
return this.position;
},
zoom() {
@@ -33,12 +57,9 @@ export default {
},
position() {
if (!this.lazyValue.position) {
return;
return latLng(0, 0);
}
return {
lat: this.lazyValue.position.latitude,
lng: this.lazyValue.position.longitude
};
return latLng(this.lazyValue.position.latitude, this.lazyValue.position.longitude);
}
}
};

View File

@@ -3,7 +3,6 @@ import './plugins/icons';
import vuetify from './plugins/vuetify';
import './plugins/script2';
import './plugins/clipboard';
import './plugins/maps';
import './plugins/async-computed';
import './plugins/apexcharts';
import './plugins/is-mobile';

View File

@@ -87,6 +87,7 @@ import {
faTimeline,
faMobile,
faBoltLightning,
faFileText,
} from '@fortawesome/free-solid-svg-icons'
import {
@@ -180,6 +181,7 @@ const icons: IconDefinition[] =[
faTimeline,
faMobile,
faBoltLightning,
faFileText,
];
for (var icon in icons) {

View File

@@ -1,16 +0,0 @@
import Vue from 'vue';
import * as VueGoogleMaps from 'vue2-google-maps';
Vue.use(VueGoogleMaps, {
load: {
key: 'AIzaSyCBbKhH_IM1oIZMOO65xOnzgDDrmC2lAoc',
libraries: 'places', // This is required if you use the Autocomplete plugin
// OR: libraries: 'places,drawing'
// OR: libraries: 'places,drawing,visualization'
// (as you require)
//// If you want to set the version, you can do so:
// v: '3.26',
},
});

View File

@@ -16,7 +16,8 @@ const store = new Vuex.Store({
isLoggedIn: undefined,
isLoggedIntoCloud: undefined,
isConnected: undefined,
hasLogin: undefined
hasLogin: undefined,
loginHostname: undefined,
},
mutations: {
setSystemState: function (store, systemState) {
@@ -65,6 +66,9 @@ const store = new Vuex.Store({
setHasLogin(store, hasLogin) {
store.hasLogin = hasLogin;
},
setLoginHostname(store, hostname) {
store.loginHostname = hostname;
},
setVersion(store, version) {
store.version = version;
},

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/coreml",
"version": "0.1.21",
"version": "0.1.28",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.21",
"version": "0.1.28",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -34,11 +34,12 @@
"type": "API",
"interfaces": [
"Settings",
"ObjectDetection"
"ObjectDetection",
"ObjectDetectionPreview"
]
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.21"
"version": "0.1.28"
}

View File

@@ -3,12 +3,10 @@ from __future__ import annotations
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
@@ -37,11 +35,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
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"
model = "yolov8n_320"
self.yolo = "yolo" in model
self.yolov8 = "yolov8" in model
model_version = "v2"
@@ -111,6 +105,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
"ssdlite_mobilenet_v2",
"yolov4-tiny",
"yolov8n",
"yolov8n_320",
],
"value": model,
},

View File

@@ -1,5 +1,5 @@
#
coremltools
coremltools==7.0b2
# 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

@@ -1,12 +1,12 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.1",
"version": "0.0.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/doorbird",
"version": "0.0.1",
"version": "0.0.2",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"doorbird": "^2.1.2"
@@ -36,7 +36,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.97",
"version": "0.2.103",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -45,7 +45,6 @@
"axios": "^0.21.4",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"cross-env": "^7.0.3",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/doorbird",
"version": "0.0.1",
"version": "0.0.2",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -29,7 +29,6 @@
],
"pluginDependencies": [
"@scrypted/prebuffer-mixin",
"@scrypted/pam-diff",
"@scrypted/snapshot"
]
},

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/ffmpeg-camera",
"version": "0.0.21",
"version": "0.0.22",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/ffmpeg-camera",
"version": "0.0.21",
"version": "0.0.22",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
@@ -36,7 +36,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.86",
"version": "0.2.103",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/ffmpeg-camera",
"version": "0.0.21",
"version": "0.0.22",
"description": "FFmpeg Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -32,7 +32,6 @@
],
"pluginDependencies": [
"@scrypted/prebuffer-mixin",
"@scrypted/pam-diff",
"@scrypted/snapshot"
]
},

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.128",
"version": "0.0.129",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/hikvision",
"version": "0.0.128",
"version": "0.0.129",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.128",
"version": "0.0.129",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -570,6 +570,7 @@ class HikvisionProvider extends RtspProvider {
device.setHttpPortOverride(settings.httpPort?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
device.updateDeviceInfo();
return nativeId;
}

Some files were not shown because too many files have changed in this diff Show More