Compare commits

...

433 Commits

Author SHA1 Message Date
Koushik Dutta
bd7c7de8a5 Update package.json 2024-01-12 16:37:55 -08:00
Koushik Dutta
a06e786d19 client: Fix cors credentials 2024-01-12 15:51:06 -08:00
Koushik Dutta
dff05e733e server: update lockfile 2024-01-12 15:39:29 -08:00
Koushik Dutta
76e34af149 postrelease 2024-01-12 15:13:29 -08:00
Koushik Dutta
4f2e9e88e1 Merge branch 'main' of github.com:koush/scrypted 2024-01-12 09:27:04 -08:00
Koushik Dutta
2761b0745a client: remove http-auth-utils dependency 2024-01-12 09:26:09 -08:00
Brett Jia
ea78b7f59e core: fix scheduler (#1259) 2024-01-12 09:10:31 -08:00
Koushik Dutta
b8d233f08d videoanalysis: cpu usage wip 2024-01-11 20:08:57 -08:00
Koushik Dutta
f4e93c82a2 onvif: publish with latest http-auth-utils 2024-01-11 20:08:39 -08:00
Koushik Dutta
c72c34f954 common: remove node-fetch 2024-01-11 20:06:51 -08:00
Koushik Dutta
baec4e71da remove axios-digest-auth 2024-01-11 19:31:18 -08:00
Koushik Dutta
245c6e3006 doorbird: remove axios digest auth 2024-01-11 19:29:15 -08:00
Koushik Dutta
566c18251c snapshot: add support for periodic snapshots via http 2024-01-11 13:15:18 -08:00
Koushik Dutta
f18e58a108 sdk/homekit/snapshot: simplify periodic snapshot requests. remove homekit debouncer. 2024-01-11 11:29:03 -08:00
Koushik Dutta
5c6ea09d3e videoanalysis: implement dynamic cpu performance profiling 2024-01-11 11:04:41 -08:00
Koushik Dutta
646a9f214a snapshot: Update url fetcher 2024-01-11 09:58:23 -08:00
Koushik Dutta
6506c4236f various: fixup http lib 2024-01-11 00:37:21 -08:00
Koushik Dutta
a3e27ce8f8 Merge branch 'main' of github.com:koush/scrypted 2024-01-11 00:35:26 -08:00
Koushik Dutta
a54d5a5f59 cli: remove axios 2024-01-11 00:35:21 -08:00
Koushik Dutta
7136759f8f Update install-scrypted-proxmox.sh 2024-01-11 00:32:48 -08:00
Koushik Dutta
049d9898b3 http/auth: simplify 2024-01-10 20:45:50 -08:00
Koushik Dutta
6d9e21b7b8 server: Fix import 2024-01-10 16:27:10 -08:00
Koushik Dutta
bfd1aef5d1 client: fixup 2024-01-10 16:23:51 -08:00
Koushik Dutta
5f02e6a272 various: more http refactoring 2024-01-10 16:08:38 -08:00
Koushik Dutta
56bbf00edc various: update http auth utils usage 2024-01-10 13:16:00 -08:00
Koushik Dutta
0c66456a87 sdk: limit chunks to 1. add support for multiple chunks. 2024-01-10 13:06:04 -08:00
Koushik Dutta
6b589b8d5a postrelease 2024-01-10 12:56:02 -08:00
Koushik Dutta
aef218c653 server: update http fetch usage, add support for relative require 2024-01-10 12:50:45 -08:00
Koushik Dutta
aea24a84f0 cameras: update http utils again 2024-01-10 11:13:37 -08:00
Koushik Dutta
bab3bef0d1 cameras: update http utils again 2024-01-10 11:13:17 -08:00
Koushik Dutta
56bda46ae9 webrtc: fix multiple ipv4/ipv6 address usage 2024-01-09 22:44:33 -08:00
Koushik Dutta
368b0fc26a onvif: fix motion resbuscribe 2024-01-09 20:17:13 -08:00
Koushik Dutta
fa50d6faab various: remove axios digest auth 2024-01-09 13:52:51 -08:00
Koushik Dutta
115d168cd3 snapshot: remove axios 2024-01-09 13:12:44 -08:00
Koushik Dutta
7ab93e8883 Merge branch 'main' of github.com:koush/scrypted 2024-01-09 13:04:09 -08:00
Koushik Dutta
e25cf860f0 common: replacement implementation for axios digest auth 2024-01-09 13:04:00 -08:00
Koushik Dutta
b5b56d81a8 Proxmox: fix error check 2024-01-08 17:58:44 -08:00
Koushik Dutta
e7b6cb021c Update install-scrypted-proxmox.sh 2024-01-08 10:45:05 -08:00
Koushik Dutta
e96f374432 Update install-scrypted-proxmox.sh 2024-01-08 10:03:26 -08:00
Koushik Dutta
c4126d7569 onvif: publish 2024-01-08 08:54:36 -08:00
Billy Zoellers
074ba733a3 ONVIF: ignore invalid (array) ONVIF events from Axis cameras (#1253) 2024-01-08 08:50:47 -08:00
Koushik Dutta
622f494703 onvif: publish fixes 2024-01-07 20:03:35 -08:00
Koushik Dutta
28c8b97c26 ha: fix submodule 2024-01-07 20:02:03 -08:00
Brett Jia
c0af17b38d core: query latest docker image via scrypted server (#1247) 2024-01-06 23:54:38 -08:00
Koushik Dutta
44a82a1afa reolink: publish 2024-01-06 16:59:26 -08:00
Koushik Dutta
c9b4c14e35 Merge branch 'main' of github.com:koush/scrypted 2024-01-06 16:59:13 -08:00
Koushik Dutta
76534f1368 onvif: fix unsubscribe crash 2024-01-06 16:58:50 -08:00
Koushik Dutta
3f9a863961 onvif/reolink: fix onvif motion debouncing on motion stop 2024-01-06 10:28:17 -08:00
Koushik Dutta
83b0ebd5f0 docker: fix nvidia script 2024-01-06 09:33:43 -08:00
Koushik Dutta
b923a4ea27 docker: fix nvidia script 2024-01-06 09:32:05 -08:00
Koushik Dutta
941d213087 docker: fix nvidia script 2024-01-06 09:30:09 -08:00
Koushik Dutta
0e11b8b4c5 docker: remove pip reinstall step 2024-01-06 09:29:58 -08:00
Koushik Dutta
1be14878d1 docker: fix nvidia 2024-01-06 09:26:08 -08:00
Koushik Dutta
3e323911c7 proxmox: install script shoud auto boot 2024-01-05 12:27:11 -08:00
Koushik Dutta
ca36ab2d2d ha: publish 2024-01-04 12:27:17 -08:00
Koushik Dutta
a80e95912e python plugins: trigger redownload of deps 2024-01-04 11:35:24 -08:00
Koushik Dutta
4432d8dd67 postrelease 2024-01-04 10:38:49 -08:00
Koushik Dutta
2d83d3ba97 external: update axios digfest auth 2024-01-04 10:03:00 -08:00
Koushik Dutta
1078faef62 common: fix regression with not sending sigterm to ffmpeg 2024-01-04 10:00:41 -08:00
Koushik Dutta
47a981e15a various: Update axios digest auth 2024-01-04 09:59:27 -08:00
Koushik Dutta
e253cab555 opencv: use wheel for aarch64 2024-01-03 19:00:51 -08:00
Koushik Dutta
80124ca83b ha/proxmox: release 2024-01-03 18:55:51 -08:00
Koushik Dutta
f883d8738c docker: update base image 2024-01-03 13:15:05 -08:00
Koushik Dutta
bdca3b545c docker: update base image 2024-01-03 13:09:17 -08:00
Koushik Dutta
3ba02c44ab docker: disable node 20 buikds 2024-01-03 11:13:43 -08:00
Koushik Dutta
fe4733bb97 gha: enable ghcr common publishing 2024-01-03 11:12:48 -08:00
Koushik Dutta
7ff893fbd3 docker: switch to ghcr 2024-01-03 11:05:55 -08:00
Chris Jones
e79c544690 Fix Home Assistant Addon backup exclusions. Closes #1239 (#1240) 2024-01-03 07:23:09 -08:00
Koushik Dutta
13bf44ce50 postrelease 2024-01-02 23:08:53 -08:00
Koushik Dutta
544531122d server: stop console spam if mixin is deleted 2024-01-02 22:13:13 -08:00
Koushik Dutta
778f0b7ad1 proxmox: update script 2024-01-02 14:03:17 -08:00
Koushik Dutta
35e8a86593 snapshot: warn qemu cpu 2024-01-02 13:55:17 -08:00
Koushik Dutta
c370773af4 common: missing file 2024-01-02 12:12:51 -08:00
Koushik Dutta
184f293b92 core: fix new script creation 2024-01-02 09:13:06 -08:00
Koushik Dutta
6e10172f7e Merge branch 'main' of github.com:koush/scrypted 2024-01-01 21:53:12 -08:00
Koushik Dutta
c5ae2cd539 onvif: forgotten file re motion sensor reset 2024-01-01 21:52:54 -08:00
Koushik Dutta
e40566e89c reolink: ptz 2024-01-01 21:52:43 -08:00
Koushik Dutta
59ccd4e4d8 docker: remove armv7 2024-01-01 18:24:51 -08:00
Koushik Dutta
ae80eb7727 docker: remove armv7 2024-01-01 18:24:35 -08:00
Koushik Dutta
f054172dcf postrelease 2024-01-01 15:43:52 -08:00
Koushik Dutta
0d7fb9e13c postrelease 2024-01-01 14:46:06 -08:00
Koushik Dutta
a526816b07 sdk/server: add mechanism for requesting device refresh 2024-01-01 14:44:00 -08:00
Koushik Dutta
563e16b08f sdk: update 2024-01-01 14:42:33 -08:00
Koushik Dutta
fd56990d64 core: watch for script worker exits 2024-01-01 14:12:27 -08:00
Koushik Dutta
d7aaf57e8f core: move scripts into their own workers. 2024-01-01 14:10:17 -08:00
Koushik Dutta
a2d50d54d5 proxmox: delete coral, make user install it. 2024-01-01 00:21:59 -08:00
Koushik Dutta
1f86745252 proxmox: install script 2024-01-01 00:15:09 -08:00
Koushik Dutta
1f4343ba2e proxmox: install script 2024-01-01 00:14:54 -08:00
Koushik Dutta
3ad311898f docker: pillow simd 2023-12-31 22:12:54 -08:00
Koushik Dutta
799e5b53c7 Merge branch 'main' of github.com:koush/scrypted 2023-12-31 21:57:08 -08:00
Koushik Dutta
833e5b34ab docker/lxc: update 2023-12-31 21:57:03 -08:00
Koushik Dutta
c99ac28e89 wyze: write_eof may fail? 2023-12-30 18:53:53 -08:00
Koushik Dutta
841475cb97 wyze: better ffmpeg kill method 2023-12-30 18:53:15 -08:00
Koushik Dutta
4b03a3a458 reolink: debounce motion on motion end 2023-12-28 13:32:14 -08:00
Koushik Dutta
d686dd815c videoanalysis: fix bug with modified default classes 2023-12-28 12:20:28 -08:00
Koushik Dutta
e0386a8922 tensorflow-lite: remove python codecs dependency 2023-12-28 12:13:36 -08:00
Koushik Dutta
9ef3478c88 Merge branch 'main' of github.com:koush/scrypted 2023-12-28 12:08:55 -08:00
Koushik Dutta
690d160f33 openvino: update openvino dependency 2023-12-28 12:08:37 -08:00
Koushik Dutta
59ff987bca server: update deps 2023-12-28 09:56:42 -08:00
Koushik Dutta
1669f17c96 postbeta 2023-12-27 22:38:23 -08:00
Koushik Dutta
b0bfd4e05e wyze: update dwb 2023-12-27 21:56:32 -08:00
Koushik Dutta
7152671913 wyze: update dwb 2023-12-27 21:55:46 -08:00
Koushik Dutta
537c178699 postbeta 2023-12-27 21:51:37 -08:00
Koushik Dutta
77ecee110b videoanalysis: fixup detection types for nvr 2023-12-27 21:40:50 -08:00
Koushik Dutta
29b163a7d8 wyze: update dwb 2023-12-27 21:01:51 -08:00
Koushik Dutta
5d74e80e90 postbeta 2023-12-27 20:13:10 -08:00
Koushik Dutta
764b6441d5 postbeta 2023-12-27 20:12:58 -08:00
Koushik Dutta
e2c43cb4ff onvif: missing file 2023-12-27 20:12:34 -08:00
Koushik Dutta
7b6d094e8c wyze: improve cpu usage 2023-12-27 20:12:06 -08:00
Koushik Dutta
3dfb2db02a snapshot: update deps 2023-12-27 20:11:45 -08:00
Koushik Dutta
e5a549db6a update werift 2023-12-27 19:31:34 -08:00
Koushik Dutta
d500c815fe local: add support for intel and tflite installation 2023-12-26 16:23:59 -08:00
Koushik Dutta
5f71c59b5a local: add support for intel and tflite installation 2023-12-26 16:15:05 -08:00
Koushik Dutta
27407942a5 local: add support for intel and tflite installation 2023-12-26 16:14:55 -08:00
Koushik Dutta
11b6963744 docker: remove libvips 2023-12-26 15:28:39 -08:00
Koushik Dutta
b9ee8866f0 docker: logging 2023-12-26 15:20:21 -08:00
Koushik Dutta
bc80d31eaa docker: add logging 2023-12-26 15:14:19 -08:00
Koushik Dutta
327688232c docker: more prompt fixups 2023-12-26 14:25:30 -08:00
Koushik Dutta
2883a4ce46 docker: more prompt fixups 2023-12-26 14:24:04 -08:00
Koushik Dutta
1ad2fb915d docker: more prompt fixups 2023-12-26 14:23:28 -08:00
Koushik Dutta
fb701a32b7 docker: intel graphics script 2023-12-26 14:19:56 -08:00
Koushik Dutta
7a8c661bb3 docker: remove bash invocation 2023-12-26 14:06:53 -08:00
Koushik Dutta
54d72fb371 docker: gpg should overwrite 2023-12-26 13:56:44 -08:00
Koushik Dutta
e48812cec7 linux: SERVICE_USER_ROOT flag 2023-12-26 13:53:46 -08:00
Koushik Dutta
6c2db072c4 linux: warn root install 2023-12-26 13:51:33 -08:00
Koushik Dutta
4bf2c0b614 docker: add node gpg 2023-12-26 13:49:13 -08:00
Koushik Dutta
a93cdb0ae4 docker: fix quoting 2023-12-26 13:21:24 -08:00
Koushik Dutta
ff85b7abc6 docker: update all node install scripts 2023-12-26 13:17:32 -08:00
Koushik Dutta
46dfb8d98e docker: Fix node version arg 2023-12-26 13:11:51 -08:00
Koushik Dutta
5240200f0f docker: update node install script 2023-12-26 13:04:56 -08:00
Koushik Dutta
3bcb94fc6b reolink: increase motion timeout 2023-12-25 12:03:14 -08:00
Koushik Dutta
a596bc712c reolink: add support for native object detection 2023-12-25 12:02:25 -08:00
Koushik Dutta
f6d2dc456e server: fix bug with constantly reissuing certs 2023-12-24 21:49:08 -08:00
Koushik Dutta
441cce239e wyze: set video timeout to 5s 2023-12-23 19:15:27 -08:00
Koushik Dutta
3016df32d1 Merge branch 'main' of github.com:koush/scrypted 2023-12-23 18:38:11 -08:00
Koushik Dutta
5bd8ed0b1a wyze: fix video inactivity leaks 2023-12-23 18:38:07 -08:00
Koushik Dutta
79286a5138 docker: allow root install 2023-12-22 22:56:09 -08:00
Koushik Dutta
8874e01072 wyze: fix broken setting 2023-12-22 15:39:28 -08:00
Koushik Dutta
0223a9f0f6 wyze: use correct fps when fetching frames 2023-12-22 11:31:58 -08:00
Koushik Dutta
890c2667d0 wyze: implement ptz 2023-12-22 10:34:07 -08:00
Koushik Dutta
ca14764e17 wyze: suppress ffmpeg 2023-12-22 09:13:32 -08:00
Koushik Dutta
1030d7d03c rebroadcast: fix rtsp server with RFC4571 parser 2023-12-22 08:23:43 -08:00
Koushik Dutta
2d40320868 ha: update 2023-12-21 22:53:01 -08:00
Koushik Dutta
3e32c3d019 werift: update 2023-12-21 22:50:32 -08:00
Koushik Dutta
1f9fa3966f wyze: send codec info on startup 2023-12-21 22:49:59 -08:00
Koushik Dutta
c2d86237d6 wyze: refactor for multiprocessing 2023-12-21 21:55:34 -08:00
Koushik Dutta
5cfcfafc00 postrelease 2023-12-21 21:51:42 -08:00
Koushik Dutta
35b4028a47 rpc: how did this ever work? 2023-12-21 21:37:11 -08:00
Koushik Dutta
bf6038a5d3 postbeta 2023-12-21 21:37:01 -08:00
Koushik Dutta
e1b2216543 docker: correct gh action 2023-12-21 21:25:10 -08:00
Koushik Dutta
89c1682421 docker: fixup 2023-12-21 21:24:41 -08:00
Koushik Dutta
5a4c527c59 docker: fixup gh action 2023-12-21 21:22:29 -08:00
Koushik Dutta
9d9c10aa1e docker: remove armv7 2023-12-21 21:21:53 -08:00
Koushik Dutta
ccf20a5fca docker: remove arm7 2023-12-21 21:20:52 -08:00
Koushik Dutta
692e7964a7 rebroadcast: fixes for wyze 2023-12-21 21:10:17 -08:00
Koushik Dutta
57e38072b1 server: fix static vs instance properties 2023-12-21 21:10:09 -08:00
Koushik Dutta
4e8e862482 postbeta 2023-12-21 21:09:41 -08:00
Koushik Dutta
eddcef8e54 rebroadcast: add trigger to reload video streams 2023-12-21 10:39:19 -08:00
Koushik Dutta
09edc6d75e wyze: add bitrate select 2023-12-21 10:28:25 -08:00
Koushik Dutta
72c7c43d79 wyze: fix race condition around teardown 2023-12-21 00:20:49 -08:00
Koushik Dutta
805f471ff9 wyze: add audio support 2023-12-21 00:10:43 -08:00
Koushik Dutta
6f797d53ec rebroadcast/webrtc: fix audio sample rate assumptions 2023-12-21 00:09:20 -08:00
Koushik Dutta
4903a0efcd sdk: update 2023-12-20 23:54:52 -08:00
Koushik Dutta
36e3fcf429 rebroadcast: fix support for rfc4571 2023-12-20 23:50:08 -08:00
Koushik Dutta
78a126fe0a wyze: make plugin visible on npm 2023-12-20 19:09:51 -08:00
Koushik Dutta
5029baf2d4 wyze: add support for substream 2023-12-20 18:05:06 -08:00
Koushik Dutta
769bc014a8 wyze: disable substream 2023-12-20 14:51:37 -08:00
Koushik Dutta
096700486a wyze: initial commit 2023-12-20 14:51:20 -08:00
Koushik Dutta
b3a7d6be9c videoanalysis: smart motion sensor now retains the last thumbnail 2023-12-20 09:36:02 -08:00
Koushik Dutta
05751bce44 Merge branch 'main' of github.com:koush/scrypted 2023-12-19 09:19:51 -08:00
Koushik Dutta
dced62a527 videoanalysis: publish 2023-12-19 09:19:46 -08:00
1vivy
359f1cfc2f docker: fixup nvidia example (#1228) 2023-12-18 15:19:56 -08:00
Koushik Dutta
d4cae8abbb common: add packet loss util to browser signaling session 2023-12-18 12:36:57 -08:00
Koushik Dutta
0e6b3346ed common: add packet loss util to browser signaling session 2023-12-18 12:34:36 -08:00
Koushik Dutta
2409cc457c sdk: remove startId to simplify implementations 2023-12-18 09:41:51 -08:00
Koushik Dutta
0b794aa381 sdk: remove reverseOrder to simplify implementations 2023-12-18 09:34:55 -08:00
Koushik Dutta
98017a5aa6 snapshot: debounce pics for 2 seconds 2023-12-17 22:58:35 -08:00
Koushik Dutta
f2e7cc4017 rebroadcast: validate device is cam or doorbell 2023-12-16 10:16:10 -08:00
Koushik Dutta
d7201a16a7 snapshot/videoanalysis: publish 2023-12-15 10:56:20 -08:00
Koushik Dutta
99d1dc7282 mqtt: support default external brokers 2023-12-13 23:51:44 -08:00
Koushik Dutta
18ae09e41c snapshot: fixup internal invocation 2023-12-13 12:22:20 -08:00
Koushik Dutta
2ebe774e59 snapshot: beta plugin that bypasses media manager for local urls 2023-12-13 12:11:49 -08:00
Koushik Dutta
b887b8a47c core: add weight to core snapshot 2023-12-13 10:51:23 -08:00
Koushik Dutta
8e391dee2f Merge branch 'main' of github.com:koush/scrypted 2023-12-13 09:07:06 -08:00
Koushik Dutta
469f693d58 snapshot: use internal prebuffer converter rather than media manager 2023-12-13 09:07:02 -08:00
slyoldfox
1c96a7d492 bticino: Implement HKSV recording for the bticino and switch the stream to rtsp (#1220)
* Implement HKSV recording for the bticino and switch the stream to rtsp

* Implement HKSV recording for the bticino and switch the stream to rtsp

---------

Co-authored-by: Marc Vanbrabant <marc@foreach.be>
2023-12-12 09:53:53 -08:00
Koushik Dutta
3f1b45c435 rebroadcast: fix unhandled rejection on stream startup failure 2023-12-12 09:44:59 -08:00
Koushik Dutta
4b715e55d2 server: beta 2023-12-11 13:01:01 -08:00
Koushik Dutta
75dc63acc3 postbeta 2023-12-11 13:00:45 -08:00
Koushik Dutta
6c79f42bb7 videoanalysis: add min object detection time 2023-12-11 10:22:32 -08:00
Koushik Dutta
9d4f006caa snapshot: fix vips rgba/gray ops 2023-12-11 09:28:24 -08:00
Koushik Dutta
05b206f897 cloud: dont attempt to register with server if not logged in 2023-12-10 10:12:39 -08:00
Koushik Dutta
1f22218b23 server: recreate cert with valid date range on startup 2023-12-10 09:21:40 -08:00
Koushik Dutta
c9568df165 mqtt: add online stat 2023-12-08 18:47:45 -08:00
Koushik Dutta
c98e91cd39 videoanalysis: add zone choices to smart sensor 2023-12-07 22:35:15 -08:00
Koushik Dutta
e3ecff04ce videoanalysis: publish 2023-12-07 13:04:51 -08:00
Koushik Dutta
f9f50f34c3 common: update digest auth 2023-12-07 12:41:07 -08:00
Koushik Dutta
cd298f7d76 common: shuffle some polygon code 2023-12-07 12:33:58 -08:00
Koushik Dutta
c95248fce0 videoanalysis: use lib that supports convex polys 2023-12-07 12:27:44 -08:00
Koushik Dutta
e50f3fa793 sdk/videoanalysis: remove filterMode from sdk. internal to video analysis only. 2023-12-07 09:43:33 -08:00
Koushik Dutta
c74be7e90f objectdetector: add zone option to smart sensor 2023-12-07 09:37:02 -08:00
Koushik Dutta
4d288727ce sdk: use a filterMode rather than a new zone type 2023-12-07 09:02:11 -08:00
Koushik Dutta
1f19dc191d sdk: add zone that doesnt filter, only used for observation 2023-12-07 08:57:14 -08:00
Koushik Dutta
37d4e5be73 homekit: send client size hints to prevent apple tv crash 2023-12-06 10:10:13 -08:00
Koushik Dutta
e64ec98211 mqtt: add temp, humidity, flood 2023-12-05 22:29:36 -08:00
Koushik Dutta
8b6c0c4f7b homekit: add hint for adaptive bitrate 2023-12-05 22:09:23 -08:00
Koushik Dutta
3b16c68c75 sdk: add fingerprint to getVideoStream 2023-12-05 22:07:36 -08:00
Koushik Dutta
67be05880c core: handle node pty failure 2023-12-04 19:28:46 -08:00
Koushik Dutta
414a9403c2 mqtt: implement basic autodiscovery 2023-12-04 13:58:59 -08:00
Koushik Dutta
053106415c tapo: publish 2023-12-02 09:25:23 -08:00
S.Feng
f3690af92a tapo: fix new firmware using sha256 hash (#1208)
ref:
5d3953a948
2023-12-02 09:04:11 -08:00
Koushik Dutta
c4cc12fdff snapshot: publish 2023-12-01 16:57:04 -08:00
Koushik Dutta
58e8772f7c sdk: publish 2023-12-01 14:17:25 -08:00
Koushik Dutta
4ae9b72471 sdk: add stream resize feedback 2023-12-01 08:51:39 -08:00
Koushik Dutta
a8c64aa2d4 sdk: improve adaptive flags 2023-12-01 08:40:08 -08:00
Koushik Dutta
8ccbba485a snapshot: publish 2023-11-30 09:59:25 -08:00
Koushik Dutta
2ec192e0fd snapshot: fix thumbnail generation near leading bounds 2023-11-30 09:59:09 -08:00
Koushik Dutta
e257953338 sdk: add landmarks and clip paths to detections 2023-11-30 08:26:14 -08:00
Koushik Dutta
9e80eca8e1 Merge branch 'main' of github.com:koush/scrypted 2023-11-29 13:17:27 -08:00
Koushik Dutta
172b32fd47 sdk: update detection result fields 2023-11-29 13:17:22 -08:00
slyoldfox
a6bf055b85 Avoid 'No audio stream detected' in prebuffer when speex is the inputAudioCodec (#1203)
Co-authored-by: Marc Vanbrabant <marc@foreach.be>
2023-11-28 08:51:19 -08:00
Koushik Dutta
dab5be1103 alexa: fix potential response race 2023-11-27 19:42:11 -08:00
Koushik Dutta
126c489934 external: update axios digest auth 2023-11-27 19:06:36 -08:00
Koushik Dutta
7f714b3d6a Merge branch 'main' of github.com:koush/scrypted 2023-11-27 19:06:16 -08:00
Koushik Dutta
fde3c47d8c common: improve ffmpeg kill func, add queue end promise 2023-11-27 19:04:09 -08:00
Koushik Dutta
4b1623dfce Update bug_report.md 2023-11-27 08:55:31 -08:00
Koushik Dutta
1e62f7a418 Update bug_report.md 2023-11-27 08:52:04 -08:00
Koushik Dutta
83c9d9a4a6 external: update axios digest auth 2023-11-26 20:13:03 -08:00
Koushik Dutta
b42afe0ca0 external: update axios digest auth 2023-11-26 20:11:27 -08:00
Koushik Dutta
e8e5f9b33e snapshot: add imageOp util function 2023-11-26 18:53:44 -08:00
Koushik Dutta
15916d83b8 rebroadcast: rollback wallclocks change, it is preventing frame updates in webassembly decoder 2023-11-26 18:31:16 -08:00
Koushik Dutta
c1327974b2 ring: update ring-client-api 2023-11-26 18:31:04 -08:00
Koushik Dutta
33e2291912 webrtc: reduce preference of turn 2023-11-26 16:10:53 -08:00
Koushik Dutta
2d2c5c436f ring: publish 2023-11-25 08:18:50 -08:00
Koushik Dutta
8088ae20b1 reolink/rebroadcast: enable wallclock timestamps on rtmp 2023-11-24 19:16:28 -08:00
Koushik Dutta
4c658b8d99 mqtt: default to empty args 2023-11-24 09:04:42 -08:00
Koushik Dutta
aab78ec797 mqtt: support invoking methods 2023-11-24 09:02:26 -08:00
Koushik Dutta
11ecff985d snapshot: fix file pathing on windows 2023-11-23 19:38:17 -08:00
Koushik Dutta
80a1a78a79 install: fix env 2023-11-23 09:12:43 -08:00
Koushik Dutta
7875c51d62 install: prevent usage of global libvips 2023-11-23 08:50:28 -08:00
Koushik Dutta
b04aa75117 alexa: fix race condition in sendResponse 2023-11-22 21:23:01 -08:00
Koushik Dutta
fc7d1eaf32 snapshot: consolidate image ops 2023-11-22 20:26:53 -08:00
Koushik Dutta
e5a7a55be8 snapshot: refactor 2023-11-22 14:33:33 -08:00
Koushik Dutta
fa9a2eb947 ha: publish 2023-11-22 13:38:39 -08:00
Koushik Dutta
30891e0769 snapshot: lazy load sharp 2023-11-22 13:37:17 -08:00
Koushik Dutta
fb8256709a postrelease 2023-11-22 13:07:51 -08:00
Koushik Dutta
06d0a4a2f1 server: verup 2023-11-22 13:07:41 -08:00
Koushik Dutta
2fb6e0a368 server: fix connectRPCObject in python. cache/optimize connect code. 2023-11-22 13:00:33 -08:00
Koushik Dutta
c6ed0d8729 snapshot: publish latest with sharp fallbacks 2023-11-22 11:49:42 -08:00
Koushik Dutta
67c6f63dbe Merge branch 'main' of github.com:koush/scrypted 2023-11-22 11:16:10 -08:00
Koushik Dutta
e62b4ad68b snapshot: publish beta with sharp + fallback 2023-11-22 11:16:05 -08:00
slyoldfox
bfec5eb3f3 Support turning on/off ringer (#1193)
Support turning on/off answering machine

Co-authored-by: Marc Vanbrabant <marc@foreach.be>
2023-11-22 10:59:56 -08:00
Koushik Dutta
0f948ea672 ha: publish 2023-11-22 10:39:27 -08:00
Koushik Dutta
a28a476d80 core: publish 2023-11-22 09:59:53 -08:00
Koushik Dutta
fdab50bf8e cli/client: update 2023-11-22 09:59:34 -08:00
Koushik Dutta
189be80a40 core: fix rpc serialization issue 2023-11-22 09:10:22 -08:00
Koushik Dutta
dae1b87825 postbeta 2023-11-21 23:47:51 -08:00
Koushik Dutta
8853ca2775 server/core: bump core 2023-11-21 23:47:40 -08:00
Koushik Dutta
296652b550 github: disable some build flavors 2023-11-21 23:34:08 -08:00
Koushik Dutta
fb2646a69f postbeta 2023-11-21 23:12:58 -08:00
Koushik Dutta
9f5787227b server: bump min core version 2023-11-21 23:12:49 -08:00
Koushik Dutta
aedcc0709b webhook: ensure response is sent before onRequest returns 2023-11-21 22:54:36 -08:00
Koushik Dutta
756585ae95 postbeta 2023-11-21 22:52:47 -08:00
Koushik Dutta
4e8ee94012 server: prebeta 2023-11-21 22:52:34 -08:00
Koushik Dutta
5689792a77 core: ensure response is sent before onRequest returns 2023-11-21 22:51:44 -08:00
Koushik Dutta
ed40f29226 sdk: include sharp 2023-11-21 22:37:54 -08:00
Koushik Dutta
aad9a2123d postbeta 2023-11-21 22:37:03 -08:00
Koushik Dutta
602b5e4983 prebeta 2023-11-21 22:36:48 -08:00
Koushik Dutta
5f01cdc73b core: publish beta 2023-11-21 22:24:04 -08:00
Koushik Dutta
a1f82dd065 postbeta 2023-11-21 20:42:53 -08:00
Koushik Dutta
469305cc3b prebeta 2023-11-21 20:42:41 -08:00
Koushik Dutta
5ed4082918 snapshot: use forked sharp 2023-11-21 12:29:56 -08:00
Koushik Dutta
1bfdacc476 server: update deps 2023-11-21 12:29:47 -08:00
Koushik Dutta
2d03b55d8e sdk: remove comments 2023-11-21 11:03:08 -08:00
Koushik Dutta
a06894b165 sdk: disable typeof transform 2023-11-21 10:44:01 -08:00
Koushik Dutta
e2c0b4d1bf snapshot: rollback 2023-11-20 21:14:17 -08:00
Koushik Dutta
501509dcd0 snapshot: publish vips support 2023-11-20 20:51:31 -08:00
Koushik Dutta
9cbc38173b rpc: further alloc cleanups 2023-11-20 19:38:46 -08:00
Koushik Dutta
8c7a4dc21e snapshot: readd sharp, publish beta 2023-11-20 19:38:33 -08:00
Koushik Dutta
edfeacd075 rpc: further alloc cleanups 2023-11-20 19:38:15 -08:00
Koushik Dutta
20d1372d2a server: fix update signature 2023-11-20 08:44:31 -08:00
Koushik Dutta
3999cb6696 rpc: further cleanups on holding buffers 2023-11-19 20:11:42 -08:00
Koushik Dutta
167360a218 common: prevent runaway zygote 2023-11-19 19:03:43 -08:00
Koushik Dutta
9b168bb012 server: suppress noisy media manager 2023-11-19 19:03:25 -08:00
Koushik Dutta
31f2d33e57 core: update lockfiles 2023-11-19 18:55:17 -08:00
Koushik Dutta
513dd4867b rpc: remove debug 2023-11-19 18:55:03 -08:00
Koushik Dutta
08e723848f rpc: fix slice vs subarray behavior on web/babel 2023-11-19 18:50:54 -08:00
Koushik Dutta
fb37061a04 rpc: fix buffer gc trashing 2023-11-19 18:11:59 -08:00
Koushik Dutta
56c6cb8947 openvino: add ability to select specific device 2023-11-19 14:36:00 -08:00
Koushik Dutta
4f38c6eea8 snapshot: fix source id 2023-11-19 14:35:24 -08:00
Koushik Dutta
5eb2c586fa common: add zygote worker. fix async queue signal abort. 2023-11-19 14:34:50 -08:00
Koushik Dutta
8fd89e75b4 reolink: fix skip validation 2023-11-19 13:39:57 -08:00
Koushik Dutta
10c9143333 webrtc: publish 2023-11-18 14:33:02 -08:00
Brett Jia
eaeae02080 rtp marker tweaks on webrtc talkback (#1187)
* set rtp marker

* set marker if last packet was recevied 1s+ ago

* fix after merge

* reorder

* set marker if last packet was recevied 1s+ ago
2023-11-18 14:32:32 -08:00
Koushik Dutta
7460c714c1 Merge branch 'main' of github.com:koush/scrypted 2023-11-18 14:32:10 -08:00
Koushik Dutta
d7874eb7a2 server: catch ffmpeg shutdown errors 2023-11-18 14:32:04 -08:00
Koushik Dutta
5847b585c7 common: move zygote 2023-11-18 14:31:47 -08:00
Sahib B
901e0a2349 cloud: Improve clarity of Cloudflare token copying instructions (#1186)
The original wording implies that `sudo cloudflared service install` is part of the token which is incorrect.
2023-11-17 21:56:39 -08:00
Koushik Dutta
8c8c7934ff common: queue end should not clear the queue 2023-11-17 21:55:50 -08:00
Koushik Dutta
dd4efcd52f common: fix waiting dequeues on queue end 2023-11-17 20:55:28 -08:00
Koushik Dutta
eab0746a0a python-codecs: fix erroneous deletion 2023-11-17 20:48:01 -08:00
Koushik Dutta
385d331953 webrtc: disable marker bit on audio packets 2023-11-17 11:03:16 -08:00
Koushik Dutta
27a01a7df8 snapshot/python-codecs: move image writer 2023-11-17 10:41:16 -08:00
Koushik Dutta
fc13a230d7 common: support modifying server client opts 2023-11-17 09:46:50 -08:00
Koushik Dutta
f7d88273e4 webrtc: talkback fixes 2023-11-16 21:17:01 -08:00
Koushik Dutta
26124b7647 sdk: fix storage settings bug 2023-11-16 19:31:22 -08:00
Koushik Dutta
5ba30e6001 videoanalysis: remove snapshot pipeline, add support for new webassembly decoder 2023-11-16 19:22:15 -08:00
Koushik Dutta
cfb78ebb7f common: queue iterator throw or return should propagate to queue. 2023-11-15 23:13:10 -08:00
Koushik Dutta
83ad4ed7bc postbeta 2023-11-15 11:29:44 -08:00
Koushik Dutta
8612d8e1fb postbeta 2023-11-15 11:27:39 -08:00
Koushik Dutta
3aeddd0347 homekit: remove object detectors, feature is now in video analysis plugin 2023-11-15 10:41:55 -08:00
Koushik Dutta
f41fa9055e videoanalysis: fix var clobbering 2023-11-15 09:54:23 -08:00
Koushik Dutta
6655cba3b6 Merge branch 'main' of github.com:koush/scrypted 2023-11-15 09:43:37 -08:00
Koushik Dutta
4726630e29 videoanalysis: fix settings crash 2023-11-15 09:43:30 -08:00
Koushik Dutta
4989aa621e videoanalysis: add smart motion sensor feature 2023-11-15 09:33:22 -08:00
Brett Jia
772bfec55a cli, core: support invoking remote commands (#1185)
* cli, core: support launching remote commands

* fixes

* preserve buffer format for child process
2023-11-14 20:32:30 -08:00
Brett Jia
cf9a0653f2 cli, core, ui: add npx scrypted shell and support interactive/noninteractive shells (#1174)
* cli: add `npx scrypted shell`

* stdin tweaks

* buffer math tweaks

* listen to stdin eof only on noninteractive

* feedback + implement noninteractive use

* change logic per feedback
2023-11-13 10:46:21 -08:00
Koushik Dutta
c5bbe5619e werift: update 2023-11-13 09:39:05 -08:00
Koushik Dutta
61be7fa58d sdk/client: publish 2023-11-12 20:06:32 -08:00
Brett Jia
8cb2e1516a client, server, core: Add StreamService interface and TerminalService device (#1171)
* wip

* clean up shell on disconnect

* fix null reference

* remove debug logs

* use async queue in buffered buffer, add max buffer size before connection teardown

* Revert "use async queue in buffered buffer, add max buffer size before connection teardown"

This reverts commit 1b3c283542.

* reimplement per feedback

* feedback
2023-11-12 20:03:19 -08:00
Koushik Dutta
1b15453997 rpc: fix babel mucking up async generators 2023-11-10 17:00:25 -08:00
Koushik Dutta
7e65605ab8 sdk: update with node pty 2023-11-09 20:21:24 -08:00
Koushik Dutta
793c583491 server/client: fix inadvertent inclusion of node:net in client. consolidate connection setup code. 2023-11-09 15:33:58 -08:00
Koushik Dutta
77d4b0a995 server/client: fixup client deps 2023-11-09 15:32:58 -08:00
Koushik Dutta
79eda5d356 server: cleanup imports 2023-11-09 15:31:13 -08:00
Koushik Dutta
86f3318133 server/client: fix inadvertent inclusion of node:net in client. consolidate connection setup code. 2023-11-09 15:30:26 -08:00
Koushik Dutta
1cf0327d2e python-codecs: todo remove comments 2023-11-09 14:28:24 -08:00
Koushik Dutta
3244956b91 sdk: remove filter from video frame generator 2023-11-09 10:25:32 -08:00
Koushik Dutta
6d5fedc931 sdk: remove filter from VideoFrameGenerator 2023-11-09 10:24:55 -08:00
Koushik Dutta
7eca7f69c0 Merge branch 'main' of github.com:koush/scrypted 2023-11-09 09:54:48 -08:00
Brett Jia
7dec399ed7 server, client: send full ClusterObject on new eio endpoint (#1170)
* server: change connectRPCObject internal signature

* server, client: send ClusterObject + hash validation

---------

Co-authored-by: Koushik Dutta <koushd@gmail.com>
2023-11-09 09:47:18 -08:00
Koushik Dutta
9edc63bd90 update werift 2023-11-09 09:22:30 -08:00
Koushik Dutta
99d2f43699 snapshot: send accept header to prevent sending application/json by default 2023-11-09 09:22:14 -08:00
Koushik Dutta
ba1ecd54c5 server: change connectRPCObject internal signature 2023-11-08 12:12:02 -08:00
Brett Jia
e49f26b410 server, client: connectRPCObject for web api clients (#1166)
* initial pass at connectRPCObject proxy

* wip: connects to server but fails on pendingResults

* fix wrong rpcpeer bug + cleanup serialization

* small cleanups

* feedback, local object lookups

* rpc: fix up additional id gens

* feedback

* update example to use frame generator

---------

Co-authored-by: Koushik Dutta <koushd@gmail.com>
2023-11-08 09:22:07 -08:00
Koushik Dutta
d7a417c984 webhook: fix mixin race condition dangling 2023-11-07 21:27:45 -08:00
Koushik Dutta
1a7e0370c9 rpc: fix up additional id gens 2023-11-07 15:23:47 -08:00
Koushik Dutta
2fe4191f12 cameras: remove snapshot debouncing, it is in the snapshot plugin now 2023-11-07 09:32:34 -08:00
Brett Jia
b2b5cde303 server: fix python rpc id gen (#1165) 2023-11-06 12:53:11 -08:00
Koushik Dutta
33b77b64de rpc: id gen fix 2023-11-05 09:00:11 -08:00
Koushik Dutta
a41d4de97a rpc: use non deterministic object ids 2023-11-04 20:24:21 -07:00
Koushik Dutta
cf367fa481 Merge branch 'main' of github.com:koush/scrypted 2023-10-31 11:42:25 -07:00
Koushik Dutta
6f483f829b common: fix readLine performance 2023-10-31 11:42:18 -07:00
Brett Jia
be69c25076 server: fix crash on changing closed pty dimensions (#1158) 2023-10-31 08:52:11 -07:00
Brett Jia
96d292d39f server: best-effort allow older clients to write to shell (#1157) 2023-10-30 21:00:38 -07:00
Ben Dews
933c731fe6 reolink: Added device detection features + trackmix stream (#1154) 2023-10-30 20:46:21 -07:00
Sahib B
aa2c1c65f9 Docs: Update and simplify port forwarding guide for beginners (#1141)
* Update and simplify port forwarding guide for beginners

* Fix wrong step incrementation
2023-10-30 20:37:52 -07:00
Koushik Dutta
5228dbff62 rtp: negotiation null check 2023-10-25 20:16:06 -07:00
Brett Jia
d3593b9e40 server & core: handle binary terminal data (#1149)
* server & core: send terminal size info

* server & core: handle binary terminal data

* send all data as buffer

* add guard to not crash on mismatched core
2023-10-25 08:58:53 -07:00
Koushik Dutta
2ef482c47f Merge branch 'main' of github.com:koush/scrypted 2023-10-25 08:49:14 -07:00
Koushik Dutta
52692c0912 python-codecs: fix potential leak/hang 2023-10-25 08:49:10 -07:00
Brett Jia
98c901486a server & core: send terminal size info (#1148) 2023-10-24 20:43:04 -07:00
Koushik Dutta
476bd3b427 sdk/client: publish 2023-10-24 12:49:55 -07:00
Koushik Dutta
f71826f6a1 sdk: recordingActive state 2023-10-24 12:43:57 -07:00
Koushik Dutta
ed72643d3e unifi-protect: fix login on 3.2.5 2023-10-23 11:35:30 -07:00
Koushik Dutta
96219456f3 python-codecs: fix startup analyze duration and probe size 2023-10-23 09:34:11 -07:00
Koushik Dutta
0e797c6ac6 python-codecs: improve pyav for zero latency 2023-10-23 09:08:56 -07:00
Koushik Dutta
672f01fd3f webrtc: documentation on raw audio packet times 2023-10-22 22:28:01 -07:00
Koushik Dutta
327acaec76 webrtc: prototype packing opus packets. seems to work. 2023-10-22 22:23:19 -07:00
Koushik Dutta
aac10c4f16 plugins: pcm_mulaw rename, intercom fixes, h264 fix 2023-10-22 16:56:20 -07:00
Koushik Dutta
da4ba776f7 h264-repacketizer: stapa codec info fix and stapa repacketization assert 2023-10-22 16:21:53 -07:00
Koushik Dutta
6cd0af492b reolink: fixups for h265 mode cams 2023-10-19 19:44:54 -07:00
Koushik Dutta
6a2474d11e unifi-protect: typos 2023-10-19 15:01:03 -07:00
Koushik Dutta
e8a5d5cfd3 unifi-protect: make connecitonHost an options 2023-10-19 14:59:55 -07:00
Koushik Dutta
f07604de4c two-way audio improvements:
rename pcm_ulaw to pcm_mulaw per ffmpeg codec name
support transcode free rtp forwarding of audio only streams
onvif two audio codec negotiation with upstream
2023-10-19 14:00:36 -07:00
Koushik Dutta
ed35811296 webrtc: initial prep for negotiated intercom codecs 2023-10-19 10:57:20 -07:00
Koushik Dutta
ae2228f2e4 homekit: opus frame duration quirk doc 2023-10-17 14:40:50 -07:00
Koushik Dutta
c92c8f2b52 homekit: publish 2023-10-17 14:37:29 -07:00
Koushik Dutta
478f1f4ad7 homekit: fix opus repacketization 2023-10-17 14:36:57 -07:00
Koushik Dutta
06c8b397f0 python-codecs: h265 parsing fixes 2023-10-17 11:02:39 -07:00
Koushik Dutta
f8bcf196d3 python-codecs: use non hw accelerated h265 by default 2023-10-17 10:38:14 -07:00
Koushik Dutta
d1b57ed3ad snapshot: avoid ffmpeg for crop if possible 2023-10-17 09:49:56 -07:00
Koushik Dutta
190914efd1 ring: update webrtc stream endpoint, start phasing out rtsp stream 2023-10-17 09:12:03 -07:00
Koushik Dutta
e26e53899e webrtc: improve private address range check 2023-10-16 14:56:34 -07:00
Koushik Dutta
43c69914a4 webrtc: publish 2023-10-16 12:54:42 -07:00
Koushik Dutta
73f859b1f6 Merge branch 'main' of github.com:koush/scrypted 2023-10-16 12:10:37 -07:00
Koushik Dutta
a362b7d6d9 webrtc: add ability to filter candidate pairs 2023-10-16 12:10:23 -07:00
Koushik Dutta
efa8515aa0 Update install-scrypted-dependencies-mac.sh 2023-10-14 19:31:00 -07:00
Koushik Dutta
7987a78239 Merge branch 'main' of github.com:koush/scrypted 2023-10-14 12:48:02 -07:00
Koushik Dutta
21752a3e7e unifi: publish 2023-10-14 12:47:57 -07:00
Koushik Dutta
b4d8f99cd5 Revert "unifi-protect: Use connectionHost to support cameras distributed between stacked nvrs (#1128)"
This reverts commit 0349977a4d.
2023-10-14 12:47:41 -07:00
Koushik Dutta
6cf8f6db32 ha: publish 2023-10-12 20:32:29 -07:00
Koushik Dutta
feb3b8f601 postrelease 2023-10-12 19:53:22 -07:00
Koushik Dutta
9e0e9bc22a cameras: publish 2023-10-12 10:02:38 -07:00
Koushik Dutta
9e495c74d9 onvif: publish 2023-10-12 10:01:14 -07:00
Koushik Dutta
a9baeafe71 postbeta 2023-10-12 09:14:41 -07:00
Koushik Dutta
ee68fcd7d2 core: fix js redirect 2023-10-12 09:11:59 -07:00
Koushik Dutta
af6e18dc1a reolink: intercom teardown fixes 2023-10-12 09:04:16 -07:00
Koushik Dutta
ddb8c7cf58 webrtc: intercom teardown fixes 2023-10-12 09:04:06 -07:00
Koushik Dutta
2be3c7f3df unifi: publish 2023-10-11 17:19:32 -07:00
Koushik Dutta
274f449e2f reolink: missing file from intercom teardown fix 2023-10-11 17:19:18 -07:00
Koushik Dutta
1109333e0f Merge branch 'main' of github.com:koush/scrypted 2023-10-11 17:18:20 -07:00
Dan Wager
0349977a4d unifi-protect: Use connectionHost to support cameras distributed between stacked nvrs (#1128)
* Use connectionHost to support cameras distributed between stacked nvrs

* Add fallback to user-configured nvr ip

* remove whitespace
2023-10-11 17:18:16 -07:00
Koushik Dutta
48548aafd5 reolink: fix teardown 2023-10-11 15:45:25 -07:00
Koushik Dutta
ab70cce1b5 reolink: fix bug where rtmp does not support 4k 2023-10-11 12:53:24 -07:00
Koushik Dutta
83fe0c2b7a videoanalysis: fix rounding issue on cpu throttle 2023-10-11 12:53:12 -07:00
Koushik Dutta
77676a27c2 Update bug_report.md 2023-10-10 08:16:34 -07:00
Koushik Dutta
015dfab7a6 Update issue templates 2023-10-10 08:15:24 -07:00
Koushik Dutta
7f0f0cb6bd client: cache bust engine io requests 2023-10-09 19:49:00 -07:00
Koushik Dutta
e49e13a167 server: fix potential rce 2023-10-09 11:47:32 -07:00
Koushik Dutta
9fd353236b server: update deps 2023-10-09 11:31:45 -07:00
Koushik Dutta
e006d599d7 client: update and publish 2023-10-09 11:29:42 -07:00
Koushik Dutta
71cbe83a2a cloud: support disabling cloudflare 2023-10-04 09:35:13 -07:00
Koushik Dutta
1438af8aea cloud: preserve port setting when disabled 2023-10-04 09:21:43 -07:00
Koushik Dutta
2237eb3221 cloud: use permanent cloudflare hostname if token is provided 2023-10-04 09:15:18 -07:00
Koushik Dutta
7b56e86383 Merge branch 'main' of github.com:koush/scrypted 2023-10-03 19:22:49 -07:00
Koushik Dutta
3653fb83d3 rebroadcast: watch for nvr overflow 2023-10-03 19:22:43 -07:00
Brett Jia
cd766a603e arlo: migrate to standalone repo (#1102) 2023-09-28 10:40:49 -07:00
Koushik Dutta
3648492299 client: dedupe addresses 2023-09-28 09:26:31 -07:00
Koushik Dutta
88e8530677 cloud: addresses should be urls not hosts 2023-09-28 09:03:38 -07:00
Koushik Dutta
325f84ca7e client: consider external addresses during negotiation 2023-09-28 08:48:21 -07:00
Koushik Dutta
0a4b862fd8 cloud: report external addresses 2023-09-28 08:38:37 -07:00
Koushik Dutta
e7d7fd6a00 openvino: bump openvino version and publish. note: 2023.1.0 is available but currently nonfunctional with yolov8 2023-09-26 10:06:02 -07:00
Koushik Dutta
f9dda8d1ca openvino: use builtin async, log execution unit 2023-09-26 09:56:42 -07:00
Koushik Dutta
8b7decd077 Merge branch 'main' of github.com:koush/scrypted 2023-09-25 07:12:12 -07:00
Koushik Dutta
9dd5e10eba python-codecs: fix float being passed to pil resize args 2023-09-25 07:12:05 -07:00
Brett Jia
475b833508 sdk: configurable webpack entrypoint (#1076) 2023-09-21 20:07:32 -07:00
Koushik Dutta
5f9006148a Merge branch 'main' of github.com:koush/scrypted 2023-09-21 16:04:15 -07:00
Koushik Dutta
b77f1a55c1 client: add missing address on cloud connect 2023-09-21 16:04:11 -07:00
Koushik Dutta
6b9163e84e server: rename fetch helper 2023-09-20 08:07:09 -07:00
Koushik Dutta
bc03bdd235 ha: publish 2023-09-20 00:21:30 -07:00
Koushik Dutta
2592a7c228 postrelease 2023-09-19 23:32:51 -07:00
Koushik Dutta
0a4336879c client: allow direct login on chrome if flag is explicitly true 2023-09-19 19:03:16 -07:00
Koushik Dutta
e5cef3f217 client: fixup alt address usage 2023-09-19 18:57:15 -07:00
Koushik Dutta
d34396afbc postbeta 2023-09-19 18:56:45 -07:00
Koushik Dutta
2622fc9256 postbeta 2023-09-19 16:46:09 -07:00
Koushik Dutta
410b1a4813 client: check token presence before using direct address 2023-09-19 16:25:08 -07:00
Koushik Dutta
403c742be3 server: token comment 2023-09-19 16:21:10 -07:00
Koushik Dutta
50a471b78f client: use long term token for direct connection 2023-09-19 15:26:23 -07:00
Koushik Dutta
9b7ead26e0 postbeta 2023-09-19 15:23:07 -07:00
Koushik Dutta
3127bc38cb server: include token for basic auth login result 2023-09-19 15:22:48 -07:00
Koushik Dutta
fb8b1a893d cloud: fix misleading port forward test error 2023-09-19 13:46:26 -07:00
Koushik Dutta
779d8eaa42 postrelease 2023-09-19 13:39:29 -07:00
299 changed files with 22594 additions and 24115 deletions

49
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,49 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
# Github Issues is not a Forum
**This issue tracker is not for hardware support or feature requests**. If you are troubleshooting adding a device for the first time, use Discord, Reddit, or Github Discussions. However, if something **was working**, and is now **no longer working**, you may create a Github issue.
Created issues that do not meet these requirements or are improperly filled out will be immediately closed.
# New Issue Instructions
1. Delete this section and everything above it.
2. Fill out the sections below.
**Describe the bug**
A clear and concise description of what the bug is. The issue tracker is only for reporting bugs in Scrypted, for general support check Discord. Hardrware support requests or assistance requests will be immediately closed.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -10,7 +10,10 @@ jobs:
# runs-on: ubuntu-latest
strategy:
matrix:
NODE_VERSION: ["18", "20"]
NODE_VERSION: [
"18",
# "20"
]
BASE: ["jammy"]
FLAVOR: ["full", "lite", "thin"]
steps:
@@ -26,21 +29,13 @@ jobs:
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: ${{ 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
platforms: linux/arm64
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
@@ -63,9 +58,10 @@ jobs:
BASE=${{ matrix.BASE }}
context: install/docker/
file: install/docker/Dockerfile.${{ matrix.FLAVOR }}
platforms: linux/amd64,linux/armhf,linux/arm64
platforms: linux/amd64,linux/arm64
push: true
tags: |
koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
ghcr.io/koush/scrypted-common:${{ matrix.NODE_VERSION }}-${{ matrix.BASE }}-${{ matrix.FLAVOR }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -19,7 +19,14 @@ jobs:
# runs-on: ubuntu-latest
strategy:
matrix:
BASE: ["18-jammy-full", "18-jammy-lite", "18-jammy-thin", "20-jammy-full", "20-jammy-lite", "20-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
@@ -45,21 +52,13 @@ jobs:
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: ${{ 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
platforms: linux/arm64
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
@@ -82,7 +81,7 @@ jobs:
SCRYPTED_INSTALL_VERSION=${{ steps.package-version.outputs.NPM_VERSION }}
context: install/docker/
file: install/docker/Dockerfile${{ matrix.SUPERVISOR }}
platforms: linux/amd64,linux/arm64,linux/armhf
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}

9
.gitmodules vendored
View File

@@ -7,9 +7,6 @@
[submodule "plugins/tensorflow/face-api.js"]
path = external/face-api.js
url = ../../koush/face-api.js
[submodule "external/axios-digest-auth"]
path = external/axios-digest-auth
url = ../../koush/axios-digest-auth
[submodule "external/scrypted-ffmpeg"]
path = external/scrypted-ffmpeg
url = ../../koush/scrypted-ffmpeg
@@ -35,3 +32,9 @@
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/external/node-nat-upnp
url = ../../koush/node-nat-upnp.git
[submodule "plugins/wyze/docker-wyze-bridge"]
path = plugins/wyze/docker-wyze-bridge
url = ../../koush/docker-wyze-bridge.git
[submodule "plugins/onvif/onvif"]
path = plugins/onvif/onvif
url = ../../koush/onvif.git

View File

@@ -5,17 +5,19 @@
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"name": "ts-node",
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
"args": [
"${workspaceFolder}/src/test.ts",
],
"program": "${workspaceFolder}/dist/common/src/test.js",
"preLaunchTask": "npm: build",
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
"runtimeArgs": [
"-r",
"ts-node/register"
],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
},
]
}

608
common/package-lock.json generated
View File

@@ -1,22 +1,22 @@
{
"name": "@scrypted/common",
"version": "1.0.2",
"version": "1.0.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/common",
"version": "1.0.2",
"version": "1.0.1",
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
},
"../external/werift/packages/webrtc": {
@@ -74,12 +74,12 @@
},
"../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.3.4",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -111,68 +111,103 @@
},
"../server": {
"name": "@scrypted/server",
"version": "0.6.19",
"version": "0.82.0",
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.63",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
"@mapbox/node-pre-gyp": "^1.0.11",
"@scrypted/types": "^0.3.4",
"adm-zip": "^0.5.10",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.2.0",
"engine.io": "^6.5.4",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"ffmpeg-static": "^5.2.0",
"follow-redirects": "^1.15.4",
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^6.0.1",
"level": "^8.0.0",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^3.4.7",
"memfs": "^4.6.0",
"mime": "^3.0.0",
"mkdirp": "^1.0.4",
"nan": "^2.17.0",
"nan": "^2.18.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^8.4.1",
"router": "^1.3.7",
"semver": "^7.3.8",
"node-gyp": "^10.0.1",
"router": "^1.3.8",
"semver": "^7.5.4",
"sharp": "^0.33.1",
"source-map-support": "^0.5.21",
"tar": "^6.1.11",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"whatwg-mimetype": "^2.3.0",
"ws": "^8.9.0"
"tar": "^6.2.0",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"whatwg-mimetype": "^4.0.0",
"ws": "^8.16.0"
},
"bin": {
"scrypted-serve": "bin/scrypted-serve"
},
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/cookie-parser": "^1.4.3",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.14",
"@types/http-auth": "^4.1.1",
"@types/ip": "^1.1.0",
"@types/lodash": "^4.14.186",
"@types/mime": "^3.0.1",
"@types/mkdirp": "^1.0.2",
"@types/node-dijkstra": "^2.5.3",
"@types/node-forge": "^1.3.0",
"@types/pem": "^1.9.6",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.12",
"@types/source-map-support": "^0.5.6",
"@types/tar": "^4.0.5",
"@types/whatwg-mimetype": "^2.1.1",
"@types/ws": "^7.4.7"
"@types/adm-zip": "^0.5.5",
"@types/cookie-parser": "^1.4.6",
"@types/debug": "^4.1.12",
"@types/express": "^4.17.21",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/ip": "^1.1.3",
"@types/lodash": "^4.14.202",
"@types/mime": "^3.0.4",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.10",
"@types/pem": "^1.14.4",
"@types/semver": "^7.5.6",
"@types/source-map-support": "^0.5.10",
"@types/tar": "^6.1.10",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.5.10"
},
"optionalDependencies": {
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
}
},
"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/@scrypted/sdk": {
"resolved": "../sdk",
"link": true
@@ -181,120 +216,215 @@
"resolved": "../server",
"link": true
},
"node_modules/@types/node": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz",
"integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==",
"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/fetch-blob": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz",
"integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"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": "20.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
"dev": true,
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
"undici-types": "~5.26.4"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"dependencies": {
"fetch-blob": "^3.1.2"
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=12.20.0"
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"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/http-auth-utils": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.2.tgz",
"integrity": "sha512-cQ8957aiUX0lgV1620uIGKGJc0sEuD/QK4ueZ0hb60MGbO0f6ahcuIgPjamAD98D/AUGizKVm+dNvUVHs0f4Ow==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-5.0.1.tgz",
"integrity": "sha512-YPiLVYdwpBEWB85iWYg7V/ZW3mBfPLCTFQWEiPAA5CKXHJOAPbnJ0xDHLiE6KbPRvrYCwLuWqTG0fLfXVjMUcQ==",
"dependencies": {
"yerror": "^6.0.0"
"yerror": "^8.0.0"
},
"engines": {
"node": ">=12.19.0"
"node": ">=18.16.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
"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/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"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
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
"@swc/wasm": {
"optional": true
}
],
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch-commonjs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-fetch-commonjs/-/node-fetch-commonjs-3.1.1.tgz",
"integrity": "sha512-TgkdVJdiEaauzWwB9NoD4TvHZFtG6KKEffvotWf9WNIyoRZHsCFjGfb3bhkIXrMt3YFgFi8ZApbwWoe1h3XTpA==",
"dependencies": {
"formdata-polyfill": "^4.0.10",
"web-streams-polyfill": "^3.1.1"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/typescript": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==",
"engines": {
"node": ">= 8"
}
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/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/yerror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.0.1.tgz",
"integrity": "sha512-0Bxo+NyeucjxhmPB5z3lmI/N/cOu8L1Q8JVta6/I5G6J/JhCSSPwk8qt9N4yOFSjwkvhDwzUSQglfBIAllvi1Q==",
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/yerror/-/yerror-8.0.0.tgz",
"integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==",
"engines": {
"node": ">=12.19.0"
"node": ">=18.16.0"
}
},
"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"
}
},
"@scrypted/sdk": {
"version": "file:../sdk",
"requires": {
@@ -302,7 +432,7 @@
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -322,117 +452,181 @@
"@scrypted/server": {
"version": "file:../server",
"requires": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.63",
"@types/adm-zip": "^0.4.34",
"@types/cookie-parser": "^1.4.3",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.14",
"@types/http-auth": "^4.1.1",
"@types/ip": "^1.1.0",
"@types/lodash": "^4.14.186",
"@types/mime": "^3.0.1",
"@types/mkdirp": "^1.0.2",
"@types/node-dijkstra": "^2.5.3",
"@types/node-forge": "^1.3.0",
"@types/pem": "^1.9.6",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.12",
"@types/source-map-support": "^0.5.6",
"@types/tar": "^4.0.5",
"@types/whatwg-mimetype": "^2.1.1",
"@types/ws": "^7.4.7",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
"@mapbox/node-pre-gyp": "^1.0.11",
"@scrypted/types": "^0.3.4",
"@types/adm-zip": "^0.5.5",
"@types/cookie-parser": "^1.4.6",
"@types/debug": "^4.1.12",
"@types/express": "^4.17.21",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/ip": "^1.1.3",
"@types/lodash": "^4.14.202",
"@types/mime": "^3.0.4",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.10",
"@types/pem": "^1.14.4",
"@types/semver": "^7.5.6",
"@types/source-map-support": "^0.5.10",
"@types/tar": "^6.1.10",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.5.10",
"adm-zip": "^0.5.10",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.2.0",
"engine.io": "^6.5.4",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"ffmpeg-static": "^5.2.0",
"follow-redirects": "^1.15.4",
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^6.0.1",
"level": "^8.0.0",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^3.4.7",
"memfs": "^4.6.0",
"mime": "^3.0.0",
"mkdirp": "^1.0.4",
"nan": "^2.17.0",
"nan": "^2.18.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^8.4.1",
"node-gyp": "^10.0.1",
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
"router": "^1.3.7",
"semver": "^7.3.8",
"router": "^1.3.8",
"semver": "^7.5.4",
"sharp": "^0.33.1",
"source-map-support": "^0.5.21",
"tar": "^6.1.11",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"whatwg-mimetype": "^2.3.0",
"ws": "^8.9.0"
"tar": "^6.2.0",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"whatwg-mimetype": "^4.0.0",
"ws": "^8.16.0"
}
},
"@types/node": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz",
"integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==",
"@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"fetch-blob": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.4.tgz",
"integrity": "sha512-Eq5Xv5+VlSrYWEqKrusxY1C3Hm/hjeAsCGVG3ft7pZahlUAChpGZT/Ms1WmSLnEAisEXszjzu/s+ce6HZB2VHA==",
"@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": "20.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz",
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
"dev": true,
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
"undici-types": "~5.26.4"
}
},
"formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"requires": {
"fetch-blob": "^3.1.2"
}
"acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true
},
"acorn-walk": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"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
},
"http-auth-utils": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-3.0.2.tgz",
"integrity": "sha512-cQ8957aiUX0lgV1620uIGKGJc0sEuD/QK4ueZ0hb60MGbO0f6ahcuIgPjamAD98D/AUGizKVm+dNvUVHs0f4Ow==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-5.0.1.tgz",
"integrity": "sha512-YPiLVYdwpBEWB85iWYg7V/ZW3mBfPLCTFQWEiPAA5CKXHJOAPbnJ0xDHLiE6KbPRvrYCwLuWqTG0fLfXVjMUcQ==",
"requires": {
"yerror": "^6.0.0"
"yerror": "^8.0.0"
}
},
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
"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-fetch-commonjs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/node-fetch-commonjs/-/node-fetch-commonjs-3.1.1.tgz",
"integrity": "sha512-TgkdVJdiEaauzWwB9NoD4TvHZFtG6KKEffvotWf9WNIyoRZHsCFjGfb3bhkIXrMt3YFgFi8ZApbwWoe1h3XTpA==",
"ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"requires": {
"formdata-polyfill": "^4.0.10",
"web-streams-polyfill": "^3.1.1"
"@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.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA=="
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="
},
"web-streams-polyfill": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"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
},
"yerror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/yerror/-/yerror-6.0.1.tgz",
"integrity": "sha512-0Bxo+NyeucjxhmPB5z3lmI/N/cOu8L1Q8JVta6/I5G6J/JhCSSPwk8qt9N4yOFSjwkvhDwzUSQglfBIAllvi1Q=="
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/yerror/-/yerror-8.0.0.tgz",
"integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g=="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@@ -11,13 +11,13 @@
"author": "",
"license": "ISC",
"dependencies": {
"@scrypted/server": "file:../server",
"@scrypted/sdk": "file:../sdk",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"@scrypted/server": "file:../server",
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
}

View File

@@ -1,30 +0,0 @@
import sdk from "@scrypted/sdk";
const { systemManager, log } = sdk;
export async function alertRecommendedPlugins(plugins: { [pkg: string]: string }) {
const pluginsComponent = await systemManager.getComponent('plugins');
let recommended: any;
try {
recommended = JSON.parse(localStorage.getItem('alert-recommended'));
}
catch (e) {
recommended = {};
}
for (const plugin of Object.keys(plugins)) {
try {
if (recommended[plugin])
continue;
recommended[plugin] = true;
localStorage.setItem('alert-recommended', JSON.stringify(recommended));
const id = await pluginsComponent.getIdForPluginId(plugin);
if (id)
continue;
const name = plugins[plugin];
log.a(`Installation of the ${name} plugin is also recommended. origin:/#/component/plugin/install/${plugin}`)
}
catch (e) {
}
}
}

View File

@@ -5,6 +5,7 @@ class EndError extends Error {
export function createAsyncQueue<T>() {
let ended: Error | undefined;
const endDeferred = new Deferred<void>();
const waiting: Deferred<T>[] = [];
const queued: { item: T, dequeued?: Deferred<void> }[] = [];
@@ -23,6 +24,17 @@ export function createAsyncQueue<T>() {
return deferred.promise;
}
const take = () => {
if (queued.length) {
const { item, dequeued: enqueue } = queued.shift()!;
enqueue?.resolve();
return item;
}
if (ended)
throw ended;
}
const submit = (item: T, dequeued?: Deferred<void>, signal?: AbortSignal) => {
if (ended)
return false;
@@ -34,36 +46,65 @@ export function createAsyncQueue<T>() {
return true;
}
if (signal)
dequeued ||= new Deferred();
const qi = {
item,
dequeued,
};
queued!.push(qi);
signal?.addEventListener('abort', () => {
if (!signal)
return true;
const h = () => {
const index = queued.indexOf(qi);
if (index === -1)
return;
queued.splice(index, 1);
dequeued?.reject(new Error('abort'));
});
};
dequeued.promise.catch(() => {}).finally(() => signal.removeEventListener('abort', h));
signal.addEventListener('abort', h);
return true;
}
function end(e?: Error) {
if (ended)
return false;
// catch to prevent unhandled rejection.
ended = e || new EndError();
endDeferred.resolve();
while (waiting.length) {
waiting.shift().reject(ended);
}
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;
try {
while (true) {
try {
const item = await dequeue();
yield item;
}
catch (e) {
// the yield above may raise an error, and the queue should be ended.
end(e);
if (e instanceof EndError)
return;
throw e;
}
}
}
finally {
// the yield above may cause an iterator return, and the queue should be ended.
end();
}
})();
}
@@ -82,6 +123,11 @@ export function createAsyncQueue<T>() {
}
return {
get ended() {
return ended;
},
endPromise: endDeferred.promise,
take,
clear() {
return clear();
},
@@ -94,14 +140,7 @@ export function createAsyncQueue<T>() {
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;
},
end,
async enqueue(item: T, signal?: AbortSignal) {
const dequeued = new Deferred<void>();
if (!submit(item, dequeued, signal))

View File

@@ -1,12 +1,8 @@
import type { TranspileOptions } from "typescript";
import sdk, { ScryptedDeviceBase, MixinDeviceBase, ScryptedInterface, ScryptedDeviceType } from "@scrypted/sdk";
import vm from "vm";
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors } from "@scrypted/sdk";
import fs from 'fs';
import type { TranspileOptions } from "typescript";
import vm from "vm";
import { ScriptDevice } from "./monaco/script-device";
import { ScryptedInterfaceDescriptors } from "@scrypted/sdk";
import fetch from 'node-fetch-commonjs';
import { PluginAPIProxy } from '../../../server/src/plugin/plugin-api';
import { SystemManagerImpl } from '../../../server/src/plugin/system';
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
@@ -58,18 +54,12 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
worker.worker.terminate();
}
const smProxy = new SystemManagerImpl();
smProxy.state = systemManager.getSystemState();
const apiProxy = new PluginAPIProxy(sdk.pluginHostAPI);
smProxy.api = apiProxy;
const allParams = Object.assign({}, params, {
sdk,
fs: require('realfs'),
fetch,
ScryptedDeviceBase,
MixinDeviceBase,
systemManager: smProxy,
systemManager,
deviceManager,
endpointManager,
mediaManager,
@@ -104,7 +94,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
return {
value,
defaultExport,
apiProxy,
};
}
catch (e) {

View File

@@ -0,0 +1,50 @@
import { httpFetch, httpFetchParseIncomingMessage } from '../../server/src/fetch/http-fetch';
import type { IncomingMessage } from 'http';
import type { Readable } from 'stream';
import { createAuthFetch } from '../../packages/auth-fetch/src/auth-fetch';
export type { HttpFetchOptions, HttpFetchResponseType } from '../../server/src/fetch/http-fetch';
export type { AuthFetchCredentialState, AuthFetchOptions } from '../../packages/auth-fetch/src/auth-fetch';
export const authHttpFetch = createAuthFetch<Readable, IncomingMessage>(httpFetch, httpFetchParseIncomingMessage);
function ensureType<T>(v: T) {
}
async function test() {
const a = await authHttpFetch({
credential: undefined,
url: 'http://example.com',
});
ensureType<Buffer>(a.body);
const b = await authHttpFetch({
credential: undefined,
url: 'http://example.com',
responseType: 'json',
});
ensureType<any>(b.body);
const c = await authHttpFetch({
credential: undefined,
url: 'http://example.com',
responseType: 'readable',
});
ensureType<IncomingMessage>(c.body);
const d = await authHttpFetch({
credential: undefined,
url: 'http://example.com',
responseType: 'buffer',
});
ensureType<Buffer>(d.body);
const e = await authHttpFetch({
credential: undefined,
url: 'http://example.com',
responseType: 'text',
});
ensureType<string>(e.body);
}

View File

@@ -78,11 +78,17 @@ export function createPromiseDebouncer<T>() {
export function createMapPromiseDebouncer<T>() {
const map = new Map<string, Promise<T>>();
return (key: any, func: () => Promise<T>): Promise<T> => {
return (key: any, debounce: number, func: () => Promise<T>): Promise<T> => {
const keyStr = JSON.stringify(key);
let value = map.get(keyStr);
if (!value) {
value = func().finally(() => map.delete(keyStr));
value = func().finally(() => {
if (!debounce) {
map.delete(keyStr);
return;
}
setTimeout(() => map.delete(keyStr), debounce);
});
map.set(keyStr, value);
}
return value;

View File

@@ -19,7 +19,7 @@ export async function read16BELengthLoop(readable: Readable, options: {
let length: number;
let skipCount = 0;
let readCount = 0;
const resumeRead = () => {
readCount++;
read();
@@ -59,13 +59,13 @@ export async function read16BELengthLoop(readable: Readable, options: {
export class StreamEndError extends Error {
constructor() {
super()
super('stream ended');
}
}
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
if (readable.readableEnded || readable.destroyed)
throw new Error("stream ended");
throw new StreamEndError();
if (!length) {
return Buffer.alloc(0);
@@ -109,17 +109,26 @@ export async function readLength(readable: Readable, length: number): Promise<Bu
const CHARCODE_NEWLINE = '\n'.charCodeAt(0);
export async function readUntil(readable: Readable, charCode: number) {
const data = [];
let count = 0;
const queued: Buffer[] = [];
while (true) {
const buffer = await readLength(readable, 1);
if (!buffer)
throw new Error("end of stream");
if (buffer[0] === charCode)
break;
data[count++] = buffer[0];
const available: Buffer = readable.read();
if (!available) {
await once(readable, 'readable');
continue;
}
const index = available.findIndex(b => b === charCode);
if (index === -1) {
queued.push(available);
continue;
}
const before = available.subarray(0, index);
queued.push(before);
const after = available.subarray(index + 1);
readable.unshift(after);
return Buffer.concat(queued).toString();
}
return Buffer.from(data).toString();
}
export async function readLine(readable: Readable) {

View File

@@ -73,6 +73,14 @@ function createOptions() {
return options;
}
// can be called on anything with getStats, ie for receiver specific reports or connection reports.
export async function getPacketsLost(t: { getStats(): Promise<RTCStatsReport> }) {
const stats = await t.getStats();
const packetsLost = ([...stats.values()] as { packetsLost: number }[]).filter(stat => 'packetsLost' in stat).map(stat => stat.packetsLost);
const total = packetsLost.reduce((p, c) => p + c, 0);
return total;
}
export class BrowserSignalingSession implements RTCSignalingSession {
private pc: RTCPeerConnection;
pcDeferred = new Deferred<RTCPeerConnection>();
@@ -90,6 +98,10 @@ export class BrowserSignalingSession implements RTCSignalingSession {
return this.options;
}
async getPacketsLost() {
return getPacketsLost(this.pc);
}
async setMicrophone(enabled: boolean) {
if (this.microphone && enabled && !this.micEnabled) {
this.micEnabled = true;

View File

@@ -1,8 +1,6 @@
import crypto, { randomBytes } from 'crypto';
import dgram from 'dgram';
import { once } from 'events';
import { BASIC } from 'http-auth-utils/dist/index';
import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
import net from 'net';
import { Duplex, Readable, Writable } from 'stream';
import tls from 'tls';
@@ -363,7 +361,7 @@ export class RtspClient extends RtspBase {
}
}
writeRequest(method: string, headers?: Headers, path?: string, body?: Buffer) {
async writeRequest(method: string, headers?: Headers, path?: string, body?: Buffer) {
headers = headers || {};
let fullUrl = this.url;
@@ -390,7 +388,7 @@ export class RtspClient extends RtspBase {
headers['User-Agent'] = 'Scrypted';
if (this.wwwAuthenticate)
headers['Authorization'] = this.createAuthorizationHeader(method, new URL(fullUrl));
headers['Authorization'] = await this.createAuthorizationHeader(method, new URL(fullUrl));
if (this.session)
headers['Session'] = this.session;
@@ -531,10 +529,13 @@ export class RtspClient extends RtspBase {
}
}
createAuthorizationHeader(method: string, url: URL) {
async createAuthorizationHeader(method: string, url: URL) {
if (!this.wwwAuthenticate)
throw new Error('no WWW-Authenticate found');
const { BASIC } = await import('http-auth-utils');
const { parseHTTPHeadersQuotedKeyValueSet } = await import('http-auth-utils/dist/utils');
if (this.wwwAuthenticate.includes('Basic')) {
const hash = BASIC.computeHash(url);
return `Basic ${hash}`;
@@ -586,7 +587,7 @@ export class RtspClient extends RtspBase {
}
async request(method: string, headers?: Headers, path?: string, body?: Buffer, authenticating?: boolean): Promise<RtspServerResponse> {
this.writeRequest(method, headers, path, body);
await this.writeRequest(method, headers, path, body);
const message = this.requestTimeout ? await timeoutPromise(this.requestTimeout, this.readMessage()) : await this.readMessage();
const statusLine = message[0];

View File

@@ -149,7 +149,7 @@ export function parseFmtp(msection: string[]) {
const paramLine = fmtpLine.substring(firstSpace + 1);
const payloadType = parseInt(fmtp.split(':')[1]);
if (!fmtp || !paramLine || Number.isNaN( payloadType )) {
if (!fmtp || !paramLine || Number.isNaN(payloadType)) {
return;
}
@@ -170,28 +170,47 @@ export function parseFmtp(msection: string[]) {
}
export type MSection = ReturnType<typeof parseMSection>;
export type RTPMap = ReturnType<typeof parseRtpMap>;
export function parseRtpMap(mlineType: string, rtpmap: string) {
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)/);
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)(\/([\d]+))?/);
rtpmap = rtpmap?.toLowerCase();
let codec: string;
let ffmpegEncoder: string;
if (rtpmap?.includes('mpeg4')) {
codec = 'aac';
ffmpegEncoder = 'aac';
}
else if (rtpmap?.includes('opus')) {
codec = 'opus';
ffmpegEncoder = 'libopus';
}
else if (rtpmap?.includes('pcma')) {
codec = 'pcm_alaw';
ffmpegEncoder = 'pcm_alaw';
}
else if (rtpmap?.includes('pcmu')) {
codec = 'pcm_ulaw';
codec = 'pcm_mulaw';
ffmpegEncoder = 'pcm_mulaw';
}
else if (rtpmap?.includes('g726')) {
codec = 'g726';
// disabled since it 48000 is non compliant in ffmpeg and fails.
// ffmpegEncoder = 'g726';
}
else if (rtpmap?.includes('pcm')) {
codec = 'pcm';
}
else if (rtpmap?.includes('l16')) {
codec = 'pcm_s16be';
ffmpegEncoder = 'pcm_s16be';
}
else if (rtpmap?.includes('speex')) {
codec = 'speex';
ffmpegEncoder = 'libspeex';
}
else if (rtpmap?.includes('h264')) {
codec = 'h264';
}
@@ -207,8 +226,10 @@ export function parseRtpMap(mlineType: string, rtpmap: string) {
return {
line: rtpmap,
codec,
ffmpegEncoder,
rawCodec: match?.[2],
clock: parseInt(match?.[3]),
channels: parseInt(match?.[5]) || undefined,
payloadType: parseInt(match?.[1]),
}
}
@@ -220,9 +241,11 @@ export function parseMSection(msection: string[]) {
const mline = parseMLine(msection[0]);
const rawRtpmaps = msection.filter(line => line.startsWith(artpmap));
const rtpmaps = rawRtpmaps.map(line => parseRtpMap(mline.type, line));
const codec = parseRtpMap(mline.type, rawRtpmaps[0]).codec;
// if no rtp map is specified, pcm_alaw is used. parsing a null rtpmap is valid.
const rtpmap = parseRtpMap(mline.type, rawRtpmaps[0]);
const { codec } = rtpmap;
let direction: string;
for (const checkDirection of ['sendonly', 'sendrecv', 'recvonly', 'inactive']) {
const found = msection.find(line => line === 'a=' + checkDirection);
if (found) {
@@ -239,6 +262,7 @@ export function parseMSection(msection: string[]) {
contents: msection.join('\r\n'),
control,
codec,
rtpmap,
direction,
toSdp: () => {
return ret.lines.join('\r\n');

77
common/src/zygote.ts Normal file
View File

@@ -0,0 +1,77 @@
import sdk, { PluginFork } from '@scrypted/sdk';
import worker_threads from 'worker_threads';
import { createAsyncQueue } from './async-queue';
import os from 'os';
export type Zygote<T> = () => PluginFork<T>;
export function createZygote<T>(): Zygote<T> {
if (!worker_threads.isMainThread)
return;
let zygote = sdk.fork<T>();
function* next() {
while (true) {
const cur = zygote;
zygote = sdk.fork<T>();
yield cur;
}
}
const gen = next();
return () => gen.next().value as PluginFork<T>;
}
export function createZygoteWorkQueue<T>(maxWorkers: number = os.cpus().length >> 1) {
const queue = createAsyncQueue<(doWork: (fork: PluginFork<T>) => Promise<any>) => Promise<any>>();
let forks = 0;
return async <R>(doWork: (fork: PluginFork<T>) => Promise<R>): Promise<R> => {
const check = queue.take();
if (check)
return check(doWork);
if (maxWorkers && forks < maxWorkers) {
let exited = false;
const controller = new AbortController();
// necessary to prevent unhandledrejection errors
controller.signal.addEventListener('abort', () => { });
const fork = sdk.fork<T>();
forks++;
fork.worker.on('exit', () => {
forks--;
exited = true;
controller.abort();
});
let timeout: NodeJS.Timeout;
const queueFork = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
// keep one alive.
if (forks === 1)
return;
fork.worker.terminate();
}, 30000);
queue.submit(async v2 => {
clearTimeout(timeout);
try {
return await v2(fork);
}
finally {
if (!exited) {
queueFork();
}
}
}, controller.signal);
}
queueFork();
}
const d = await queue.dequeue();
return d(doWork);
};
}

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "18-jammy-full.s6-v0.50.0"
version: "18-jammy-full.s6-v0.80.0"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"
@@ -29,9 +29,9 @@ environment:
SCRYPTED_ADMIN_USERNAME: "homeassistant"
SCRYPTED_INSTALL_ENVIRONMENT: "ha"
backup_exclude:
- '/server/**'
- '/data/scrypted_nvr/**'
- '/data/scrypted_data/plugins/**'
- '*/server/**'
- '*/scrypted_nvr/**'
- '*/scrypted_data/plugins/**'
map:
- config:rw
- media:rw

View File

@@ -1,5 +1,5 @@
ARG BASE="18-jammy-full"
FROM koush/scrypted-common:${BASE}
FROM ghcr.io/koush/scrypted-common:${BASE}
WORKDIR /
# cache bust
@@ -14,4 +14,8 @@ WORKDIR /server
# https://github.com/nodejs/node/issues/41145#issuecomment-992948130
ENV NODE_OPTIONS="--dns-result-order=ipv4first"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20240103"
CMD npm --prefix /server exec scrypted-serve

View File

@@ -1,5 +1,5 @@
ARG BASE="16-jammy"
FROM koush/scrypted-common:${BASE}
FROM ghcr.io/koush/scrypted-common:${BASE}
WORKDIR /
RUN git clone --depth=1 https://github.com/koush/scrypted

View File

@@ -25,10 +25,14 @@ RUN apt-get update && apt-get -y install \
apt-get -y upgrade
ARG NODE_VERSION=18
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
# python native
RUN echo "Installing python."
RUN apt-get -y install \
python3 \
python3-dev \
@@ -38,44 +42,25 @@ RUN apt-get -y install \
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN echo "Installing pillow-simd dependencies."
RUN apt-get -y install \
libjpeg-dev zlib1g-dev
# plugins support fallback to pillow, but vips is faster.
RUN apt-get -y install \
libvips
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN echo "Installing gstreamer."
RUN apt-get -y install \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python3 gstreamer bindings
RUN echo "Installing gstreamer bindings."
RUN apt-get -y install \
python3-gst-1.0
# armv7l does not have wheels for any of these
# and compile times would forever, if it works at all.
# furthermore, it's possible to run 32bit docker on 64bit arm,
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
# this bit is not necessary on amd64, but leaving it for consistency.
RUN apt-get -y install \
python3-matplotlib \
python3-numpy \
python3-opencv \
python3-pil \
python3-skimage
# 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 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
################################################################
@@ -87,20 +72,11 @@ RUN python3 -m pip install debugpy typing_extensions psutil
FROM header as base
# intel opencl gpu for openvino
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
then \
apt-get update && apt-get install -y gpg-agent && \
rm -f /usr/share/keyrings/intel-graphics.gpg && \
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
apt-get -y update && \
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
apt-get -y dist-upgrade; \
fi"
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite
RUN add-apt-repository ppa:deadsnakes/ppa && \
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
apt-get -y install \
python3.9 \
python3.9-dev \
@@ -110,7 +86,6 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
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
@@ -127,9 +102,6 @@ ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="full"
################################################################

View File

@@ -17,7 +17,10 @@ RUN apt-get update && apt-get -y install \
apt-get -y upgrade
ARG NODE_VERSION=18
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
# python native
@@ -41,7 +44,4 @@ ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="lite"

View File

@@ -1,4 +1,4 @@
FROM koush/scrypted-common
FROM ghcr.io/koush/scrypted-common
WORKDIR /
COPY . .

View File

@@ -1,4 +1,4 @@
FROM koush/18-jammy-full.s6
FROM ghcr.io/koush/scrypted:18-jammy-full.s6
WORKDIR /
@@ -12,11 +12,3 @@ ENV PATH=$CONDA_DIR/bin:$PATH
RUN conda install -c conda-forge cudatoolkit=11.2.2 cudnn=8.1.0
ENV CONDA_PREFIX=/opt/conda
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/
# this is a copy pasta, seems to need a reinstall.
# python pip
RUN python3 -m pip install --upgrade pip
# 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 python3 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install debugpy typing_extensions psutil

View File

@@ -1,5 +1,5 @@
ARG BASE="18-jammy-full"
FROM koush/scrypted-common:${BASE}
FROM ghcr.io/koush/scrypted-common:${BASE}
# avahi advertiser support
RUN apt-get update && apt-get -y install \
@@ -44,4 +44,8 @@ WORKDIR /server
# https://github.com/nodejs/node/issues/41145#issuecomment-992948130
ENV NODE_OPTIONS="--dns-result-order=ipv4first"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20240103"
CMD npm --prefix /server exec scrypted-serve

View File

@@ -9,7 +9,7 @@ RUN apt-get -y update && \
# switch to nvm?
ARG NODE_VERSION=18
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && apt-get update && apt-get install -y nodejs
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
@@ -19,7 +19,4 @@ ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="thin"

View File

@@ -1,4 +1,4 @@
./template/generate-dockerfile.sh
docker build -t koush/scrypted-common -f Dockerfile.common . && \
docker build -t koush/scrypted -f Dockerfile.local .
docker build -t ghcr.io/koush/scrypted-common -f Dockerfile.full . && \
docker build -t ghcr.io/koush/scrypted -f Dockerfile.local .

View File

@@ -1,3 +1,3 @@
./docker-build.sh
docker build -t koush/scrypted:18-jammy-full.nvidia -f Dockerfile.nvidia
docker build -t ghcr.io/koush/scrypted:18-jammy-full.nvidia -f Dockerfile.nvidia .

View File

@@ -11,8 +11,8 @@ echo $BASE
SUPERVISOR=.s6
SUPERVISOR_BASE=$BASE$SUPERVISOR
docker build -t koush/scrypted-common:$BASE -f Dockerfile.$FLAVOR \
docker build -t ghcr.io/koush/scrypted-common:$BASE -f Dockerfile.$FLAVOR \
--build-arg NODE_VERSION=$NODE_VERSION --build-arg BASE=$IMAGE_BASE . && \
\
docker build -t koush/scrypted:$SUPERVISOR_BASE -f Dockerfile$SUPERVISOR \
docker build -t ghcr.io/koush/scrypted:$SUPERVISOR_BASE -f Dockerfile$SUPERVISOR \
--build-arg BASE=$BASE --build-arg SCRYPTED_INSTALL_VERSION=$SCRYPTED_INSTALL_VERSION .

View File

@@ -34,14 +34,14 @@ services:
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
# Uncomment next 3 lines for Nvidia GPU support.
# - NVIDIA_VISIBLE_DEVICES=all
# - NVIDIA_DRIVER_CAPABILITIES=all
# Uncomment next line to run avahi-daemon inside the container
# Don't use if dbus and avahi run on the host and are bind-mounted
# (see below under "volumes")
# - SCRYPTED_DOCKER_AVAHI=true
# Uncomment next 3 lines for Nvidia GPU support.
# - NVIDIA_VISIBLE_DEVICES=all
# - NVIDIA_DRIVER_CAPABILITIES=all
# runtime: nvidia
volumes:
@@ -90,7 +90,7 @@ services:
container_name: scrypted
restart: unless-stopped
network_mode: host
image: koush/scrypted
image: ghcr.io/koush/scrypted
# logging is noisy and will unnecessarily wear on flash storage.
# scrypted has per device in memory logging that is preferred.

View File

@@ -0,0 +1,16 @@
if [ "$(uname -m)" = "x86_64" ]
then
echo "Installing Intel graphics packages."
apt-get update && apt-get install -y gpg-agent &&
rm -f /usr/share/keyrings/intel-graphics.gpg &&
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
apt-get -y update &&
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free &&
apt-get -y dist-upgrade;
exit $?
else
echo "Intel graphics will not be installed on this architecture."
fi
exit 0

View File

@@ -6,19 +6,6 @@ then
exit 0
fi
if [ "$SERVICE_USER" == "root" ]
then
echo "Scrypted SERVICE_USER root is not allowed."
exit 1
fi
USER_HOME=$(eval echo ~$SERVICE_USER)
SCRYPTED_HOME=$USER_HOME/.scrypted
mkdir -p $SCRYPTED_HOME
set -e
cd $SCRYPTED_HOME
function readyn() {
while true; do
read -p "$1 (y/n) " yn
@@ -30,6 +17,22 @@ function readyn() {
done
}
if [ "$SERVICE_USER" == "root" ]
then
readyn "Scrypted will store its files in the root user home directory. Running as a non-root user is recommended. Are you sure?"
if [ "$yn" == "n" ]
then
exit 1
fi
fi
USER_HOME=$(eval echo ~$SERVICE_USER)
SCRYPTED_HOME=$USER_HOME/.scrypted
mkdir -p $SCRYPTED_HOME
set -e
cd $SCRYPTED_HOME
readyn "Install Docker?"
if [ "$yn" == "y" ]

View File

@@ -4,20 +4,11 @@
FROM header as base
# intel opencl gpu for openvino
RUN bash -c "if [ \"$(uname -m)\" == \"x86_64\" ]; \
then \
apt-get update && apt-get install -y gpg-agent && \
rm -f /usr/share/keyrings/intel-graphics.gpg && \
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --output /usr/share/keyrings/intel-graphics.gpg && \
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list && \
apt-get -y update && \
apt-get -y install intel-opencl-icd intel-media-va-driver-non-free && \
apt-get -y dist-upgrade; \
fi"
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite
RUN add-apt-repository ppa:deadsnakes/ppa && \
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
apt-get -y install \
python3.9 \
python3.9-dev \
@@ -27,7 +18,6 @@ RUN add-apt-repository ppa:deadsnakes/ppa && \
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
@@ -44,9 +34,6 @@ ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20230727"
ENV SCRYPTED_DOCKER_FLAVOR="full"
################################################################

View File

@@ -22,10 +22,14 @@ RUN apt-get update && apt-get -y install \
apt-get -y upgrade
ARG NODE_VERSION=18
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash -
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
# python native
RUN echo "Installing python."
RUN apt-get -y install \
python3 \
python3-dev \
@@ -35,44 +39,25 @@ RUN apt-get -y install \
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN echo "Installing pillow-simd dependencies."
RUN apt-get -y install \
libjpeg-dev zlib1g-dev
# plugins support fallback to pillow, but vips is faster.
RUN apt-get -y install \
libvips
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN echo "Installing gstreamer."
RUN apt-get -y install \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python3 gstreamer bindings
RUN echo "Installing gstreamer bindings."
RUN apt-get -y install \
python3-gst-1.0
# armv7l does not have wheels for any of these
# and compile times would forever, if it works at all.
# furthermore, it's possible to run 32bit docker on 64bit arm,
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
# this bit is not necessary on amd64, but leaving it for consistency.
RUN apt-get -y install \
python3-matplotlib \
python3-numpy \
python3-opencv \
python3-pil \
python3-skimage
# 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 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

@@ -12,6 +12,26 @@ then
exit 1
fi
function readyn() {
while true; do
read -p "$1 (y/n) " yn
case $yn in
[Yy]* ) break;;
[Nn]* ) break;;
* ) echo "Please answer yes or no. (y/n)";;
esac
done
}
if [ "$SERVICE_USER" = "root" ] && [ -z "$SERVICE_USER_ROOT" ]
then
readyn "Scrypted will store its files in the root user home directory. Running as a non-root user is recommended. Are you sure?"
if [ "$yn" == "n" ]
then
exit 1
fi
fi
echo "Stopping existing service if it is running..."
systemctl stop scrypted.service
@@ -49,6 +69,14 @@ ENV() {
}
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.header)
if [ -z "$SCRYPTED_INSTALL_ENVIRONMENT" ]
then
SCRYPTED_INSTALL_ENVIRONMENT=local
fi
if [ "$SCRYPTED_INSTALL_ENVIRONMENT" = "lxc" ]
then
source <(curl -s https://raw.githubusercontent.com/koush/scrypted/main/install/docker/template/Dockerfile.full.footer)
fi
if [ -z "$SERVICE_USER" ]
then
@@ -56,12 +84,6 @@ then
exit 0
fi
if [ "$SERVICE_USER" == "root" ]
then
echo "Scrypted SERVICE_USER root is not allowed."
exit 1
fi
# this is not RUN as we do not care about the result
USER_HOME=$(eval echo ~$SERVICE_USER)
echo "Setting permissions on $USER_HOME/.scrypted"
@@ -84,6 +106,7 @@ ExecStart=/usr/bin/npx -y scrypted serve
Restart=on-failure
RestartSec=3
Environment="NODE_OPTIONS=$NODE_OPTIONS"
Environment="SCRYPTED_INSTALL_ENVIRONMENT=$SCRYPTED_INSTALL_ENVIRONMENT"
[Install]
WantedBy=multi-user.target

View File

@@ -45,26 +45,11 @@ RUN brew install libvips
# dlib
RUN brew install cmake
### HACK WORKAROUND
### https://github.com/koush/scrypted/issues/544
brew unpin gstreamer
brew unpin gst-plugins-base
brew unpin gst-plugins-good
brew unpin gst-plugins-bad
brew unpin gst-plugins-ugly
brew unpin gst-libav
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
RUN_IGNORE brew install gst-python
RUN_IGNORE brew install gstreamer
ARCH=$(arch)
if [ "$ARCH" = "arm64" ]

View File

@@ -0,0 +1,84 @@
function readyn() {
while true; do
read -p "$1 (y/n) " yn
case $yn in
[Yy]* ) break;;
[Nn]* ) break;;
* ) echo "Please answer yes or no. (y/n)";;
esac
done
}
cd /tmp
SCRYPTED_VERSION=v0.80.0
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
if [ -z "$VMID" ]
then
VMID=10443
fi
echo "Downloading scrypted container backup."
if [ ! -f "$SCRYPTED_TAR_ZST" ]
then
curl -O -L https://github.com/koush/scrypted/releases/download/$SCRYPTED_VERSION/scrypted.tar.zst
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
fi
echo "Checking for existing container."
pct config $VMID
if [ "$?" == "0" ]
then
echo ""
echo "Existing container $VMID found. Run this script with --force to overwrite the existing container."
echo "This will wipe all existing data. Clone the existing container to retain the data, then reassign the owner of the scrypted volume after installation is complete."
echo ""
echo "bash $0 --force"
echo ""
fi
pct restore $VMID $SCRYPTED_TAR_ZST $@
if [ "$?" != "0" ]
then
echo ""
echo "pct restore failed"
echo ""
echo "This may be caused by the server's 'local' storage not supporting containers."
echo "Try running this script again with a different storage device (local-lvm, local-zfs). For example:"
echo ""
echo "bash $0 --storage local-lvm"
echo ""
exit 1
fi
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto
if [ "$?" != "0" ]
then
echo ""
echo "pct set network failed"
echo ""
echo "Ignoring... Please verify your container's network settings."
fi
CONF=/etc/pve/lxc/$VMID.conf
if [ -f "$CONF" ]
then
echo "onboot: 1" >> $CONF
else
echo "$CONF not found? Start on boot must be enabled manually."
fi
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
if [ "$yn" == "y" ]
then
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
udevadm control --reload-rules && udevadm trigger
fi
echo "Scrypted setup is complete and the container resources can be started."
echo "Scrypted NVR users should provide at least 4 cores and 16GB RAM prior to starting."

11
packages/auth-fetch/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
node_modules
.DS_Store
.gcloud/
dist/
volume
scrypted.db
out
scrypted.db.bak
.exit
.update
.venv

580
packages/auth-fetch/package-lock.json generated Normal file
View File

@@ -0,0 +1,580 @@
{
"name": "@scrypted/auth-fetch",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/auth-fetch",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"follow-redirects": "^1.15.4",
"http-auth-utils": "^5.0.1"
},
"devDependencies": {
"@types/node": "^20.9.4",
"rimraf": "^5.0.5",
"typescript": "^5.3.2"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/http-auth-utils": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/http-auth-utils/-/http-auth-utils-5.0.1.tgz",
"integrity": "sha512-YPiLVYdwpBEWB85iWYg7V/ZW3mBfPLCTFQWEiPAA5CKXHJOAPbnJ0xDHLiE6KbPRvrYCwLuWqTG0fLfXVjMUcQ==",
"dependencies": {
"yerror": "^8.0.0"
},
"engines": {
"node": ">=18.16.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
"integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==",
"dev": true,
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"dev": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dev": true,
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
"dev": true,
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yerror": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/yerror/-/yerror-8.0.0.tgz",
"integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==",
"engines": {
"node": ">=18.16.0"
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"name": "@scrypted/auth-fetch",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"prebuild": "rimraf dist",
"build": "tsc --outDir dist",
"prepublishOnly": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@types/node": "^20.9.4",
"rimraf": "^5.0.5",
"typescript": "^5.3.2"
},
"dependencies": {
"follow-redirects": "^1.15.4",
"http-auth-utils": "^5.0.1"
},
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,135 @@
import { HttpFetchOptions, HttpFetchResponseType, checkStatus, fetcher, getFetchMethod, setDefaultHttpFetchAccept } from '../../../server/src/fetch';
export interface AuthFetchCredentialState {
username: string;
password: string;
}
export interface AuthFetchOptions {
credential?: AuthFetchCredentialState;
}
async function getAuth(options: AuthFetchOptions, url: string | URL, method: string) {
if (!options.credential)
return;
const { BASIC, DIGEST, parseWWWAuthenticateHeader } = await import('http-auth-utils');
const credential = options.credential as AuthFetchCredentialState & {
count?: number;
digest?: ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
basic?: ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
};
const { digest, basic } = credential;
if (digest) {
credential.count ||= 0;
++credential.count;
const nc = ('00000000' + credential.count).slice(-8);
const cnonce = [...Array(24)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
const uri = new URL(url).pathname;
const { DIGEST, buildAuthorizationHeader } = await import('http-auth-utils');
const response = DIGEST.computeHash({
username: options.credential.username,
password: options.credential.password,
method,
uri,
nc,
cnonce,
algorithm: 'MD5',
qop: digest.data.qop!,
...digest.data,
});
const header = buildAuthorizationHeader(DIGEST, {
username: options.credential.username,
uri,
nc,
cnonce,
algorithm: digest.data.algorithm!,
qop: digest.data.qop!,
response,
...digest.data,
});
return header;
}
else if (basic) {
const { BASIC, buildAuthorizationHeader } = await import('http-auth-utils');
const header = buildAuthorizationHeader(BASIC, {
username: options.credential.username,
password: options.credential.password,
});
return header;
}
}
export function createAuthFetch<B, M>(
h: fetcher<B, M>,
parser: (body: M, responseType: HttpFetchResponseType) => Promise<any>
) {
const authHttpFetch = async <T extends HttpFetchOptions<B>>(options: T & AuthFetchOptions): ReturnType<typeof h<T>> => {
const method = getFetchMethod(options);
const headers = new Headers(options.headers);
options.headers = headers;
setDefaultHttpFetchAccept(headers, options.responseType);
const initialHeader = await getAuth(options, options.url, method);
// try to provide an authorization if a session exists, but don't override Authorization if provided already.
// 401 will trigger a proper auth.
if (initialHeader && !headers.has('Authorization'))
headers.set('Authorization', initialHeader);
const initialResponse = await h({
...options,
ignoreStatusCode: true,
responseType: 'readable',
});
if (initialResponse.statusCode !== 401 || !options.credential) {
if (!options?.ignoreStatusCode)
checkStatus(initialResponse.statusCode);
return {
...initialResponse,
body: await parser(initialResponse.body, options.responseType),
};
}
let authenticateHeaders: string | string[] = initialResponse.headers.get('www-authenticate');
if (!authenticateHeaders)
throw new Error('Did not find WWW-Authenticate header.');
if (typeof authenticateHeaders === 'string')
authenticateHeaders = [authenticateHeaders];
const { BASIC, DIGEST, parseWWWAuthenticateHeader } = await import('http-auth-utils');
const parsedHeaders = authenticateHeaders.map(h => parseWWWAuthenticateHeader(h));
const digest = parsedHeaders.find(p => p.type === 'Digest') as ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
const basic = parsedHeaders.find(p => p.type === 'Basic') as ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
const credential = options.credential as AuthFetchCredentialState & {
count?: number;
digest?: ReturnType<typeof parseWWWAuthenticateHeader<typeof DIGEST>>;
basic?: ReturnType<typeof parseWWWAuthenticateHeader<typeof BASIC>>;
};
credential.digest = digest;
credential.basic = basic;
if (!digest && !basic)
throw new Error(`Unknown WWW-Authenticate type: ${parsedHeaders[0]?.type}`);
const header = await getAuth(options, options.url, method);
if (header)
headers.set('Authorization', header);
return h(options);
}
return authHttpFetch;
}

View File

@@ -0,0 +1,19 @@
import { createAuthFetch } from "./auth-fetch";
import { httpFetch, httpFetchParseIncomingMessage } from '../../../server/src/fetch/http-fetch';
import type { Readable } from "stream";
import type { IncomingMessage } from "http";
import { domFetch, domFetchParseIncomingMessage } from "../../../server/src/fetch";
function init() {
try {
require('net');
require('events');
return createAuthFetch<Readable, IncomingMessage>(httpFetch, httpFetchParseIncomingMessage);
}
catch (e) {
}
return createAuthFetch<BodyInit, Response>(domFetch, domFetchParseIncomingMessage);
}
export const authFetch = init();

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
},
"include": [
"src/**/*"
],
}

View File

@@ -21,9 +21,7 @@
],
"preLaunchTask": "npm: build",
"args": [
"ffplay",
"Baby Camera@192.168.2.109",
"getVideoStream",
"shell",
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,37 +1,31 @@
{
"name": "scrypted",
"version": "1.0.69",
"version": "1.3.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.0.69",
"version": "1.3.6",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.1.43",
"@scrypted/types": "^0.2.66",
"adm-zip": "^0.5.10",
"axios": "^0.21.4",
"engine.io-client": "^5.2.0",
"ip": "^1.1.8",
"mkdirp": "^1.0.4",
"@scrypted/client": "^1.3.3",
"@scrypted/types": "^0.2.99",
"engine.io-client": "^6.5.3",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.8",
"tslib": "^2.5.0"
"semver": "^7.5.4",
"tslib": "^2.6.2"
},
"bin": {
"scrypted": "dist/main.js"
"scrypted": "dist/packages/cli/src/main.js"
},
"devDependencies": {
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.14.2",
"@types/readline-sync": "^1.4.4",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.13",
"@types/node": "^20.9.4",
"@types/readline-sync": "^1.4.8",
"@types/semver": "^7.5.6",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.3.2"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -46,6 +40,22 @@
"node": ">=12"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
@@ -71,69 +81,35 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@scrypted/client": {
"version": "1.1.43",
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.1.43.tgz",
"integrity": "sha512-qpeGdqFga/Fx51MoF3E0iBPCjE/SDEIVdGh8Ws5dqw38bxUJD264c9NsNyCguLKyYguErKTAWnQkzqhO0bUbaA==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.3.3.tgz",
"integrity": "sha512-Wuy7x02TCRy1buaDNX8NOIaL1j4ZXu4dqTTJsKHlPe3+umsBvpwbylD+YyyU8ghQJC6a40Bs5UMsvnCvNa/1fg==",
"dependencies": {
"@scrypted/types": "^0.2.66",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
"@scrypted/types": "^0.3.4",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.4",
"rimraf": "^5.0.5"
}
},
"node_modules/@scrypted/client/node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"dependencies": {
"follow-redirects": "^1.14.7"
}
},
"node_modules/@scrypted/client/node_modules/engine.io-client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/@scrypted/client/node_modules/engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@scrypted/client/node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
"node_modules/@scrypted/client/node_modules/@scrypted/types": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.4.tgz",
"integrity": "sha512-k/YMx8lIWOkePgXfKW9POr12mb+erFU2JKxO7TW92GyW8ojUWw9VOc0PK6O9bybi0vhsEnvMFkO6pO6bAonsVA=="
},
"node_modules/@scrypted/types": {
"version": "0.2.66",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.66.tgz",
"integrity": "sha512-AL2iD7OmpqZlQMlpZKUBHpzL7H1IHhwKOi9uhRbVwG7EIDwenTspqtziH2Hyu0+XeCLf+gN69uQB6Qlz+QPf9A=="
"version": "0.2.99",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.99.tgz",
"integrity": "sha512-2J1FH7tpAW5X3rgA70gJ+z0HFM90c/tBA+JXdP1vI1d/0yVmh9TSxnHoCuADN4R2NQXHmoZ6Nbds9kKAQ/25XQ=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -164,57 +140,25 @@
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/glob": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
"dev": true,
"dependencies": {
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"node_modules/@types/mkdirp": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
"version": "20.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz",
"integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/readline-sync": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.8.tgz",
"integrity": "sha512-BL7xOf0yKLA6baAX6MMOnYkoflUyj/c7y3pqMRfU0va7XlwHAOTOIo4x55P/qLfMsuaYdJJKubToLqRVmRtRZA==",
"dev": true
},
"node_modules/@types/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
"dev": true,
"dependencies": {
"@types/glob": "*",
"@types/node": "*"
}
},
"node_modules/@types/semver": {
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"node_modules/acorn": {
@@ -238,12 +182,26 @@
"node": ">=0.4.0"
}
},
"node_modules/adm-zip": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"engines": {
"node": ">=6.0"
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/arg": {
@@ -252,45 +210,34 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^1.0.0"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/create-require": {
"version": "1.1.1",
@@ -298,6 +245,19 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -323,38 +283,40 @@
"node": ">=0.3.1"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/engine.io-client": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.2.0.tgz",
"integrity": "sha512-BcIBXGBkT7wKecwnfrSV79G2X5lSUSgeAGgoo60plXf8UsQEvCQww/KMwXSMhVjb98fFYNq20CC5eo8IOAPqsg==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"dependencies": {
"base64-arraybuffer": "0.1.4",
"component-emitter": "~1.3.0",
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.1",
"has-cors": "1.1.0",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~2.0.0",
"yeast": "0.1.2"
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
"dependencies": {
"base64-arraybuffer": "0.1.4"
},
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
"engines": {
"node": ">=8.0.0"
"node": ">=10.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
@@ -370,53 +332,71 @@
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": "*"
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"node_modules/glob": {
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
@@ -436,25 +416,25 @@
"dev": true
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dependencies": {
"brace-expansion": "^1.1.7"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "*"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"bin": {
"mkdirp": "bin/cmd.js"
},
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"engines": {
"node": ">=10"
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/ms": {
@@ -462,30 +442,35 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
},
"node_modules/parseuri": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dependencies": {
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=0.10.0"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.3.tgz",
"integrity": "sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg==",
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/readline-sync": {
@@ -497,23 +482,26 @@
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
"dependencies": {
"glob": "^7.1.3"
"glob": "^10.3.7"
},
"bin": {
"rimraf": "bin.js"
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -524,6 +512,124 @@
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -568,40 +674,139 @@
}
},
"node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/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/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=8.3.0"
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
@@ -629,11 +834,6 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",

View File

@@ -1,10 +1,10 @@
{
"name": "scrypted",
"version": "1.0.69",
"version": "1.3.6",
"description": "",
"main": "./dist/main.js",
"main": "./dist/packages/cli/src/main.js",
"bin": {
"scrypted": "./dist/main.js"
"scrypted": "./dist/packages/cli/src/main.js"
},
"scripts": {
"prebuild": "rimraf dist",
@@ -16,25 +16,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.1.43",
"@scrypted/types": "^0.2.66",
"adm-zip": "^0.5.10",
"axios": "^0.21.4",
"engine.io-client": "^5.2.0",
"ip": "^1.1.8",
"mkdirp": "^1.0.4",
"@scrypted/client": "^1.3.3",
"@scrypted/types": "^0.2.99",
"engine.io-client": "^6.5.3",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.8",
"tslib": "^2.5.0"
"semver": "^7.5.4",
"tslib": "^2.6.2"
},
"devDependencies": {
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.14.2",
"@types/readline-sync": "^1.4.4",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.13",
"@types/node": "^20.9.4",
"@types/readline-sync": "^1.4.8",
"@types/semver": "^7.5.6",
"rimraf": "^5.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.3.2"
}
}

View File

@@ -1,16 +1,16 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import axios, { AxiosRequestConfig } from 'axios';
import readline from 'readline-sync';
import https from 'https';
import mkdirp from 'mkdirp';
import { installServe, serveMain } from './service';
import { connectScryptedClient } from '@scrypted/client';
import { ScryptedMimeTypes, FFmpegInput } from '@scrypted/types';
import semver from 'semver';
import { FFmpegInput, ScryptedMimeTypes } from '@scrypted/types';
import child_process from 'child_process';
import fs from 'fs';
import https from 'https';
import path from 'path';
import readline from 'readline-sync';
import semver from 'semver';
import { authHttpFetch } from '../../../common/src/http-auth-fetch';
import { installServe, serveMain } from './service';
import { connectShell } from './shell';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
@@ -54,17 +54,20 @@ async function doLogin(host: string) {
});
const url = `https://${host}/login`;
const response = await axios(Object.assign({
const response = await authHttpFetch({
method: 'GET',
auth: {
credential: {
username,
password,
},
url,
httpsAgent,
}, axiosConfig));
rejectUnauthorized: false,
responseType: 'json',
});
mkdirp.sync(scryptedHome);
fs.mkdirSync(scryptedHome, {
recursive: true,
});
let login: LoginFile;
try {
login = JSON.parse(fs.readFileSync(loginPath).toString());
@@ -76,7 +79,7 @@ async function doLogin(host: string) {
login = {};
login = login || {};
login[host] = response.data;
login[host] = response.body;
fs.writeFileSync(loginPath, JSON.stringify(login));
return login;
}
@@ -100,12 +103,6 @@ async function getOrDoLogin(host: string): Promise<{
return login[host];
}
const axiosConfig: AxiosRequestConfig = {
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
}
async function runCommand() {
const [idOrName, optionalHost] = process.argv[3].split('@');
const host = toIpAndPort(optionalHost || '127.0.0.1');
@@ -117,9 +114,6 @@ async function runCommand() {
pluginId: '@scrypted/core',
username: login.username,
password: login.token,
axiosConfig: {
httpsAgent,
}
});
const device: any = sdk.systemManager.getDeviceById(idOrName) || sdk.systemManager.getDeviceByName(idOrName);
@@ -209,16 +203,34 @@ async function main() {
const login = await getOrDoLogin(ip);
const url = `https://${ip}/web/component/script/install/${pkg}`;
const response = await axios(Object.assign({
const response = await authHttpFetch({
method: 'POST',
auth: {
credential: {
username: login.username,
password: login.token,
},
url,
}, axiosConfig));
rejectUnauthorized: false,
responseType: 'json',
});
console.log('install successful. id:', response.data.id);
console.log('install successful. id:', response.body.id);
}
else if (process.argv[2] === 'shell') {
console.log = () => { };
const host = toIpAndPort(process.argv[3] || '127.0.0.1');
const login = await getOrDoLogin(host);
const sdk = await connectScryptedClient({
baseUrl: `https://${host}`,
pluginId: '@scrypted/core',
username: login.username,
password: login.token,
});
const separator = process.argv.indexOf("--");
const cmd = separator != -1 ? process.argv.slice(separator + 1) : [];
await connectShell(sdk, ...cmd);
}
else {
console.log('usage:');
@@ -231,6 +243,7 @@ async function main() {
console.log(' npx scrypted command name-or-id[@127.0.0.1[:10443]] method-name [...method-arguments]');
console.log(' npx scrypted ffplay name-or-id[@127.0.0.1[:10443]] method-name [...method-arguments]');
console.log(' npx scrypted create-cert-json /path/to/key.pem /path/to/cert.pem');
console.log(' npx scrypted shell [127.0.0.1[:10443]] [-- cmd [...cmd-args]]');
console.log();
console.log('examples:');
console.log(' npx scrypted install @scrypted/rtsp');

View File

@@ -2,10 +2,8 @@
import child_process from 'child_process';
import { once } from 'events';
import fs from 'fs';
import rimraf from 'rimraf';
import path from 'path';
import os from 'os';
import mkdirp from 'mkdirp';
import semver from 'semver';
async function sleep(ms: number) {
@@ -20,7 +18,12 @@ async function runCommand(command: string, ...args: string[]) {
command += '.cmd';
console.log('running', command, ...args);
const cp = child_process.spawn(command, args, {
stdio: 'inherit'
stdio: 'inherit',
env: {
...process.env,
// https://github.com/lovell/sharp/blob/eefaa998725cf345227d94b40615e090495c6d09/lib/libvips.js#L115C19-L115C46
SHARP_IGNORE_GLOBAL_LIBVIPS: 'true',
},
});
await once(cp, 'exit');
if (cp.exitCode)
@@ -57,17 +60,26 @@ export function getInstallDir() {
export function cwdInstallDir(): { volume: string, installDir: string } {
const installDir = getInstallDir();
const volume = path.join(installDir, 'volume');
mkdirp.sync(volume);
fs.mkdirSync(volume, {
recursive: true,
});
process.chdir(installDir);
return { volume, installDir };
}
function rimrafSync(p: string) {
fs.rmSync(p, {
recursive: true,
force: true,
});
}
export async function installServe(installVersion: string, ignoreError?: boolean) {
const { installDir } = cwdInstallDir();
const packageLockJson = path.join(installDir, 'package-lock.json');
// apparently corrupted or old version of package-lock.json prevents upgrades, so
// nuke it before installing.
rimraf.sync(packageLockJson);
rimrafSync(packageLockJson);
const installJson = path.join(installDir, 'install.json');
try {
@@ -78,7 +90,7 @@ export async function installServe(installVersion: string, ignoreError?: boolean
catch (e) {
const nodeModules = path.join(installDir, 'node_modules');
console.log('Node version mismatch, missing, or corrupt. Clearing node_modules.');
rimraf.sync(nodeModules);
rimrafSync(nodeModules);
}
fs.writeFileSync(installJson, JSON.stringify({
version: process.version,
@@ -112,8 +124,8 @@ export async function serveMain(installVersion?: string) {
console.log('cwd', process.cwd());
while (true) {
rimraf.sync(EXIT_FILE);
rimraf.sync(UPDATE_FILE);
rimrafSync(EXIT_FILE);
rimrafSync(UPDATE_FILE);
await startServer(installDir);

90
packages/cli/src/shell.ts Normal file
View File

@@ -0,0 +1,90 @@
import { DeviceProvider, ScryptedStatic, StreamService } from "@scrypted/types";
import { createAsyncQueue } from '../../../common/src/async-queue';
export async function connectShell(sdk: ScryptedStatic, ...cmd: string[]) {
const termSvc = await sdk.systemManager.getDeviceByName<DeviceProvider>("@scrypted/core").getDevice("terminalservice");
if (!termSvc) {
throw Error("@scrypted/core does not provide a Terminal Service");
}
const termSvcDirect = await sdk.connectRPCObject<StreamService>(termSvc);
const dataQueue = createAsyncQueue<Buffer>();
const ctrlQueue = createAsyncQueue<any>();
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
} else {
process.stdin.on("end", () => {
ctrlQueue.enqueue({ eof: true });
dataQueue.enqueue(Buffer.alloc(0));
});
}
ctrlQueue.enqueue({ interactive: Boolean(process.stdin.isTTY), cmd: cmd });
const dim = { cols: process.stdout.columns, rows: process.stdout.rows };
ctrlQueue.enqueue({ dim });
let bufferedLength = 0;
const MAX_BUFFERED_LENGTH = 64000;
process.stdin.on('data', async data => {
bufferedLength += data.length;
const promise = dataQueue.enqueue(data).then(() => bufferedLength -= data.length);
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
process.stdin.pause();
await promise;
if (bufferedLength < MAX_BUFFERED_LENGTH)
process.stdin.resume();
}
});
async function* generator() {
while (true) {
const ctrlBuffers = ctrlQueue.clear();
if (ctrlBuffers.length) {
for (const ctrl of ctrlBuffers) {
if (ctrl.eof) {
// flush the buffer before sending eof
const dataBuffers = dataQueue.clear();
const concat = Buffer.concat(dataBuffers);
if (concat.length) {
yield concat;
}
}
yield JSON.stringify(ctrl);
}
continue;
}
const dataBuffers = dataQueue.clear();
if (dataBuffers.length === 0) {
const buf = await dataQueue.dequeue();
if (buf.length)
yield buf;
continue;
}
const concat = Buffer.concat(dataBuffers);
if (concat.length)
yield concat;
}
}
process.stdout.on('resize', () => {
const dim = { cols: process.stdout.columns, rows: process.stdout.rows };
ctrlQueue.enqueue({ dim });
dataQueue.enqueue(Buffer.alloc(0));
});
try {
for await (const message of await termSvcDirect.connectStream(generator())) {
if (!message) {
process.exit();
}
process.stdout.write(new Uint8Array(Buffer.from(message)));
}
} catch {
// ignore
} finally {
process.exit();
}
}

View File

@@ -9,3 +9,4 @@ scrypted.db.bak
.exit
.update
.venv
.vscode/launch.json

View File

@@ -0,0 +1,25 @@
import { Camera, VideoCamera, VideoFrameGenerator } from '@scrypted/types';
import { connectScryptedClient } from '../dist/packages/client/src';
async function example() {
const sdk = await connectScryptedClient({
baseUrl: 'https://localhost:10443',
pluginId: "@scrypted/core",
username: process.env.SCRYPTED_USERNAME || 'admin',
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
});
console.log('server version', sdk.serverVersion);
const office = sdk.systemManager.getDeviceByName<VideoCamera & Camera>("Office");
const libav = sdk.systemManager.getDeviceByName<VideoFrameGenerator>("Libav");
const mo = await office.getVideoStream();
const generator = await libav.generateVideoFrames(mo);
const remote = await sdk.connectRPCObject!(generator);
for await (const frame of remote) {
console.log(frame);
}
}
example();

View File

@@ -1,25 +1,16 @@
import { ObjectDetector, ObjectsDetected, ScryptedInterface } from '@scrypted/types';
import { connectScryptedClient } from '../dist/packages/client/src';
import https from 'https';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
async function example() {
const sdk = await connectScryptedClient({
baseUrl: 'https://localhost:10443',
pluginId: "@scrypted/core",
username: process.env.SCRYPTED_USERNAME || 'admin',
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
axiosConfig: {
httpsAgent,
}
});
console.log('server version', sdk.serverVersion);
const backyard = sdk.systemManager.getDeviceByName<ObjectDetector>("Hikvision Test");
const backyard = sdk.systemManager.getDeviceByName<ObjectDetector>("IP CAMERA");
if (!backyard)
throw new Error('Device not found');

View File

@@ -1,21 +1,12 @@
import { connectScryptedClient } from '../dist/packages/client/src';
import { OnOff } from '@scrypted/types';
import https from 'https';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
})
async function example() {
const sdk = await connectScryptedClient({
baseUrl: 'https://localhost:10443',
pluginId: "@scrypted/core",
username: process.env.SCRYPTED_USERNAME || 'admin',
password: process.env.SCRYPTED_PASSWORD || 'swordfish',
axiosConfig: {
httpsAgent,
}
});
console.log('server version', sdk.serverVersion);

View File

@@ -1,76 +1,236 @@
{
"name": "@scrypted/client",
"version": "1.1.55",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.55",
"version": "1.3.4",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.94",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
"@scrypted/types": "^0.3.4",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.4",
"rimraf": "^5.0.5"
},
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
"@types/ip": "^1.1.3",
"@types/node": "^20.10.8",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
},
"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/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"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/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@scrypted/types": {
"version": "0.2.94",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.94.tgz",
"integrity": "sha512-615C6lLnJGk0qhp+Y72B3xeD2CS9p/h8JUmFDjKh4H4IjL6zlV10tZVAXWQt3Q5rmy1WAaS3nScR6NgxZ5woOA=="
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.4.tgz",
"integrity": "sha512-k/YMx8lIWOkePgXfKW9POr12mb+erFU2JKxO7TW92GyW8ojUWw9VOc0PK6O9bybi0vhsEnvMFkO6pO6bAonsVA=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"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/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.3.tgz",
"integrity": "sha512-64waoJgkXFTYnCYDUWgSATJ/dXEBanVkaP5d4Sbk7P6U7cTTMhxVyROTckc6JKdwCrgnAjZMn0k3177aQxtDEA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.7"
"undici-types": "~5.26.4"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz",
"integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"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/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
"balanced-match": "^1.0.0"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"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/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/debug": {
"version": "4.3.4",
@@ -88,30 +248,49 @@
}
}
},
"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/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/engine.io-client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"funding": [
{
"type": "individual",
@@ -127,53 +306,106 @@
}
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": "*"
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"node_modules/glob": {
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
"minimatch": "^9.0.1",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
"path-scurry": "^1.10.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "*"
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
"engines": {
"node": "14 || >=16.14"
}
},
"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/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/ms": {
@@ -181,53 +413,329 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=0.10.0"
"node": ">=8"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"node_modules/path-scurry": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
"dependencies": {
"glob": "^7.1.3"
"lru-cache": "^9.1.1 || ^10.0.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"bin": {
"rimraf": "bin.js"
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
"integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==",
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"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.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/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/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ws": {
"version": "8.11.0",
@@ -256,6 +764,15 @@
"engines": {
"node": ">=0.4.0"
}
},
"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"
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.55",
"version": "1.3.4",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -12,14 +12,15 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
"@types/ip": "^1.1.3",
"@types/node": "^20.10.8",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"@scrypted/types": "^0.2.94",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
"@scrypted/types": "^0.3.4",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.4",
"rimraf": "^5.0.5"
}
}

View File

@@ -1,11 +1,11 @@
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
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";
import { timeoutPromise } from "../../../common/src/promise-utils";
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
import type { ClusterObject, ConnectRPCObject } from '../../../server/src/cluster/connect-rpc-object';
import type { IOSocket } from '../../../server/src/io';
import { MediaObject } from '../../../server/src/plugin/mediaobject';
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
@@ -14,6 +14,21 @@ import { createRpcDuplexSerializer, createRpcSerializer } from '../../../server/
import packageJson from '../package.json';
import { isIPAddress } from "./ip";
import { domFetch } from "../../../server/src/fetch";
import { httpFetch } from '../../../server/src/fetch/http-fetch';
let fetcher: typeof httpFetch | typeof domFetch;
try {
require('net');
require('events');
fetcher = httpFetch;
}
catch (e) {
fetcher = domFetch;
}
const sourcePeerId = RpcPeer.generateId();
type IOClientSocket = eio.Socket & IOSocket;
function once(socket: IOClientSocket, event: 'open' | 'message') {
@@ -56,7 +71,6 @@ export interface ScryptedConnectionOptions {
local?: boolean;
webrtc?: boolean;
baseUrl?: string;
axiosConfig?: AxiosRequestConfig;
previousLoginResult?: ScryptedClientLoginResult;
}
@@ -87,10 +101,13 @@ function isRunningStandalone() {
export async function logoutScryptedClient(baseUrl?: string) {
const url = combineBaseUrl(baseUrl, 'logout');
const response = await axios(url, {
const response = await fetcher({
url,
withCredentials: true,
responseType: 'json',
rejectUnauthorized: false,
});
return response.data;
return response.body;
}
export function getCurrentBaseUrl() {
@@ -119,75 +136,90 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
maxAge = 365 * 24 * 60 * 60 * 1000;
const url = combineBaseUrl(baseUrl, 'login');
const response = await axios.post(url, {
username,
password,
change_password,
maxAge,
}, {
const response = await fetcher({
url,
body: {
username,
password,
change_password,
maxAge,
},
rejectUnauthorized: false,
withCredentials: true,
...options.axiosConfig,
responseType: 'json',
});
if (response.status !== 200)
throw new Error('status ' + response.status);
if (response.statusCode !== 200)
throw new Error('status ' + response.statusCode);
const addresses = response.data.addresses as string[] || [];
// the cloud plugin will include this header.
// 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'];
const { body } = response;
return {
error: response.data.error as string,
authorization: response.data.authorization as string,
queryToken: response.data.queryToken as any,
token: response.data.token as string,
addresses,
scryptedCloud,
directAddress,
cloudAddress,
error: body.error as string,
authorization: body.authorization as string,
queryToken: body.queryToken as any,
token: body.token as string,
addresses: body.addresses as string[],
externalAddresses: body.externalAddresses as string[],
// the cloud plugin will include this header.
// should maybe move this into the cloud server itself.
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
directAddress: response.headers.get('x-scrypted-direct-address'),
cloudAddress: response.headers.get('x-scrypted-cloud-address'),
};
}
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, {
let url = combineBaseUrl(baseUrl, 'login');
const headers = new Headers();
if (options?.previousLoginResult?.queryToken) {
// headers.Authorization = options?.previousLoginResult?.authorization;
// const search = new URLSearchParams(options.previousLoginResult.queryToken);
// url += '?' + search.toString();
const token = options?.previousLoginResult.username + ":" + options.previousLoginResult.token;
const hash = Buffer.from(token).toString('base64');
headers.set('Authorization', `Basic ${hash}`);
}
const response = await fetcher({
url,
withCredentials: true,
headers,
...options?.axiosConfig,
rejectUnauthorized: false,
responseType: 'json',
});
const scryptedCloud = response.headers['x-scrypted-cloud'] === 'true';
const directAddress = response.headers['x-scrypted-direct-address'];
const cloudAddress = response.headers['x-scrypted-cloud-address'];
const { body } = response;
return {
baseUrl,
hostname: response.data.hostname as string,
redirect: response.data.redirect as string,
username: response.data.username as string,
expiration: response.data.expiration as number,
hasLogin: !!response.data.hasLogin,
error: response.data.error as string,
authorization: response.data.authorization as string,
queryToken: response.data.queryToken as any,
token: response.data.token as string,
addresses: response.data.addresses as string[],
scryptedCloud,
directAddress,
cloudAddress,
hostname: body.hostname as string,
redirect: body.redirect as string,
username: body.username as string,
expiration: body.expiration as number,
hasLogin: !!body.hasLogin,
error: body.error as string,
authorization: body.authorization as string,
queryToken: body.queryToken as any,
token: body.token as string,
addresses: body.addresses as string[],
externalAddresses: body.externalAddresses as string[],
// the cloud plugin will include this header.
// should maybe move this into the cloud server itself.
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
directAddress: response.headers.get('x-scrypted-direct-address'),
cloudAddress: response.headers.get('x-scrypted-cloud-address'),
};
}
export interface ScryptedClientLoginResult {
username: string;
token: string;
authorization: string;
queryToken: { [parameter: string]: string };
localAddresses: string[];
externalAddresses: string[];
scryptedCloud: boolean;
directAddress: string;
cloudAddress: string;
@@ -232,35 +264,54 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
let authorization: string;
let queryToken: any;
let localAddresses: string[];
let externalAddresses: string[];
let scryptedCloud: boolean;
let directAddress: string;
let cloudAddress: string;
let token: string;
console.log('@scrypted/client', packageJson.version);
const extraHeaders: { [header: string]: string } = {};
// Chrome will complain about websites making xhr requests to self signed https sites, even
// if the cert has been accepted. Other browsers seem fine.
// So the default is not to connect to IP addresses on Chrome, but do so on other browsers.
const isChrome = globalThis.navigator?.userAgent.includes('Chrome');
const isNotChromeOrIsInstalledApp = !isChrome || isInstalledApp();
let tryAlternateAddresses = false;
if (username && password) {
const loginResult = await loginScryptedClient(options as ScryptedLoginOptions);
if (loginResult.authorization)
extraHeaders['Authorization'] = loginResult.authorization;
localAddresses = loginResult.addresses;
externalAddresses = loginResult.externalAddresses;
scryptedCloud = loginResult.scryptedCloud;
directAddress = loginResult.directAddress;
cloudAddress = loginResult.cloudAddress;
authorization = loginResult.authorization;
queryToken = loginResult.queryToken;
token = loginResult.token;
console.log('login result', Date.now() - start, loginResult);
}
else {
const urlsToCheck = new Set<string>();
for (const u of [
...options?.previousLoginResult?.localAddresses || [],
options?.previousLoginResult?.directAddress,
options?.previousLoginResult?.cloudAddress,
]) {
if (u)
urlsToCheck.add(u);
if (options?.previousLoginResult?.token) {
for (const u of [
...options?.previousLoginResult?.localAddresses || [],
options?.previousLoginResult?.directAddress,
]) {
if (u && (isNotChromeOrIsInstalledApp || options.direct))
urlsToCheck.add(u);
}
for (const u of [
...options?.previousLoginResult?.externalAddresses || [],
options?.previousLoginResult?.cloudAddress,
]) {
if (u)
urlsToCheck.add(u);
}
}
// the alternate urls must have a valid response.
@@ -289,28 +340,39 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
let loginCheck: Awaited<ReturnType<typeof checkScryptedClientLogin>>;
try {
loginCheck = await Promise.any(loginCheckPromises);
tryAlternateAddresses ||= loginCheck.baseUrl !== baseUrl;
}
catch (e) {
loginCheck = await baseUrlCheck;
}
if (tryAlternateAddresses)
console.log('Found direct login. Allowing alternate addresses.')
if (loginCheck.error || loginCheck.redirect)
throw new ScryptedClientLoginError(loginCheck);
localAddresses = loginCheck.addresses;
externalAddresses = loginCheck.externalAddresses;
scryptedCloud = loginCheck.scryptedCloud;
directAddress = loginCheck.directAddress;
cloudAddress = loginCheck.cloudAddress;
username = loginCheck.username;
authorization = loginCheck.authorization;
queryToken = loginCheck.queryToken;
token = loginCheck.token;
console.log('login checked', Date.now() - start, loginCheck);
}
let socket: IOClientSocket;
const eioPath = `endpoint/${pluginId}/engine.io/api`;
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
// https://github.com/socketio/engine.io/issues/690
const cacehBust = Math.random().toString(36).substring(3, 10);
const eioOptions: Partial<SocketOptions> = {
path: eioEndpoint,
query: {
cacehBust,
},
withCredentials: true,
extraHeaders,
rejectUnauthorized: false,
@@ -323,25 +385,26 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
// watch for this flush.
const flush = new Deferred<void>();
// Chrome will complain about websites making xhr requests to self signed https sites, even
// if the cert has been accepted. Other browsers seem fine.
// So the default is not to connect to IP addresses on Chrome, but do so on other browsers.
const isChrome = globalThis.navigator?.userAgent.includes('Chrome');
const isNotChromeOrIsInstalledApp = !isChrome || isInstalledApp();
const addresses: string[] = [];
const localAddressDefault = isNotChromeOrIsInstalledApp;
if (((scryptedCloud && options.local === undefined && localAddressDefault) || options.local) && localAddresses) {
tryAlternateAddresses ||= scryptedCloud;
if (((tryAlternateAddresses && options.local === undefined && localAddressDefault) || options.local) && localAddresses) {
addresses.push(...localAddresses);
}
const directAddressDefault = directAddress && (isNotChromeOrIsInstalledApp || !isIPAddress(directAddress));
if (((scryptedCloud && options.direct === undefined && directAddressDefault) || options.direct) && directAddress) {
if (((tryAlternateAddresses && options.direct === undefined && directAddressDefault) || options.direct) && directAddress) {
addresses.push(directAddress);
}
if (((scryptedCloud && options.direct === undefined) || options.direct) && cloudAddress) {
addresses.push(cloudAddress);
if ((tryAlternateAddresses && options.direct === undefined) || options.direct) {
if (cloudAddress)
addresses.push(cloudAddress);
for (const externalAddress of externalAddresses || []) {
addresses.push(externalAddress);
}
}
const tryAddresses = !!addresses.length;
@@ -388,7 +451,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
// It is probably better to simply prompt and redirect to the LAN address
// if it is reacahble.
for (const address of addresses) {
for (const address of new Set(addresses)) {
console.log('trying', address);
const check = new eio.Socket(address, localEioOptions);
sockets.push(check);
@@ -407,6 +470,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
console.log('trying webrtc');
const webrtcEioOptions: Partial<SocketOptions> = {
path: '/endpoint/@scrypted/webrtc/engine.io/',
query: {
cacehBust,
},
withCredentials: true,
extraHeaders,
rejectUnauthorized: false,
@@ -557,6 +623,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
await once(check, 'open');
return {
ready: check,
address: explicitBaseUrl,
connectionType: 'http',
};
})());
@@ -666,6 +733,105 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
.map(id => systemManager.getDeviceById(id))
.find(device => device.pluginId === '@scrypted/core' && device.nativeId === `user:${username}`);
const clusterPeers = new Map<number, Promise<RpcPeer>>();
const ensureClusterPeer = (clusterObject: ClusterObject) => {
let clusterPeerPromise = clusterPeers.get(clusterObject.port);
if (!clusterPeerPromise) {
clusterPeerPromise = (async () => {
const eioPath = 'engine.io/connectRPCObject';
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
const clusterPeerOptions = {
path: eioEndpoint,
query: {
cacehBust,
clusterObject: JSON.stringify(clusterObject),
},
withCredentials: true,
extraHeaders,
rejectUnauthorized: false,
transports: options?.transports,
};
const clusterPeerSocket = new eio.Socket(explicitBaseUrl, clusterPeerOptions);
let peerReady = false;
clusterPeerSocket.on('close', () => {
clusterPeers.delete(clusterObject.port);
if (!peerReady) {
throw new Error("peer disconnected before setup completed");
}
});
try {
await once(clusterPeerSocket, 'open');
const serializer = createRpcDuplexSerializer({
write: data => clusterPeerSocket.send(data),
});
clusterPeerSocket.on('message', data => serializer.onData(Buffer.from(data)));
const clusterPeer = new RpcPeer(clientName || 'engine.io-client', "cluster-proxy", (message, reject, serializationContext) => {
try {
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e);
}
});
serializer.setupRpcPeer(clusterPeer);
clusterPeer.tags.localPort = sourcePeerId;
peerReady = true;
return clusterPeer;
}
catch (e) {
console.error('failure ipc connect', e);
clusterPeerSocket.close();
throw e;
}
})();
clusterPeers.set(clusterObject.port, clusterPeerPromise);
}
return clusterPeerPromise;
};
const resolveObject = async (proxyId: string, sourcePeerPort: number) => {
const sourcePeer = await clusterPeers.get(sourcePeerPort);
if (sourcePeer?.remoteWeakProxies) {
return Object.values(sourcePeer.remoteWeakProxies).find(
v => v.deref()?.__cluster?.proxyId == proxyId
)?.deref();
}
return null;
}
const connectRPCObject = async (value: any) => {
const clusterObject: ClusterObject = value?.__cluster;
if (!clusterObject) {
return value;
}
const { port, proxyId } = clusterObject;
// check if object is already connected
const resolved = await resolveObject(proxyId, port);
if (resolved) {
return resolved;
}
try {
const clusterPeerPromise = ensureClusterPeer(clusterObject);
const clusterPeer = await clusterPeerPromise;
const connectRPCObject: ConnectRPCObject = await clusterPeer.getParam('connectRPCObject');
const newValue = await connectRPCObject(clusterObject);
if (!newValue)
throw new Error('ipc object not found?');
return newValue;
}
catch (e) {
console.error('failure ipc', e);
return value;
}
}
const ret: ScryptedClientStatic = {
userId: userDevice?.id,
serverVersion,
@@ -686,13 +852,17 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
browserSignalingSession,
rpcPeer,
loginResult: {
username,
token,
directAddress,
localAddresses,
externalAddresses,
scryptedCloud,
queryToken,
authorization,
cloudAddress,
}
},
connectRPCObject,
}
socket.on('close', () => {

View File

@@ -1,24 +1,40 @@
{
"name": "@scrypted/alexa",
"version": "0.2.7",
"version": "0.2.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.2.7",
"version": "0.2.10",
"dependencies": {
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@scrypted/common": "../../common",
"@scrypted/sdk": "../../sdk",
"@types/node": "^18.4.2"
}
},
"../../common": {
"version": "1.0.1",
"dev": true,
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.104",
"version": "0.2.108",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -54,6 +70,13 @@
"typedoc": "^0.23.21"
}
},
"../common": {
"extraneous": true
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
"link": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
@@ -70,9 +93,9 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.2.8",
"version": "0.2.10",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -39,6 +39,7 @@
},
"devDependencies": {
"@types/node": "^18.4.2",
"@scrypted/sdk": "../../sdk"
"@scrypted/sdk": "../../sdk",
"@scrypted/common": "../../common"
}
}

View File

@@ -79,7 +79,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
if (status === DeviceMixinStatus.Setup)
await this.syncEndpoints();
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
if (!this.devices.has(eventSource.id)) {
this.devices.set(eventSource.id, eventSource);
@@ -142,7 +142,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
await this.syncEndpoints();
}
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any): Promise<void> {
if (!eventSource)
return;
@@ -194,14 +194,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
return;
data = await this.addAccessToken(data);
await this.postEvent(data);
}
private async addAccessToken(data: any) : Promise<any> {
private async addAccessToken(data: any): Promise<any> {
const accessToken = await this.getAccessToken();
if (data.event === undefined)
@@ -232,7 +232,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
'api.fe.amazonalexa.com'
];
async getAlexaEndpoint() : Promise<string> {
async getAlexaEndpoint(): Promise<string> {
if (this.storageSettings.values.apiEndpoint)
return this.storageSettings.values.apiEndpoint;
@@ -276,7 +276,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
});
}
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
async getEndpoints(): Promise<DiscoveryEndpoint[]> {
const endpoints: DiscoveryEndpoint[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
@@ -284,7 +284,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
if (!device.mixins?.includes(this.id))
continue;
const endpoint = await this.getEndpointForDevice(device);
if (endpoint)
endpoints.push(endpoint);
@@ -319,7 +319,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
const endpoints = await this.getEndpoints();
if (!endpoints.length)
return [];
return [];
const accessToken = await this.getAccessToken();
const data = {
@@ -448,7 +448,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
self.console.warn(error?.response?.data);
self.log.a(error?.response?.data?.error_description);
break;
case 'expired_token':
self.console.warn(error?.response?.data);
self.log.a(error?.response?.data?.error_description);
@@ -480,9 +480,14 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
this.storageSettings.values.tokenInfo = grant;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
const self = this;
let accessToken = await this.getAccessToken().catch(reason => {
let accessToken: any;
try {
accessToken = await this.getAccessToken();
}
catch (reason) {
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
this.storageSettings.values.tokenInfo = undefined;
@@ -491,36 +496,23 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
return undefined;
});
if (accessToken !== undefined) {
this.log.clearAlerts();
try {
response.send({
"event": {
"header": {
"namespace": "Alexa.Authorization",
"name": "AcceptGrant.Response",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {}
}
});
} catch (error) {
this.console.error(`AcceptGrant.Response failed because ${error}`);
this.storageSettings.values.tokenInfo = undefined;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
throw error;
return;
};
this.log.clearAlerts();
response.send({
"event": {
"header": {
"namespace": "Alexa.Authorization",
"name": "AcceptGrant.Response",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {}
}
}
});
}
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
async getEndpointForDevice(device: ScryptedDevice): Promise<DiscoveryEndpoint> {
if (!device)
return;
@@ -545,7 +537,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
};
let supportedEndpointHealths: any[] = [];
if (device.interfaces.includes(ScryptedInterface.Online)) {
supportedEndpointHealths.push({
"name": "connectivity"
@@ -632,8 +624,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
debug("received directive from alexa", mapName, body);
const handler = alexaHandlers.get(mapName);
if (handler)
return handler.apply(this, [request, response, directive]);
if (handler) {
await handler.apply(this, [request, response, directive]);
return;
}
const deviceHandler = alexaDeviceHandlers.get(mapName);
@@ -644,7 +638,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
return;
}
return deviceHandler.apply(this, [request, response, directive, device]);
await deviceHandler.apply(this, [request, response, directive, device]);
return;
} else {
this.console.error(`no handler for: ${mapName}`);
}

View File

@@ -4,6 +4,7 @@ import { v4 as createMessageId } from 'uuid';
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
import { Deferred } from '@scrypted/common/src/deferred';
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: AlexaHttpResponse, public directive: any) {
@@ -13,7 +14,8 @@ export class AlexaSignalingSession implements RTCSignalingSession {
__proxy_props: { options: RTCSignalingOptions; };
options: RTCSignalingOptions;
remoteDescription = new Deferred<void>();
async getOptions(): Promise<RTCSignalingOptions> {
return this.options;
}
@@ -39,11 +41,17 @@ export class AlexaSignalingSession implements RTCSignalingSession {
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (type !== 'offer') {
const e = new Error('Alexa only supports RTC offer');
this.remoteDescription.reject(e);
throw e;
}
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
if (sendIceCandidate) {
const e = new Error("Alexa does not support trickle ICE");
this.remoteDescription.reject(e);
throw e;
}
return {
type: type,
@@ -67,15 +75,16 @@ export class AlexaSignalingSession implements RTCSignalingSession {
},
context: undefined
};
data.event.header.name = "AnswerGeneratedForSession";
data.event.header.messageId = createMessageId();
data.event.payload.answer = {
format: 'SDP',
value: description.sdp,
};
this.remoteDescription.resolve();
this.response.send(data);
}
}
@@ -85,13 +94,14 @@ const sessionCache = new Map<string, RTCSessionControl>();
alexaDeviceHandlers.set('Alexa.RTCSessionController/InitiateSessionWithOffer', async (request, response, directive: any, device: ScryptedDevice & RTCSignalingChannel) => {
const { header, endpoint, payload } = directive;
const { sessionId } = payload;
const session = new AlexaSignalingSession(response, directive);
const control = await device.startRTCSignalingSession(session);
control.setPlayback({
audio: true,
video: false,
})
});
await session.remoteDescription.promise;
sessionCache.set(sessionId, control);
});
@@ -115,13 +125,13 @@ alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (re
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async (request, response, directive: any, device: ScryptedDevice) => {
const { header, endpoint, payload } = directive;
const { sessionId } = payload;
const session = sessionCache.get(sessionId);
if (session) {
sessionCache.delete(sessionId);
await session.endSession();
}
const data: WebRTCSessionDisconnectedEvent = {
"event": {
header,
@@ -130,9 +140,9 @@ alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
@@ -152,14 +162,14 @@ alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetect
},
"context": {
"properties": [{
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": detectionTypes.classes.map(type => ({
"imageNetClass": type
})),
timeOfSample: new Date().toISOString(),
uncertaintyInMilliseconds: 0
}]
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": detectionTypes.classes.map(type => ({
"imageNetClass": type
})),
timeOfSample: new Date().toISOString(),
uncertaintyInMilliseconds: 0
}]
}
};

View File

@@ -1,25 +1,23 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.127",
"version": "0.0.131",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.127",
"version": "0.0.131",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/multiparty": "^0.0.33",
"multiparty": "^4.2.3"
"@scrypted/sdk": "file:../../sdk"
},
"devDependencies": {
"@types/node": "^18.16.18"
"@types/node": "^20.10.8"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"license": "ISC",
"dependencies": {
@@ -27,14 +25,16 @@
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.10.8",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"version": "0.2.103",
"name": "@scrypted/sdk",
"version": "0.3.4",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -69,15 +69,6 @@
"typedoc": "^0.23.21"
}
},
"node_modules/@koush/axios-digest-auth": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/@koush/axios-digest-auth/-/axios-digest-auth-0.8.5.tgz",
"integrity": "sha512-EZMM0gMJ3hMUD4EuUqSwP6UGt5Vmw2TZtY7Ypec55AnxkExSXM0ySgPtqkAcnL43g1R27yAg/dQL7dRTLMqO3Q==",
"dependencies": {
"auth-header": "^1.0.0",
"axios": "^0.21.4"
}
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
"link": true
@@ -86,150 +77,20 @@
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/multiparty": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/@types/multiparty/-/multiparty-0.0.33.tgz",
"integrity": "sha512-Il6cJUpSqgojT7NxbVJUvXkCblm50/yEJYtblISDsNIeNYf4yMAhdizzidUk6h8pJ8yhwK/3Fkb+3Dwcgtwl8w==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "18.16.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw=="
},
"node_modules/auth-header": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
},
"node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.0"
"undici-types": "~5.26.4"
}
},
"node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/multiparty": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz",
"integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==",
"dependencies": {
"http-errors": "~1.8.1",
"safe-buffer": "5.2.1",
"uid-safe": "2.1.5"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.127",
"version": "0.0.131",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -35,13 +35,10 @@
]
},
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/multiparty": "^0.0.33",
"multiparty": "^4.2.3"
"@scrypted/sdk": "file:../../sdk"
},
"devDependencies": {
"@types/node": "^18.16.18"
"@types/node": "^20.10.8"
}
}

View File

@@ -1,8 +1,6 @@
import AxiosDigestAuth from '@koush/axios-digest-auth';
import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { Readable } from 'stream';
import https from 'https';
import { IncomingMessage } from 'http';
import { amcrestHttpsAgent, getDeviceInfo } from './probe';
import { getDeviceInfo } from './probe';
export enum AmcrestEvent {
MotionStart = "Code=VideoMotion;action=Start",
@@ -22,35 +20,42 @@ export enum AmcrestEvent {
DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse",
}
export class AmcrestCameraClient {
digestAuth: AxiosDigestAuth;
credential: AuthFetchCredentialState;
constructor(public ip: string, username: string, password: string, public console?: Console) {
this.digestAuth = new AxiosDigestAuth({
this.credential = {
username,
password,
};
}
async request(urlOrOptions: string | URL | HttpFetchOptions<Readable>, body?: Readable) {
const response = await authHttpFetch({
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
rejectUnauthorized: false,
credential: this.credential,
body,
});
return response;
}
async reboot() {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
const response = await this.request({
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=reboot`,
responseType: 'text',
});
return response.data as string;
return response.body;
}
async checkTwoWayAudio() {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
const response = await this.request({
url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`,
responseType: 'text',
});
return (response.data as string).includes('result=1');
return response.body.includes('result=1');
}
// appAutoStart=true
@@ -61,33 +66,27 @@ export class AmcrestCameraClient {
// updateSerial=IPC-AW46WN-S2
// updateSerialCloudUpgrade=IPC-AW46WN-.....
async getDeviceInfo() {
return getDeviceInfo(this.digestAuth, this.ip);
return getDeviceInfo(this.credential, this.ip);
}
async jpegSnapshot(): Promise<Buffer> {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'arraybuffer',
const response = await this.request({
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
timeout: 60000,
});
return Buffer.from(response.data);
return response.body;
}
async listenEvents() {
const url = `http://${this.ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]`;
console.log('preparing event listener', url);
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'stream',
const response = await this.request({
url,
responseType: 'readable',
});
const stream = response.data as IncomingMessage;
const stream = response.body;
stream.socket.setKeepAlive(true);
stream.on('data', (buffer: Buffer) => {
@@ -118,12 +117,12 @@ export class AmcrestCameraClient {
async enableContinousRecording(channel: number) {
for (let i = 0; i < 7; i++) {
const url = `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&Record[${channel - 1}].TimeSection[${i}][0]=1 00:00:00-23:59:59`;
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "POST",
const response = await this.request({
url,
});
this.console.log(response.data);
method: 'POST',
responseType: 'text',
},);
this.console.log(response.body);
}
}
}

View File

@@ -6,7 +6,6 @@ import { PassThrough, Readable, Stream } from "stream";
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
import { amcrestHttpsAgent } from './probe';
const { mediaManager } = sdk;
@@ -94,12 +93,11 @@ 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}`
const response = await this.getClient().request({
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=${element.action}`,
responseType: 'text',
});
const result = String(response.data).replace(element.replace, "").trim();
const result = String(response.body).replace(element.replace, "").trim();
deviceInfo[element.parameter] = result;
}
catch (e) {
@@ -147,11 +145,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (![...params.keys()].length)
return;
const response = await this.getClient().digestAuth.request({
httpsAgent: amcrestHttpsAgent,
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`
const response = await this.getClient().request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reconfigure result', response.data);
this.console.log('reconfigure result', response.body);
}
getClient() {
@@ -309,9 +307,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
}
@@ -334,7 +329,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
return this.storage.getItem('rtspChannel');
}
createRtspMediaStreamOptions(url: string, index: number) {
const ret = super.createRtspMediaStreamOptions(url, index);
ret.tool = 'scrypted';
@@ -348,12 +342,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
this.videoStreamOptions = (async () => {
let mas: string;
try {
const response = await client.digestAuth.request({
const response = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
responseType: 'text',
httpsAgent: amcrestHttpsAgent,
})
mas = response.data.split('=')[1].trim();
mas = response.body.split('=')[1].trim();
this.storage.setItem('maxExtraStreams', mas.toString());
}
catch (e) {
@@ -366,18 +359,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
try {
const capResponse = await client.digestAuth.request({
const capResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
responseType: 'text',
httpsAgent: amcrestHttpsAgent,
});
this.console.log(capResponse.data);
const encodeResponse = await client.digestAuth.request({
this.console.log(capResponse.body);
const encodeResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
responseType: 'text',
httpsAgent: amcrestHttpsAgent,
});
this.console.log(encodeResponse.data);
this.console.log(encodeResponse.body);
for (let i = 0; i < vsos.length; i++) {
const vso = vsos[i];
@@ -392,16 +383,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
encName = `table.Encode[${channel - 1}].ExtraFormat[${i - 1}]`;
}
const videoCodec = findValue(encodeResponse.data, encName, 'Video.Compression')
const videoCodec = findValue(encodeResponse.body, encName, 'Video.Compression')
?.replace('.', '')?.toLowerCase()?.trim();
let audioCodec = findValue(encodeResponse.data, encName, 'Audio.Compression')
let audioCodec = findValue(encodeResponse.body, encName, 'Audio.Compression')
?.replace('.', '')?.toLowerCase()?.trim();
if (audioCodec?.includes('aac'))
audioCodec = 'aac';
else if (audioCodec?.includes('g711a'))
audioCodec = 'pcm_alaw';
else if (audioCodec?.includes('g711u'))
audioCodec = 'pcm_ulaw';
audioCodec = 'pcm_mulaw';
else if (audioCodec?.includes('g711'))
audioCodec = 'pcm';
@@ -409,18 +400,18 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
vso.audio.codec = audioCodec;
vso.video.codec = videoCodec;
const width = findValue(encodeResponse.data, encName, 'Video.Width');
const height = findValue(encodeResponse.data, encName, 'Video.Height');
const width = findValue(encodeResponse.body, encName, 'Video.Width');
const height = findValue(encodeResponse.body, encName, 'Video.Height');
if (width && height) {
vso.video.width = parseInt(width);
vso.video.height = parseInt(height);
}
const bitrateOptions = findValue(capResponse.data, capName, 'Video.BitRateOptions');
const bitrateOptions = findValue(capResponse.body, capName, 'Video.BitRateOptions');
if (!bitrateOptions)
continue;
const encodeOptions = findValue(encodeResponse.data, encName, 'Video.BitRate');
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
if (!encodeOptions)
continue;
@@ -538,16 +529,15 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// seems the dahua doorbells preferred 1024 chunks. should investigate adts
// parsing and sending multipart chunks instead.
const passthrough = new PassThrough();
this.getClient().digestAuth.request({
method: 'POST',
this.getClient().request({
url,
method: 'POST',
headers: {
'Content-Type': 'Audio/AAC',
'Content-Length': '9999999'
},
httpsAgent: amcrestHttpsAgent,
data: passthrough,
});
responseType: 'readable',
}, passthrough);
try {
while (true) {

View File

@@ -1,8 +1,4 @@
import https from 'https';
export const amcrestHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
import { AuthFetchCredentialState, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
// appAutoStart=true
// deviceType=IP4M-1041B
@@ -11,17 +7,16 @@ export const amcrestHttpsAgent = new https.Agent({
// serialNumber=12345
// updateSerial=IPC-AW46WN-S2
import AxiosDigestAuth from "@koush/axios-digest-auth";
// updateSerialCloudUpgrade=IPC-AW46WN-.....
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
const response = await digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
export async function getDeviceInfo(credential: AuthFetchCredentialState, address: string) {
const response = await authHttpFetch({
credential,
url: `http://${address}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
rejectUnauthorized: false,
responseType: 'text',
});
const lines = (response.data as string).split('\n');
const lines = response.body.split('\n');
const vals: {
[key: string]: string,
} = {};

View File

@@ -1,27 +0,0 @@
{
// specify the following paths on the target scrypted server:
// 1) where @scrypted/server node module resides: this may either be a checkout or a install.
// 2) where the scrypted "volume" data is located on the server. ie, the docker volume.
// the following default examples are provided for local and docker installations,
// only modifying the debugHost should be necessary:
// local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted/node_modules/@scrypted/server",
// "scrypted.volumeRoot": "/home/pi/.scrypted/volume",
// docker installation
// "scrypted.debugHost": "192.168.2.109",
"scrypted.serverRoot": "/server/node_modules/@scrypted/server",
"scrypted.volumeRoot": "/server/volume",
// local checkout
"scrypted.debugHost": "127.0.0.1",
//"scrypted.serverRoot": "/Volumes/Dev/scrypted/server",
//"scrypted.volumeRoot": "${config:scrypted.serverRoot}/volume",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}

View File

@@ -1,43 +0,0 @@
# Arlo Plugin for Scrypted
The Arlo Plugin connects Scrypted to Arlo Cloud, allowing you to access all of your Arlo cameras in Scrypted.
It is highly recommended to create a dedicated Arlo account for use with this plugin and share your cameras from your main account, as Arlo only permits one active login to their servers per account. Using a separate account allows you to use the Arlo app or website simultaneously with this plugin, otherwise logging in from one place will log you out from all other devices.
The account you use for this plugin must have either SMS or email set as the default 2FA option. Once you enter your username and password on the plugin settings page, you should receive a 2FA code through your default 2FA option. Enter that code into the provided box, and your cameras will appear in Scrypted. Or, see below for configuring IMAP to auto-login with 2FA.
If you experience any trouble logging in, clear the username and password boxes, reload the plugin, and try again.
If you are unable to see shared cameras in your separate Arlo account, ensure that both your primary and secondary accounts are upgraded according to this [forum post](https://web.archive.org/web/20230710141914/https://community.arlo.com/t5/Arlo-Secure/Invited-friend-cannot-see-devices-on-their-dashboard-Arlo-Pro-2/m-p/1889396#M1813). Verify the sharing worked by logging in via the Arlo web dashboard.
**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.
* Motion events notifications should be turned on in the Arlo app. If you are receiving motion push notifications, Scrypted will also receive motion events.
* Disable smart detection and any cloud/local recording in the Arlo app. Arlo Cloud only permits one active stream per camera, so any smart detection or recording features may prevent downstream plugins (e.g. Homekit) from successfully pulling the video feed after a motion event.
* It is highly recommended to enable the Rebroadcast plugin to allow multiple downstream plugins to pull the video feed within Scrypted.
* If there is no audio on your camera, switch to the `FFmpeg (TCP)` parser under the `Cloud RTSP` settings.
* Prebuffering should only be enabled if the camera is wired to a persistent power source, such as a wall outlet. Prebuffering will only work if your camera does not have a battery or `Plugged In to External Power` is selected.
* The plugin supports pulling RTSP or DASH streams from Arlo Cloud. It is recommended to use RTSP for the lowest latency streams. DASH is inconsistent in reliability, and may return finicky codecs that require additional FFmpeg output arguments, e.g. `-vcodec h264`. *Note that both RTSP and DASH will ultimately pull the same video stream feed from your camera, and they cannot both be used at the same time due to the single stream limitation.*
Note that streaming cameras uses extra Internet bandwidth, since video and audio packets will need to travel from the camera through your network, out to Arlo Cloud, and then back to your network and into Scrypted.
## IMAP 2FA
The Arlo Plugin supports using the IMAP protocol to check an email mailbox for Arlo 2FA codes. This requires you to specify an email 2FA option as the default in your Arlo account settings.
The plugin should work with any mailbox that supports IMAP, but so far has been tested with Gmail. To configure a Gmail mailbox, see [here](https://support.google.com/mail/answer/7126229?hl=en) to see the Gmail IMAP settings, and [here](https://support.google.com/accounts/answer/185833?hl=en) to create an App Password. Enter the App Password in place of your normal Gmail password.
The plugin searches for emails sent by Arlo's `do_not_reply@arlo.com` address when looking for 2FA codes. If you are using a service to forward emails to the mailbox registered with this plugin (e.g. a service like iCloud's Hide My Email), it is possible that Arlo's email sender address has been overwritten by the mail forwarder. Check the email registered with this plugin to see what address the mail forwarder uses to replace Arlo's sender address, and update that in the IMAP 2FA settings.
## Virtual Security System for Arlo Sirens
In external integrations like Homekit, sirens are exposed as simple on-off switches. This makes it easy to accidentally hit the switch when using the Home app. The Arlo Plugin creates a "virtual" security system device per siren to allow Scrypted to arm or disarm the siren switch to protect against accidental triggers. This fake security system device will be synced into Homekit as a separate accessory from the camera, with the siren itself merged into the security system accessory.
Note that the virtual security system is NOT tied to your Arlo account at all, and will not make any changes such as switching your device's motion alert armed/disarmed modes. For more information, please see the README on the virtual security system device in Scrypted.
## Video Clips
The Arlo Plugin will show video clips available in Arlo Cloud for cameras with cloud recording enabled. These clips are not downloaded onto your Scrypted server, but rather streamed on-demand. Deleting clips is not available in Scrypted and should be done through the Arlo app or the Arlo web dashboard.

View File

@@ -1 +0,0 @@
from .provider import ArloProvider

View File

@@ -1 +0,0 @@
from .arlo_async import Arlo

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
import ssl
from socket import setdefaulttimeout
import requests
from requests_toolbelt.adapters import host_header_ssl
import scrypted_arlo_go
from .logging import logger
setdefaulttimeout(15)
def pick_host(hosts, hostname_to_match, endpoint_to_test):
setdefaulttimeout(5)
try:
session = requests.Session()
session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
for host in hosts:
try:
c = ssl.get_server_certificate((host, 443))
scrypted_arlo_go.VerifyCertHostname(c, hostname_to_match)
r = session.post(f"https://{host}{endpoint_to_test}", headers={"Host": hostname_to_match})
r.raise_for_status()
return host
except Exception as e:
logger.warning(f"{host} is invalid: {e}")
raise Exception("no valid hosts found!")
finally:
setdefaulttimeout(15)

View File

@@ -1,16 +0,0 @@
import logging
import sys
# construct logger instance to be used by package arlo
logger = logging.getLogger("lib")
logger.setLevel(logging.INFO)
# output logger to stdout
ch = logging.StreamHandler(sys.stdout)
# log formatting
fmt = logging.Formatter("[Arlo]: %(message)s")
ch.setFormatter(fmt)
# configure handler to logger
logger.addHandler(ch)

View File

@@ -1,85 +0,0 @@
import asyncio
import json
import random
import paho.mqtt.client as mqtt
from .stream_async import Stream
from .logging import logger
class MQTTStream(Stream):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.cached_topics = []
def _gen_client_number(self):
return random.randint(1000000000, 9999999999)
async def start(self):
if self.event_stream is not None:
return
def on_connect(client, userdata, flags, rc):
self.connected = True
self.initializing = False
logger.info(f"MQTT {id(client)} connected")
client.subscribe([
(f"u/{self.arlo.user_id}/in/userSession/connect", 0),
(f"u/{self.arlo.user_id}/in/userSession/disconnect", 0),
])
def on_disconnect(client, *args, **kwargs):
logger.info(f"MQTT {id(client)} disconnected")
def on_message(client, userdata, msg):
payload = msg.payload.decode()
logger.debug(f"Received event: {payload}")
try:
response = json.loads(payload.strip())
except json.JSONDecodeError:
return
if response.get('resource') is not None:
self.event_loop.call_soon_threadsafe(self._queue_response, response)
self.event_stream = mqtt.Client(client_id=f"user_{self.arlo.user_id}_{self._gen_client_number()}", transport="websockets", clean_session=False)
self.event_stream.username_pw_set(self.arlo.user_id, password=self.arlo.request.session.headers.get('Authorization'))
self.event_stream.ws_set_options(path="/mqtt", headers={"Origin": "https://my.arlo.com"})
self.event_stream.on_connect = on_connect
self.event_stream.on_disconnect = on_disconnect
self.event_stream.on_message = on_message
self.event_stream.tls_set()
self.event_stream.connect_async("mqtt-cluster.arloxcld.com", port=443)
self.event_stream.loop_start()
while not self.connected and not self.event_stream_stop_event.is_set():
await asyncio.sleep(0.5)
if not self.event_stream_stop_event.is_set():
self.resubscribe()
async def restart(self):
self.reconnecting = True
self.connected = False
self.event_stream.disconnect()
self.event_stream = None
await self.start()
# give it an extra sleep to ensure any previous connections have disconnected properly
# this is so we can mark reconnecting to False properly
await asyncio.sleep(1)
self.reconnecting = False
def subscribe(self, topics):
if topics:
new_subscriptions = [(topic, 0) for topic in topics]
self.event_stream.subscribe(new_subscriptions)
self.cached_topics.extend(new_subscriptions)
def resubscribe(self):
if self.cached_topics:
self.event_stream.subscribe(self.cached_topics)
def disconnect(self):
super().disconnect()
self.event_stream.disconnect()

View File

@@ -1,114 +0,0 @@
##
# Copyright 2016 Jeffrey D. Walter
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
from functools import partialmethod
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
#from requests_toolbelt.utils import dump
#def print_raw_http(response):
# data = dump.dump_all(response, request_prefix=b'', response_prefix=b'')
# print('\n' * 2 + data.decode('utf-8'))
class Request(object):
"""HTTP helper class"""
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")
elif mode == "cloudscraper":
logger.debug("HTTP helper using cloudscraper")
from .arlo_async import USER_AGENTS
self.session = cloudscraper.CloudScraper(browser={"custom": USER_AGENTS["android"]})
elif mode == "ip":
logger.debug("HTTP helper using requests with HostHeaderSSLAdapter")
self.session = requests.Session()
self.session.mount('https://', host_header_ssl.HostHeaderSSLAdapter())
else:
logger.debug("HTTP helper using requests")
self.session = requests.Session()
self.timeout = timeout
def gen_event_id(self):
return f'FE!{str(uuid.uuid4())}'
def get_time(self):
return int(time.time_ns() / 1_000_000)
def _request(self, url, method='GET', params={}, headers={}, raw=False, skip_event_id=False):
## uncomment for debug logging
"""
import logging
import http.client
http.client.HTTPConnection.debuglevel = 1
#logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
req_log = logging.getLogger('requests.packages.urllib3')
req_log.setLevel(logging.DEBUG)
req_log.propagate = True
#"""
if not skip_event_id:
url = f'{url}?eventId={self.gen_event_id()}&time={self.get_time()}'
if method == 'GET':
#print('COOKIES: ', self.session.cookies.get_dict())
r = self.session.get(url, params=params, headers=headers, timeout=self.timeout)
r.raise_for_status()
elif method == 'PUT':
r = self.session.put(url, json=params, headers=headers, timeout=self.timeout)
r.raise_for_status()
elif method == 'POST':
r = self.session.post(url, json=params, headers=headers, timeout=self.timeout)
r.raise_for_status()
elif method == 'OPTIONS':
r = self.session.options(url, headers=headers, timeout=self.timeout)
r.raise_for_status()
return
body = r.json()
if raw:
return body
else:
if ('success' in body and body['success'] == True) or ('meta' in body and body['meta']['code'] == 200):
if 'data' in body:
return body['data']
else:
raise HTTPError('Request ({0} {1}) failed: {2}'.format(method, url, r.json()), response=r)
def get(self, url, **kwargs):
return self._request(url, 'GET', **kwargs)
def put(self, url, **kwargs):
return self._request(url, 'PUT', **kwargs)
def post(self, url, **kwargs):
return self._request(url, 'POST', **kwargs)
def options(self, url, **kwargs):
return self._request(url, 'OPTIONS', **kwargs)

View File

@@ -1,82 +0,0 @@
import asyncio
import json
import threading
import scrypted_arlo_go
from .stream_async import Stream
from .logging import logger
class EventStream(Stream):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.shutting_down_stream = None # record the eventstream that is currently shutting down
async def start(self):
if self.event_stream is not None:
return
def thread_main(self):
event_stream = self.event_stream
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
logger.debug(f"Received event: {event}")
if event.strip() == "":
continue
try:
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 {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 {event_stream.UUID} connected")
self.initializing = False
self.connected = True
else:
self.event_loop.call_soon_threadsafe(self._queue_response, response)
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()
while not self.connected and not self.event_stream_stop_event.is_set():
await asyncio.sleep(0.5)
async def restart(self):
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:
# ensure any previous connections have disconnected properly
# this is so we can mark reconnecting to False properly
await asyncio.sleep(1)
self.reconnecting = False
def subscribe(self, topics):
pass

View File

@@ -1,238 +0,0 @@
# This file has been modified to support async semantics and better
# integration with scrypted.
# Original: https://github.com/jeffreydwalter/arlo
##
# Copyright 2016 Jeffrey D. Walter
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##
import asyncio
import random
import threading
import time
import uuid
from .logging import logger
class Stream:
"""This class provides a queue-based EventStream object."""
def __init__(self, arlo, expire=5):
self.event_stream = None
self.initializing = True
self.connected = False
self.reconnecting = False
self.queues = {}
self.expire = expire
self.refresh = 0
self.refresh_loop_signal = asyncio.Queue()
self.event_stream_stop_event = threading.Event()
self.event_stream_thread = None
self.arlo = arlo
self.event_loop = asyncio.get_event_loop()
self.event_loop.create_task(self._clean_queues())
self.event_loop.create_task(self._refresh_interval())
def __del__(self):
self.disconnect()
@property
def active(self):
"""Represents if this stream is connected or in the process of reconnecting."""
return self.connected or self.reconnecting
async def _refresh_interval(self):
while not self.event_stream_stop_event.is_set():
if self.refresh == 0:
# to avoid spinning, wait until an interval is set
signal = await self.refresh_loop_signal.get()
if signal is None:
# exit signal received from disconnect()
return
continue
interval = self.refresh * 60 # interval in seconds from refresh in minutes
signal_task = asyncio.create_task(self.refresh_loop_signal.get())
# wait until either we receive a signal or the refresh interval expires
done, pending = await asyncio.wait([signal_task, asyncio.sleep(interval)], return_when=asyncio.FIRST_COMPLETED)
for task in pending:
task.cancel()
done_task = done.pop()
if done_task is signal_task and done_task.result() is None:
# exit signal received from disconnect()
return
logger.info("Refreshing event stream")
await self.restart()
def set_refresh_interval(self, interval):
self.refresh = interval
self.refresh_loop_signal.put_nowait(object())
async def _clean_queues(self):
interval = self.expire * 4
await asyncio.sleep(interval)
while not self.event_stream_stop_event.is_set():
# since we interrupt the cleanup loop after every queue, there's
# a chance the self.queues dict is modified during iteration.
# so, we first make a copy of all the items of the dict and any
# new queues will be processed on the next loop through
queue_items = [i for i in self.queues.items()]
for key, q in queue_items:
if q.empty():
continue
items = []
num_dropped = 0
while not q.empty():
item = q.get_nowait()
q.task_done()
if not item:
# exit signal received
return
if item.expired:
num_dropped += 1
continue
items.append(item)
for item in items:
q.put_nowait(item)
if num_dropped > 0:
logger.debug(f"Cleaned {num_dropped} events from queue {key}")
# cleanup is not urgent, so give other tasks a chance
await asyncio.sleep(0.1)
await asyncio.sleep(interval)
async def get(self, resource, action, property=None, skip_uuids={}):
if not property:
key = f"{resource}/{action}"
else:
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
first_requeued = None
while True:
event = await q.get()
q.task_done()
if not event:
# exit signal received
return None, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so sleep and give the next
# subscriber a chance
q.put_nowait(event)
await asyncio.sleep(random.uniform(0, 0.01))
continue
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
async def start(self):
raise NotImplementedError()
async def restart(self):
raise NotImplementedError()
def subscribe(self, topics):
raise NotImplementedError()
def _queue_response(self, response):
resource = response.get('resource')
action = response.get('action')
key = f"{resource}/{action}"
now = time.time()
event = StreamEvent(response, now, now + self.expire)
self._queue_impl(key, event)
# specialized setup for error responses
if 'error' in response:
key = f"{resource}/error"
self._queue_impl(key, event)
# for optimized lookups, notify listeners of individual properties
properties = response.get('properties', {})
for property in properties.keys():
key = f"{resource}/{action}/{property}"
self._queue_impl(key, event)
def _queue_impl(self, key, event):
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
q.put_nowait(event)
def requeue(self, event, resource, action, property=None):
if not property:
key = f"{resource}/{action}"
else:
key = f"{resource}/{action}/{property}"
self.queues[key].put_nowait(event)
def disconnect(self):
if self.reconnecting:
# disconnect may be called when an old stream is being refreshed/restarted,
# so don't completely shut down if we are reconnecting
return
self.connected = False
def exit_queues(self):
for q in self.queues.values():
q.put_nowait(None)
self.refresh_loop_signal.put_nowait(None)
self.event_loop.call_soon_threadsafe(exit_queues, self)
self.event_stream_stop_event.set()
class StreamEvent:
item = None
timestamp = None
expiration = None
uuid = None
def __init__(self, item, timestamp, expiration):
self.item = item
self.timestamp = timestamp
self.expiration = expiration
self.uuid = str(uuid.uuid4())
@property
def expired(self):
return time.time() > self.expiration

View File

@@ -1,79 +0,0 @@
from __future__ import annotations
import traceback
from typing import List, TYPE_CHECKING
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Device
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
nativeId: str = None
arlo_device: dict = None
arlo_basestation: dict = None
arlo_capabilities: dict = None
provider: ArloProvider = None
stop_subscriptions: bool = False
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId)
self.logger_name = nativeId
self.nativeId = nativeId
self.arlo_device = arlo_device
self.arlo_basestation = arlo_basestation
self.provider = provider
self.logger.setLevel(self.provider.get_current_log_level())
try:
self.arlo_capabilities = self.provider.arlo.GetDeviceCapabilities(self.arlo_device)
except Exception as e:
self.logger.warning(f"Could not load device capabilities: {e}")
self.arlo_capabilities = {}
def __del__(self) -> None:
self.stop_subscriptions = True
self.cancel_pending_tasks()
def get_applicable_interfaces(self) -> List[str]:
"""Returns the list of Scrypted interfaces that applies to this device."""
return []
def get_device_type(self) -> str:
"""Returns the Scrypted device type that applies to this device."""
return ""
def get_device_manifest(self) -> Device:
"""Returns the Scrypted device manifest representing this device."""
parent = None
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(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": self.arlo_device["deviceId"],
"name": self.arlo_device["deviceName"],
"interfaces": self.get_applicable_interfaces(),
"type": self.get_device_type(),
"providerNativeId": parent,
}
def get_builtin_child_device_manifests(self) -> List[Device]:
"""Returns the list of child device manifests representing hardware features built into this device."""
return []

View File

@@ -1,90 +0,0 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Device, DeviceProvider, Setting, SettingValue, Settings, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .vss import ArloSirenVirtualSecuritySystem
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloBasestation(ArloDeviceBase, DeviceProvider, Settings):
MODELS_WITH_SIRENS = [
"vmb4000",
"vmb4500"
]
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_basestation, arlo_basestation=arlo_basestation, provider=provider)
@property
def has_siren(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS])
def get_applicable_interfaces(self) -> List[str]:
return [
ScryptedInterface.DeviceProvider.value,
ScryptedInterface.Settings.value,
]
def get_device_type(self) -> str:
return ScryptedDeviceType.DeviceProvider.value
def get_builtin_child_device_manifests(self) -> List[Device]:
if not self.has_siren:
# this basestation has no builtin siren, so no manifests to return
return []
vss = self.get_or_create_vss()
return [
{
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": vss.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
"interfaces": vss.get_applicable_interfaces(),
"type": vss.get_device_type(),
"providerNativeId": self.nativeId,
},
] + vss.get_builtin_child_device_manifests()
async def getDevice(self, nativeId: str) -> ScryptedDeviceBase:
if not nativeId.startswith(self.nativeId):
# must be a camera, so get it from the provider
return await self.provider.getDevice(nativeId)
if not nativeId.endswith("vss"):
return None
return self.get_or_create_vss()
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
if not self.vss:
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.vss
async def getSettings(self) -> List[Setting]:
return [
{
"group": "General",
"key": "print_debug",
"title": "Debug Info",
"description": "Prints information about this device to console.",
"type": "button",
}
]
async def putSetting(self, key: str, value: SettingValue) -> None:
if key == "print_debug":
self.logger.info(f"Device Capabilities: {self.arlo_capabilities}")
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +0,0 @@
import multiprocessing
import subprocess
import time
import threading
import scrypted_arlo_go
HEARTBEAT_INTERVAL = 5
def multiprocess_main(name, logger_port, child_conn, exe, args):
logger = scrypted_arlo_go.NewTCPLogger(logger_port, "HeartbeatChildProcess")
logger.Send(f"{name} starting\n")
sp = subprocess.Popen([exe, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# pull stdout and stderr from the subprocess and forward it over to
# our tcp logger
def logging_thread(stdstream):
while True:
line = stdstream.readline()
if not line:
break
line = str(line, 'utf-8')
logger.Send(line)
stdout_t = threading.Thread(target=logging_thread, args=(sp.stdout,))
stderr_t = threading.Thread(target=logging_thread, args=(sp.stderr,))
stdout_t.start()
stderr_t.start()
while True:
has_data = child_conn.poll(HEARTBEAT_INTERVAL * 3)
if not has_data:
break
# check if the subprocess is still alive, if not then exit
if sp.poll() is not None:
break
keep_alive = child_conn.recv()
if not keep_alive:
break
logger.Send(f"{name} exiting\n")
sp.terminate()
sp.wait()
stdout_t.join()
stderr_t.join()
logger.Send(f"{name} exited\n")
logger.Close()
class HeartbeatChildProcess:
"""Class to manage running a child process that gets cleaned up if the parent exits.
When spawining subprocesses in Python, if the parent is forcibly killed (as is the case
when Scrypted restarts plugins), subprocesses get orphaned. This approach uses parent-child
heartbeats for the child to ensure that the parent process is still alive, and to cleanly
exit the child if the parent has terminated.
"""
def __init__(self, name, logger_port, exe, *args):
self.name = name
self.logger_port = logger_port
self.exe = exe
self.args = args
self.parent_conn, self.child_conn = multiprocessing.Pipe()
self.process = multiprocessing.Process(target=multiprocess_main, args=(name, logger_port, self.child_conn, exe, args))
self.process.daemon = True
self._stop = False
self.thread = threading.Thread(target=self.heartbeat)
def start(self):
self.process.start()
self.thread.start()
def stop(self):
self._stop = True
self.parent_conn.send(False)
def heartbeat(self):
while not self._stop:
time.sleep(HEARTBEAT_INTERVAL)
if not self.process.is_alive():
self.stop()
break
self.parent_conn.send(True)

View File

@@ -1,34 +0,0 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import BinarySensor, ScryptedInterface, ScryptedDeviceType
from .camera import ArloCamera
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloDoorbell(ArloCamera, BinarySensor):
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.start_doorbell_subscription()
def start_doorbell_subscription(self) -> None:
def callback(doorbellPressed):
self.binaryState = doorbellPressed
return self.stop_subscriptions
self.register_task(
self.provider.arlo.SubscribeToDoorbellEvents(self.arlo_basestation, self.arlo_device, callback)
)
def get_device_type(self) -> str:
return ScryptedDeviceType.Doorbell.value
def get_applicable_interfaces(self) -> List[str]:
camera_interfaces = super().get_applicable_interfaces()
camera_interfaces.append(ScryptedInterface.BinarySensor.value)
return camera_interfaces

View File

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

View File

@@ -1,43 +0,0 @@
import logging
class ScryptedDeviceLoggingWrapper(logging.Handler):
scrypted_device = None
def __init__(self, scrypted_device):
super().__init__()
self.scrypted_device = scrypted_device
def emit(self, record):
self.scrypted_device.print(self.format(record))
def createScryptedLogger(scrypted_device, name):
logger = logging.getLogger(name)
if logger.hasHandlers():
return logger
logger.setLevel(logging.INFO)
# configure logger to output to scrypted's log stream
sh = ScryptedDeviceLoggingWrapper(scrypted_device)
# log formatting
fmt = logging.Formatter("[Arlo %(name)s]: %(message)s")
sh.setFormatter(fmt)
# configure handler to logger
logger.addHandler(sh)
return logger
class ScryptedDeviceLoggerMixin:
_logger = None
logger_name = None
@property
def logger(self):
if self._logger is None:
self._logger = createScryptedLogger(self, self.logger_name)
return self._logger

View File

@@ -1,814 +0,0 @@
import asyncio
from bs4 import BeautifulSoup
import email
import functools
import imaplib
import json
import logging
import re
import requests
from typing import List
import scrypted_sdk
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, ScryptedInterface
from .arlo import Arlo
from .arlo.arlo_async import change_stream_class
from .arlo.logging import logger as arlo_lib_logger
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin, async_print_exception_guard
from .camera import ArloCamera
from .doorbell import ArloDoorbell
from .basestation import ArloBasestation
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: Arlo = None
_arlo_mfa_complete_auth = None
device_discovery_lock: asyncio.Lock = None
plugin_verbosity_choices = {
"Normal": logging.INFO,
"Verbose": logging.DEBUG
}
arlo_transport_choices = ["MQTT", "SSE"]
mfa_strategy_choices = ["Manual", "IMAP"]
def __init__(self, nativeId: str = None) -> None:
super().__init__(nativeId=nativeId)
self.logger_name = "Provider"
self.arlo_cameras = {}
self.arlo_basestations = {}
self.scrypted_devices = {}
self.imap = None
self.imap_signal = None
self.imap_skip_emails = None
self.device_discovery_lock = asyncio.Lock()
self.propagate_verbosity()
self.propagate_transport()
def load(self):
if self.mfa_strategy == "IMAP":
self.initialize_imap()
else:
_ = self.arlo
asyncio.get_event_loop().call_soon(load, self)
self.create_task(self.onDeviceEvent(ScryptedInterface.Settings.value, None))
def print(self, *args, **kwargs) -> None:
"""Overrides the print() from ScryptedDeviceBase to avoid double-printing in the main plugin console."""
print(*args, **kwargs)
@property
def arlo_username(self) -> str:
return self.storage.getItem("arlo_username")
@property
def arlo_password(self) -> str:
return self.storage.getItem("arlo_password")
@property
def arlo_auth_headers(self) -> str:
return self.storage.getItem("arlo_auth_headers")
@property
def arlo_user_id(self) -> str:
return self.storage.getItem("arlo_user_id")
@property
def arlo_transport(self) -> str:
return "SSE"
# This code is here for posterity, however it looks that as of 06/01/2023
# Arlo has disabled the MQTT backend
transport = self.storage.getItem("arlo_transport")
if transport is None or transport not in ArloProvider.arlo_transport_choices:
transport = "SSE"
self.storage.setItem("arlo_transport", transport)
return transport
@property
def plugin_verbosity(self) -> str:
verbosity = self.storage.getItem("plugin_verbosity")
if verbosity is None or verbosity not in ArloProvider.plugin_verbosity_choices:
verbosity = "Normal"
self.storage.setItem("plugin_verbosity", verbosity)
return verbosity
@property
def mfa_strategy(self) -> str:
strategy = self.storage.getItem("mfa_strategy")
if strategy is None or strategy not in ArloProvider.mfa_strategy_choices:
strategy = "Manual"
self.storage.setItem("mfa_strategy", strategy)
return strategy
@property
def refresh_interval(self) -> int:
interval = self.storage.getItem("refresh_interval")
if interval is None:
interval = 90
self.storage.setItem("refresh_interval", interval)
return int(interval)
@property
def imap_mfa_host(self) -> str:
return self.storage.getItem("imap_mfa_host")
@property
def imap_mfa_port(self) -> int:
port = self.storage.getItem("imap_mfa_port")
if port is None:
port = 993
self.storage.setItem("imap_mfa_port", port)
return int(port)
@property
def imap_mfa_username(self) -> str:
return self.storage.getItem("imap_mfa_username")
@property
def imap_mfa_password(self) -> str:
return self.storage.getItem("imap_mfa_password")
@property
def imap_mfa_sender(self) -> str:
sender = self.storage.getItem("imap_mfa_sender")
if sender is None or sender == "":
sender = "do_not_reply@arlo.com"
self.storage.setItem("imap_mfa_sender", sender)
return sender
@property
def imap_mfa_interval(self) -> int:
interval = self.storage.getItem("imap_mfa_interval")
if interval is None:
interval = 7
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:
if self._arlo_mfa_complete_auth is not None:
if not self._arlo_mfa_code:
return None
self.logger.info("Completing Arlo MFA...")
try:
self._arlo_mfa_complete_auth(self._arlo_mfa_code)
finally:
self._arlo_mfa_complete_auth = None
self._arlo_mfa_code = None
self.logger.info("Arlo MFA done")
self.storage.setItem("arlo_auth_headers", json.dumps(dict(self._arlo.request.session.headers.items())))
self.storage.setItem("arlo_user_id", self._arlo.user_id)
self.create_task(self.do_arlo_setup())
return self._arlo
if not self.arlo_username or not self.arlo_password:
return None
self.logger.info("Trying to initialize Arlo client...")
try:
self._arlo = Arlo(self.arlo_username, self.arlo_password)
headers = self.arlo_auth_headers
if headers:
self._arlo.UseExistingAuth(self.arlo_user_id, json.loads(headers))
self.logger.info(f"Initialized Arlo client, reusing stored auth headers")
self.create_task(self.do_arlo_setup())
return self._arlo
else:
self._arlo_mfa_complete_auth = self._arlo.LoginMFA()
self.logger.info(f"Initialized Arlo client, waiting for MFA code")
return None
except Exception:
self.logger.exception("Error initializing Arlo client")
self._arlo = None
self._arlo_mfa_complete_auth = None
self._arlo_mfa_code = None
raise
async def do_arlo_setup(self) -> None:
try:
await self.discover_devices()
await self.arlo.Subscribe([
(self.arlo_basestations[camera["parentId"]], camera) for camera in self.arlo_cameras.values()
])
self.arlo.event_stream.set_refresh_interval(self.refresh_interval)
except requests.exceptions.HTTPError:
self.logger.exception("Error logging in")
self.logger.error("Will retry with fresh login")
self._arlo = None
self._arlo_mfa_code = None
self.storage.setItem("arlo_auth_headers", None)
_ = self.arlo
except Exception:
self.logger.exception("Error logging in")
def invalidate_arlo_client(self) -> None:
if self._arlo is not None:
self._arlo.Unsubscribe()
self._arlo = None
self._arlo_mfa_code = None
self._arlo_mfa_complete_auth = None
self.storage.setItem("arlo_auth_headers", "")
self.storage.setItem("arlo_user_id", "")
def get_current_log_level(self) -> int:
return ArloProvider.plugin_verbosity_choices[self.plugin_verbosity]
def propagate_verbosity(self) -> None:
self.print(f"Setting plugin verbosity to {self.plugin_verbosity}")
log_level = self.get_current_log_level()
self.logger.setLevel(log_level)
for _, device in self.scrypted_devices.items():
device.logger.setLevel(log_level)
arlo_lib_logger.setLevel(log_level)
def propagate_transport(self) -> None:
self.print(f"Setting plugin transport to {self.arlo_transport}")
change_stream_class(self.arlo_transport)
def initialize_imap(self, try_count=1) -> None:
if not self.imap_mfa_host or not self.imap_mfa_port or \
not self.imap_mfa_username or not self.imap_mfa_password or \
not self.imap_mfa_interval:
return
self.exit_imap()
try:
self.logger.info(f"Trying connect to IMAP (attempt {try_count})")
self.imap = imaplib.IMAP4_SSL(self.imap_mfa_host, port=self.imap_mfa_port)
res, _ = self.imap.login(self.imap_mfa_username, self.imap_mfa_password)
if res.lower() != "ok":
raise Exception(f"IMAP login failed: {res}")
res, _ = self.imap.select(mailbox="INBOX", readonly=True)
if res.lower() != "ok":
raise Exception(f"IMAP failed to fetch INBOX: {res}")
# fetch existing arlo emails so we skip them going forward
res, self.imap_skip_emails = self.imap.search(None, "FROM", "do_not_reply@arlo.com")
if res.lower() != "ok":
raise Exception(f"IMAP failed to fetch old Arlo emails: {res}")
except Exception:
self.logger.exception("IMAP initialization error")
if try_count >= 10:
self.logger.error("Tried to connect to IMAP too many times. Will request a plugin restart.")
self.create_task(scrypted_sdk.deviceManager.requestRestart())
asyncio.get_event_loop().call_later(try_count*try_count, functools.partial(self.initialize_imap, try_count=try_count+1))
else:
self.logger.info("Connected to IMAP")
self.imap_signal = asyncio.Queue()
self.create_task(self.imap_relogin_loop())
def exit_imap(self) -> None:
if self.imap_signal:
self.imap_signal.put_nowait(None)
self.imap_signal = None
self.imap_skip_emails = None
self.imap = None
async def imap_relogin_loop(self) -> None:
imap_signal = self.imap_signal
self.logger.info(f"Starting IMAP refresh loop {id(imap_signal)}")
while True:
self.logger.info("Performing IMAP login flow")
# save old client and details in case of error
old_arlo = self._arlo
old_headers = self.storage.getItem("arlo_auth_headers")
old_user_id = self.storage.getItem("arlo_user_id")
# clear everything
self._arlo = None
self._arlo_mfa_code = None
self._arlo_mfa_complete_auth = None
self.storage.setItem("arlo_auth_headers", "")
self.storage.setItem("arlo_user_id", "")
# initialize login and prompt for MFA
try:
_ = self.arlo
except Exception:
self.logger.exception("Unrecoverable login error")
self.logger.error("Will request a plugin restart")
await scrypted_sdk.deviceManager.requestRestart()
return
# do imap lookup
# adapted from https://github.com/twrecked/pyaarlo/blob/77c202b6f789c7104a024f855a12a3df4fc8df38/pyaarlo/tfa.py
try:
try_count = 0
while True:
try_count += 1
sleep_duration = 1
if try_count > 5:
sleep_duration = 2
elif try_count > 10:
sleep_duration = 5
elif try_count > 20:
sleep_duration = 10
self.logger.info(f"Checking IMAP for MFA codes (attempt {try_count})")
self.imap.check()
res, emails = self.imap.search(None, "FROM", self.imap_mfa_sender)
if res.lower() != "ok":
raise Exception("IMAP error: {res}")
if emails == self.imap_skip_emails:
self.logger.info("No new emails found, will sleep and retry")
await asyncio.sleep(sleep_duration)
continue
skip_emails = self.imap_skip_emails[0].split()
def search_email(msg_id):
if msg_id in skip_emails:
return None
res, msg = self.imap.fetch(msg_id, "(BODY.PEEK[])")
if res.lower() != "ok":
raise Exception("IMAP error: {res}")
if isinstance(msg[0][1], bytes):
for part in email.message_from_bytes(msg[0][1]).walk():
if part.get_content_type() != "text/html":
continue
try:
soup = BeautifulSoup(part.get_payload(decode=True), 'html.parser')
for line in soup.get_text().splitlines():
code = re.match(r"^\W*(\d{6})\W*$", line)
if code is not None:
return code.group(1)
except:
continue
return None
for msg_id in emails[0].split():
res = search_email(msg_id)
if res is not None:
self._arlo_mfa_code = res
break
# update previously seen emails list
self.imap_skip_emails = emails
if self._arlo_mfa_code is not None:
self.logger.info("Found MFA code")
break
self.logger.info("No MFA code found, will sleep and retry")
await asyncio.sleep(sleep_duration)
except Exception:
self.logger.exception("Error while checking for MFA codes")
self._arlo = old_arlo
self.storage.setItem("arlo_auth_headers", old_headers)
self.storage.setItem("arlo_user_id", old_user_id)
self._arlo_mfa_code = None
self._arlo_mfa_complete_auth = None
self.logger.error("Will reload IMAP connection")
asyncio.get_event_loop().call_soon(self.initialize_imap)
else:
# finish login
if old_arlo:
old_arlo.Unsubscribe()
try:
_ = self.arlo
except Exception:
self.logger.exception("Unrecoverable login error")
self.logger.error("Will request a plugin restart")
await scrypted_sdk.deviceManager.requestRestart()
return
# continue by sleeping/waiting for a signal
interval = self.imap_mfa_interval * 24 * 60 * 60 # convert interval days to seconds
signal_task = asyncio.create_task(imap_signal.get())
# wait until either we receive a signal or the refresh interval expires
done, pending = await asyncio.wait([signal_task, asyncio.sleep(interval)], return_when=asyncio.FIRST_COMPLETED)
for task in pending:
task.cancel()
done_task = done.pop()
if done_task is signal_task and done_task.result() is None:
# exit signal received
self.logger.info(f"Exiting IMAP refresh loop {id(imap_signal)}")
return
async def getSettings(self) -> List[Setting]:
results = [
{
"group": "General",
"key": "arlo_username",
"title": "Arlo Username",
"value": self.arlo_username,
},
{
"group": "General",
"key": "arlo_password",
"title": "Arlo Password",
"type": "password",
"value": self.arlo_password,
},
{
"group": "General",
"key": "mfa_strategy",
"title": "Two Factor Strategy",
"description": "Mechanism to fetch the two factor code for Arlo login. Save after changing this field for more settings.",
"value": self.mfa_strategy,
"choices": self.mfa_strategy_choices,
},
]
if self.mfa_strategy == "Manual":
results.extend([
{
"group": "General",
"key": "arlo_mfa_code",
"title": "Two Factor Code",
"description": "Enter the code sent by Arlo to your email or phone number.",
},
{
"group": "General",
"key": "force_reauth",
"title": "Force Re-Authentication",
"description": "Resets the authentication flow of the plugin. Will also re-do 2FA.",
"value": False,
"type": "boolean",
},
])
else:
results.extend([
{
"group": "IMAP 2FA",
"key": "imap_mfa_host",
"title": "IMAP Hostname",
"value": self.imap_mfa_host,
},
{
"group": "IMAP 2FA",
"key": "imap_mfa_port",
"title": "IMAP Port",
"value": self.imap_mfa_port,
},
{
"group": "IMAP 2FA",
"key": "imap_mfa_username",
"title": "IMAP Username",
"value": self.imap_mfa_username,
},
{
"group": "IMAP 2FA",
"key": "imap_mfa_password",
"title": "IMAP Password",
"type": "password",
"value": self.imap_mfa_password,
},
{
"group": "IMAP 2FA",
"key": "imap_mfa_sender",
"title": "IMAP Email Sender",
"value": self.imap_mfa_sender,
"description": "The sender email address to search for when loading 2FA codes. See plugin README for more details.",
},
{
"group": "IMAP 2FA",
"key": "imap_mfa_interval",
"title": "Refresh Login Interval",
"description": "Interval, in days, to refresh the login session to Arlo Cloud. "
"Must be a value greater than 0.",
"type": "number",
"value": self.imap_mfa_interval,
}
])
results.extend([
{
"group": "General",
"key": "arlo_transport",
"title": "Underlying Transport Protocol",
"description": "Arlo Cloud currently only supports the SSE protocol.",
"value": self.arlo_transport,
"readonly": True,
},
{
"group": "General",
"key": "refresh_interval",
"title": "Refresh Event Stream Interval",
"description": "Interval, in minutes, to refresh the underlying event stream connection to Arlo Cloud. "
"A value of 0 disables this feature.",
"type": "number",
"value": self.refresh_interval,
},
{
"group": "General",
"key": "plugin_verbosity",
"title": "Verbose Logging",
"description": "Enable this option to show debug messages, including events received from connected Arlo cameras.",
"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
@async_print_exception_guard
async def putSetting(self, key: str, value: SettingValue) -> None:
if not self.validate_setting(key, value):
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
return
skip_arlo_client = False
if key == "arlo_mfa_code":
self._arlo_mfa_code = value
elif key == "force_reauth":
# force arlo client to be invalidated and reloaded
self.invalidate_arlo_client()
elif key == "plugin_verbosity":
self.storage.setItem(key, "Verbose" if value == "true" or value == True else "Normal")
self.propagate_verbosity()
skip_arlo_client = True
else:
self.storage.setItem(key, value)
if key == "arlo_transport":
self.propagate_transport()
# force arlo client to be invalidated and reloaded, but
# keep any mfa codes
if self._arlo is not None:
self._arlo.Unsubscribe()
self._arlo = None
elif key == "mfa_strategy":
if value == "IMAP":
self.initialize_imap()
else:
self.exit_imap()
skip_arlo_client = True
elif key == "refresh_interval":
if self._arlo is not None and self._arlo.event_stream:
self._arlo.event_stream.set_refresh_interval(self.refresh_interval)
skip_arlo_client = True
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()
if not skip_arlo_client:
# initialize Arlo client or continue MFA
_ = self.arlo
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
def validate_setting(self, key: str, val: SettingValue) -> bool:
if key == "refresh_interval":
try:
val = int(val)
except ValueError:
self.logger.error(f"Invalid refresh interval '{val}' - must be an integer")
return False
if val < 0:
self.logger.error(f"Invalid refresh interval '{val}' - must be nonnegative")
return False
elif key == "imap_mfa_port":
try:
val = int(val)
except ValueError:
self.logger.error(f"Invalid IMAP port '{val}' - must be an integer")
return False
if val < 0:
self.logger.error(f"Invalid IMAP port '{val}' - must be nonnegative")
return False
elif key == "imap_mfa_interval":
try:
val = int(val)
except ValueError:
self.logger.error(f"Invalid IMAP interval '{val}' - must be an integer")
return False
if val < 1:
self.logger.error(f"Invalid IMAP interval '{val}' - must be positive")
return False
return True
@async_print_exception_guard
async def discover_devices(self) -> None:
async with self.device_discovery_lock:
return await self.discover_devices_impl()
async def discover_devices_impl(self) -> None:
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 = []
provider_to_device_map = {None: []}
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()
self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}")
# for basestations, we want to add them to the top level DeviceProvider
provider_to_device_map.setdefault(None, []).append(manifest)
# we want to trickle discover them so they are added without deleting all existing
# root level devices - this is for backward compatibility
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
# add any builtin child devices and trickle discover them
child_manifests = device.get_builtin_child_device_manifests()
for child_manifest in child_manifests:
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
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:
self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because its basestation was not found")
continue
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"]:
# these are standalone cameras with no basestation, so they act as their
# own basestation
self.arlo_basestations[camera["deviceId"]] = camera
device = await self.getDevice_impl(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']} parent {camera['parentId']}): {scrypted_interfaces}")
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)
# trickle discover this camera so it exists for later steps
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
# add any builtin child devices and trickle discover them
child_manifests = device.get_builtin_child_device_manifests()
for child_manifest in child_manifests:
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
camera_devices.append(manifest)
if len(cameras) != len(camera_devices):
self.logger.info(f"Discovered {len(cameras)} cameras, but only {len(camera_devices)} are usable")
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")
for provider_id in provider_to_device_map.keys():
if provider_id is None:
continue
if len(provider_to_device_map[provider_id]) > 0:
self.logger.debug(f"Sending {provider_id} and children to scrypted server")
else:
self.logger.debug(f"Sending {provider_id} to scrypted server")
await scrypted_sdk.deviceManager.onDevicesChanged({
"devices": provider_to_device_map[provider_id],
"providerNativeId": provider_id,
})
# ensure devices at the root match all that was discovered
self.logger.debug("Sending top level devices to scrypted server")
await scrypted_sdk.deviceManager.onDevicesChanged({
"devices": provider_to_device_map[None]
})
self.logger.debug("Done discovering devices")
# 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)
async def getDevice_impl(self, nativeId: str) -> ArloDeviceBase:
ret = self.scrypted_devices.get(nativeId)
if ret is None:
ret = self.create_device(nativeId)
if ret is not None:
self.scrypted_devices[nativeId] = ret
return ret
def create_device(self, nativeId: str) -> ArloDeviceBase:
if nativeId not in self.arlo_cameras and nativeId not in self.arlo_basestations:
self.logger.warning(f"Cannot create device for nativeId {nativeId}, maybe it hasn't been loaded yet?")
return None
arlo_device = self.arlo_cameras.get(nativeId)
if not arlo_device:
# this is a basestation, so build the basestation object
arlo_device = self.arlo_basestations[nativeId]
return ArloBasestation(nativeId, arlo_device, self)
if arlo_device["parentId"] not in self.arlo_basestations:
self.logger.warning(f"Cannot create camera with nativeId {nativeId} when {arlo_device['parentId']} is not a valid basestation")
return None
arlo_basestation = self.arlo_basestations[arlo_device["parentId"]]
if arlo_device["deviceType"] == "doorbell":
return ArloDoorbell(nativeId, arlo_device, arlo_basestation, self)
else:
return ArloCamera(nativeId, arlo_device, arlo_basestation, self)

View File

@@ -1,107 +0,0 @@
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,72 +0,0 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import OnOff, SecuritySystemMode, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .util import async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .vss import ArloSirenVirtualSecuritySystem
class ArloSiren(ArloDeviceBase, OnOff):
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, vss: ArloSirenVirtualSecuritySystem) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.vss = vss
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.OnOff.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.Siren.value
@async_print_exception_guard
async def turnOn(self) -> None:
from .basestation import ArloBasestation
self.logger.info("Turning on")
if self.vss.securitySystemState["mode"] == SecuritySystemMode.Disarmed.value:
self.logger.info("Virtual security system is disarmed, ignoring trigger")
# set and unset this property to force homekit to display the
# switch as off
self.on = True
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": False,
}
return
if isinstance(self.vss.parent, ArloBasestation):
self.logger.debug("Parent device is a basestation")
self.provider.arlo.SirenOn(self.arlo_basestation)
else:
self.logger.debug("Parent device is a camera")
self.provider.arlo.SirenOn(self.arlo_basestation, self.arlo_device)
self.on = True
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": True,
}
@async_print_exception_guard
async def turnOff(self) -> None:
from .basestation import ArloBasestation
self.logger.info("Turning off")
if isinstance(self.vss.parent, ArloBasestation):
self.provider.arlo.SirenOff(self.arlo_basestation)
else:
self.provider.arlo.SirenOff(self.arlo_basestation, self.arlo_device)
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": False,
}

View File

@@ -1,72 +0,0 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import OnOff, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .util import async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .camera import ArloCamera
class ArloSpotlight(ArloDeviceBase, OnOff):
camera: ArloCamera = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, camera: ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.camera = camera
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.OnOff.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.Light.value
@async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.SpotlightOn(self.arlo_basestation, self.arlo_device)
self.on = True
@async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.SpotlightOff(self.arlo_basestation, self.arlo_device)
self.on = False
class ArloFloodlight(ArloSpotlight):
@async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.FloodlightOn(self.arlo_basestation, self.arlo_device)
self.on = True
@async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.FloodlightOff(self.arlo_basestation, self.arlo_device)
self.on = False
class ArloNightlight(ArloSpotlight):
def __init__(self, nativeId: str, arlo_device: dict, provider: ArloProvider, camera: ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_device, provider=provider, camera=camera)
@async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.NightlightOn(self.arlo_device)
self.on = True
@async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.NightlightOff(self.arlo_device)
self.on = False

View File

@@ -1,44 +0,0 @@
import asyncio
import traceback
class BackgroundTaskMixin:
def create_task(self, coroutine) -> asyncio.Task:
task = asyncio.get_event_loop().create_task(coroutine)
self.register_task(task)
return task
def register_task(self, task) -> None:
if not hasattr(self, "background_tasks"):
self.background_tasks = set()
assert task is not None
def print_exception(task):
if task.exception():
self.logger.error(f"task exception: {task.exception()}")
self.background_tasks.add(task)
task.add_done_callback(print_exception)
task.add_done_callback(self.background_tasks.discard)
def cancel_pending_tasks(self) -> None:
if not hasattr(self, "background_tasks"):
return
for task in self.background_tasks:
task.cancel()
def async_print_exception_guard(fn):
"""Decorator to print an exception's stack trace before re-raising the exception."""
async def wrapped(*args, **kwargs):
try:
return await fn(*args, **kwargs)
except Exception:
# hack to detect if the applied function is actually a method
# on a scrypted object
if len(args) > 0 and hasattr(args[0], "logger"):
getattr(args[0], "logger").exception(f"{fn.__qualname__} raised an exception")
else:
traceback.print_exc()
raise
return wrapped

View File

@@ -1,156 +0,0 @@
from __future__ import annotations
import asyncio
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import Device, DeviceProvider, Setting, Settings, SettingValue, SecuritySystem, SecuritySystemMode, Readme, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .siren import ArloSiren
from .util import async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .basestation import ArloBasestation
from .camera import ArloCamera
class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, Settings, Readme, DeviceProvider):
"""A virtual, emulated security system that controls when scrypted events can trip the real physical siren."""
SUPPORTED_MODES = [SecuritySystemMode.AwayArmed.value, SecuritySystemMode.HomeArmed.value, SecuritySystemMode.Disarmed.value]
siren: ArloSiren = None
parent: ArloBasestation | ArloCamera = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, parent: ArloBasestation | ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.parent = parent
self.create_task(self.delayed_init())
@property
def mode(self) -> str:
mode = self.storage.getItem("mode")
if mode is None or mode not in ArloSirenVirtualSecuritySystem.SUPPORTED_MODES:
mode = SecuritySystemMode.Disarmed.value
return mode
@mode.setter
def mode(self, mode: str) -> None:
if mode not in ArloSirenVirtualSecuritySystem.SUPPORTED_MODES:
raise ValueError(f"invalid mode {mode}")
self.storage.setItem("mode", mode)
self.securitySystemState = {
**self.securitySystemState,
"mode": mode,
}
self.create_task(self.onDeviceEvent(ScryptedInterface.Settings.value, None))
async def delayed_init(self) -> None:
iterations = 1
while not self.stop_subscriptions:
if iterations > 100:
self.logger.error("Delayed init exceeded iteration limit, giving up")
return
try:
self.securitySystemState = {
"supportedModes": ArloSirenVirtualSecuritySystem.SUPPORTED_MODES,
"mode": self.mode,
}
return
except Exception as e:
self.logger.debug(f"Delayed init failed, will try again: {e}")
await asyncio.sleep(0.1)
iterations += 1
def get_applicable_interfaces(self) -> List[str]:
return [
ScryptedInterface.SecuritySystem.value,
ScryptedInterface.DeviceProvider.value,
ScryptedInterface.Settings.value,
ScryptedInterface.Readme.value,
]
def get_device_type(self) -> str:
return ScryptedDeviceType.SecuritySystem.value
def get_builtin_child_device_manifests(self) -> List[Device]:
siren = self.get_or_create_siren()
return [
{
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": siren.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren',
"interfaces": siren.get_applicable_interfaces(),
"type": siren.get_device_type(),
"providerNativeId": self.nativeId,
}
]
async def getSettings(self) -> List[Setting]:
return [
{
"key": "mode",
"title": "Arm Mode",
"description": "If disarmed, the associated siren will not be physically triggered even if toggled.",
"value": self.mode,
"choices": ArloSirenVirtualSecuritySystem.SUPPORTED_MODES,
},
]
async def putSetting(self, key: str, value: SettingValue) -> None:
if key != "mode":
raise ValueError(f"invalid setting {key}")
self.mode = value
if self.mode == SecuritySystemMode.Disarmed.value:
await self.get_or_create_siren().turnOff()
async def getReadmeMarkdown(self) -> str:
return """
# Virtual Security System for Arlo Sirens
This security system device is not a real physical device, but a virtual, emulated device provided by the Arlo Scrypted plugin. Its purpose is to grant security system semantics of Arm/Disarm to avoid the accidental, unwanted triggering of the real physical siren through integrations such as Homekit.
To allow the siren to trigger, set the Arm Mode to any of the Armed options. When Disarmed, any triggers of the siren will be ignored. Switching modes will not perform any changes to Arlo cloud or your Arlo account, but rather only to this Scrypted device.
If this virtual security system is synced to Homekit, the siren device will be merged into the same security system accessory as a switch. The siren device will not be added as a separate accessory. To access the siren as a switch without the security system, disable syncing of the virtual security system and enable syncing of the siren, then ensure that the virtual security system is armed manually in its settings in Scrypted.
""".strip()
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
if not nativeId.endswith("siren"):
return None
return self.get_or_create_siren()
def get_or_create_siren(self) -> ArloSiren:
siren_id = f'{self.arlo_device["deviceId"]}.siren'
if not self.siren:
self.siren = ArloSiren(siren_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.siren
@async_print_exception_guard
async def armSecuritySystem(self, mode: SecuritySystemMode) -> None:
self.logger.info(f"Arming {mode}")
self.mode = mode
self.securitySystemState = {
**self.securitySystemState,
"mode": mode,
}
if mode == SecuritySystemMode.Disarmed.value:
await self.get_or_create_siren().turnOff()
@async_print_exception_guard
async def disarmSecuritySystem(self) -> None:
self.logger.info(f"Disarming")
self.mode = SecuritySystemMode.Disarmed.value
self.securitySystemState = {
**self.securitySystemState,
"mode": SecuritySystemMode.Disarmed.value,
}
await self.get_or_create_siren().turnOff()

View File

@@ -1,4 +0,0 @@
from arlo_plugin import ArloProvider
def create_scrypted_plugin():
return ArloProvider()

View File

@@ -1,14 +0,0 @@
paho-mqtt==1.6.1
aiohttp==3.8.4
requests==2.28.2
cachetools==5.3.0
scrypted-arlo-go==0.5.2
cloudscraper==1.2.71
curl-cffi==0.5.7
async-timeout==4.0.2
beautifulsoup4==4.12.2
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.11",
"version": "0.0.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/bticino",
"version": "0.0.11",
"version": "0.0.13",
"dependencies": {
"@slyoldfox/sip": "^0.0.6-1",
"sdp": "^3.0.3",
@@ -40,7 +40,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.103",
"version": "0.3.2",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/bticino",
"version": "0.0.11",
"version": "0.0.13",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -0,0 +1,61 @@
import { ScryptedDeviceBase, HttpRequest, HttpResponse, HttpRequestHandler, OnOff } from "@scrypted/sdk";
import { BticinoSipCamera } from "./bticino-camera";
import { VoicemailHandler } from "./bticino-voicemailHandler";
export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
private timeout : NodeJS.Timeout
constructor(private camera: BticinoSipCamera, private voicemailHandler : VoicemailHandler) {
super( camera.nativeId + "-aswm-switch")
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
}
turnOff(): Promise<void> {
this.on = false
return this.camera.turnOffAswm()
}
turnOn(): Promise<void> {
this.on = true
return this.camera.turnOnAswm()
}
syncStatus() {
this.on = this.voicemailHandler.isAswmEnabled()
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
}
cancelTimer() {
if( this.timeout ) {
clearTimeout(this.timeout)
}
}
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
if (request.url.endsWith('/disabled')) {
this.on = false
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/enabled') ) {
this.on = true
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/enable') ) {
this.turnOn()
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/disable') ) {
this.turnOff()
response.send('Success', {
code: 200,
});
} else {
response.send('Unsupported operation', {
code: 400,
});
}
}
}

View File

@@ -1,10 +1,10 @@
import { closeQuiet, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { sleep } from '@scrypted/common/src/sleep';
import { RtspServer } from '@scrypted/common/src/rtsp-server';
import { addTrackControls } from '@scrypted/common/src/sdp-utils';
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, MotionSensor, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
import { SipCallSession } from '../../sip/src/sip-call-session';
import { RtpDescription } from '../../sip/src/rtp-utils';
import { RtpDescription, getPayloadType, getSequenceNumber, isRtpMessagePayloadType, isStunMessage } from '../../sip/src/rtp-utils';
import { VoicemailHandler } from './bticino-voicemailHandler';
import { CompositeSipMessageHandler } from '../../sip/src/compositeSipMessageHandler';
import { SipHelper } from './sip-helper';
@@ -16,22 +16,22 @@ import { BticinoSipLock } from './bticino-lock';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
import { PersistentSipManager } from './persistent-sip-manager';
import { InviteHandler } from './bticino-inviteHandler';
import { SipRequest } from '../../sip/src/sip-manager';
import { SipOptions, SipRequest } from '../../sip/src/sip-manager';
import { get } from 'http'
import { ControllerApi } from './c300x-controller-api';
import { BticinoAswmSwitch } from './bticino-aswm-switch';
import { BticinoMuteSwitch } from './bticino-mute-switch';
const STREAM_TIMEOUT = 65000;
const { mediaManager } = sdk;
export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor, DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
private session: SipCallSession
private remoteRtpDescription: Promise<RtpDescription>
private audioOutForwarder: dgram.Socket
private audioOutProcess: ChildProcess
private currentMedia: FFmpegInput | MediaStreamUrl
private currentMediaMimeType: string
private refreshTimeout: NodeJS.Timeout
public requestHandlers: CompositeSipMessageHandler = new CompositeSipMessageHandler()
public incomingCallRequest : SipRequest
@@ -39,16 +39,21 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
private voicemailHandler : VoicemailHandler = new VoicemailHandler(this)
private inviteHandler : InviteHandler = new InviteHandler(this)
private controllerApi : ControllerApi = new ControllerApi(this)
private muteSwitch : BticinoMuteSwitch
private aswmSwitch : BticinoAswmSwitch
private deferredCleanup
private currentMediaObject : Promise<MediaObject>
private lastImageRefresh : number
//TODO: randomize this
private keyAndSalt : string = "/qE7OPGKp9hVGALG2KcvKWyFEZfSSvm7bYVDjT8X"
//private decodedSrtpOptions : SrtpOptions = decodeSrtpOptions( this.keyAndSalt )
private persistentSipManager : PersistentSipManager
public doorbellWebhookUrl : string
public doorbellLockWebhookUrl : string
private cachedImage : Buffer
constructor(nativeId: string, public provider: BticinoSipPlugin) {
super(nativeId)
this.requestHandlers.add( this.voicemailHandler ).add( this.inviteHandler )
this.persistentSipManager = new PersistentSipManager( this );
(async() => {
@@ -63,10 +68,50 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
get(`http://${c300x}:8080/reboot?now`, (res) => {
console.log("Reboot API result: " + res.statusCode)
});
}).on('error', (error) => {
this.console.error(error)
reject(error)
} ).end();
})
}
muteRinger(mute : boolean): Promise<void> {
return new Promise<void>( (resolve,reject ) => {
let c300x = SipHelper.getIntercomIp(this)
get(`http://${c300x}:8080/mute?raw=true&enable=` + mute, (res) => {
console.log("Mute API result: " + res.statusCode)
}).on('error', (error) => {
this.console.error(error)
reject(error)
} ).end();
})
}
muteStatus(): Promise<boolean> {
return new Promise<boolean>( (resolve,reject ) => {
let c300x = SipHelper.getIntercomIp(this)
get(`http://${c300x}:8080/mute?status=true&raw=true`, (res) => {
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; })
res.on('error', (error) => this.console.log(error))
res.on('end', () => {
try {
return resolve(JSON.parse(rawData))
} catch (e) {
console.error(e.message);
reject(e.message)
}
})
}).on('error', (error) => {
this.console.error(error)
reject(error)
} ).end();
})
}
getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
return new Promise<VideoClip[]>( (resolve,reject ) => {
let c300x = SipHelper.getIntercomIp(this)
@@ -95,7 +140,10 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
console.error(e.message);
}
})
});
}).on('error', (error) => {
this.console.error(error)
reject(error)
} ).end(); ;
});
}
@@ -132,8 +180,34 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
} )
}
turnOnAswm() : Promise<void> {
return this.persistentSipManager.enable().then( (sipCall) => {
sipCall.message( "*8*91##" )
} )
}
turnOffAswm() : Promise<void> {
return this.persistentSipManager.enable().then( (sipCall) => {
sipCall.message( "*8*92##" )
} )
}
async takePicture(option?: PictureOptions): Promise<MediaObject> {
throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
const thumbnailCacheTime : number = parseInt( this.storage?.getItem('thumbnailCacheTime') ) * 1000 || 300000
const now = new Date().getTime()
if( !this.lastImageRefresh || this.lastImageRefresh + thumbnailCacheTime < now ) {
// get a proxy object to make sure we pass prebuffer when already watching a stream
let cam : VideoCamera = sdk.systemManager.getDeviceById<VideoCamera>(this.id)
let vs : MediaObject = await cam.getVideoStream()
let buf : Buffer = await mediaManager.convertMediaObjectToBuffer(vs, 'image/jpeg');
this.cachedImage = buf
this.lastImageRefresh = new Date().getTime()
this.console.log(`Camera picture updated and cached: ${this.lastImageRefresh} + cache time: ${thumbnailCacheTime} < ${now}`)
} else {
this.console.log(`Not refreshing camera picture: ${this.lastImageRefresh} + cache time: ${thumbnailCacheTime} < ${now}`)
}
return mediaManager.createMediaObject(this.cachedImage, 'image/jpeg')
}
async getPictureOptions(): Promise<PictureOptions[]> {
@@ -149,8 +223,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
}
async startIntercom(media: MediaObject): Promise<void> {
if (!this.session)
throw new Error("not in call");
if (!this.session) {
const cleanup = () => {
this.console.log("STARTINTERCOM CLEANUP CALLED: " + this.session )
this.session?.stop()
this.session = undefined
this.deferredCleanup()
this.console.log("STARTINTERCOM CLEANUP ENDED")
}
this.session = await this.callIntercom( cleanup )
}
this.stopIntercom();
@@ -223,27 +306,24 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
throw new Error('Please configure from/to/domain settings')
}
if (options?.metadata?.refreshAt) {
if (!this.currentMedia?.mediaStreamOptions)
throw new Error("no stream to refresh");
const currentMedia = this.currentMedia
currentMedia.mediaStreamOptions.refreshAt = Date.now() + STREAM_TIMEOUT;
currentMedia.mediaStreamOptions.metadata = {
refreshAt: currentMedia.mediaStreamOptions.refreshAt
};
this.resetStreamTimeout()
return mediaManager.createMediaObject(currentMedia, this.currentMediaMimeType)
}
this.console.log("Before stopping session")
this.stopSession();
const { clientPromise: playbackPromise, port: playbackPort, url: clientUrl } = await listenZeroSingleClient()
this.console.log("After stopping session")
const playbackUrl = clientUrl
let rebroadcastEnabled = this.interfaces?.includes( "mixin:@scrypted/prebuffer-mixin")
const { clientPromise: playbackPromise, port: playbackPort } = await listenZeroSingleClient()
const playbackUrl = `rtsp://127.0.0.1:${playbackPort}`
this.console.log("PLAYBACKURL: " +playbackUrl)
playbackPromise.then(async (client) => {
client.setKeepAlive(true, 10000)
let sip: SipCallSession
let audioSplitter
let videoSplitter
try {
if( !this.incomingCallRequest ) {
// If this is a "view" call, update the stream endpoint to send it only to "us"
@@ -252,61 +332,37 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
}
let rtsp: RtspServer;
const cleanup = () => {
this.console.log("CLEANUP CALLED")
client.destroy();
if (this.session === sip)
this.session = undefined
try {
this.log.d('cleanup(): stopping sip session.')
sip.stop()
sip?.stop()
this.currentMediaObject = undefined
}
catch (e) {
}
audioSplitter?.server?.close()
videoSplitter?.server?.close()
rtsp?.destroy()
this.console.log("CLEANUP ENDED")
this.deferredCleanup = undefined
this.remoteRtpDescription = undefined
}
this.deferredCleanup = cleanup
client.on('close', cleanup)
client.on('error', cleanup)
let sipOptions = SipHelper.sipOptions( this )
sip = await this.persistentSipManager.session( sipOptions );
// Validate this sooner
if( !sip ) return Promise.reject("Cannot create session")
sip.onCallEnded.subscribe(cleanup)
// Call the C300X
this.remoteRtpDescription = sip.callOrAcceptInvite(
( audio ) => {
return [
//TODO: Payload types are hardcoded
`m=audio 65000 RTP/SAVP 110`,
`a=rtpmap:110 speex/8000`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
]
}, ( video ) => {
if( false ) {
//TODO: implement later
return [
`m=video 0 RTP/SAVP 0`
]
} else {
return [
//TODO: Payload types are hardcoded
`m=video 65002 RTP/SAVP 96`,
`a=rtpmap:96 H264/90000`,
`a=fmtp:96 profile-level-id=42801F`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
'a=recvonly'
]
}
}, this.incomingCallRequest );
this.incomingCallRequest = undefined
if( !rebroadcastEnabled || (rebroadcastEnabled && !this.incomingCallRequest ) ) {
sip = await this.callIntercom( cleanup )
}
//let sdp: string = replacePorts(this.remoteRtpDescription.sdp, 0, 0 )
let sdp : string = [
let sdp : string = [
"v=0",
"m=audio 5000 RTP/AVP 110",
"c=IN IP4 127.0.0.1",
@@ -318,42 +374,141 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
//sdp = sdp.replaceAll(/a=crypto\:1.*/g, '')
//sdp = sdp.replaceAll(/RTP\/SAVP/g, 'RTP\/AVP')
//sdp = sdp.replaceAll('\r\n\r\n', '\r\n')
sdp = addTrackControls(sdp)
sdp = sdp.split('\n').filter(line => !line.includes('a=rtcp-mux')).join('\n')
if( sipOptions.debugSip )
this.log.d('SIP: Updated SDP:\n' + sdp);
client.write(sdp)
client.end()
let vseq = 0;
let vseen = 0;
let vlost = 0;
let aseq = 0;
let aseen = 0;
let alost = 0;
sdp = addTrackControls(sdp);
sdp = sdp.split('\n').filter(line => !line.includes('a=rtcp-mux')).join('\n');
this.console.log('proposed sdp', sdp);
this.console.log("================= AUDIOSPLITTER CREATING.... ============" )
audioSplitter = await createBindUdp(5000)
this.console.log("================= AUDIOSPLITTER CREATED ============" )
audioSplitter.server.on('close', () => {
this.console.log("================= CLOSED AUDIOSPLITTER ================")
audioSplitter = undefined
})
this.console.log("================= VIDEOSPLITTER CREATING.... ============" )
videoSplitter = await createBindUdp(5002)
this.console.log("================= VIDEOSPLITTER CREATED.... ============" )
videoSplitter.server.on('close', () => {
this.console.log("================= CLOSED VIDEOSPLITTER ================")
videoSplitter = undefined
})
rtsp = new RtspServer(client, sdp, false);
const parsedSdp = parseSdp(rtsp.sdp);
const videoTrack = parsedSdp.msections.find(msection => msection.type === 'video').control;
const audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio').control;
rtsp.console = this.console;
await rtsp.handlePlayback();
this.session = sip
videoSplitter.server.on('message', (message, rinfo) => {
if ( !isStunMessage(message)) {
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
if (!isRtpMessage)
return;
vseen++;
try {
rtsp.sendTrack(videoTrack, message, !isRtpMessage);
} catch(e ) {
this.console.log(e)
}
const seq = getSequenceNumber(message);
if (seq !== (vseq + 1) % 0x0FFFF)
vlost++;
vseq = seq;
}
});
audioSplitter.server.on('message', (message, rinfo ) => {
if ( !isStunMessage(message)) {
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
if (!isRtpMessage)
return;
aseen++;
try {
rtsp.sendTrack(audioTrack, message, !isRtpMessage);
} catch(e) {
this.console.log(e)
}
const seq = getSequenceNumber(message);
if (seq !== (aseq + 1) % 0x0FFFF)
alost++;
aseq = seq;
}
});
try {
await rtsp.handleTeardown();
this.console.log('rtsp client ended');
} catch (e) {
this.console.log('rtsp client ended ungracefully', e);
} finally {
cleanup();
}
}
catch (e) {
this.console.error(e)
sip?.stop()
throw e;
}
});
this.resetStreamTimeout();
const mediaStreamOptions = Object.assign(this.getSipMediaStreamOptions(), {
refreshAt: Date.now() + STREAM_TIMEOUT,
});
const ffmpegInput: FFmpegInput = {
url: undefined,
container: 'sdp',
mediaStreamOptions,
inputArguments: [
'-f', 'sdp',
'-i', playbackUrl,
],
const mediaStreamUrl: MediaStreamUrl = {
url: playbackUrl,
mediaStreamOptions: this.getSipMediaStreamOptions(),
};
this.currentMedia = ffmpegInput;
this.currentMediaMimeType = ScryptedMimeTypes.FFmpegInput;
return mediaManager.createFFmpegMediaObject(ffmpegInput);
sleep(2500).then( () => this.takePicture() )
this.currentMediaObject = mediaManager.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl);
// Invalidate any cached image and take a picture after some seconds to take into account the opening of the lens
this.lastImageRefresh = undefined
return this.currentMediaObject
}
async callIntercom( cleanup ) : Promise<SipCallSession> {
let sipOptions : SipOptions = SipHelper.sipOptions( this )
let sip : SipCallSession = await this.persistentSipManager.session( sipOptions );
// Validate this sooner
if( !sip ) return Promise.reject("Cannot create session")
sip.onCallEnded.subscribe(cleanup)
// Call the C300X
this.remoteRtpDescription = sip.callOrAcceptInvite(
( audio ) => {
return [
// this SDP is used by the intercom and will send the encrypted packets which we don't care about to the loopback on port 65000 of the intercom
`m=audio 65000 RTP/SAVP 110`,
`a=rtpmap:110 speex/8000`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
]
}, ( video ) => {
return [
// this SDP is used by the intercom and will send the encrypted packets which we don't care about to the loopback on port 65000 of the intercom
`m=video 65002 RTP/SAVP 96`,
`a=rtpmap:96 H264/90000`,
`a=fmtp:96 profile-level-id=42801F`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
'a=recvonly'
]
}, this.incomingCallRequest );
this.incomingCallRequest = undefined
return sip
}
getSipMediaStreamOptions(): ResponseMediaStreamOptions {
@@ -362,13 +517,16 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
name: 'SIP',
// this stream is NOT scrypted blessed due to wackiness in the h264 stream.
// tool: "scrypted",
container: 'sdp',
container: 'rtsp',
video: {
codec: 'h264'
},
audio: {
// this is a hint to let homekit, et al, know that it's speex audio and needs transcoding.
codec: 'speex',
},
source: 'cloud', // to disable prebuffering
userConfigurable: false,
userConfigurable: true,
};
}
@@ -378,14 +536,28 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvid
]
}
async getDevice(nativeId: string) : Promise<BticinoSipLock> {
async getDevice(nativeId: string) : Promise<any> {
if( nativeId && nativeId.endsWith('-aswm-switch')) {
this.aswmSwitch = new BticinoAswmSwitch(this, this.voicemailHandler)
return this.aswmSwitch
} else if( nativeId && nativeId.endsWith('-mute-switch') ) {
this.muteSwitch = new BticinoMuteSwitch(this)
return this.muteSwitch
}
return new BticinoSipLock(this)
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
this.voicemailHandler.cancelTimer()
this.persistentSipManager.cancelTimer()
this.controllerApi.cancelTimer()
if( nativeId?.endsWith('-aswm-switch') ) {
this.aswmSwitch.cancelTimer()
} else if( nativeId?.endsWith('mute-switch') ) {
this.muteSwitch.cancelTimer()
} else {
this.stopIntercom()
this.voicemailHandler.cancelTimer()
this.persistentSipManager.cancelTimer()
this.controllerApi.cancelTimer()
}
}
reset() {

View File

@@ -0,0 +1,64 @@
import { ScryptedDeviceBase, HttpRequest, HttpResponse, HttpRequestHandler, OnOff } from "@scrypted/sdk";
import { BticinoSipCamera } from "./bticino-camera";
export class BticinoMuteSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
private timeout : NodeJS.Timeout
constructor(private camera: BticinoSipCamera) {
super( camera.nativeId + "-mute-switch");
this.on = false;
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
}
turnOff(): Promise<void> {
this.on = false
return this.camera.muteRinger(false)
}
turnOn(): Promise<void> {
this.on = true
return this.camera.muteRinger(true)
}
syncStatus() {
this.camera.muteStatus().then( (value) => {
this.on = value["status"]
} ).catch( (e) => { this.camera.console.error(e) } ).finally( () => {
this.timeout = setTimeout( () => this.syncStatus() , 60000 )
} )
}
cancelTimer() {
if( this.timeout ) {
clearTimeout(this.timeout)
}
}
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
if (request.url.endsWith('/disabled')) {
this.on = false
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/enabled') ) {
this.on = true
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/enable') ) {
this.turnOn()
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/disable') ) {
this.turnOff()
response.send('Success', {
code: 200,
});
} else {
response.send('Unsupported operation', {
code: 400,
});
}
}
}

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