Compare commits

..

213 Commits

Author SHA1 Message Date
Koushik Dutta
e5fb65d75e prerelease 2023-03-17 23:42:47 -07:00
Koushik Dutta
290b73f3d9 python-codecs: fix hw acceleration 2023-03-17 23:42:33 -07:00
Koushik Dutta
f717e87306 snapshot: include ffmpeg path 2023-03-17 23:37:20 -07:00
Koushik Dutta
b80ac7c60d prebeta 2023-03-17 23:21:33 -07:00
Koushik Dutta
997a4732ec server: additional python rpc transport fixes 2023-03-17 23:21:07 -07:00
Koushik Dutta
6e08f11578 snapshot/python-codecs: move high performance native image library to larger package as optional dependency 2023-03-17 22:58:29 -07:00
Koushik Dutta
87c4814e6f prebeta 2023-03-17 22:22:39 -07:00
Koushik Dutta
2e0e009719 server: update publish scripts 2023-03-17 22:22:30 -07:00
Koushik Dutta
77399038e9 server: clean up python rpc transports 2023-03-17 22:21:07 -07:00
Koushik Dutta
fae66619fb prepublish 2023-03-17 19:16:50 -07:00
Koushik Dutta
d979b9ec0c server: connection.poll should provide None to block forever 2023-03-17 19:16:15 -07:00
Koushik Dutta
975319a65d motion: implement a default inclusion zone that prevents on screen clocks from triggering motion 2023-03-17 16:19:50 -07:00
Koushik Dutta
7b5aa4ba2d python-codecs: remove erroneous libav from gstreamer settings 2023-03-17 16:19:20 -07:00
Koushik Dutta
670739c82b python-codecs: restructure, add gstreamer decoder option 2023-03-17 10:28:41 -07:00
Koushik Dutta
8511bd15a8 server: update package lock 2023-03-16 23:59:19 -07:00
Koushik Dutta
06d3c89274 prepublish 2023-03-16 23:59:10 -07:00
Koushik Dutta
e13f3eb2f1 server: add python forked processes to stats 2023-03-16 23:59:01 -07:00
Koushik Dutta
001918d613 predict: fix detections from webui 2023-03-16 23:58:45 -07:00
Koushik Dutta
c859c3aa40 detect: publish plugins with new video pipeline support 2023-03-16 23:40:33 -07:00
Koushik Dutta
2bce019677 predict: make models a separate download 2023-03-16 23:29:02 -07:00
Koushik Dutta
6ba3386157 detect: fix peer kill causing exception inside finally handler 2023-03-16 22:10:25 -07:00
Koushik Dutta
51e66d98f9 videoanalysis: changing motion detect mode should restart motion detection 2023-03-16 22:09:56 -07:00
Koushik Dutta
6484804649 server: update package lock 2023-03-16 20:37:54 -07:00
Koushik Dutta
b2a05c099d prepublish 2023-03-16 20:37:42 -07:00
Koushik Dutta
898331da4c Merge branch 'main' of github.com:koush/scrypted 2023-03-16 20:37:33 -07:00
Koushik Dutta
9044e782b2 python-codecs: add gray decoding support 2023-03-16 20:37:28 -07:00
Koushik Dutta
aedb985941 detect: support motion on new pipeline 2023-03-16 20:37:12 -07:00
Koushik Dutta
9ba22e4058 server: fix python rpc kill handling 2023-03-16 20:33:09 -07:00
Alex Leeds
ab0afb61ae ring: add video clips support (#635)
* ring: add video clips support

* fix merge
2023-03-16 18:40:36 -07:00
Alex Leeds
bf00ba0adc ring: add support for locks (#634) 2023-03-16 18:32:20 -07:00
Koushik Dutta
d564cf1b62 server: update package lock 2023-03-16 11:13:24 -07:00
Koushik Dutta
544dfb3b24 Update rtsp-proxy.ts 2023-03-16 10:40:19 -07:00
Koushik Dutta
cf9af910be rtsp: rtsp proxy example 2023-03-16 10:03:24 -07:00
Koushik Dutta
e2e65f93af prepublish 2023-03-16 09:37:34 -07:00
Koushik Dutta
b271567428 server: Fix device initialization on first report 2023-03-16 09:37:25 -07:00
Koushik Dutta
a88a295d9a server: fixup project file 2023-03-15 23:09:16 -07:00
Koushik Dutta
38ba31ca7d tensorflow-lite: use multiple tpu 2023-03-15 23:08:48 -07:00
Koushik Dutta
1c8ff2493b coreml: move prediction onto background thread 2023-03-15 23:04:45 -07:00
Koushik Dutta
5c9f62e6b6 videoanalysis: add snapshot pipeline 2023-03-15 23:04:13 -07:00
Koushik Dutta
6fd8018c52 python-codecs: fix nre 2023-03-15 23:02:50 -07:00
Koushik Dutta
d900ddf5f1 mac: fix erroneous typing installation 2023-03-15 21:54:17 -07:00
Koushik Dutta
e3a8d311ce python-codecs: add libav support 2023-03-15 20:33:44 -07:00
Koushik Dutta
8bbc3d5470 videoanalysis: generator cleanup 2023-03-15 17:18:28 -07:00
Koushik Dutta
00cf987cec videoanalysis: reimplemnet snapshots for new pipeline 2023-03-15 17:03:34 -07:00
Koushik Dutta
7e5dcae64a webrtc/alexa: add option to disable TURN on peers that already have externally reachable addresses 2023-03-15 10:31:25 -07:00
Koushik Dutta
cb67237d7c server: update package lock 2023-03-15 01:28:39 -07:00
Koushik Dutta
4be848c440 prepublish 2023-03-15 01:28:05 -07:00
Koushik Dutta
b33422b066 server: fix python fork hangs 2023-03-15 01:28:01 -07:00
Koushik Dutta
77418684da server: publish 2023-03-14 23:50:22 -07:00
Koushik Dutta
08cf9f7774 prepublish 2023-03-14 23:49:51 -07:00
Koushik Dutta
9f2fabf9c0 Merge branch 'main' of github.com:koush/scrypted 2023-03-14 23:47:24 -07:00
Koushik Dutta
e2e1c7be44 server: remove python log statement 2023-03-14 23:47:05 -07:00
Koushik Dutta
ba030ba197 server: fix multiprocessing blocking read on linux 2023-03-14 23:45:06 -07:00
Koushik Dutta
a4f37bdc16 snapshot: publish 2023-03-14 23:42:33 -07:00
Koushik Dutta
f6c7b00562 tensorflow-lite: fix numpy serialization issue 2023-03-14 23:41:55 -07:00
Koushik Dutta
b951614f7c Merge branch 'main' of github.com:koush/scrypted 2023-03-14 20:13:28 -07:00
Koushik Dutta
f1dfdb3494 coreml: revert tracker dependency removal 2023-03-14 20:13:22 -07:00
Nick Berardi
ffbd25b13b alexa: set screen ratio to 720p (#625) 2023-03-14 18:40:47 -07:00
Koushik Dutta
4f03fe2420 docker: fix pyvips cffi mismatch 2023-03-14 18:00:10 -07:00
Koushik Dutta
ffdb386afa mac: include libvips in installer 2023-03-14 17:25:47 -07:00
Koushik Dutta
9eeeaa79d0 docker: include libvips 2023-03-14 16:08:22 -07:00
Koushik Dutta
4163142d1e Merge branch 'main' of github.com:koush/scrypted 2023-03-14 15:45:35 -07:00
Koushik Dutta
71cddc67e0 predict: publish new pipeline support 2023-03-14 15:45:30 -07:00
Alex Leeds
2cbc4eb54f eufy: support multiple p2p streams (#624) 2023-03-14 15:26:46 -07:00
Koushik Dutta
fc94fb4221 core: republish 2023-03-14 15:21:13 -07:00
Koushik Dutta
85ed41c590 server: publish 2023-03-14 15:15:09 -07:00
Koushik Dutta
59f889a200 prepublish 2023-03-14 15:15:05 -07:00
Koushik Dutta
7dc476fe02 prepublish 2023-03-14 15:14:59 -07:00
Koushik Dutta
f5070f1ff1 server: publish 2023-03-14 15:14:41 -07:00
Koushik Dutta
15283e13f0 prepublish 2023-03-14 15:14:23 -07:00
Koushik Dutta
0cde5bf8e7 prepublish 2023-03-14 15:14:10 -07:00
Koushik Dutta
fe3a1a023d prepublish 2023-03-14 15:14:02 -07:00
Koushik Dutta
369dcff2bd server: support large file transfers on engine io 2023-03-14 14:50:47 -07:00
Koushik Dutta
ed341a12b1 predict: rgba to rgb conversion 2023-03-14 14:50:28 -07:00
Koushik Dutta
00e523e268 core: add object detection ui 2023-03-14 14:50:04 -07:00
Koushik Dutta
4e25aedbe7 python-codecs: multiprocessing decode 2023-03-14 10:22:01 -07:00
Koushik Dutta
45bd3cbb7c server: fix various python mutiprocesisng quirks 2023-03-14 10:21:45 -07:00
Koushik Dutta
8e34bc2130 server: fix dangling thread if glib main loop fails 2023-03-14 09:18:00 -07:00
Koushik Dutta
457fc96332 predict: support for new pipeline redetection 2023-03-14 09:16:54 -07:00
Koushik Dutta
e2186401bf videoanalysis: new working pipeline 2023-03-14 09:16:34 -07:00
Koushik Dutta
a19d916ef0 python-codecs: improve memory management 2023-03-14 09:16:08 -07:00
Koushik Dutta
42bc7dc644 rebroadcast: publish update, current version was using actual addresses? 2023-03-14 08:52:38 -07:00
Koushik Dutta
f9d6308154 rpc: python rpc should be killed on disconnect 2023-03-13 17:10:06 -07:00
Koushik Dutta
dcb6627fb1 predict: publish fix that validates settings input 2023-03-13 11:15:45 -07:00
Koushik Dutta
1d5c71d617 videoanalysis: publish 2023-03-13 10:28:07 -07:00
Koushik Dutta
d5157fb868 predict: new detection pipeline around 50% faster! 2023-03-13 10:17:38 -07:00
Koushik Dutta
98096845dc rtp: add utility method for adding timestamps 2023-03-13 10:17:09 -07:00
Koushik Dutta
28ac97f4c9 predict: new pipeline 2023-03-12 22:11:06 -07:00
Koushik Dutta
2fc39e3979 videoanalysis: new pipeline 2023-03-12 22:10:40 -07:00
Koushik Dutta
9c89c3c2b8 snapshot: vips fixes 2023-03-12 22:10:15 -07:00
Koushik Dutta
15c7747f48 sdk: update 2023-03-12 22:09:59 -07:00
Koushik Dutta
940d4b7fd4 rpc: various python fixes 2023-03-12 22:09:50 -07:00
Koushik Dutta
a1c8ce754e python-codecs: working prototype 2023-03-12 22:09:33 -07:00
Koushik Dutta
5e6364850a onvif: fix ptz causing creation issues 2023-03-12 10:44:21 -07:00
Koushik Dutta
8df52e7595 python-codecs: wip 2023-03-11 20:01:26 -08:00
Koushik Dutta
1e004d6700 rpc: fixup various async iterator bugs, add memoryview support to python 2023-03-11 19:38:43 -08:00
Koushik Dutta
4570f9cd38 python-codecs: wip 2023-03-11 00:17:50 -08:00
Koushik Dutta
601cd39ba4 rpc: fix proxied iterator proxy 2023-03-10 21:38:48 -08:00
Koushik Dutta
923475fab2 Merge branch 'main' of github.com:koush/scrypted 2023-03-10 19:46:59 -08:00
Koushik Dutta
21ce5dfad4 sdk: image support 2023-03-10 19:46:51 -08:00
Koushik Dutta
2bd3592aad server: fix mediaobject polymorphism 2023-03-10 19:46:38 -08:00
Koushik Dutta
44f083ca23 webrtc: remove potential converter with permission escalation 2023-03-10 19:46:03 -08:00
Koushik Dutta
cc7271f0a2 snapshot: use libvips 2023-03-10 19:45:37 -08:00
Koushik Dutta
11a1a1134d predict: validate args 2023-03-10 16:50:11 -08:00
Alex Leeds
70cfa13e67 eufy: motion, partial livestream removal & minor improvement in snapshots (#618) 2023-03-10 16:16:36 -08:00
Koushik Dutta
291f90b2b2 rtp: expose child process in rtp forwarder 2023-03-10 11:55:22 -08:00
Koushik Dutta
d0ae7eb841 eufy: all cleaned up 2023-03-10 11:11:13 -08:00
Koushik Dutta
8444102cca eufy: functional audio 2023-03-10 10:49:45 -08:00
Alex Leeds
5a1c052c77 eufy: support captcha (#616) 2023-03-10 09:46:42 -08:00
Koushik Dutta
fb7eeece54 eufy: more logging 2023-03-10 07:53:34 -08:00
Koushik Dutta
d479bcece9 eufy: fix encoder codecs 2023-03-10 07:34:04 -08:00
Koushik Dutta
deefac2347 eufy: fix encoder codecs 2023-03-10 07:33:04 -08:00
Koushik Dutta
53808a04b7 google-cloud-tts: move to org 2023-03-09 21:40:06 -08:00
Koushik Dutta
a1785c2658 tensorflow-legacy: remove 2023-03-09 21:37:56 -08:00
Koushik Dutta
601cf46b1e thermostat: move to org 2023-03-09 21:37:28 -08:00
Koushik Dutta
6bba1b1cbd eufy: fix output url 2023-03-09 20:56:22 -08:00
Koushik Dutta
ab0122420b eufy: codec copy 2023-03-09 20:55:48 -08:00
Koushik Dutta
74ae2aab91 eufy: try mpegts 2023-03-09 20:54:43 -08:00
Koushik Dutta
c5fa131a44 eufy: revert stream manager change 2023-03-09 20:44:00 -08:00
Koushik Dutta
8dcf4dda9f eufy: use ffmpeg and adts audio 2023-03-09 20:29:00 -08:00
Koushik Dutta
cd59125ada eufy: revert 2023-03-09 20:24:08 -08:00
Koushik Dutta
d284eb6738 eufy: mute audio 2023-03-09 19:07:42 -08:00
Koushik Dutta
a78cc943cc eufy: mark stream as scrypted parser safe 2023-03-09 19:07:22 -08:00
Koushik Dutta
7ddeda1595 eufy: add audio toggle 2023-03-09 19:06:20 -08:00
Koushik Dutta
f02dfa5e14 eufy: remove some logging 2023-03-09 18:56:31 -08:00
Koushik Dutta
b2a4f20381 eufy: audio maybe 2023-03-09 18:53:36 -08:00
Koushik Dutta
dec3c354f0 eufy: use per session live stream manager 2023-03-09 18:47:45 -08:00
Koushik Dutta
2ee581d48d Merge branch 'main' of github.com:koush/scrypted 2023-03-09 18:07:34 -08:00
Koushik Dutta
d74c3a3fc5 eufy: generate some timestamps 2023-03-09 18:07:29 -08:00
Nick Berardi
405d9f0c09 onvif: add absolute and speed support to movement (#612) 2023-03-09 17:17:08 -08:00
Koushik Dutta
db25c5babe Merge branch 'main' of github.com:koush/scrypted 2023-03-09 14:13:17 -08:00
Alex Leeds
d5c90ab8da eufy: add plugin (#614) 2023-03-09 14:13:03 -08:00
Koushik Dutta
81a5c143d6 snapshot: add/use sharp (libvips) 2023-03-09 09:29:28 -08:00
Koushik Dutta
ebf2176618 remote: wip 2023-03-08 13:36:55 -08:00
Koushik Dutta
f435f8eff5 sdk: update 2023-03-08 13:36:40 -08:00
Koushik Dutta
f8c16edaae Merge branch 'main' of github.com:koush/scrypted 2023-03-08 07:37:03 -08:00
Koushik Dutta
ea86065d99 tapo: add cloud password instructions 2023-03-08 07:36:57 -08:00
Alex Leeds
ed5c7b126c ring: update dependencies (#607) 2023-03-07 20:49:28 -08:00
Koushik Dutta
806e015823 tapo: make it searchable in plugin install 2023-03-07 16:55:15 -08:00
Koushik Dutta
41c4cbc96c client: update 2023-03-07 16:24:31 -08:00
Koushik Dutta
143a0b2c41 webrtc: startRtpForwarderProcess remove werift dependency 2023-03-07 16:24:22 -08:00
Koushik Dutta
f582db3f11 common: http message parsing helpers 2023-03-07 16:24:00 -08:00
Koushik Dutta
103855ca50 Merge branch 'main' of github.com:koush/scrypted 2023-03-07 16:07:22 -08:00
Koushik Dutta
70c6fe4c68 tapo: initial commit of two way audio 2023-03-07 16:07:15 -08:00
Nick Berardi
c85d45050f alexa: refactor code structure (#606) 2023-03-07 12:04:52 -08:00
Alex Leeds
16a39ac76a ring: update ring api client (#605) 2023-03-07 07:51:25 -08:00
Koushik Dutta
fdc7519db0 onvif: ptz 2023-03-06 18:17:54 -08:00
Koushik Dutta
83af0c5ec7 core: cleanup device discovery 2023-03-06 17:03:21 -08:00
Koushik Dutta
ee22686bff videoanalysis: prevent double motion detector or double object detector 2023-03-06 10:32:35 -08:00
Koushik Dutta
7dc1f9736a pam-diff: add support for motion objects 2023-03-06 10:10:16 -08:00
Koushik Dutta
6e2aa37d75 server: implement missing setMixins 2023-03-06 09:34:29 -08:00
Koushik Dutta
fbaa8a31cf predict: fix bug where memory can leak if detection fails
tf: request restart if detection fails
2023-03-06 09:34:04 -08:00
Koushik Dutta
fa89a5ad24 sort: fix crash if no detection id is provided 2023-03-06 09:33:38 -08:00
Koushik Dutta
464deaf35e cameras: fix bug where device creation fails when no name is provided 2023-03-06 09:33:15 -08:00
Koushik Dutta
9cc8f50ff7 client: update sdk 2023-03-06 09:32:53 -08:00
Koushik Dutta
c17a1184cc core: fix settings subgroup regression 2023-03-06 07:39:17 -08:00
Koushik Dutta
b5004739c3 core: fix wonky settings 2023-03-05 23:09:31 -08:00
Koushik Dutta
d01c0fa72b sdk: fix StorageSettings 'device' defaults 2023-03-05 22:39:04 -08:00
Koushik Dutta
bb9f3d5aab predict: revert object tracker changes until custom NVR detector with face recognition is in place 2023-03-05 22:38:36 -08:00
Koushik Dutta
b23daa6735 Merge branch 'main' of github.com:koush/scrypted 2023-03-05 21:36:54 -08:00
Koushik Dutta
bb8b0125b6 server/sdk: update 2023-03-05 21:36:50 -08:00
Koushik Dutta
8e5f44f998 server: add support for polymorphic media objects 2023-03-05 21:33:44 -08:00
Brett Jia
9015af4902 arlo: optimize event handling (#601)
* optimize event waiting by keying on properties

* bump 0.6.6

* interrupt cleanup for other tasks

* bump 0.6.7 for race condition fix
2023-03-05 19:25:47 -08:00
Koushik Dutta
7902a091a9 core: fix listener leak 2023-03-04 20:48:24 -08:00
Koushik Dutta
615357befb werift: update 2023-03-04 19:20:31 -08:00
Koushik Dutta
34b26c81dc server: fix bug where express sets Cache-Control: max-age=0 on all file responses 2023-03-04 19:19:52 -08:00
Koushik Dutta
ea99a54e1b cloud: cleanup logging 2023-03-04 19:18:36 -08:00
Koushik Dutta
f726826391 core: fix changing password escalating user privileges 2023-03-04 19:00:49 -08:00
Koushik Dutta
dc5148c856 rpc: dont throw on oneway methods even if the peer is closed 2023-03-04 18:59:55 -08:00
Koushik Dutta
373c11ffee webrtc: add connection logging 2023-03-04 18:34:45 -08:00
Koushik Dutta
bea1f019b4 server: update deps 2023-03-04 14:05:08 -08:00
Koushik Dutta
29c98777e9 server: add python plugin id to command line 2023-03-04 14:05:04 -08:00
Koushik Dutta
9eb5029128 cloud: Fix x-scrypted-cloud header to come from upstream proxy 2023-03-04 08:59:23 -08:00
Koushik Dutta
33607796d1 cloud: log incoming connections 2023-03-04 07:51:08 -08:00
Koushik Dutta
f23fa0c335 coreml: update deps 2023-03-03 23:39:43 -08:00
Koushik Dutta
e6cfecfc1a videoanalysis: configurable object tracker 2023-03-03 23:39:18 -08:00
Koushik Dutta
44346d5b33 server: fix python rpc connect 2023-03-03 23:34:15 -08:00
Koushik Dutta
19da68884b server: implement python connectRPCObject 2023-03-03 23:17:43 -08:00
Koushik Dutta
544349de8d snapshot: update sdk 2023-03-03 16:48:37 -08:00
Koushik Dutta
6f90b1a0e3 server: add support for direct ipc 2023-03-03 16:48:29 -08:00
Koushik Dutta
fbbb9163d7 sdk: add ipcObject 2023-03-03 14:56:40 -08:00
Koushik Dutta
445581eefa server: plugin worker cleanups 2023-03-03 11:36:15 -08:00
Koushik Dutta
096c036ea2 rpc: implement python async iterator 2023-03-02 21:03:29 -08:00
Koushik Dutta
b2e5801426 rpc: improve error serialization and handling 2023-03-02 16:02:48 -08:00
Koushik Dutta
41061854f1 rpc: add intrinsic support for async iterators 2023-03-02 13:49:20 -08:00
Koushik Dutta
d91e625973 sort-tracker: publish 2023-03-02 09:09:53 -08:00
Koushik Dutta
ec5b59a00c Merge branch 'main' of github.com:koush/scrypted 2023-03-01 21:34:12 -08:00
Koushik Dutta
172790b18f sdk: fix device StorageSetting deserialzation
predict: externalize tracker
2023-03-01 21:33:43 -08:00
Nick Berardi
de0e6ee955 unifi-protect: added new smart event and updated snapshot to use login (#595) 2023-03-01 20:13:01 -08:00
Koushik Dutta
69d7ff2ced Merge branch 'main' of github.com:koush/scrypted 2023-03-01 14:51:55 -08:00
Koushik Dutta
3c237eac91 tensorflow-lite: cleanup dead code 2023-03-01 14:51:52 -08:00
Koushik Dutta
694c195024 rpc: fixup WeakRef typing 2023-03-01 14:51:40 -08:00
Koushik Dutta
c1f0281030 core: add finer grain user permissions 2023-03-01 14:51:14 -08:00
Brett Jia
fa218cbcbd remote: cleanup remote hint now that rebroadcast uses external by default (#594) 2023-03-01 14:18:42 -08:00
Koushik Dutta
a89700acc2 cli/client: decouple, upgrade packages, publish 2023-03-01 13:55:40 -08:00
Koushik Dutta
82fb24e275 rebroadcast: move url expansion into separate file 2023-03-01 12:22:33 -08:00
Koushik Dutta
eef67a9383 cli: fix arg parsing 2023-03-01 11:56:37 -08:00
Koushik Dutta
1180d9fa2c cli: rebuild 2023-03-01 11:51:36 -08:00
Koushik Dutta
57734f1d3c videoanalysis: remove extra settings 2023-02-28 23:56:39 -08:00
Koushik Dutta
dace750829 predict: publish 2023-02-28 21:53:33 -08:00
Koushik Dutta
f359a7167a server: nuke python prefix prior to install to purge old conflicting deps 2023-02-28 21:53:15 -08:00
Koushik Dutta
39c0759d1b tensorflow-lite: add simd support 2023-02-28 21:34:06 -08:00
Koushik Dutta
fee90334fb videoanalysis: snapshot mode cleanups 2023-02-28 20:48:31 -08:00
Koushik Dutta
80db6e50ab rebroadcast: fix external url behavior 2023-02-28 20:44:57 -08:00
Koushik Dutta
1fa6c2d842 tensorflow: reduce several expensice cpu resizes 2023-02-28 20:44:21 -08:00
Koushik Dutta
8b39c4c22c snapshot: fix debug file logging 2023-02-28 20:26:15 -08:00
Koushik Dutta
4b6fd5b5a8 server: remove debug logging 2023-02-28 20:20:17 -08:00
Koushik Dutta
f2d1909b6d docker: gstreamer vaapi is apparently xplat 2023-02-28 19:42:21 -08:00
Koushik Dutta
7917fb96dc docker: incude ffmpeg 2023-02-28 19:39:12 -08:00
Koushik Dutta
ad5fae98f1 docker: incude ffmpeg 2023-02-28 19:35:33 -08:00
Koushik Dutta
8412eb36fe rebroadcast: fix erroneous external check. 2023-02-28 11:22:51 -08:00
Koushik Dutta
822455383b rebroadcast: include error in warning message 2023-02-28 11:20:27 -08:00
Koushik Dutta
2d4357e4c0 server: preserve MediaObject name in constructor 2023-02-28 11:17:58 -08:00
241 changed files with 9595 additions and 8440 deletions

9
.gitmodules vendored
View File

@@ -19,6 +19,7 @@
[submodule "external/ring-client-api"]
path = external/ring-client-api
url = ../../koush/ring
branch = fork
[submodule "plugins/vscode-typescript"]
path = plugins/vscode-typescript
url = ../../koush/scrypted-vscode-typescript/
@@ -28,20 +29,14 @@
[submodule "plugins/zwave/file-stream-rotator"]
path = plugins/zwave/file-stream-rotator
url = ../../koush/file-stream-rotator.git
[submodule "external/push-receiver"]
path = external/push-receiver
url = ../../koush/push-receiver.git
[submodule "sdk/developer.scrypted.app"]
path = sdk/developer.scrypted.app
url = ../../koush/developer.scrypted.app
[submodule "plugins/sample-cameraprovider"]
path = plugins/sample-cameraprovider
url = ../../koush/scrypted-sample-cameraprovider
[submodule "plugins/objectdetector/node-moving-things-tracker"]
path = plugins/objectdetector/node-moving-things-tracker
url = ../../koush/node-moving-things-tracker.git
[submodule "plugins/tensorflow-lite/sort_oh"]
path = plugins/tensorflow-lite/sort_oh
path = plugins/sort-tracker/sort_oh
url = ../../koush/sort_oh.git
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/node-nat-upnp

View File

@@ -2,14 +2,15 @@ import type { TranspileOptions } from "typescript";
import sdk, { ScryptedDeviceBase, MixinDeviceBase, ScryptedInterface, ScryptedDeviceType } from "@scrypted/sdk";
import vm from "vm";
import fs from 'fs';
import { newThread } from '@scrypted/server/src/threading';
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;
function tsCompile(source: string, options: TranspileOptions = null): string {
export async function tsCompile(source: string, options: TranspileOptions = null): Promise<string> {
const ts = require("typescript");
const { ScriptTarget } = ts;
@@ -25,27 +26,6 @@ function tsCompile(source: string, options: TranspileOptions = null): string {
return ts.transpileModule(source, options).outputText;
}
async function tsCompileThread(source: string, options: TranspileOptions = null): Promise<string> {
return newThread({
source, options,
customRequire: '__webpack_require__',
}, ({ source, options }) => {
const ts = global.require("typescript");
const { ScriptTarget } = ts;
// Default options -- you could also perform a merge, or use the project tsconfig.json
if (null === options) {
options = {
compilerOptions: {
target: ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS
}
};
}
return ts.transpileModule(source, options).outputText;
});
}
function getTypeDefs() {
const scryptedTypesDefs = fs.readFileSync('@types/sdk/types.d.ts').toString();
const scryptedIndexDefs = fs.readFileSync('@types/sdk/index.d.ts').toString();
@@ -61,14 +41,27 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
}, extraLibs);
const allScripts = Object.values(libs).join('\n').toString() + script;
let compiled: string;
const worker = sdk.fork<{
tsCompile: typeof tsCompile,
}>();
worker.worker.on('error', () => { })
try {
compiled = await tsCompileThread(allScripts);
const result = await worker.result;
compiled = await result.tsCompile(allScripts);
}
catch (e) {
device.log.e('Error compiling typescript.');
device.console.error(e);
throw e;
}
finally {
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,
@@ -76,7 +69,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
fetch,
ScryptedDeviceBase,
MixinDeviceBase,
systemManager,
systemManager: smProxy,
deviceManager,
endpointManager,
mediaManager,
@@ -111,6 +104,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
return {
value,
defaultExport,
apiProxy,
};
}
catch (e) {

View File

@@ -4,7 +4,7 @@ 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 } from 'stream';
import { Duplex, Readable, Writable } from 'stream';
import tls from 'tls';
import { Deferred } from './deferred';
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
@@ -47,6 +47,29 @@ export async function readMessage(client: Readable): Promise<string[]> {
}
}
export async function readBody(client: Readable, response: Headers) {
const cl = parseInt(response['content-length']);
if (cl)
return readLength(client, cl)
}
export function writeMessage(client: Writable, messageLine: string, body: Buffer, headers: Headers, console?: Console) {
let message = messageLine !== undefined ? `${messageLine}\r\n` : '';
if (body)
headers['Content-Length'] = body.length.toString();
for (const [key, value] of Object.entries(headers)) {
message += `${key}: ${value}\r\n`;
}
message += '\r\n';
client.write(message);
console?.log('rtsp outgoing message\n', message);
console?.log();
if (body)
client.write(body);
}
// https://yumichan.net/video-processing/video-compression/introduction-to-h264-nal-unit/
export const H264_NAL_TYPE_RESERVED0 = 0;
@@ -284,18 +307,7 @@ export class RtspBase {
}
write(messageLine: string, headers: Headers, body?: Buffer) {
let message = `${messageLine}\r\n`;
if (body)
headers['Content-Length'] = body.length.toString();
for (const [key, value] of Object.entries(headers)) {
message += `${key}: ${value}\r\n`;
}
message += '\r\n';
this.client.write(message);
this.console?.log('rtsp outgoing message\n', message);
this.console?.log();
if (body)
this.client.write(body);
writeMessage(this.client, messageLine, body, headers, this.console);
}
async readMessage(): Promise<string[]> {
@@ -590,9 +602,7 @@ export class RtspClient extends RtspBase {
}
async readBody(response: Headers) {
const cl = parseInt(response['content-length']);
if (cl)
return readLength(this.client, cl)
return readBody(this.client, response);
}
async request(method: string, headers?: Headers, path?: string, body?: Buffer, authenticating?: boolean): Promise<RtspServerResponse> {

56
common/test/rtsp-proxy.ts Normal file
View File

@@ -0,0 +1,56 @@
import net from 'net';
import { listenZero } from '../src/listen-cluster';
import { RtspClient, RtspServer } from '../src/rtsp-server';
async function main() {
const server = net.createServer(async serverSocket => {
const client = new RtspClient('rtsp://localhost:57594/911db962087f904d');
await client.options();
const describeResponse = await client.describe();
const sdp = describeResponse.body.toString();
const server = new RtspServer(serverSocket, sdp, true);
const setupResponse = await server.handlePlayback();
if (setupResponse !== 'play') {
serverSocket.destroy();
client.client.destroy();
return;
}
console.log('playback handled');
let channel = 0;
for (const track of Object.keys(server.setupTracks)) {
const setupTrack = server.setupTracks[track];
await client.setup({
// type: 'udp',
type: 'tcp',
port: channel,
path: setupTrack.control,
onRtp(rtspHeader, rtp) {
server.sendTrack(setupTrack.control, rtp, false);
},
});
channel += 2;
}
await client.play();
console.log('client playing');
await client.readLoop();
});
let port: number;
if (false) {
port = await listenZero(server);
}
else {
port = 5555;
server.listen(5555)
}
console.log(`rtsp://127.0.0.1:${port}`);
}
main();

View File

@@ -33,13 +33,17 @@ RUN apt-get -y install \
gcc \
libgirepository1.0-dev \
libglib2.0-dev \
pkg-config
pkg-config \
libvips
# ffmpeg
RUN apt-get -y install \
ffmpeg
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
RUN bash -c 'if [ $(uname -m) == "x86_64" ]; then apt-get -y install gstreamer1.0-vaapi; fi'
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \
@@ -58,6 +62,9 @@ RUN apt-get -y install \
# 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 pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
################################################################

View File

@@ -20,6 +20,11 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
# python native
RUN apt-get -y install \
python3 \

View File

@@ -19,6 +19,11 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
ENV SCRYPTED_DOCKER_SERVE="true"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"

View File

@@ -40,13 +40,14 @@ echo "Installing Scrypted dependencies..."
RUN_IGNORE xcode-select --install
RUN brew update
RUN_IGNORE brew install node@18
# needed by scrypted-ffmpeg
RUN_IGNORE brew install sdl2
# snapshot plugin and others
RUN brew install libvips
# gstreamer plugins
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly
# gst python bindings
RUN_IGNORE brew install gst-python
# python image library
# todo: consider removing this
RUN_IGNORE brew install pillow
### HACK WORKAROUND
@@ -102,7 +103,11 @@ then
fi
RUN python$PYTHON_VERSION -m pip install --upgrade pip
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions typing opencv-python psutil
if [ "$PYTHON_VERSION" != "3.10" ]
then
RUN python$PYTHON_VERSION -m pip install typing
fi
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions opencv-python psutil
echo "Installing Scrypted Launch Agent..."

View File

@@ -30,13 +30,17 @@ RUN apt-get -y install \
gcc \
libgirepository1.0-dev \
libglib2.0-dev \
pkg-config
pkg-config \
libvips
# ffmpeg
RUN apt-get -y install \
ffmpeg
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
RUN bash -c 'if [ $(uname -m) == "x86_64" ]; then apt-get -y install gstreamer1.0-vaapi; fi'
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \
@@ -55,6 +59,9 @@ RUN apt-get -y install \
# 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 pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
################################################################

Submodule external/push-receiver deleted from d054e083d6

View File

@@ -22,7 +22,8 @@
"args": [
"ffplay",
"Kitchen",
"getVideoStream"
"getRecordingStream",
"{\"startTime\":1677699495709}"
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,119 +1,183 @@
{
"name": "scrypted",
"version": "1.0.58",
"lockfileVersion": 2,
"version": "1.0.67",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.0.58",
"version": "1.0.67",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",
"adm-zip": "^0.5.9",
"@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",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"mkdirp": "^1.0.4",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tslib": "^2.3.1"
"semver": "^7.3.8",
"tslib": "^2.5.0"
},
"bin": {
"scrypted": "dist/packages/cli/src/main.js"
"scrypted": "dist/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.9",
"ts-node": "^10.2.1",
"typescript": "^4.8.2"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"extraneous": 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"
}
},
"node_modules/@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true,
"engines": {
"node": ">= 12"
"@types/semver": "^7.3.13",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"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": {
"@cspotcode/source-map-consumer": "0.8.0"
"@jridgewell/trace-mapping": "0.3.9"
},
"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",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"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/client": {
"version": "1.1.43",
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.1.43.tgz",
"integrity": "sha512-qpeGdqFga/Fx51MoF3E0iBPCjE/SDEIVdGh8Ws5dqw38bxUJD264c9NsNyCguLKyYguErKTAWnQkzqhO0bUbaA==",
"dependencies": {
"@scrypted/types": "^0.2.66",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
}
},
"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/types": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
"version": "0.2.66",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.66.tgz",
"integrity": "sha512-AL2iD7OmpqZlQMlpZKUBHpzL7H1IHhwKOi9uhRbVwG7EIDwenTspqtziH2Hyu0+XeCLf+gN69uQB6Qlz+QPf9A=="
},
"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.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"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.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"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.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"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.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"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": "*",
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"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": {
@@ -126,9 +190,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/@types/readline-sync": {
@@ -148,15 +212,15 @@
}
},
"node_modules/@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"node_modules/acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -175,9 +239,9 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"engines": {
"node": ">=6.0"
}
@@ -204,7 +268,7 @@
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
"engines": {
"node": ">= 0.6.0"
}
@@ -226,7 +290,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/create-require": {
"version": "1.1.1",
@@ -235,9 +299,9 @@
"dev": true
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -288,9 +352,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"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",
@@ -306,25 +370,20 @@
}
}
},
"node_modules/fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -338,12 +397,12 @@
"node_modules/has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -359,11 +418,6 @@
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"node_modules/linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -381,17 +435,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"dependencies": {
"fs-monkey": "1.0.3"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -422,7 +465,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
@@ -440,7 +483,7 @@
"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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
@@ -468,9 +511,9 @@
}
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -482,12 +525,12 @@
}
},
"node_modules/ts-node": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "0.7.0",
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
@@ -498,11 +541,13 @@
"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"
@@ -523,14 +568,14 @@
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/typescript": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -540,10 +585,16 @@
"node": ">=4.2.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "7.4.6",
@@ -581,7 +632,7 @@
"node_modules/yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
},
"node_modules/yn": {
"version": "3.1.1",
@@ -592,413 +643,5 @@
"node": ">=6"
}
}
},
"dependencies": {
"@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true
},
"@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"dev": true,
"requires": {
"@cspotcode/source-map-consumer": "0.8.0"
}
},
"@scrypted/types": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
},
"@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"dev": true
},
"@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"dev": true
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"dev": true
},
"@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"dev": true,
"requires": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"dev": true
},
"@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,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"dev": true
},
"@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==",
"dev": true
},
"@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,
"requires": {
"@types/glob": "*",
"@types/node": "*"
}
},
"@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"dev": true
},
"acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"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=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"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
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"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==",
"requires": {
"base64-arraybuffer": "0.1.4",
"component-emitter": "~1.3.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": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
"requires": {
"base64-arraybuffer": "0.1.4"
}
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"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
},
"memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"requires": {
"fs-monkey": "1.0.3"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
},
"parseuri": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"readline-sync": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"ts-node": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
"dev": true,
"requires": {
"@cspotcode/source-map-support": "0.7.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",
"yn": "3.1.1"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"typescript": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@@ -1,10 +1,10 @@
{
"name": "scrypted",
"version": "1.0.58",
"version": "1.0.67",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"main": "./dist/main.js",
"bin": {
"scrypted": "./dist/packages/cli/src/main.js"
"scrypted": "./dist/main.js"
},
"scripts": {
"prebuild": "rimraf dist",
@@ -16,25 +16,25 @@
"author": "",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",
"adm-zip": "^0.5.9",
"@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",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"mkdirp": "^1.0.4",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tslib": "^2.3.1"
"semver": "^7.3.8",
"tslib": "^2.5.0"
},
"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.9",
"ts-node": "^10.2.1",
"typescript": "^4.8.2"
"@types/semver": "^7.3.13",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
}

View File

@@ -7,8 +7,8 @@ import readline from 'readline-sync';
import https from 'https';
import mkdirp from 'mkdirp';
import { installServe, serveMain } from './service';
import { connectScryptedClient } from '../../client/src/index';
import { ScryptedMimeTypes, FFmpegInput } from '../../../sdk/types/src/types.input';
import { connectScryptedClient } from '@scrypted/client';
import { ScryptedMimeTypes, FFmpegInput } from '@scrypted/types';
import semver from 'semver';
import child_process from 'child_process';
@@ -126,7 +126,7 @@ async function runCommand() {
if (!device)
throw new Error('device not found: ' + idOrName);
const method = process.argv[4];
const args = process.argv.slice(5).map(arg => () => {
const args = process.argv.slice(5).map(arg => {
try {
return JSON.parse(arg);
}

View File

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

View File

@@ -1,58 +1,29 @@
{
"name": "@scrypted/client",
"version": "1.1.40",
"lockfileVersion": 2,
"version": "1.1.43",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.40",
"version": "1.1.43",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.65",
"@scrypted/types": "^0.2.76",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"extraneous": 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/types": {
"name": "@scrypted/types",
"version": "0.0.9",
"extraneous": true,
"license": "ISC",
"devDependencies": {}
},
"../common": {
"extraneous": true
},
"../sdk/types": {
"extraneous": true
},
"node_modules/@scrypted/types": {
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.65.tgz",
"integrity": "sha512-V/gfPy+xeRds6WMHwU6trt2YBkH9qcC/3Bx9q5hOxpE+rZSL4ru+nvlaumCRM3mSNWXBav4nbd23JCoGJ0F2eA=="
"version": "0.2.76",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.76.tgz",
"integrity": "sha512-/7n8ICkXj8TGba4cHvckLCgSNsOmOGQ8I+Jd8fX9sxkthgsZhF5At8PHhHdkCDS+yfSmfXHkcqluZZOfYPkpAg=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -69,9 +40,9 @@
}
},
"node_modules/@types/node": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"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": {
@@ -99,12 +70,12 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -118,29 +89,29 @@
}
},
"node_modules/engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"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.2.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
"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/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"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",
@@ -159,17 +130,17 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -183,7 +154,7 @@
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -213,7 +184,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
@@ -221,7 +192,7 @@
"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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
@@ -241,9 +212,9 @@
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -256,12 +227,12 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"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"
},
@@ -286,177 +257,5 @@
"node": ">=0.4.0"
}
}
},
"dependencies": {
"@scrypted/types": {
"version": "0.2.65",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.65.tgz",
"integrity": "sha512-V/gfPy+xeRds6WMHwU6trt2YBkH9qcC/3Bx9q5hOxpE+rZSL4ru+nvlaumCRM3mSNWXBav4nbd23JCoGJ0F2eA=="
},
"@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=="
},
"@types/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"dev": true
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"requires": {
"follow-redirects": "^1.14.7"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.40",
"version": "1.1.43",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -13,13 +13,13 @@
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
},
"dependencies": {
"@scrypted/types": "^0.2.65",
"@scrypted/types": "^0.2.76",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import { RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import axios, { AxiosRequestConfig } from 'axios';
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
@@ -504,7 +504,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
} = scrypted;
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async (data, mimeType, options) => {
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
const mo: MediaObjectRemote & {
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
@@ -520,7 +520,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
return data;
},
};
return mo;
return mo as any;
}
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;

View File

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

View File

@@ -1,175 +1,106 @@
{
"name": "@scrypted/alexa",
"version": "0.0.20",
"lockfileVersion": 2,
"version": "0.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.0.20",
"version": "0.2.3",
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^0.24.0",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/server": "file:../../server"
}
},
"../../common": {
"name": "@scrypted/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"
"@scrypted/sdk": "../../sdk",
"@types/node": "^18.4.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.39",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"typescript": "^4.9.3",
"webpack": "^5.74.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.9",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
}
},
"../../server": {
"version": "0.4.9",
"dev": true,
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.36",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.2.0",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^6.0.1",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^3.4.7",
"mime": "^3.0.0",
"mkdirp": "^1.0.4",
"nan": "^2.17.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^8.4.1",
"router": "^1.3.7",
"semver": "^7.3.8",
"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"
},
"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"
},
"optionalDependencies": {
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
}
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
"link": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
},
"node_modules/@scrypted/server": {
"resolved": "../../server",
"link": true
},
"node_modules/@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/alexa-smarthome-ts": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/alexa-smarthome-ts/-/alexa-smarthome-ts-0.0.1.tgz",
"integrity": "sha512-Pbbs/fJ/2P/AN6f6/5UCH6WhW+HP3z9FtXpcuRgBI+WpT9dru9kYt/HiBeihmTPvvwmHMqKSCp0yodMqRJ2Zhw=="
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.14.4"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
@@ -185,6 +116,43 @@
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
@@ -193,124 +161,5 @@
"uuid": "dist/bin/uuid"
}
}
},
"dependencies": {
"@scrypted/common": {
"version": "file:../../common",
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"@types/node": "^16.9.0",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
}
},
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^18.11.9",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.3",
"webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@scrypted/server": {
"version": "file:../../server",
"requires": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.36",
"@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",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.2.0",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^6.0.1",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^3.4.7",
"mime": "^3.0.0",
"mkdirp": "^1.0.4",
"nan": "^2.17.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^8.4.1",
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
"router": "^1.3.7",
"semver": "^7.3.8",
"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"
}
},
"@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
},
"alexa-smarthome-ts": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/alexa-smarthome-ts/-/alexa-smarthome-ts-0.0.1.tgz",
"integrity": "sha512-Pbbs/fJ/2P/AN6f6/5UCH6WhW+HP3z9FtXpcuRgBI+WpT9dru9kYt/HiBeihmTPvvwmHMqKSCp0yodMqRJ2Zhw=="
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.1.0",
"version": "0.2.3",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -21,11 +21,12 @@
"amazon"
],
"scrypted": {
"name": "Alexa Plugin",
"name": "Alexa",
"type": "API",
"interfaces": [
"HttpRequestHandler",
"MixinProvider"
"MixinProvider",
"Settings"
],
"pluginDependencies": [
"@scrypted/cloud",
@@ -33,14 +34,11 @@
]
},
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/server": "file:../../server"
"@types/node": "^18.4.2",
"@scrypted/sdk": "../../sdk"
}
}

221
plugins/alexa/src/alexa.ts Normal file
View File

@@ -0,0 +1,221 @@
export declare type DisplayCategory = 'ACTIVITY_TRIGGER' | 'CAMERA' | 'CONTACT_SENSOR' | 'DOOR' | 'DOORBELL' | 'GARAGE_DOOR' | 'LIGHT' | 'MICROWAVE' | 'MOTION_SENSOR' | 'OTHER' | 'SCENE_TRIGGER' | 'SECURITY_PANEL' | 'SMARTLOCK' | 'SMARTPLUG' | 'SPEAKER' | 'SWITCH' | 'TEMPERATURE_SENSOR' | 'THERMOSTAT' | 'TV';
/*
COMMON DIRECTIVES AND RESPONSES
*/
export interface AddOrUpdateReport {
event: {
header: Header<"Alexa.Discovery", "AddOrUpdateReport">;
payload: AddOrUpdateReportPayload;
}
}
export interface DeleteReport {
event: {
header: Header<"Alexa.Discovery", "DeleteReport">;
payload: DeleteReportPayload;
}
}
export interface StateReport extends Report<"Alexa", "StateReport"> { }
export interface ChangeReport extends Report<"Alexa", "ChangeReport", ChangePayload> { }
export interface Response {
event: Event<"Alexa", "Response">;
context?: Context;
}
export interface DeferredResponse {
event: Event<"Alexa", "DeferredResponse", DeferredPayload>;
}
export interface ErrorResponse {
event: Event<"Alexa", "ErrorResponse", ErrorPayload>;
}
/*
DEVICE EVENTS
*/
export interface WebRTCAnswerGeneratedForSessionEvent extends Report<"Alexa.RTCSessionController", "AnswerGeneratedForSession", WebRTCAnswerGeneratedForSessionPayload> { }
export interface WebRTCSessionConnectedEvent extends Report<"Alexa.RTCSessionController", "SessionConnected", WebRTCSessionPayload> { }
export interface WebRTCSessionDisconnectedEvent extends Report<"Alexa.RTCSessionController", "SessionDisconnected", WebRTCSessionPayload> { }
export interface ObjectDetectionEvent extends Report<"Alexa.SmartVision.ObjectDetectionSensor", "ObjectDetection", ObjectDetectionPayload> { }
export interface DoorbellPressEvent extends Report<"Alexa.DoorbellEventSource", "DoorbellPress", DoorbellPressPayload> { }
/*
IMPLIMENTATION TYPES
*/
export interface Header<NS = string, N = string> {
namespace: NS;
name: N;
messageId: string;
correlationToken?: string;
payloadVersion: string;
}
export interface Scope {
type: string;
token: string;
partition?: string;
userId?: string;
}
export interface Endpoint {
endpointId: string;
scope?: Scope;
cookie?: any;
}
export interface Payload { }
export interface Directive<NS = string, N = string, P = Payload> {
header: Header<NS, N>;
endpoint: Endpoint;
payload: P;
}
export interface Event<NS = string, N = string, P = Payload> {
header: Header<NS, N>;
endpoint: Endpoint;
payload: P;
}
export interface Property {
namespace: string;
instance?: string;
name: string;
value: any;
timeOfSample: string;
uncertaintyInMilliseconds?: number;
}
export interface Context {
properties: Property[];
}
export interface Report<NS = string, N = string, P = Payload> {
event: Event<NS, N, P>;
context: Context;
}
export interface DeferredPayload {
estimatedDeferralInSeconds: number;
}
export interface ErrorPayload {
type: string;
message: string;
}
export interface ChangePayload {
change: {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION" | "RULE_TRIGGER";
},
properties: Property[];
}
}
export interface WebRTCSessionPayload {
sessionId: string;
}
export interface WebRTCAnswerGeneratedForSessionPayload {
answer: {
format: string;
value: string;
}
}
export interface ObjectDetectionPayloadEvent {
eventIdenifier: string;
imageNetClass: string;
timeOfSample: string;
uncertaintyInMilliseconds: number;
objectIdentifier: string;
frameImageUri: string;
croppedImageUri: string;
}
export interface ObjectDetectionPayload {
events: ObjectDetectionPayloadEvent[]
}
export interface DoorbellPressPayload {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION";
},
timestamp: string;
}
export interface DiscoveryProperty {
supported: any[];
proactivelyReported: boolean;
retrievable: boolean;
}
export interface DiscoveryCapability {
type: string;
interface: string;
instance?: string;
version: string;
properties?: DiscoveryProperty;
capabilityResources?: any;
configuration?: any;
semantics?: any;
}
export interface DiscoveryEndpoint {
endpointId: string;
manufacturerName: string;
description: string;
friendlyName: string;
displayCategories: DisplayCategory[];
additionalAttributes?: {
"manufacturer"?: string;
"model"?: string;
"serialNumber"?: string;
"firmwareVersion"? : string;
"softwareVersion"?: string;
"customIdentifier"?: string;
};
capabilities?: DiscoveryCapability[];
connections?: any[];
relationships?: any;
cookie?: any;
}
export interface DiscoverPayload {
endpoints: DiscoveryEndpoint[]
}
export interface Discovery {
event: {
header: Header<"Alexa.Discovery", "Discover.Response">;
payload: DiscoverPayload;
}
}
export interface AddOrUpdateReportPayload {
endpoints: DiscoveryEndpoint[]
scope: Scope;
}
export interface DeleteReportEndpoint {
endpointId: string;
}
export interface DeleteReportPayload {
endpoints: DeleteReportEndpoint[]
scope: Scope;
}

131
plugins/alexa/src/common.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Battery, Online, PowerSensor, ScryptedDevice, ScryptedInterface, HttpResponse } from "@scrypted/sdk";
import { v4 as createMessageId } from 'uuid';
export interface AlexaHttpResponse extends HttpResponse {
send(body: any, options?: any): void;
}
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
if (!device.interfaces.includes(ScryptedInterface.Online))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": device.online ? "OK" : "UNREACHABLE",
"reason": device.online ? undefined : "INTERNET_UNREACHABLE"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
if (!device.interfaces.includes(ScryptedInterface.Battery))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
const lowPower = device.batteryLevel < 20;
let health = undefined;
if (lowPower) {
health = {
"state": "WARNING",
"reasons": [
"LOW_CHARGE"
]
};
}
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "battery",
"value": {
health,
"levelPercentage": device.batteryLevel,
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function authErrorResponse(errorType: string, errorMessage: string, directive: any): any {
const { header } = directive;
const data = {
"event": {
header,
"payload": {
"type": errorType,
"message": errorMessage
}
}
};
data.event.header.name = "ErrorResponse";
data.event.header.messageId = createMessageId();
return data;
}
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-errorresponse.html#error-types
export function deviceErrorResponse (errorType: string, errorMessage: string, directive: any): any{
const { header, endpoint } = directive;
const data = {
"event": {
header,
endpoint,
"payload": {
"type": errorType,
"message": errorMessage
}
}
};
data.event.header.name = "ErrorResponse";
data.event.header.messageId = createMessageId();
return data;
}
export function mirroredResponse (directive: any): any {
const { header, endpoint, payload } = directive;
const data = {
"event": {
header,
endpoint,
payload
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
return data;
}
export function sendDeviceResponse(data: any, response: any, device: ScryptedDevice) {
data = addBattery(data, device);
data = addOnline(data, device);
response.send(data);
}

View File

@@ -0,0 +1,34 @@
import { HttpRequest, ScryptedDevice } from "@scrypted/sdk";
import { AlexaHttpResponse, sendDeviceResponse } from "./common";
import { supportedTypes } from "./types";
import { v4 as createMessageId } from 'uuid';
import { Directive, StateReport } from "./alexa";
export type AlexaHandler = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive) => Promise<void>
export type AlexaDeviceHandler<T> = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
export const alexaDeviceHandlers = new Map<string, AlexaDeviceHandler<any>>();
export const alexaHandlers = new Map<string, AlexaHandler>();
alexaDeviceHandlers.set('Alexa/ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const report = await supportedType.sendReport(device);
let data = {
"event": {
header,
endpoint,
payload
},
context: report?.context
} as StateReport;
data.event.header.name = "StateReport";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
});

View File

@@ -1,18 +1,21 @@
import axios from 'axios';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
import sdk, { HttpRequest, HttpRequestHandler, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, EventDetails, Setting, SettingValue, Settings, HttpResponseOptions, HttpResponse } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
import { isSupported } from './types';
import { DiscoveryEndpoint, DiscoverEvent } from 'alexa-smarthome-ts';
import { AlexaHandler, addBattery, addOnline, addPowerSensor, capabilityHandlers, supportedTypes } from './types/common';
import { createMessageId } from './message';
import { addBattery, addOnline, deviceErrorResponse, mirroredResponse, authErrorResponse, AlexaHttpResponse } from './common';
import { supportedTypes } from './types';
import { v4 as createMessageId } from 'uuid';
import { ChangeReport, Discovery, DiscoveryEndpoint } from './alexa';
import { alexaHandlers, alexaDeviceHandlers } from './handlers';
const { systemManager, deviceManager } = sdk;
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
const includeToken = 4;
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
export let DEBUG = false;
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
hide: true,
@@ -22,6 +25,10 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
multiple: true,
hide: true
},
defaultIncluded: {
hide: true,
json: true
},
apiEndpoint: {
title: 'Alexa Endpoint',
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
@@ -30,88 +37,169 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
});
handlers = new Map<string, AlexaHandler>();
accessToken: Promise<string>;
validAuths = new Set<string>();
devices = new Map<string, ScryptedDevice>();
constructor(nativeId?: string) {
super(nativeId);
this.handlers.set('Alexa.Authorization', this.alexaAuthorization);
this.handlers.set('Alexa.Discovery', this.alexaDiscovery);
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
this.syncDevices();
this.start();
}
systemManager.listen(async (eventSource, eventDetails, eventData) => {
if (!eventSource)
return;
async start() {
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
await this.tryEnableMixin(device);
}
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType) {
this.console.warn(`${eventSource.name} no longer supported type?`);
return;
systemManager.listen((async (eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) => {
const status = await this.tryEnableMixin(eventSource);
// sync new devices when added or removed
if (status === DeviceMixinStatus.Setup)
await this.syncEndpoints();
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
if (!this.devices.has(eventSource.id)) {
this.devices.set(eventSource.id, eventSource);
eventSource.listen(ScryptedInterface.ObjectDetector, this.deviceListen.bind(this));
}
this.deviceListen(eventSource, eventDetails, eventData);
}
}).bind(this));
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
let data = {
"event": {
"header": {
"messageId": createMessageId(),
"namespace": report?.namespace ?? "Alexa",
"name": report?.name ?? "ChangeReport",
"payloadVersion": "3"
},
"endpoint": {
"endpointId": eventSource.id,
"scope": undefined
},
"payload": report?.payload,
await this.syncEndpoints();
}
private async tryEnableMixin(device: ScryptedDevice): Promise<DeviceMixinStatus> {
if (!device)
return DeviceMixinStatus.NotSupported;
const mixins = (device.mixins || []).slice();
if (mixins.includes(this.id))
return DeviceMixinStatus.AlreadySetup;
const defaultIncluded = this.storageSettings.values.defaultIncluded || {};
if (defaultIncluded[device.id] === includeToken)
return DeviceMixinStatus.AlreadySetup;
if (!supportedTypes.has(device.type))
return DeviceMixinStatus.NotSupported;
mixins.push(this.id);
const plugins = await systemManager.getComponent('plugins');
await plugins.setMixins(device.id, mixins);
defaultIncluded[device.id] = includeToken;
this.storageSettings.values.defaultIncluded = defaultIncluded;
return DeviceMixinStatus.Setup;
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
const available = supportedTypes.has(type);
if (available)
return [];
return;
}
async getMixin(device: ScryptedDevice, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): Promise<any> {
return device;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
const device = systemManager.getDeviceById(id);
const mixins = (device.mixins || []).slice();
if (mixins.includes(this.id))
return;
this.log.i(`Device removed from Alexa: ${device.name}. Requesting sync.`);
await this.syncEndpoints();
}
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
if (!eventSource)
return;
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice)
return;
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType)
return;
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
let data = {
"event": {
"header": {
"messageId": createMessageId(),
"namespace": report?.event?.header?.namespace ?? "Alexa",
"name": report?.event?.header?.name ?? "ChangeReport",
"payloadVersion": "3"
},
"context": report?.context
}
"endpoint": {
"endpointId": eventSource.id,
},
payload: report?.event?.payload
},
context: report?.context
} as ChangeReport;
data = addOnline(data, eventSource);
data = addBattery(data, eventSource);
data = addPowerSensor(data, eventSource);
data = addOnline(data, eventSource);
data = addBattery(data, eventSource);
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
const accessToken = await this.getAccessToken();
data.event.endpoint.scope = {
"type": "BearerToken",
"token": accessToken,
};
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
data = await this.addAccessToken(data);
await this.postEvent(data);
});
await this.postEvent(data);
}
private async addAccessToken(data: any) : Promise<any> {
const accessToken = await this.getAccessToken();
if (data.event === undefined)
data.event = {};
if (data.event.endpoint === undefined)
data.event.endpoint = [];
data.event.endpoint.scope = {
"type": "BearerToken",
"token": accessToken,
};
return data;
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
return mixinDevice;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
const device = systemManager.getDeviceById(id);
if (device.mixins?.includes(this.id)) {
return;
}
this.console.log('release mixin', id);
this.log.a(`${device.name} was removed. The Alexa plugin will reload momentarily.`);
deviceManager.requestRestart();
}
readonly endpoints: string[] = [
'api.amazonalexa.com',
'api.eu.amazonalexa.com',
@@ -146,6 +234,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
const endpoint = await this.getAlexaEndpoint();
const self = this;
this.console.assert(!DEBUG, `event:`, data);
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
'Authorization': 'Bearer ' + accessToken,
@@ -160,25 +250,59 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
});
}
async syncDevices() {
const endpoints = await this.addOrUpdateReport();
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
const endpoints: DiscoveryEndpoint[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
if (!device.mixins?.includes(this.id))
continue;
const endpoint = await this.getEndpointForDevice(device);
if (endpoint)
endpoints.push(endpoint);
}
return endpoints;
}
async onDiscoverEndpoints(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
const endpoints = await this.getEndpoints();
const data = {
"event": {
"header": {
"namespace": 'Alexa.Discovery',
"name": 'Discover.Response',
"payloadVersion": '3',
"messageId": createMessageId()
},
"payload": {
endpoints
}
}
} as Discovery;
response.send(data);
await this.saveEndpoints(endpoints);
}
async addOrUpdateReport() {
const endpoints = this.getDiscoveryEndpoints();
async syncEndpoints() {
const endpoints = await this.getEndpoints();
if (!endpoints.length)
return [];
return [];
const accessToken = await this.getAccessToken();
await this.postEvent({
const data = {
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "AddOrUpdateReport",
"payloadVersion": "3",
"messageId": createMessageId(),
"messageId": createMessageId()
},
"payload": {
endpoints,
@@ -188,12 +312,35 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
}
}
});
};
return endpoints;
await this.postEvent(data);
await this.saveEndpoints(endpoints);
}
async deleteReport(...ids: string[]) {
async saveEndpoints(endpoints: DiscoveryEndpoint[]) {
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
const deleted = new Set(existingEndpoints);
for (const id of newEndpoints) {
deleted.delete(id);
}
const all = new Set([...existingEndpoints, ...newEndpoints]);
// save all the endpoints
this.storageSettings.values.syncedDevices = [...all];
// delete leftover endpoints
await this.deleteEndpoints(...deleted);
// prune if the delete report completed successfully
this.storageSettings.values.syncedDevices = newEndpoints;
}
async deleteEndpoints(...ids: string[]) {
if (!ids.length)
return;
@@ -219,17 +366,6 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
})
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
const discovery = isSupported({
type,
interfaces,
} as any);
if (!discovery)
return;
return [];
}
getAccessToken(): Promise<string> {
if (this.accessToken)
return this.accessToken;
@@ -306,9 +442,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
return this.accessToken;
}
async alexaAuthorization(request: HttpRequest, response: HttpResponse) {
const json = JSON.parse(request.body);
const { grant } = json.directive.payload;
async onAlexaAuthorization(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
const { grant } = directive.payload;
this.storageSettings.values.tokenInfo = grant;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
@@ -321,27 +456,14 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
response.send(JSON.stringify({
"event": {
"header": {
"namespace": "Alexa.Authorization",
"name": "ErrorResponse",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
"type": "ACCEPT_GRANT_FAILED",
"message": `Failed to handle the AcceptGrant directive because ${reason}`
}
}
}));
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
return undefined;
});
if (accessToken !== undefined) {
try {
response.send(JSON.stringify({
response.send({
"event": {
"header": {
"namespace": "Alexa.Authorization",
@@ -351,7 +473,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
},
"payload": {}
}
}));
});
} catch (error) {
this.console.error(`AcceptGrant.Response failed because ${error}`);
@@ -363,14 +485,15 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
}
createEndpoint(device: ScryptedDevice): DiscoveryEndpoint<any> {
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
if (!device)
return;
const discovery = isSupported(device);
const discovery = await supportedTypes.get(device.type)?.discover(device);
if (!discovery)
return;
const ret = Object.assign({
const data: DiscoveryEndpoint = {
endpointId: device.id,
manufacturerName: "Scrypted",
description: `${device.info?.manufacturer ?? 'Unknown'} ${device.info?.model ?? `device of type ${device.type}`}, connected via Scrypted`,
@@ -380,13 +503,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
model: device.info?.model || undefined,
serialNumber: device.info?.serialNumber || undefined,
firmwareVersion: device.info?.firmware || undefined,
//softwareVersion: device.info?.version || undefined
}
}, discovery);
softwareVersion: device.info?.version || undefined
},
displayCategories: discovery.displayCategories,
capabilities: discovery.capabilities
};
let supportedEndpointHealths: any[] = [];
if (device.interfaces.includes(ScryptedInterface.Online)) {
supportedEndpointHealths.push({
"name": "connectivity"
});
}
let supportedEndpointHealths = [{
"name": "connectivity"
}];
// {
// "name": "radioDiagnostics"
// },
@@ -400,17 +530,22 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
})
}
ret.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.2" as any,
"properties": {
"supported": supportedEndpointHealths,
"proactivelyReported": true,
"retrievable": true
if (supportedEndpointHealths.length > 0) {
data.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.2",
"properties": {
"supported": supportedEndpointHealths,
"proactivelyReported": true,
"retrievable": true
}
}
},
);
}
data.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa",
@@ -418,76 +553,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
);
//if (device.info?.mac !== undefined)
// ret.connections.push(
// {
// "type": "TCP_IP",
// "macAddress": device.info?.mac || undefined
// }
// );
return ret as any;
}
async saveEndpoints(endpoints: DiscoveryEndpoint<any>[]) {
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
const deleted = new Set(existingEndpoints);
for (const id of newEndpoints) {
deleted.delete(id);
}
const all = new Set([...existingEndpoints, ...newEndpoints]);
// save all the endpoints
this.storageSettings.values.syncedDevices = [...all];
// delete leftover endpoints
await this.deleteReport(...deleted);
// prune if the delete report completed successfully
this.storageSettings.values.syncedDevices = newEndpoints;
}
getDiscoveryEndpoints() {
const endpoints: DiscoveryEndpoint<any>[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
if (!device.mixins?.includes(this.id))
continue;
const endpoint = this.createEndpoint(device);
if (endpoint)
endpoints.push(endpoint);
}
return endpoints;
}
async alexaDiscovery(request: HttpRequest, response: HttpResponse) {
const endpoints = this.getDiscoveryEndpoints();
const ret: DiscoverEvent<any> = {
event: {
header: {
namespace: 'Alexa.Discovery',
name: 'Discover.Response',
messageId: createMessageId(),
payloadVersion: '3',
},
payload: {
endpoints,
if (device.info?.mac !== undefined)
data.connections = [
{
"type": "TCP_IP",
"macAddress": device.info.mac
}
}
}
];
response.send(JSON.stringify(ret));
this.saveEndpoints(endpoints);
return data as any;
}
async onRequest(request: HttpRequest, response: HttpResponse) {
async onRequest(request: HttpRequest, rawResponse: HttpResponse) {
const response = new HttpResponseLoggingImpl(rawResponse, this.console);
const { authorization } = request.headers;
if (!this.validAuths.has(authorization)) {
try {
@@ -501,42 +580,81 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
catch (e) {
this.console.error(`request failed due to invalid authorization`, e);
response.send(e.message, {
code: 500
code: 500,
});
return;
}
}
try {
const body = JSON.parse(request.body);
const { directive } = body;
const { namespace } = directive.header;
const handler = this.handlers.get(namespace);
if (handler)
return handler.apply(this, arguments);
const body = JSON.parse(request.body);
const { directive } = body;
const { namespace, name } = directive.header;
const capHandler = capabilityHandlers.get(namespace);
if (capHandler) {
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
if (!device) {
response.send('Not Found', {
code: 404,
});
return;
}
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
return capHandler.apply(this, [request, response, directive, device]);
const mapName = `${namespace}/${name}`;
const handler = alexaHandlers.get(mapName);
if (handler)
return handler.apply(this, [request, response, directive]);
const deviceHandler = alexaDeviceHandlers.get(mapName);
if (deviceHandler) {
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
if (!device) {
response.send(deviceErrorResponse("NO_SUCH_ENDPOINT", "The device doesn't exist in Scrypted", directive));
return;
}
response.send('Not Found', {
code: 404,
});
}
catch (e) {
response.send(e.message, {
code: 500,
});
return deviceHandler.apply(this, [request, response, directive, device]);
} else {
this.console.error(`no handler for: ${mapName}`);
}
// it is better to send a non-specific response than an error, as the API might get rate throttled
response.send(mirroredResponse(directive));
}
}
enum DeviceMixinStatus {
NotSupported = 0,
Setup = 1,
AlreadySetup = 2
}
class HttpResponseLoggingImpl implements AlexaHttpResponse {
constructor(private response: HttpResponse, private console: Console) {
}
send(body: string): void;
send(body: string, options: HttpResponseOptions): void;
send(body: Buffer): void;
send(body: Buffer, options: HttpResponseOptions): void;
send(body: any, options?: any): void {
if (!options)
options = {};
if (!options.code)
options.code = 200;
if (options.code !== 200)
this.console.error(`response error ${options.code}:`, body);
else
this.console.assert(!DEBUG, `response ${options.code}:`, body);
if (typeof body === 'object')
body = JSON.stringify(body);
this.response.send(body, options);
}
sendFile(path: string): void;
sendFile(path: string, options: HttpResponseOptions): void;
sendFile(path: any, options?: any): void {
this.response.sendFile(path, options);
}
sendSocket(socket: any, options: HttpResponseOptions): void {
this.response.sendSocket(socket, options);
}
}

View File

@@ -1,5 +0,0 @@
import {v4 as uuidv4} from 'uuid';
export function createMessageId() {
return uuidv4();
}

View File

@@ -1,190 +1,24 @@
import { HttpResponse, MotionSensor, RTCAVSignalingSetup, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VideoCamera } from "@scrypted/sdk";
import { addSupportedType, AlexaCapabilityHandler, capabilityHandlers, EventReport, StateReport } from "./common";
import { createMessageId } from "../message";
import { Capability } from "alexa-smarthome-ts/lib/skill/Capability";
import { DisplayCategory } from "alexa-smarthome-ts";
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, Report } from "../alexa";
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
import { supportedTypes } from ".";
export function getCameraCapabilities(device: ScryptedDevice): Capability<any>[] {
const capabilities: Capability<any>[] = [
{
"type": "AlexaInterface",
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
}
} as any,
];
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
)
}
return capabilities;
}
addSupportedType(ScryptedDeviceType.Camera, {
probe(device) {
supportedTypes.set(ScryptedDeviceType.Camera, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel))
return;
const capabilities = getCameraCapabilities(device);
const capabilities = await getCameraCapabilities(device);
return {
displayCategories: ['CAMERA'],
capabilities
}
},
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport> {
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
};
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
return reportCameraState(device);
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor, eventDetails, eventData): Promise<EventReport> {
if (eventDetails.eventInterface !== ScryptedInterface.MotionSensor)
return undefined;
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
},
};
sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
return sendCameraEvent(eventSource, eventDetails, eventData);
}
});
export const rtcHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: HttpResponse, public directive: any) {
}
async getOptions(): Promise<RTCSignalingOptions> {
return {
proxy: true,
offer: {
type: 'offer',
sdp: this.directive.payload.offer.value,
},
disableTrickle: true,
// this could be a low resolution screen, no way of knowing, so never send a
// 1080p+ stream.
screen: {
devicePixelRatio: 1, // TODO: get this from the device
width: 1280,
height: 720,
}
}
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
return {
type: 'offer',
sdp: this.directive.payload.offer.value,
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
throw new Error("Alexa does not support trickle ICE");
}
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
this.response.send(JSON.stringify({
"event": {
"header": {
"namespace": "Alexa.RTCSessionController",
"name": "AnswerGeneratedForSession",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
"answer": {
"format": "SDP",
"value": description.sdp,
}
}
}
}));
}
}
rtcHandlers.set('InitiateSessionWithOffer', async (request, response, directive: any,
device: ScryptedDevice & RTCSignalingChannel) => {
const session = new AlexaSignalingSession(response, directive);
const control = await device.startRTCSignalingSession(session);
control.setPlayback({
audio: true,
video: false,
})
});
capabilityHandlers.set('Alexa.RTCSessionController', async (request, response, directive: any, device: ScryptedDevice & VideoCamera) => {
const { name } = directive.header;
const handler = rtcHandlers.get(name);
if (handler)
return handler.apply(this, [request, response, directive, device]);
const { sessionId } = directive.payload;
const body = {
"event": {
"header": {
"namespace": "Alexa.RTCSessionController",
name,
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
sessionId,
}
}
};
response.send(JSON.stringify(body));
});

View File

@@ -0,0 +1,194 @@
import sdk, { MediaObject, MotionSensor, ObjectDetector, ScryptedDevice, ScryptedInterface } from "@scrypted/sdk";
import { ChangeReport, DiscoveryCapability, ObjectDetectionEvent, Report, StateReport, Property } from "../../alexa";
const { mediaManager } = sdk;
export async function reportCameraState(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
data.context.properties.push({
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": classNames.map(type => ({
"imageNetClass": type
})),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
data.context.properties.push({
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
};
export async function sendCameraEvent (eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.ObjectDetector) {
// ring and motion are not valid objects
if (eventData.detections.has('ring') || eventData.detections.has('motion'))
return undefined;
console.debug('ObjectDetector event', eventData);
let mediaObj: MediaObject = undefined;
let frameImageUri: string = undefined;
try {
mediaObj = await eventSource.getDetectionInput(eventData.detectionId, eventData.eventId);
frameImageUri = await mediaManager.convertMediaObjectToUrl(mediaObj, 'image/jpeg');
} catch (e) { }
let data = {
event: {
header: {
namespace: 'Alexa.SmartVision.ObjectDetectionSensor',
name: 'ObjectDetection'
},
payload: {
"events": [eventData.detections.map(detection => {
let event = {
"eventIdentifier": eventData.eventId,
"imageNetClass": detection.className,
"timeOfSample": new Date(eventData.timestamp).toISOString(),
"uncertaintyInMilliseconds": 500
};
if (detection.id) {
event["objectIdentifier"] = detection.id;
}
if (frameImageUri) {
event["frameImageUri"] = frameImageUri;
}
return event;
})]
}
}
} as Partial<ObjectDetectionEvent>;
return data;
}
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
},
}
} as Partial<ChangeReport>;
return undefined;
};
export async function getCameraCapabilities(device: ScryptedDevice): Promise<DiscoveryCapability[]> {
const capabilities = [
{
"type": "AlexaInterface",
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
}
} as DiscoveryCapability
];
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.SmartVision.ObjectDetectionSensor",
"version": "1.0",
"properties": {
"supported": [{
"name": "objectDetectionClasses"
}],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"objectDetectionConfiguration": classNames.map(type => ({
"imageNetClass": type
}))
}
} as DiscoveryCapability
);
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DataController",
"instance": "Camera.SmartVisionData",
"version": "1.0",
"properties": undefined,
"configuration": {
"targetCapability": {
"name": "Alexa.SmartVision.ObjectDetectionSensor",
"version": "1.0"
},
"dataRetrievalSchema": {
"type": "JSON",
"schema": "SmartVisionData"
},
"supportedAccess": ["BY_IDENTIFIER", "BY_TIMESTAMP_RANGE"]
}
} as DiscoveryCapability
);
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
}
return capabilities;
};

View File

@@ -0,0 +1,159 @@
import { ObjectDetector, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { v4 as createMessageId } from 'uuid';
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: AlexaHttpResponse, public directive: any) {
}
async getOptions(): Promise<RTCSignalingOptions> {
return {
proxy: true,
offer: {
type: 'offer',
sdp: this.directive.payload.offer.value,
},
disableTrickle: true,
disableTurn: true,
// this could be a low resolution screen, no way of knowning, so never send a 1080p stream
screen: {
devicePixelRatio: 1,
width: 1280,
height: 720
}
}
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
return {
type: type,
sdp: this.directive.payload.offer.value,
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
throw new Error("Alexa does not support trickle ICE");
}
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
const { header, endpoint, payload } = this.directive;
const data: WebRTCAnswerGeneratedForSessionEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.name = "AnswerGeneratedForSession";
data.event.header.messageId = createMessageId();
data.event.payload.answer = {
format: 'SDP',
value: description.sdp,
};
this.response.send(data);
}
}
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,
})
sessionCache.set(sessionId, control);
});
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (request, response, directive: any, device: ScryptedDevice) => {
const { header, endpoint, payload } = directive;
const data: WebRTCSessionConnectedEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
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,
endpoint,
payload
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetectionClasses', async (request, response, directive: any, device: ScryptedDevice & ObjectDetector) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const detectionTypes = await device.getObjectTypes();
const data: Response = {
"event": {
header,
endpoint,
payload: {}
},
"context": {
"properties": [{
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": detectionTypes.classes.map(type => ({
"imageNetClass": type
})),
timeOfSample: new Date().toISOString(),
uncertaintyInMilliseconds: 0
}]
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
});

View File

@@ -1,180 +0,0 @@
import { Battery, EventDetails, HttpRequest, HttpResponse, Online, PowerSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import {DiscoveryEndpoint, Directive} from 'alexa-smarthome-ts';
import { createMessageId } from "../message";
export type AlexaHandler = (request: HttpRequest, response: HttpResponse, directive: Directive) => Promise<void>
export type AlexaCapabilityHandler<T> = (request: HttpRequest, response: HttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
export const capabilityHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export const alexaHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export interface EventReport {
type: 'event';
payload?: any;
context?: any;
namespace?: string;
name?: string;
}
export interface StateReport {
type: 'state';
payload?: any;
context?: any;
namespace?: string;
name?: string;
}
export interface SupportedType {
probe(device: ScryptedDevice): Partial<DiscoveryEndpoint<any>>;
sendEvent(eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<EventReport>;
reportState(device: ScryptedDevice): Promise<StateReport>;
}
export function addSupportedType(type: ScryptedDeviceType, supportedType: SupportedType) {
supportedTypes.set(type, supportedType);
}
export function isSupported(device: ScryptedDevice) {
return supportedTypes.get(device.type)?.probe(device);
}
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
if (!device.interfaces.includes(ScryptedInterface.Online))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": device.online ? "OK" : "UNREACHABLE",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addPowerSensor(data: any, device: ScryptedDevice & PowerSensor) : any {
if (!device.interfaces.includes(ScryptedInterface.PowerSensor))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": device.powerDetected ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
if (!device.interfaces.includes(ScryptedInterface.Battery))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
const lowPower = device.batteryLevel < 20;
let health = undefined;
if (lowPower) {
health = {
"state": "WARNING",
"reasons": [
"LOW_CHARGE"
]
};
}
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "battery",
"value": {
health,
"levelPercentage": device.batteryLevel,
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
function sendResponse(data: any, response: any, device: ScryptedDevice) {
data = addBattery(data, device);
data = addOnline(data, device);
data = addPowerSensor(data, device);
response.send(JSON.stringify(data));
}
alexaHandlers.set('ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint } = directive;
const report = await supportedType.reportState(device);
if (report.type === 'state') {
const data = {
"event": {
header,
endpoint,
payload: report.payload,
},
"context": report.context
};
data.event.header.name = "StateReport";
data.event.header.messageId = createMessageId();
sendResponse(data, response, device);
}
});
capabilityHandlers.set('Alexa', async (request, response, directive: any, device: ScryptedDevice) => {
const { name } = directive.header;
let handler = alexaHandlers.get(name);
if (handler)
return handler.apply(this, [request, response, directive, device]);
const { header, endpoint, payload } = directive;
const data = {
"event": {
header,
endpoint,
payload
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendResponse(data, response, device);
});

View File

@@ -1,81 +1,57 @@
import { BinarySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities } from "./camera";
import { addSupportedType, EventReport, StateReport } from "./common";
import { DisplayCategory } from "alexa-smarthome-ts";
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
import { DiscoveryEndpoint, DisplayCategory, Report, DoorbellPressEvent } from "../alexa";
import { supportedTypes } from ".";
addSupportedType(ScryptedDeviceType.Doorbell, {
probe(device) {
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel) || !device.interfaces.includes(ScryptedInterface.BinarySensor))
return;
supportedTypes.set(ScryptedDeviceType.Doorbell, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
let capabilities: any[] = [];
let category: DisplayCategory = 'DOORBELL';
const capabilities = getCameraCapabilities(device);
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
} as any,
);
return {
displayCategories: ['CAMERA'],
capabilities
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
capabilities = await getCameraCapabilities(device);
category = 'CAMERA';
}
},
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport>{
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
} as any,
);
}
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
displayCategories: [category],
capabilities
};
},
async sendEvent(eventSource: ScryptedDevice, eventDetails, eventData): Promise<EventReport> {
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
},
};
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
return reportCameraState(device);
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
let response = await sendCameraEvent(eventSource, eventDetails, eventData);
if (response)
return response;
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
return {
type: 'event',
namespace: 'Alexa.DoorbellEventSource',
name: 'DoorbellPress',
payload: {
"cause": {
"type": "PHYSICAL_INTERACTION"
event: {
header: {
namespace: 'Alexa.DoorbellEventSource',
name: 'DoorbellPress'
},
"timestamp": new Date().toISOString(),
}
};
payload: {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"timestamp": new Date(eventDetails.eventTime).toISOString(),
}
}
} as Partial<DoorbellPressEvent>;
}
});

View File

@@ -1,14 +1,13 @@
import { BinarySensor, Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities } from "./camera";
import { addSupportedType, EventReport, StateReport } from "./common";
import { DisplayCategory } from "alexa-smarthome-ts";
import { Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report } from "../alexa";
import { supportedTypes } from ".";
addSupportedType(ScryptedDeviceType.Garage, {
probe(device) {
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
supportedTypes.set(ScryptedDeviceType.Garage, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
return;
const capabilities = getCameraCapabilities(device);
const capabilities: DiscoveryCapability[] = [];
capabilities.push(
{
"type": "AlexaInterface",
@@ -115,19 +114,16 @@ addSupportedType(ScryptedDeviceType.Garage, {
}
]
}
} as any,
},
);
return {
displayCategories: ['GARAGE_DOOR'] as any,
displayCategories: ['GARAGE_DOOR'],
capabilities
}
},
async reportState(eventSource: ScryptedDevice & EntrySensor): Promise<StateReport> {
async sendReport(eventSource: ScryptedDevice & EntrySensor): Promise<Partial<Report>> {
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
@@ -142,14 +138,12 @@ addSupportedType(ScryptedDeviceType.Garage, {
}
};
},
async sendEvent(eventSource: ScryptedDevice & EntrySensor, eventDetails, eventData): Promise<EventReport> {
async sendEvent(eventSource: ScryptedDevice & Entry & EntrySensor, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.EntrySensor)
return undefined;
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
event: {
payload: {
change: {
cause: {
@@ -167,6 +161,30 @@ addSupportedType(ScryptedDeviceType.Garage, {
]
}
},
};
}
}
} as Partial<ChangeReport>;
},
async setState(eventSource: ScryptedDevice & Entry & EntrySensor, payload: any): Promise<Partial<Report>> {
if (payload.mode === 'Position.Up') {
await eventSource.openEntry();
}
else if (payload.mode === 'Position.Down') {
await eventSource.closeEntry();
}
return {
context: {
"properties": [
{
"namespace": "Alexa.ModeController",
"instance": "GarageDoor.Position",
"name": "mode",
"value": payload.mode,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
};
}
});

View File

@@ -0,0 +1,31 @@
import { ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { v4 as createMessageId } from 'uuid';
import { Response } from "../../alexa";
async function sendResponse (request, response, directive: any, device: ScryptedDevice) {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const report = await supportedType.setState(device, payload);
const data = {
"event": {
header,
endpoint,
payload
},
context: report?.context
} as Response;
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.ModeController/SetMode', sendResponse);
alexaDeviceHandlers.set('Alexa.ModeController/AdjustMode', sendResponse);

View File

@@ -1,6 +1,21 @@
import { ScryptedDeviceType, ScryptedDevice, EventDetails } from '@scrypted/sdk';
import { DiscoveryEndpoint, Report } from '../alexa';
export interface SupportedType {
discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>>;
sendEvent(device: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<Partial<Report>>;
sendReport(device: ScryptedDevice): Promise<Partial<Report>>;
setState?(device: ScryptedDevice, payload: any): Promise<Partial<Report>>;
}
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
import '../handlers';
import './camera';
import './camera/handlers';
import './doorbell';
import './garagedoor';
export { isSupported} from './common';
import './switch';
import './switch/handlers';
import './sensor';
import './securitysystem';

View File

@@ -0,0 +1,179 @@
import { EventDetails, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SecuritySystem, SecuritySystemMode } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
import { supportedTypes } from ".";
function getArmState(mode: SecuritySystemMode): string {
switch(mode) {
case SecuritySystemMode.AwayArmed:
return 'ARMED_AWAY';
case SecuritySystemMode.HomeArmed:
return 'ARMED_STAY';
case SecuritySystemMode.NightArmed:
return 'ARMED_NIGHT';
case SecuritySystemMode.Disarmed:
return 'DISARMED';
}
}
supportedTypes.set(ScryptedDeviceType.SecuritySystem, {
async discover(device: ScryptedDevice & SecuritySystem): Promise<Partial<DiscoveryEndpoint>> {
const capabilities: DiscoveryCapability[] = [];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.SecuritySystem)) {
const supportedModes = device.securitySystemState.supportedModes;
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.SecurityPanelController",
"version": "3",
"properties": {
"supported": [
{
"name": "armState"
},
{
"name": "burglaryAlarm"
},
//{
// "name": "waterAlarm"
//},
//{
// "name": "fireAlarm"
//},
//{
// "name": "carbonMonoxideAlarm"
//}
],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"supportedArmStates": supportedModes.map(mode => {
return {
"value": getArmState(mode)
}
}),
"supportedAuthorizationTypes": [
{
"type": "FOUR_DIGIT_PIN"
}
]
}
} as DiscoveryCapability
);
displayCategories.push('SECURITY_PANEL');
}
if (capabilities.length === 0)
return;
return {
displayCategories,
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & SecuritySystem): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.SecuritySystem)) {
data.context.properties.push({
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventSource.securitySystemState.mode),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property);
data.context.properties.push({
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property);
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & SecuritySystem, eventDetails: EventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "mode") {
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventData),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
},
context: {
properties: [{
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property]
}
} as Partial<ChangeReport>;
}
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "triggered") {
return {
event: {
payload: {
change: {
cause: {
type: "RULE_TRIGGER"
},
properties: [
{
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventData ? "ALARM" : "OK"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
},
context: {
properties: [{
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventSource.securitySystemState.mode),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property]
}
} as Partial<ChangeReport>;
}
return undefined;
}
});

View File

@@ -0,0 +1,196 @@
import { EntrySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Thermometer } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Sensor, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
const capabilities: DiscoveryCapability[] = [];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.Thermometer)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.TemperatureSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "temperature"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('TEMPERATURE_SENSOR');
}
if (device.interfaces.includes(ScryptedInterface.EntrySensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.ContactSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('CONTACT_SENSOR');
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('MOTION_SENSOR');
}
if (capabilities.length === 0)
return;
return {
displayCategories: displayCategories,
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.Thermometer)) {
data.context.properties.push({
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": eventSource.temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.EntrySensor)) {
data.context.properties.push({
"namespace": "Alexa.ContactSensor",
"name": "detectionState",
"value": eventSource.entryOpen ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.MotionSensor)) {
data.context.properties.push({
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventSource.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface === ScryptedInterface.EntrySensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.ContactSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface === ScryptedInterface.Thermometer)
return {
event: {
payload: {
change: {
cause: {
type: "PERIODIC_POLL"
},
properties: [
{
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": eventSource.temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
return undefined;
}
});

View File

@@ -0,0 +1,71 @@
import { OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, ChangeReport, Report, Property, ChangePayload, DiscoveryCapability } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Switch, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.OnOff))
return;
const capabilities: DiscoveryCapability[] = [];
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
});
return {
displayCategories: ['SWITCH'],
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & OnOff): Promise<Partial<Report>> {
return {
context: {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventSource.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
};
},
async sendEvent(eventSource: ScryptedDevice & OnOff, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.OnOff)
return undefined;
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventData ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
}
});

View File

@@ -0,0 +1,55 @@
import { OnOff, ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { sendDeviceResponse } from "../../common";
import { v4 as createMessageId } from 'uuid';
import { alexaDeviceHandlers } from "../../handlers";
import { Directive, Response } from "../../alexa";
function commonResponse(header, endpoint, payload, response, device: ScryptedDevice & OnOff) {
const data : Response = {
"event": {
header,
endpoint,
payload
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": device.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
};
data.event.header.namespace = "Alexa";
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.PowerController/TurnOn', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.turnOn();
commonResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.PowerController/TurnOff', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.turnOff();
commonResponse(header, endpoint, payload, response, device);
});

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.63",
"version": "0.2.78",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",

View File

@@ -27,7 +27,7 @@ from .request import Request
from .mqtt_stream_async import MQTTStream
from .sse_stream_async import EventStream
from .logging import logger
# Import all of the other stuff.
from datetime import datetime
@@ -227,7 +227,7 @@ class Arlo(object):
when subsequent calls to /notify are made.
"""
async def heartbeat(self, basestations, interval=30):
while self.event_stream and self.event_stream.connected:
while self.event_stream and self.event_stream.active:
for basestation in basestations:
try:
self.Ping(basestation)
@@ -378,7 +378,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
)
def SubscribeToBatteryEvents(self, basestation, camera, callback):
"""
@@ -403,7 +405,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'batteryLevel')], callbackwrapper)
)
def SubscribeToDoorbellEvents(self, basestation, doorbell, callback):
"""
@@ -437,7 +441,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'buttonPressed')], callbackwrapper)
)
def SubscribeToSDPAnswers(self, basestation, camera, callback):
"""
@@ -456,14 +462,16 @@ class Arlo(object):
def callbackwrapper(self, event):
properties = event.get("properties", {})
stop = None
stop = None
if properties.get("type") == "answerSdp":
stop = callback(properties.get("data"))
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
)
def SubscribeToCandidateAnswers(self, basestation, camera, callback):
"""
@@ -482,14 +490,16 @@ class Arlo(object):
def callbackwrapper(self, event):
properties = event.get("properties", {})
stop = None
stop = None
if properties.get("type") == "answerCandidate":
stop = callback(properties.get("data"))
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
)
async def HandleEvents(self, basestation, resource, actions, callback):
"""
@@ -502,9 +512,17 @@ class Arlo(object):
await self.Subscribe()
async def loop_action_listener(action):
# in this function, action can either be a tuple or a string
# if it is a tuple, we expect there to be a property key in the tuple
property = None
if isinstance(action, tuple):
action, property = action
if not isinstance(action, str):
raise Exception('Actions must be either a tuple or a str')
seen_events = {}
while self.event_stream.active:
event, _ = await self.event_stream.get(resource, [action], seen_events)
event, _ = await self.event_stream.get(resource, action, property, seen_events)
if event is None or self.event_stream is None \
or self.event_stream.event_stream_stop_event.is_set():
@@ -514,7 +532,7 @@ class Arlo(object):
response = callback(self, event.item)
# always requeue so other listeners can see the event too
self.event_stream.requeue(event, resource, action)
self.event_stream.requeue(event, resource, action, property)
if response is not None:
return response
@@ -606,7 +624,13 @@ class Arlo(object):
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
return None
return await self.TriggerAndHandleEvent(basestation, resource, ["is"], trigger, callback)
return await self.TriggerAndHandleEvent(
basestation,
resource,
[("is", "activityState")],
trigger,
callback,
)
def StartPushToTalk(self, basestation, camera):
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
@@ -644,8 +668,6 @@ class Arlo(object):
async def TriggerFullFrameSnapshot(self, basestation, camera):
"""
This function causes the camera to record a fullframe snapshot.
The presignedFullFrameSnapshotUrl url is returned.
Use DownloadSnapshot() to download the actual image file.
"""
resource = f"cameras/{camera.get('deviceId')}"
@@ -676,4 +698,14 @@ class Arlo(object):
return url
return None
return await self.TriggerAndHandleEvent(basestation, resource, ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"], trigger, callback)
return await self.TriggerAndHandleEvent(
basestation,
resource,
[
(action, property)
for action in ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"]
for property in ["presignedFullFrameSnapshotUrl", "presignedLastImageUrl"]
],
trigger,
callback,
)

View File

@@ -28,7 +28,7 @@ from .logging import logger
class Stream:
"""This class provides a queue-based EventStream object."""
def __init__(self, arlo, expire=10):
def __init__(self, arlo, expire=5):
self.event_stream = None
self.initializing = True
self.connected = False
@@ -43,7 +43,7 @@ class Stream:
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()
@@ -83,11 +83,16 @@ class Stream:
self.refresh_loop_signal.put_nowait(object())
async def _clean_queues(self):
interval = self.expire * 2
interval = self.expire * 4
await asyncio.sleep(interval)
while not self.event_stream_stop_event.is_set():
for key, q in self.queues.items():
# 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
@@ -114,81 +119,47 @@ class Stream:
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, actions, skip_uuids={}):
if len(actions) == 1:
action = actions[0]
async def get(self, resource, action, property=None, skip_uuids={}):
if not property:
key = f"{resource}/{action}"
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
else:
while True:
for action in actions:
key = f"{resource}/{action}"
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
if q.empty():
continue
first_requeued = None
while True:
event = await q.get()
q.task_done()
first_requeued = None
while not q.empty():
event = q.get_nowait()
q.task_done()
if not event:
# exit signal received
return None, action
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 go to the next queue
q.put_nowait(event)
break
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
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()
@@ -203,15 +174,31 @@ class Stream:
resource = response.get('resource')
action = response.get('action')
key = f"{resource}/{action}"
now = time.time()
event = StreamEvent(response, now, now + self.expire)
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
now = time.time()
q.put_nowait(StreamEvent(response, now, now + self.expire))
q.put_nowait(event)
def requeue(self, event, resource, action):
key = f"{resource}/{action}"
# for optimized lookups, notify listeners of individual properties
properties = response.get('properties', {})
for property in properties.keys():
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
q.put_nowait(event)
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):

View File

@@ -1,26 +1,28 @@
{
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.13",
"dependencies": {
"@eneris/push-receiver": "../../external/push-receiver",
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
}
@@ -40,39 +42,9 @@
"@types/node": "^16.9.0"
}
},
"../../external/push-receiver": {
"name": "@eneris/push-receiver",
"version": "3.0.2",
"license": "MIT",
"dependencies": {
"axios": "^0.27.1",
"http_ece": "^1.0.5",
"long": "^5.2.0",
"protobufjs": "^6.11.2",
"request-promise": "^4.2.6"
},
"devDependencies": {
"@types/jest": "^28.1.0",
"@types/long": "^4.0.1",
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.4.6",
"http-proxy": "^1.16.2",
"husky": "^7.0.4",
"jest": "^28.0.2",
"ts-jest": "^28.0.4",
"typescript": "^4.4.3",
"yargs": "^17.2.1"
},
"engines": {
"node": ">=16"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.56",
"version": "0.2.82",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -108,8 +80,82 @@
}
},
"node_modules/@eneris/push-receiver": {
"resolved": "../../external/push-receiver",
"link": true
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
"dependencies": {
"axios": "^1.2.1",
"http_ece": "^1.0.5",
"long": "^5.2.1",
"protobufjs": "^7.1.2"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@eneris/push-receiver/node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
@@ -137,6 +183,12 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -155,8 +207,7 @@
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"node_modules/ansi-regex": {
"version": "1.1.1",
@@ -730,6 +781,17 @@
"he": "bin/he"
}
},
"node_modules/http_ece": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
"dependencies": {
"urlsafe-base64": "~1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
@@ -1160,6 +1222,11 @@
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
"dev": true
},
"node_modules/long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1432,6 +1499,29 @@
"node": ">= 0.6.6"
}
},
"node_modules/protobufjs": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -1636,6 +1726,11 @@
"node": ">=0.8.0"
}
},
"node_modules/urlsafe-base64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
},
"node_modules/utile": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
@@ -1889,28 +1984,82 @@
},
"dependencies": {
"@eneris/push-receiver": {
"version": "file:../../external/push-receiver",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
"requires": {
"@types/jest": "^28.1.0",
"@types/long": "^4.0.1",
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"axios": "^0.27.1",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.4.6",
"axios": "^1.2.1",
"http_ece": "^1.0.5",
"http-proxy": "^1.16.2",
"husky": "^7.0.4",
"jest": "^28.0.2",
"long": "^5.2.0",
"protobufjs": "^6.11.2",
"request-promise": "^4.2.6",
"ts-jest": "^28.0.4",
"typescript": "^4.4.3",
"yargs": "^17.2.1"
"long": "^5.2.1",
"protobufjs": "^7.1.2"
},
"dependencies": {
"axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
}
}
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"requires": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@scrypted/common": {
"version": "file:../../common",
"requires": {
@@ -1964,6 +2113,12 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -1982,8 +2137,7 @@
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"ansi-regex": {
"version": "1.1.1",
@@ -2399,6 +2553,14 @@
"integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
"dev": true
},
"http_ece": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
"requires": {
"urlsafe-base64": "~1.0.0"
}
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
@@ -2737,6 +2899,11 @@
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
"dev": true
},
"long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -2966,6 +3133,25 @@
"winston": "0.8.x"
}
},
"protobufjs": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3109,6 +3295,11 @@
"integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==",
"dev": true
},
"urlsafe-base64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
},
"utile": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",

View File

@@ -37,21 +37,23 @@
]
},
"dependencies": {
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@eneris/push-receiver": "../../external/push-receiver",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
},
"version": "0.1.11"
"version": "0.1.13"
}

View File

@@ -1,23 +1,23 @@
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios from 'axios';
import bpmux from 'bpmux';
import crypto from 'crypto';
import { once } from 'events';
import http from 'http';
import https from 'https';
import HttpProxy from 'http-proxy';
import https from 'https';
import throttle from "lodash/throttle";
import upnp from 'nat-upnp';
import net from 'net';
import net, { AddressInfo } from 'net';
import os from 'os';
import path from 'path';
import qs from 'query-string';
import { Duplex } from 'stream';
import tls from 'tls';
import Url from 'url';
import type { CORSControlLegacy } from '../../../server/src/services/cors';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { PushManager } from './push';
import tls from 'tls';
const { deviceManager, endpointManager, systemManager } = sdk;
@@ -547,6 +547,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
};
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
this.console.log(req.socket?.remoteAddress, req.url);
const url = Url.parse(req.url);
if (url.path.startsWith('/web/oauth/callback') && url.query) {
const query = qs.parse(url.query);
@@ -620,7 +622,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
});
this.proxy.on('error', () => { });
this.proxy.on('proxyRes', (res, req) => {
res.headers['X-Scrypted-Cloud'] = 'true';
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
});

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.1.94",
"version": "0.1.103",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.1.94",
"version": "0.1.103",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.1.94",
"version": "0.1.103",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -47,12 +47,11 @@ export class Scheduler {
throw new Error('sunrise/sunset clock not supported');
}
const ret: ScryptedDevice = {
async setName() { },
async setType() { },
async setRoom() { },
async setMixins() { },
async probe() { return true },
listen(event: EventListenerOptions, callback, source?: ScryptedDeviceBase) {
function reschedule(): Date {

View File

@@ -1,8 +1,7 @@
import { BufferConverter, BufferConvertorOptions, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import sdk from "@scrypted/sdk";
import sdk, { BufferConverter, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import crypto from 'crypto';
import mime from "mime/lite";
import path from 'path';
import crypto from 'crypto';
const { endpointManager } = sdk;

View File

@@ -1,3 +1,4 @@
import { tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
import sdk, { DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk';
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import fs from 'fs';
@@ -237,3 +238,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
}
export default ScryptedCore;
export async function fork() {
return {
tsCompile,
}
}

View File

@@ -3,10 +3,13 @@ import { scryptedEval } from "./scrypted-eval";
import { monacoEvalDefaults } from "./monaco";
import { createScriptDevice, ScriptDeviceImpl } from "@scrypted/common/src/eval/scrypted-eval";
import { ScriptCoreNativeId } from "./script-core";
import { PluginAPIProxy } from "../../../server/src/plugin/plugin-api";
const { log, deviceManager, systemManager } = sdk;
export class Script extends ScryptedDeviceBase implements Scriptable, Program, ScriptDeviceImpl {
apiProxy: PluginAPIProxy;
constructor(nativeId: string) {
super(nativeId);
}
@@ -67,6 +70,8 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
}
prepareScript() {
this.apiProxy?.removeListeners();
Object.assign(this, createScriptDevice([
ScryptedInterface.Scriptable,
ScryptedInterface.Program,
@@ -79,10 +84,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
try {
const data = JSON.parse(this.storage.getItem('data'));
const { value, defaultExport } = await scryptedEval(this, data['script.ts'], Object.assign({
const { value, defaultExport, apiProxy } = await scryptedEval(this, data['script.ts'], Object.assign({
device: this,
}, variables));
this.apiProxy = apiProxy;
await this.postRunScript(defaultExport);
return value;
}
@@ -95,10 +102,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
async eval(source: ScriptSource, variables?: { [name: string]: any }) {
this.prepareScript();
const { value, defaultExport } = await scryptedEval(this, source.script, Object.assign({
const { value, defaultExport, apiProxy } = await scryptedEval(this, source.script, Object.assign({
device: this,
}, variables));
this.apiProxy = apiProxy;
await this.postRunScript(defaultExport);
return value;
}

View File

@@ -3,14 +3,20 @@ import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
export const UsersNativeId = 'users';
type DBUser = { username: string, aclId: string };
type DBUser = { username: string, admin: boolean };
export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
storageSettings = new StorageSettings(this, {
devices: {
title: 'Devices',
description: 'The devices this user can access. Admin users can access all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
type: 'device',
defaultAccess: {
title: 'Default Access',
description: 'Grant access to @scrypted/core and @scrypted/webrtc',
defaultValue: true,
type: 'boolean',
},
interfaces: {
title: 'Interfaces',
description: 'The interfaces this user can access. Admin users can access all interfaces on all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
type: 'interface',
multiple: true,
defaultValue: [],
},
@@ -19,26 +25,27 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
const self = sdk.deviceManager.getDeviceState(this.nativeId);
const ret: ScryptedUserAccessControl = {
const ret: ScryptedUserAccessControl = {
devicesAccessControls: [
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
...this.storageSettings.values.devices.map((id: string) => ({
id,
})),
...this.storageSettings.values.defaultAccess
? [
// grant this? not sure.
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
]
: [],
...this.storageSettings.values.interfaces.map((deviceInterface: string) => {
const [id, scryptedInterface] = deviceInterface.split('#');
return addAccessControlsForInterface(id, ScryptedInterface.ScryptedDevice, scryptedInterface as ScryptedInterface);
}),
]
};
if (self) {
}
return ret;
}
@@ -72,7 +79,19 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
const user = users.find(user => user.username === this.username);
if (!user)
return;
await usersService.addUser(user.username, value.toString(), user.aclId);
const { username, admin } = user;
const nativeId = `user:${username}`;
const aclId = await sdk.deviceManager.onDeviceDiscovered({
providerNativeId: this.nativeId,
name: username.toString(),
nativeId,
interfaces: [
ScryptedInterface.ScryptedUser,
ScryptedInterface.Settings,
],
type: ScryptedDeviceType.Person,
})
await usersService.addUser(user.username, value.toString(), admin ? undefined : aclId);
}
}

View File

@@ -5,7 +5,7 @@
color="primary" icon="mdi-vuetify" border="left">
<template v-slot:prepend>
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
getAlertIcon(alert)
getAlertIcon(alert)
}}</v-icon>
</template>
<div class="caption">{{ alert.title }}</div>
@@ -185,7 +185,8 @@
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
</v-flex>
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<v-flex xs12
v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<Settings :device="device"></Settings>
</v-flex>
</v-layout>
@@ -240,6 +241,7 @@ import Scene from "../interfaces/Scene.vue";
import TemperatureSetting from "../interfaces/TemperatureSetting.vue";
import PositionSensor from "../interfaces/sensors/PositionSensor.vue";
import DeviceProvider from "../interfaces/DeviceProvider.vue";
import ObjectDetection from "../interfaces/ObjectDetection.vue";
import MixinProvider from "../interfaces/MixinProvider.vue";
import Readme from "../interfaces/Readme.vue";
import Scriptable from "../interfaces/automation/Scriptable.vue";
@@ -286,7 +288,9 @@ const leftInterfaces = [
ScryptedInterface.DeviceProvider,
ScryptedInterface.Readme,
];
const leftAboveInterfaces = [ScryptedInterface.Camera];
const leftAboveInterfaces = [
ScryptedInterface.Camera,
];
const noCardInterfaces = [
ScryptedInterface.Camera,
@@ -294,7 +298,10 @@ const noCardInterfaces = [
ScryptedInterface.Scriptable,
];
const aboveInterfaces = [ScryptedInterface.Scriptable];
const aboveInterfaces = [
ScryptedInterface.ObjectDetection,
ScryptedInterface.Scriptable
];
const cardActionInterfaces = [
ScryptedInterface.OauthClient,
@@ -379,6 +386,8 @@ export default {
Automation,
Program,
Scriptable,
ObjectDetection,
},
mixins: [Mixin],
data() {

View File

@@ -23,6 +23,9 @@ export function createSystemSettingsDevice(systemManager: SystemManager): Scrypt
},
async probe() {
return true;
},
async setMixins() {
},
listen(event, callback) {
let listeners = systemSettings.map(d => d.listen(event, callback));

View File

@@ -24,22 +24,45 @@
Devices</v-btn>
</v-card-actions>
<v-simple-table v-if="discoveredDevices && discoveredDevices.length">
<thead>
<tr>
<th style="width: 10px;"></th>
<th>Discovered</th>
<th></th>
</tr>
</thead>
<tbody v-if="discoveredDevices.length">
<tr v-for="device in discoveredDevices" :key="device.id">
<td>
<v-btn x-small outlined fab color="info" @click="openDeviceAdoptionDialog(device)">
<v-icon>fa-solid
fa-plus</v-icon>
</v-btn>
</td>
<td>
{{ device.name }}
</td>
<td> {{ device.description }}</td>
</tr>
</tbody>
<tbody v-else>
<td></td>
<td>None found.</td>
<td></td>
</tbody>
</v-simple-table>
<v-card-text>These things were created by {{ device.name }}.</v-card-text>
<v-text-field v-model="search" append-icon="search" label="Search" single-line hide-details></v-text-field>
<v-data-table :headers="headers" :items="providerDevices.devices" :items-per-page="10" :search="search">
<v-text-field v-if="managedDevices.devices.length > 10" v-model="search" append-icon="search" label="Search"
single-line hide-details></v-text-field>
<v-data-table v-if="managedDevices.devices.length > 10" :headers="headers" :items="managedDevices.devices"
:items-per-page="10" :search="search">
<template v-slot:[`item.icon`]="{ item }">
<v-icon v-if="!item.nativeId" x-small color="grey">
<v-icon x-small color="grey">
{{ typeToIcon(item.type) }}
</v-icon>
<v-tooltip bottom v-else>
<template v-slot:activator="{ on }">
<v-btn x-small outlined fab v-on="on" color="info" @click="openDeviceAdoptionDialog(item)"><v-icon>fa-solid
fa-plus</v-icon></v-btn>
</template>
<span>Add Discovered Device</span>
</v-tooltip>
</template>
<template v-slot:[`item.name`]="{ item }">
<a v-if="!item.nativeId" link :href="'#' + getDeviceViewPath(item.id)">{{ item.name }}</a>
@@ -48,7 +71,7 @@
</template>
</v-data-table>
<!-- <DeviceGroup v-else :deviceGroup="providerDevices"></DeviceGroup> -->
<DeviceGroup v-else :deviceGroup="managedDevices"></DeviceGroup>
</v-flex>
</template>
<script>
@@ -167,8 +190,8 @@ export default {
});
return ret;
},
providerDevices() {
const currentDevices = this.$store.state.scrypted.devices
managedDevices() {
const devices = this.$store.state.scrypted.devices
.filter(
(id) =>
this.$store.state.systemState[id].providerId.value ===
@@ -181,7 +204,7 @@ export default {
}));
return {
devices: [...this.discoveredDevices || [], ...currentDevices],
devices,
};
},
},

View File

@@ -0,0 +1,91 @@
<template>
<v-sheet :height="600" width="100%" class="d-flex align-center justify-center flex-wrap text-center mx-auto"
@drop="onDrop" @dragover="allowDrop">
<div v-if="!img">Drag and Drop a JPG or PNG to analyze.</div>
<div v-else style="position: relative; height: 100%;">
<img :src="img" style="height: 100%">
<svg v-if="lastDetection" :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
top: 0;
left: 0;
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
" v-html="svgContents"></svg>
</div>
</v-sheet>
</template>
<script>
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
data() {
return {
img: null,
lastDetection: null,
}
},
mounted() {
},
computed: {
svgWidth() {
return this.lastDetection?.inputDimensions?.[0] || 1920;
},
svgHeight() {
return this.lastDetection?.inputDimensions?.[1] || 1080;
},
svgContents() {
if (!this.lastDetection) return "";
let contents = "";
for (const detection of this.lastDetection.detections || []) {
if (!detection.boundingBox) continue;
const svgScale = this.svgWidth / 1080;
const sw = 6 * svgScale;
const s = "red";
const x = detection.boundingBox[0];
const y = detection.boundingBox[1];
const w = detection.boundingBox[2];
const h = detection.boundingBox[3];
let t = ``;
let toffset = 0;
if (detection.score && detection.className !== 'motion') {
t += `<tspan x='${x}' dy='${toffset}em'>${Math.round(detection.score * 100) / 100}</tspan>`
toffset -= 1.2;
}
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
const fs = 30 * svgScale;
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
`;
contents += box;
}
return contents;
},
},
methods: {
async onDrop(ev) {
ev.preventDefault()
const file = ev.dataTransfer.files[0];
this.img = URL.createObjectURL(file);
const buffer = Buffer.from(await file.arrayBuffer());
const mediaManager = this.$scrypted.mediaManager;
const mo = await mediaManager.createMediaObject(buffer, 'image/*');
const detected = await this.rpc().detectObjects(mo);
this.lastDetection = detected;
},
allowDrop(ev) {
ev.preventDefault();
}
}
}
</script>

View File

@@ -102,6 +102,7 @@ export default {
},
set(value) {
this.rawSettingsGroupName = value;
this.rawSettingsSubgroupName = undefined;
},
},
settingsSubgroupName: {
@@ -110,7 +111,7 @@ export default {
return;
if (this.settingsSubgroups.findIndex(sg => sg === this.rawSettingsSubgroupName) !== -1)
return this.rawSettingsSubgroupName;
return Object.keys(this.settingsSubgroups)?.[0];
return Object.values(this.settingsSubgroups)?.[0];
},
set(value) {
this.rawSettingsSubgroupName = value;

View File

@@ -1,20 +0,0 @@
#!/bin/sh
# Copyright 2019 Google LLC
#
# 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
#
# https://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.
rm -rf all_models
mkdir -p all_models
cd all_models
wget https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel
wget https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt

View File

@@ -1 +0,0 @@
../all_models/MobileNetV2_SSDLite.mlmodel

View File

@@ -1 +0,0 @@
../all_models/coco_labels.txt

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/coreml",
"version": "0.0.21",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.0.21",
"version": "0.1.2",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.57",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -41,5 +41,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.21"
"version": "0.1.2"
}

View File

@@ -6,6 +6,10 @@ from predict import PredictPlugin, Prediction, Rectangle
import coremltools as ct
import os
from PIL import Image
import asyncio
import concurrent.futures
predictExecutor = concurrent.futures.ThreadPoolExecutor(2, "CoreML-Predict")
def parse_label_contents(contents: str):
lines = contents.splitlines()
@@ -25,17 +29,19 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
def __init__(self, nativeId: str | None = None):
super().__init__(MIME_TYPE, nativeId=nativeId)
modelPath = os.path.join(os.environ['SCRYPTED_PLUGIN_VOLUME'], 'zip', 'unzipped', 'fs', 'MobileNetV2_SSDLite.mlmodel')
self.model = ct.models.MLModel(modelPath)
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt', 'coco_labels.txt')
modelFile = self.downloadFile('https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel', 'MobileNetV2_SSDLite.mlmodel')
self.model = ct.models.MLModel(modelFile)
self.modelspec = self.model.get_spec()
self.inputdesc = self.modelspec.description.input[0]
self.inputheight = self.inputdesc.type.imageType.height
self.inputwidth = self.inputdesc.type.imageType.width
labels_contents = scrypted_sdk.zip.open(
'fs/coco_labels.txt').read().decode('utf8')
labels_contents = open(labelsFile, 'r').read()
self.labels = parse_label_contents(labels_contents)
self.loop = asyncio.get_event_loop()
# width, height, channels
def get_input_details(self) -> Tuple[int, int, int]:
@@ -44,8 +50,12 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
def get_input_size(self) -> Tuple[float, float]:
return (self.inputwidth, self.inputheight)
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
# run in executor if this is the plugin loop
if asyncio.get_event_loop() is self.loop:
out_dict = await asyncio.get_event_loop().run_in_executor(predictExecutor, lambda: self.model.predict({'image': input, 'confidenceThreshold': .2 }))
else:
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
coordinatesList = out_dict['coordinates']

View File

@@ -7,4 +7,5 @@ src
.vscode
dist/*.js
dist/*.txt
face-api.js
HAP-NodeJS
.gitattributes

View File

@@ -10,6 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",
@@ -19,4 +20,4 @@
"type": "pwa-node"
}
]
}
}

View File

@@ -1,3 +1,3 @@
{
"scrypted.debugHost": "127.0.0.1",
}
}

1
plugins/eufy/README.md Normal file
View File

@@ -0,0 +1 @@
# Eufy Plugin for Scrypted

1334
plugins/eufy/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
plugins/eufy/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@scrypted/eufy",
"description": "Eufy Plugin for Scrypted",
"version": "0.0.1",
"keywords": [
"scrypted",
"plugin",
"eufy",
"camera"
],
"scripts": {
"build": "scrypted-webpack",
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
"prescrypted-vscode-launch": "scrypted-webpack",
"scrypted-vscode-launch": "scrypted-deploy-debug",
"scrypted-deploy-debug": "scrypted-deploy-debug",
"scrypted-debug": "scrypted-debug",
"scrypted-deploy": "scrypted-deploy",
"scrypted-readme": "scrypted-readme",
"scrypted-package-json": "scrypted-package-json",
"scrypted-webpack": "scrypted-webpack"
},
"scrypted": {
"name": "Eufy",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"Settings"
]
},
"dependencies": {
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@scrypted/h264-repacketizer": "file:../../packages/h264-repacketizer ",
"@types/node": "^18.14.6"
},
"optionalDependencies": {
"eufy-security-client": "^2.4.2"
}
}

306
plugins/eufy/src/main.ts Normal file
View File

@@ -0,0 +1,306 @@
import { listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
import sdk, { Battery, Camera, Device, DeviceProvider, FFmpegInput, MediaObject, MotionSensor, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import eufy, { CaptchaOptions, EufySecurity, P2PClientProtocol, P2PConnectionType } from 'eufy-security-client';
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { Deferred } from '@scrypted/common/src/deferred';
import { Writable } from 'stream';
import { LocalLivestreamManager } from './stream';
const { deviceManager, mediaManager, systemManager } = sdk;
class EufyCamera extends ScryptedDeviceBase implements VideoCamera, MotionSensor {
client: EufySecurity;
device: eufy.Camera;
constructor(nativeId: string, client: EufySecurity, device: eufy.Camera) {
super(nativeId);
this.client = client;
this.device = device;
this.setupMotionDetection();
}
setupMotionDetection() {
const handle = (device: eufy.Device, state: boolean) => {
this.motionDetected = state;
};
this.device.on('motion detected', handle);
this.device.on('person detected', handle);
this.device.on('pet detected', handle);
this.device.on('vehicle detected', handle);
this.device.on('dog detected', handle);
this.device.on('radar motion detected', handle);
}
getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
return this.createVideoStream(options);
}
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
return [
{
container: 'rtsp',
id: 'p2p',
name: 'P2P',
video: {
codec: 'h264',
},
audio: {
codec: 'aac',
},
tool: 'scrypted',
userConfigurable: false,
},
{
container: 'rtsp',
id: 'p2p-low',
name: 'P2P (Low Resolution)',
video: {
codec: 'h264',
width: 1280,
height: 720,
},
audio: {
codec: 'aac',
},
tool: 'scrypted',
userConfigurable: false,
},
];
}
async createVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
const livestreamManager = new LocalLivestreamManager(options.id, this.client, this.device, this.console);
const kill = new Deferred<void>();
kill.promise.finally(() => {
this.console.log('video stream exited');
livestreamManager.stopLocalLiveStream();
});
const rtspServer = await listenSingleRtspClient();
rtspServer.rtspServerPromise.then(async rtsp => {
kill.promise.finally(() => rtsp.client.destroy());
rtsp.client.on('close', () => kill.resolve());
try {
const process = await startRtpForwarderProcess(this.console, {
inputArguments: [
'-f', 'h264', '-i', 'pipe:4',
'-f', 'aac', '-i', 'pipe:5',
]
}, {
video: {
onRtp: rtp => {
if (videoTrack)
rtsp.sendTrack(videoTrack.control, rtp, false);
},
encoderArguments: [
'-vcodec', 'copy',
]
},
audio: {
onRtp: rtp => {
if (audioTrack)
rtsp.sendTrack(audioTrack.control, rtp, false);
},
encoderArguments: [
'-acodec', 'copy',
'-rtpflags', 'latm',
]
}
});
process.killPromise.finally(() => kill.resolve());
kill.promise.finally(() => process.kill());
let parsedSdp: ReturnType<typeof parseSdp>;
let videoTrack: typeof parsedSdp.msections[0]
let audioTrack: typeof parsedSdp.msections[0]
process.sdpContents.then(async sdp => {
sdp = addTrackControls(sdp);
rtsp.sdp = sdp;
parsedSdp = parseSdp(sdp);
videoTrack = parsedSdp.msections.find(msection => msection.type === 'video');
audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio');
await rtsp.handlePlayback();
});
const proxyStream = await livestreamManager.getLocalLivestream();
proxyStream.videostream.pipe(process.cp.stdio[4] as Writable);
proxyStream.audiostream.pipe((process.cp.stdio as any)[5] as Writable);
}
catch (e) {
rtsp.client.destroy();
}
});
const input: FFmpegInput = {
url: rtspServer.url,
mediaStreamOptions: options,
inputArguments: [
'-i', rtspServer.url,
]
};
return mediaManager.createFFmpegMediaObject(input);
}
}
class EufyPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings {
client: EufySecurity;
devices = new Map<string, any>();
storageSettings = new StorageSettings(this, {
country: {
title: 'Country',
defaultValue: 'US',
},
email: {
title: 'Email',
onPut: async () => this.tryLogin(),
},
password: {
title: 'Password',
type: 'password',
onPut: async () => this.tryLogin(),
},
twoFactorCode: {
title: 'Two Factor Code',
description: 'Optional: If 2FA is enabled on your account, enter the code sent to your email or phone number.',
onPut: async (oldValue, newValue) => {
await this.tryLogin(newValue);
},
noStore: true,
},
captcha: {
title: 'Captcha',
description: 'Optional: If a captcha request is recieved, enter the code in the image.',
onPut: async (oldValue, newValue) => {
await this.tryLogin(undefined, newValue);
},
noStore: true,
},
captchaId: {
title: 'Captcha Id',
hide: true,
}
});
constructor() {
super();
this.tryLogin()
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async tryLogin(twoFactorCode?: string, captchaCode?: string) {
this.log.clearAlerts();
if (!this.storageSettings.values.email || !this.storageSettings.values.email) {
this.log.a('Enter your Eufy email and password to complete setup.');
throw new Error('Eufy email and password are missing.');
}
await this.initializeClient();
var captchaOptions: CaptchaOptions = undefined
if (captchaCode) {
captchaOptions = {
captchaCode: captchaCode,
captchaId: this.storageSettings.values.captchaId,
}
}
await this.client.connect({ verifyCode: twoFactorCode, captcha: captchaOptions, force: false });
}
private async initializeClient() {
const config = {
username: this.storageSettings.values.email,
password: this.storageSettings.values.password,
country: this.storageSettings.values.country,
language: 'en',
p2pConnectionSetup: P2PConnectionType.QUICKEST,
pollingIntervalMinutes: 10,
eventDurationSeconds: 10
}
this.client = await EufySecurity.initialize(config);
this.client.on('device added', this.deviceAdded.bind(this));
this.client.on('station added', this.stationAdded.bind(this));
this.client.on('tfa request', () => {
this.log.a('Login failed: 2FA is enabled, check your email or texts for your code, then enter it into the Two Factor Code setting to conplete login.');
});
this.client.on('captcha request', (id, captcha) => {
this.log.a(`Login failed: Captcha was requested, fill out the Captcha setting to conplete login. </br> <img src="${captcha}" />`);
this.storageSettings.putSetting('captchaId', id);
});
this.client.on('connect', () => {
this.console.debug(`[${this.name}] (${new Date().toLocaleString()}) Client connected.`);
this.log.clearAlerts();
});
this.client.on('push connect', () => {
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Connected.`);
});
this.client.on('push close', () => {
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Closed.`);
});
}
private async deviceAdded(eufyDevice: eufy.Device) {
if (!eufyDevice.isCamera) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Ignoring unsupported discovered device: `, eufyDevice.getName(), eufyDevice.getModel());
return;
}
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device discovered: `, eufyDevice.getName(), eufyDevice.getModel());
const nativeId = eufyDevice.getSerial();
const interfaces = [
ScryptedInterface.VideoCamera
];
if (eufyDevice.hasBattery())
interfaces.push(ScryptedInterface.Battery);
if (eufyDevice.hasProperty('motionDetection'))
interfaces.push(ScryptedInterface.MotionSensor);
const device: Device = {
info: {
model: eufyDevice.getModel(),
manufacturer: 'Eufy',
firmware: eufyDevice.getSoftwareVersion(),
serialNumber: nativeId
},
nativeId,
name: eufyDevice.getName(),
type: ScryptedDeviceType.Camera,
interfaces,
};
this.devices.set(nativeId, new EufyCamera(nativeId, this.client, eufyDevice as eufy.Camera))
await deviceManager.onDeviceDiscovered(device);
}
private async stationAdded(station: eufy.Station) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Station discovered: `, station.getName(), station.getModel(), `but stations are not currently supported.`);
}
async getDevice(nativeId: string): Promise<any> {
return this.devices.get(nativeId);
}
async releaseDevice(id: string, nativeId: string) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device with id '${nativeId}' was removed.`);
}
}
export default new EufyPlugin();

227
plugins/eufy/src/stream.ts Normal file
View File

@@ -0,0 +1,227 @@
// Based off of https://github.com/homebridge-eufy-security/plugin/blob/master/src/plugin/controller/LocalLivestreamManager.ts
import { Camera, CommandData, CommandName, CommandType, Device, DeviceType, EufySecurity, isGreaterEqualMinVersion, P2PClientProtocol, ParamType, Station, StreamMetadata, VideoCodec } from 'eufy-security-client';
import { EventEmitter, Readable } from 'stream';
type StationStream = {
station: Station;
device: Device;
metadata: StreamMetadata;
videostream: Readable;
audiostream: Readable;
createdAt: number;
};
export class LocalLivestreamManager extends EventEmitter {
private stationStream: StationStream | null;
private console: Console;
private livestreamStartedAt: number | null;
private livestreamIsStarting = false;
private readonly id: string;
private readonly client: EufySecurity;
private readonly device: Camera;
private station: Station;
private p2pSession: P2PClientProtocol;
constructor(id: string, client: EufySecurity, device: Camera, console: Console) {
super();
this.id = id;
this.console = console;
this.client = client;
this.device = device;
this.client.getStation(this.device.getStationSerial()).then( (station) => {
this.station = station;
this.p2pSession = new P2PClientProtocol(station.getRawStation(), this.client.getApi(), station.getIPAddress());
this.p2pSession.on("livestream started", (channel: number, metadata: StreamMetadata, videostream: Readable, audiostream: Readable) => {
this.onStationLivestreamStart(station, device, metadata, videostream, audiostream);
});
this.p2pSession.on("livestream stopped", (channel: number) => {
this.onStationLivestreamStop(station, device);
});
this.p2pSession.on("livestream error", (channel: number, error: Error) => {
this.stopLivestream();
});
});
this.stationStream = null;
this.livestreamStartedAt = null;
this.initialize();
}
private initialize() {
if (this.stationStream) {
this.stationStream.audiostream.unpipe();
this.stationStream.audiostream.destroy();
this.stationStream.videostream.unpipe();
this.stationStream.videostream.destroy();
}
this.stationStream = null;
this.livestreamStartedAt = null;
}
public async getLocalLivestream(): Promise<StationStream> {
this.console.debug(this.device.getName(), this.id, 'New instance requests livestream.');
if (this.stationStream) {
const runtime = (Date.now() - this.livestreamStartedAt!) / 1000;
this.console.debug(this.device.getName(), this.id, 'Using livestream that was started ' + runtime + ' seconds ago.');
return this.stationStream;
} else {
return await this.startAndGetLocalLiveStream();
}
}
private async startAndGetLocalLiveStream(): Promise<StationStream> {
return new Promise((resolve, reject) => {
this.console.debug(this.device.getName(), this.id, 'Start new station livestream...');
if (!this.livestreamIsStarting) { // prevent multiple stream starts from eufy station
this.livestreamIsStarting = true;
this.startStationLivestream();
} else {
this.console.debug(this.device.getName(), this.id, 'stream is already starting. waiting...');
}
this.once('livestream start', async () => {
if (this.stationStream !== null) {
this.console.debug(this.device.getName(), this.id, 'New livestream started.');
this.livestreamIsStarting = false;
resolve(this.stationStream);
} else {
reject('no started livestream found');
}
});
});
}
private async startStationLivestream(videoCodec: VideoCodec = VideoCodec.H264): Promise<void> {
const commandData: CommandData = {
name: CommandName.DeviceStartLivestream,
value: videoCodec
};
this.console.debug(this.device.getName(), this.id, `Sending start livestream command to station ${this.station.getSerial()}`);
const rsa_key = this.p2pSession.getRSAPrivateKey();
if (this.device.isSoloCameras() || this.device.getDeviceType() === DeviceType.FLOODLIGHT_CAMERA_8423 || this.device.isWiredDoorbellT8200X()) {
this.console.debug(this.device.getName(), this.id, `Using CMD_DOORBELL_SET_PAYLOAD (1) for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithStringPayload({
commandType: CommandType.CMD_DOORBELL_SET_PAYLOAD,
value: JSON.stringify({
"commandType": ParamType.COMMAND_START_LIVESTREAM,
"data": {
"accountId": this.station.getRawStation().member.admin_user_id,
"encryptkey": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
"streamtype": videoCodec
}
}),
channel: this.device.getChannel()
}, {
command: commandData
});
} else if (this.device.isWiredDoorbell() || (this.device.isFloodLight() && this.device.getDeviceType() !== DeviceType.FLOODLIGHT) || this.device.isIndoorCamera() || (this.device.getSerial().startsWith("T8420") && isGreaterEqualMinVersion("2.0.4.8", this.station.getSoftwareVersion()))) {
this.console.debug(this.device.getName(), this.id, `Using CMD_DOORBELL_SET_PAYLOAD (2) for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithStringPayload({
commandType: CommandType.CMD_DOORBELL_SET_PAYLOAD,
value: JSON.stringify({
"commandType": ParamType.COMMAND_START_LIVESTREAM,
"data": {
"account_id": this.station.getRawStation().member.admin_user_id,
"encryptkey": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
"streamtype": videoCodec
}
}),
channel: this.device.getChannel()
}, {
command: commandData
});
} else {
if ((Device.isIntegratedDeviceBySn(this.station.getSerial()) || !isGreaterEqualMinVersion("2.0.9.7", this.station.getSoftwareVersion())) && (!this.station.getSerial().startsWith("T8420") || !isGreaterEqualMinVersion("1.0.0.25", this.station.getSoftwareVersion()))) {
this.console.debug(this.device.getName(), this.id, `Using CMD_START_REALTIME_MEDIA for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithInt({
commandType: CommandType.CMD_START_REALTIME_MEDIA,
value: this.device.getChannel(),
strValue: rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
channel: this.device.getChannel()
}, {
command: commandData
});
} else {
this.console.debug(this.device.getName(), this.id, `Using CMD_SET_PAYLOAD for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithStringPayload({
commandType: CommandType.CMD_SET_PAYLOAD,
value: JSON.stringify({
"account_id": this.station.getRawStation().member.admin_user_id,
"cmd": CommandType.CMD_START_REALTIME_MEDIA,
"mValue3": CommandType.CMD_START_REALTIME_MEDIA,
"payload": {
"ClientOS": "Android",
"key": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
"streamtype": videoCodec === VideoCodec.H264 ? 1 : 2,
}
}),
channel: this.device.getChannel()
}, {
command: commandData
});
}
}
}
public stopLocalLiveStream(): void {
this.console.debug(this.device.getName(), this.id, 'Stopping station livestream.');
this.stopLivestream();
this.initialize();
}
private async stopLivestream(): Promise<void> {
const commandData: CommandData = {
name: CommandName.DeviceStopLivestream
};
this.console.debug(this.device.getName(), this.id, `Sending stop livestream command to station ${this.station.getSerial()}`);
await this.p2pSession.sendCommandWithInt({
commandType: CommandType.CMD_STOP_REALTIME_MEDIA,
value: this.device.getChannel(),
channel: this.device.getChannel()
}, {
command: commandData
});
}
private onStationLivestreamStop(station: Station, device: Device) {
if (device.getSerial() === this.device.getSerial()) {
this.console.info(this.id + ' - ' + station.getName() + ' station livestream for ' + device.getName() + ' has stopped.');
this.initialize();
}
}
private async onStationLivestreamStart(
station: Station,
device: Device,
metadata: StreamMetadata,
videostream: Readable,
audiostream: Readable,
) {
if (device.getSerial() === this.device.getSerial()) {
if (this.stationStream) {
const diff = (Date.now() - this.stationStream.createdAt) / 1000;
if (diff < 5) {
this.console.warn(this.device.getName(), this.id, 'Second livestream was started from station. Ignore.');
return;
}
}
this.initialize(); // important to prevent unwanted behaviour when the eufy station emits the 'livestream start' event multiple times
this.console.info(this.id + ' - ' + station.getName() + ' station livestream (P2P session) for ' + device.getName() + ' has started.');
this.livestreamStartedAt = Date.now();
const createdAt = Date.now();
this.stationStream = {station, device, metadata, videostream, audiostream, createdAt};
this.console.debug(this.device.getName(), this.id, 'Stream metadata: ' + JSON.stringify(this.stationStream.metadata));
this.emit('livestream start');
}
}
}

View File

@@ -10,4 +10,4 @@
"include": [
"src/**/*"
]
}
}

View File

@@ -174,7 +174,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
async createDevice(settings: DeviceCreatorSettings, nativeId?: ScryptedNativeId): Promise<string> {
nativeId ||= randomBytes(4).toString('hex');
const name = settings.newCamera.toString();
const name = settings.newCamera?.toString() || 'New Camera';
await this.updateDevice(nativeId, name, this.getInterfaces());
return nativeId;
}

View File

@@ -1,15 +0,0 @@
# Google Cloud Text to Speech plugin
## npm commands
* npm run scrypted-webpack
* npm run scrypted-deploy <ipaddress>
* npm run scrypted-debug <ipaddress>
## scrypted distribution via npm
1. Ensure package.json is set up properly for publishing on npm.
2. npm publish
## Visual Studio Code configuration
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
* Launch Scrypted Debugger from the launch menu.

View File

@@ -1,141 +0,0 @@
{
"name": "@scrypted/google-cloud-tts",
"version": "0.0.21",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/google-cloud-tts",
"version": "0.0.21",
"hasInstallScript": true,
"dependencies": {
"axios": "^0.24.0"
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^17.0.8"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.199",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
},
"bin": {
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"../sdk": {
"extraneous": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/node": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
"dev": true
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
}
},
"dependencies": {
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@types/node": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
"dev": true
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
}
}
}

View File

@@ -1,95 +0,0 @@
// https://developer.scrypted.app/#getting-started
import axios from 'axios';
import sdk, { BufferConverter, ScryptedDeviceBase, Settings, Setting } from "@scrypted/sdk";
class GoogleCloudTts extends ScryptedDeviceBase implements BufferConverter, Settings {
constructor() {
super();
this.fromMimeType = 'text/plain';
this.toMimeType = 'audio/mpeg';
if (!this.getApiKey())
this.log.a('API key missing.');
}
getApiKey() {
const apiKey = this.storage.getItem('api_key');
return apiKey;
}
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer> {
const voice_name = this.storage.getItem("voice_name") || "en-GB-Standard-A";
const voice_gender = this.storage.getItem("voice_gender") || "FEMALE";
const voice_language_code = this.storage.getItem("voice_language_code") || "en-GB";
const from = Buffer.from(data);
var json = {
"input": {
"text": from.toString()
},
"voice": {
"languageCode": voice_language_code,
"name": voice_name,
"ssmlGender": voice_gender
},
"audioConfig": {
"audioEncoding": "MP3"
}
};
var result = await axios.post(`https://texttospeech.googleapis.com/v1/text:synthesize?key=${this.getApiKey()}`, json);
console.log(JSON.stringify(result.data, null, 2));
const buffer = Buffer.from(result.data.audioContent, 'base64');
return buffer;
}
voices: any;
async getSettings(): Promise<Setting[]> {
const ret: Setting[] = [
{
title: 'API Key',
description: 'API Key used by Google Cloud TTS.',
key: 'api_key',
value: this.storage.getItem('api_key'),
}
];
if (!this.getApiKey())
return ret;
try {
if (!this.voices) {
const response = await axios.get(`https://texttospeech.googleapis.com/v1/voices?key=${this.getApiKey()}`)
this.voices = response.data;
}
}
catch (e) {
this.log.a('Error retrieving settings from Google Cloud Text to Speech. Is your API Key correct?');
return ret;
}
ret.push({
title: "Voice",
choices: this.voices.voices.map(voice => voice.name),
key: "voice",
value: this.storage.getItem("voice_name"),
});
return ret;
}
async putSetting(key: string, value: string | number | boolean) {
if (key !== 'voice') {
this.storage.setItem(key, value.toString());
return;
}
const found = this.voices.voices.find((voice: any) => voice.name === value);
if (!found) {
console.error('Voice not found.');
return;
}
localStorage.setItem('voice_name', found.name);
localStorage.setItem('voice_language_code', found.languageCodes[0]);
localStorage.setItem('voice_gender', found.ssmlGender);
}
}
export default new GoogleCloudTts();

View File

@@ -32,6 +32,11 @@ export function nextSequenceNumber(current: number, increment = 1) {
return (current + increment + 0x10000) % 0x10000;
}
const maxRtpTimestamp = BigInt(0xFFFFFFFF);
export function addRtpTimestamp(current: number, adjust: number) {
return Number(maxRtpTimestamp & (BigInt(current) + BigInt(adjust)));
}
export function isNextSequenceNumber(current: number, next: number) {
return nextSequenceNumber(current) === next;
}

View File

@@ -10,7 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"**/plugin-console.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",

View File

@@ -1,19 +1,18 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.92",
"version": "0.0.108",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/objectdetector",
"version": "0.0.92",
"version": "0.0.108",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"jpeg-js": "^0.4.3",
"lodash": "^4.17.21",
"node-moving-things-tracker": "file:./node-moving-things-tracker",
"point-inside-polygon": "^1.0.3",
"polygon-overlap": "^1.0.5",
"semver": "^7.3.8"
@@ -42,7 +41,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.51",
"version": "0.2.71",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -61,11 +60,11 @@
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
@@ -191,28 +190,6 @@
"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==",
"dev": true
},
"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==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -228,67 +205,6 @@
"node": ">=0.3.1"
}
},
"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==",
"dev": true
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"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"
},
"engines": {
"node": "*"
},
"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==",
"dev": true,
"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==",
"dev": true
},
"node_modules/jasmine": {
"version": "3.99.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz",
"integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==",
"dev": true,
"dependencies": {
"glob": "^7.1.6",
"jasmine-core": "~3.99.0"
},
"bin": {
"jasmine": "bin/jasmine.js"
}
},
"node_modules/jasmine-core": {
"version": "3.99.1",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
"integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
"dev": true
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@@ -304,11 +220,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -326,53 +237,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"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==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/munkres-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz",
"integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ=="
},
"node_modules/node-moving-things-tracker": {
"resolved": "node-moving-things-tracker",
"link": true
},
"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==",
"dev": true,
"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==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/point-inside-polygon": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
@@ -463,27 +327,12 @@
"node": ">=4.2.0"
}
},
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"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==",
"dev": true
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -500,6 +349,7 @@
},
"node-moving-things-tracker": {
"version": "0.9.1",
"extraneous": true,
"license": "MIT",
"dependencies": {
"lodash.isequal": "^4.5.0",
@@ -642,28 +492,6 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"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
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -676,58 +504,6 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"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"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"jasmine": {
"version": "3.99.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz",
"integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==",
"dev": true,
"requires": {
"glob": "^7.1.6",
"jasmine-core": "~3.99.0"
}
},
"jasmine-core": {
"version": "3.99.1",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz",
"integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==",
"dev": true
},
"jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@@ -743,11 +519,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -762,50 +533,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
},
"munkres-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/munkres-js/-/munkres-js-1.2.2.tgz",
"integrity": "sha512-0oF4tBDvzx20CYzQ44tTJMfwTBJWXe7cE73Sa/u7Mz7X8jRtyOXOGE9kJBhCfX7Akku3Iy/WHa0sRgqLRq2xaQ=="
},
"node-moving-things-tracker": {
"version": "file:node-moving-things-tracker",
"requires": {
"jasmine": "^3.6.1",
"lodash.isequal": "^4.5.0",
"minimist": "^1.2.0",
"munkres-js": "^1.2.2",
"uuid": "^3.2.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"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==",
"dev": true
},
"point-inside-polygon": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/point-inside-polygon/-/point-inside-polygon-1.0.3.tgz",
@@ -863,23 +590,12 @@
"dev": true,
"peer": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"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
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.0.92",
"version": "0.0.108",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",
@@ -35,16 +35,18 @@
"name": "Video Analysis Plugin",
"type": "API",
"interfaces": [
"Settings",
"MixinProvider"
],
"realfs": true
"realfs": true,
"pluginDependencies": [
"@scrypted/python-codecs"
]
},
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"jpeg-js": "^0.4.3",
"lodash": "^4.17.21",
"node-moving-things-tracker": "file:./node-moving-things-tracker",
"point-inside-polygon": "^1.0.3",
"polygon-overlap": "^1.0.5",
"semver": "^7.3.8"

View File

@@ -1,5 +1,3 @@
const Tracker = require('node-moving-things-tracker').Tracker;
export class DenoisedDetectionEntry<T> {
id?: string;
boundingBox?: [number, number, number, number];
@@ -24,75 +22,18 @@ export interface DenoisedDetectionOptions<T> {
now?: number;
}
export interface TrackerItem<T> {
x: number,
y: number,
w: number,
h: number,
confidence: number,
name: string,
};
export interface TrackedItem<T> extends TrackerItem<T> {
id: string;
isZombie: boolean;
bearing: number;
frameUnmatchedLeftBeforeDying: number;
velocity: {
dx: number,
dy: number,
}
}
export interface DenoisedDetectionState<T> {
previousDetections?: DenoisedDetectionEntry<T>[];
tracker?: any;
tracked?: TrackedItem<T>[];
frameCount?: number;
lastDetection?: number;
// id to time
externallyTracked?: Map<string, DenoisedDetectionEntry<T>>;
}
type Rectangle = {
xmin: number;
xmax: number;
ymin: number;
ymax: number;
};
function intersect_area(a: Rectangle, b: Rectangle) {
const dx = Math.min(a.xmax, b.xmax) - Math.max(a.xmin, b.xmin)
const dy = Math.min(a.ymax, b.ymax) - Math.max(a.ymin, b.ymin)
if (dx >= 0 && dy >= 0)
return dx * dy
}
function trackedItemToRectangle(item: TrackedItem<any>): Rectangle {
return {
xmin: item.x,
xmax: item.x + item.w,
ymin: item.y,
ymax: item.y + item.h,
};
}
export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
currentDetections: DenoisedDetectionEntry<T>[],
options?: DenoisedDetectionOptions<T>
) {
if (!state.tracker) {
state.frameCount = 0;
const tracker = Tracker.newTracker();
tracker.reset();
tracker.setParams({
fastDelete: true,
unMatchedFramesTolerance: Number.MAX_SAFE_INTEGER,
iouLimit: 0.05
});
state.tracker = tracker;
}
if (!state.previousDetections)
state.previousDetections = [];
@@ -100,157 +41,52 @@ export function denoiseDetections<T>(state: DenoisedDetectionState<T>,
const lastDetection = state.lastDetection || now;
const sinceLastDetection = now - lastDetection;
const externallyTracked = currentDetections.filter(d => d.id);
if (externallyTracked.length) {
if (!state.externallyTracked)
state.externallyTracked = new Map();
if (!state.externallyTracked)
state.externallyTracked = new Map();
for (const tracked of currentDetections) {
tracked.durationGone = 0;
tracked.lastSeen = now;
tracked.lastBox = tracked.boundingBox;
for (const tracked of currentDetections) {
tracked.durationGone = 0;
tracked.lastSeen = now;
tracked.lastBox = tracked.boundingBox;
if (!tracked.id) {
const id = tracked.id = `untracked-${tracked.name}`;
if (!state.externallyTracked.get(id)) {
// crappy track untracked objects for 1 minute.
setTimeout(() => state.externallyTracked.delete(id), 60000);
}
}
let previous = state.externallyTracked.get(tracked.id);
if (previous) {
state.externallyTracked.delete(tracked.id);
tracked.firstSeen = previous.firstSeen;
tracked.firstBox = previous.firstBox;
previous.durationGone = 0;
previous.lastSeen = now;
previous.lastBox = tracked.boundingBox;
options?.retained(tracked, previous);
}
else {
tracked.firstSeen = now;
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
options?.added(tracked);
}
}
for (const previous of state.externallyTracked.values()) {
if (now - previous.lastSeen) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
options?.expiring(previous);
}
if (!tracked.id) {
const id = tracked.id = `untracked-${tracked.name}`;
if (!state.externallyTracked.get(id)) {
// crappy track untracked objects for 1 minute.
setTimeout(() => state.externallyTracked.delete(id), 60000);
}
}
for (const tracked of currentDetections) {
state.externallyTracked.set(tracked.id, tracked);
}
}
let previous = state.externallyTracked.get(tracked.id);
if (previous) {
state.externallyTracked.delete(tracked.id);
tracked.firstSeen = previous.firstSeen;
tracked.firstBox = previous.firstBox;
if (state.externallyTracked)
return;
const { tracker, previousDetections } = state;
const items: TrackerItem<T>[] = currentDetections.filter(cd => cd.boundingBox).map(cd => {
const [x, y, w, h] = cd.boundingBox;
return {
x, y, w, h,
confidence: cd.score,
name: cd.name,
}
});
tracker.updateTrackedItemsWithNewFrame(items, state.frameCount);
// console.log(tracker.getAllTrackedItems());
const trackedObjects: TrackedItem<T>[] = [...tracker.getTrackedItems().values()];
// for (const to of trackedObjects) {
// console.log(to.velocity);
// }
const previousCopy = previousDetections.slice();
previousDetections.splice(0, previousDetections.length);
const map = new Map<string, DenoisedDetectionEntry<T>>();
for (const pd of previousCopy) {
map.set(pd.id, pd);
}
for (const trackedObject of trackedObjects) {
map.delete(trackedObject.id);
const previous = previousCopy.find(d => d.id === trackedObject.id);
const current = currentDetections.find(d => {
const [x, y, w, h] = d.boundingBox;
return !d.id && x === trackedObject.x && y === trackedObject.y && w === trackedObject.w && h === trackedObject.h;
});
if (current) {
current.id = trackedObject.id;
current.lastSeen = now;
current.durationGone = 0;
if (previous) {
previous.lastSeen = now;
current.firstSeen = previous.firstSeen;
current.firstBox = previous.firstBox;
current.lastBox = previous.boundingBox;
previous.lastBox = current.boundingBox;
previous.durationGone = 0;
options.retained?.(current, previous);
}
else {
current.firstSeen = now;
current.firstBox = current.boundingBox;
current.lastBox = current.boundingBox;
options.added?.(current);
}
previousDetections.push(current);
}
else if (previous) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
let foundContainer = false;
// the detector may combine multiple detections into one.
// handle that scenario by not expiring the individual detections that
// are globbed into a larger one.
for (const other of trackedObjects) {
if (other === trackedObject || other.isZombie)
continue;
const area = intersect_area(trackedItemToRectangle(trackedObject), trackedItemToRectangle(other));
if (area) {
const trackedObjectArea = trackedObject.w * trackedObject.h;
if (area / trackedObjectArea > .5) {
foundContainer = true;
break;
}
}
}
if (!foundContainer)
trackedObject.frameUnmatchedLeftBeforeDying = -1;
// else
// console.log('globbed!');
}
else {
options.expiring?.(previous);
previousDetections.push(previous);
}
previous.durationGone = 0;
previous.lastSeen = now;
previous.lastBox = tracked.boundingBox;
options?.retained(tracked, previous);
}
else {
// console.warn('unprocessed denoised detection?', trackedObject);
tracked.firstSeen = now;
tracked.firstBox = tracked.lastBox = tracked.boundingBox;
options?.added(tracked);
}
}
for (const previous of state.externallyTracked.values()) {
if (now - previous.lastSeen) {
previous.durationGone += sinceLastDetection;
if (previous.durationGone >= options.timeout) {
options?.expiring(previous);
}
}
}
// should never reach here?
for (const r of map.values()) {
options.removed?.(r)
for (const tracked of currentDetections) {
state.externallyTracked.set(tracked.id, tracked);
}
state.tracked = trackedObjects;
state.lastDetection = now;
state.frameCount++;
}

View File

@@ -1,4 +1,4 @@
import sdk, { Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, VideoCamera } from '@scrypted/sdk';
import sdk, { ScryptedMimeTypes, Image, VideoFrame, VideoFrameGenerator, Camera, DeviceState, EventListenerRegister, MediaObject, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionResult, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, MediaStreamDestination } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import cloneDeep from 'lodash/cloneDeep';
@@ -7,7 +7,7 @@ import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
import { DenoisedDetectionEntry, DenoisedDetectionState, denoiseDetections } from './denoise';
import { serverSupportsMixinEventMasking } from './server-version';
import { sleep } from './sleep';
import { safeParseJson } from './util';
import { getAllDevices, safeParseJson } from './util';
const polygonOverlap = require('polygon-overlap');
const insidePolygon = require('point-inside-polygon');
@@ -24,6 +24,8 @@ const defaultSecondScoreThreshold = .7;
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
type ClipPath = [number, number][];
type Zones = { [zone: string]: ClipPath };
interface ZoneInfo {
@@ -48,6 +50,22 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
detections = new Map<string, MediaObject>();
cameraDevice: ScryptedDevice & Camera & VideoCamera & MotionSensor & ObjectDetector;
storageSettings = new StorageSettings(this, {
newPipeline: {
title: 'Video Pipeline',
description: 'Configure how frames are provided to the video analysis pipeline.',
onGet: async () => {
const choices = [
'Default',
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
if (!this.hasMotionType)
choices.push('Snapshot');
return {
choices,
}
},
defaultValue: 'Default',
},
motionSensorSupplementation: {
title: 'Built-In Motion Sensor',
description: `This camera has a built in motion sensor. Using ${this.objectDetection.name} may be unnecessary and will use additional CPU. Replace will ignore the built in motion sensor. Filter will verify the motion sent by built in motion sensor. The Default is ${BUILTIN_MOTION_SENSOR_REPLACE}.`,
@@ -57,6 +75,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
BUILTIN_MOTION_SENSOR_REPLACE,
],
defaultValue: "Default",
onPut: () => {
this.endObjectDetection();
this.maybeStartMotionDetection();
}
},
captureMode: {
title: 'Capture Mode',
@@ -126,7 +148,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
analyzeStop = 0;
lastDetectionInput = 0;
constructor(mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, modelName: string, group: string, public hasMotionType: boolean, public settings: Setting[]) {
constructor(public plugin: ObjectDetectionPlugin, mixinDevice: VideoCamera & Camera & MotionSensor & ObjectDetector & Settings, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }, providerNativeId: string, public objectDetection: ObjectDetection & ScryptedDevice, public model: ObjectDetectionModel, group: string, public hasMotionType: boolean, public settings: Setting[]) {
super({
mixinDevice, mixinDeviceState,
mixinProviderNativeId: providerNativeId,
@@ -137,7 +159,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
});
this.cameraDevice = systemManager.getDeviceById<Camera & VideoCamera & MotionSensor & ObjectDetector>(this.id);
this.detectionId = modelName + '-' + this.cameraDevice.id;
this.detectionId = model.name + '-' + this.cameraDevice.id;
this.bindObjectDetection();
this.register();
@@ -155,7 +177,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (this.hasMotionType) {
// force a motion detection restart if it quit
if (this.motionSensorSupplementation === BUILTIN_MOTION_SENSOR_REPLACE)
await this.startVideoDetection();
await this.startStreamAnalysis();
return;
}
}, this.storageSettings.values.detectionInterval * 1000);
@@ -187,16 +209,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
|| setting.value;
}
if (this.hasMotionType)
ret['motionAsObjects'] = this.storageSettings.values.motionAsObjects;
return ret;
}
async snapshotDetection() {
const picture = await this.cameraDevice.takePicture();
const detections = await this.objectDetection.detectObjects(picture, {
let detections = await this.objectDetection.detectObjects(picture, {
detectionId: this.detectionId,
settings: this.getCurrentSettings(),
});
this.trackObjects(detections, true);
detections = await this.trackObjects(detections, true);
this.reportObjectDetections(detections);
}
@@ -205,13 +230,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
if (this.motionSensorSupplementation !== BUILTIN_MOTION_SENSOR_REPLACE)
return;
await this.startVideoDetection();
await this.startStreamAnalysis();
}
endObjectDetection() {
this.detectorRunning = false;
this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
settings: this.getCurrentSettings(),
});
}
@@ -290,7 +316,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
if (!this.detectorRunning)
this.console.log('built in motion sensor started motion, starting video detection.');
await this.startVideoDetection();
await this.startStreamAnalysis();
return;
}
@@ -308,8 +334,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
async handleDetectionEvent(detection: ObjectsDetected, redetect?: (boundingBox: [number, number, number, number]) => Promise<ObjectDetectionResult[]>, mediaObject?: MediaObject) {
this.detectorRunning = detection.running;
// track the objects on a pre-zoned set.
this.trackObjects(detection);
detection = await this.trackObjects(detection);
// apply the zones to the detections and get a shallow copy list of detections after
// exclusion zones have applied
@@ -347,7 +372,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
// retain passing the second pass threshold for first time.
if (d.bestSecondPassScore < this.secondScoreThreshold && secondPassScore >= this.secondScoreThreshold) {
this.console.log('improved', d.id, d.bestSecondPassScore, d.score);
this.console.log('improved', d.id, secondPassScore, d.score);
better = true;
retainImage = true;
}
@@ -401,7 +426,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (this.lastDetectionInput + this.storageSettings.values.detectionTimeout * 1000 < Date.now())
retainImage = true;
if (retainImage) {
if (retainImage && mediaObject) {
this.lastDetectionInput = now;
this.console.log('retaining detection image');
this.setDetection(detection, mediaObject);
@@ -436,6 +461,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return;
this.detectorRunning = true;
this.analyzeStop = Date.now() + this.getDetectionDuration();
while (this.detectorRunning) {
const now = Date.now();
if (now > this.analyzeStop)
@@ -445,12 +472,13 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
reason: 'event',
});
const found = await this.objectDetection.detectObjects(mo, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
settings: this.getCurrentSettings(),
});
found.running = this.detectorRunning;
this.handleDetectionEvent(found, undefined, mo);
}, this);
}
catch (e) {
this.console.error('snapshot detection error', e);
}
// cameras tend to only refresh every 1s at best.
// maybe get this value from somewhere? or sha the jpeg?
@@ -458,17 +486,136 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (diff > 0)
await sleep(diff);
}
this.detectorRunning = false;
this.handleDetectionEvent({
detectionId: this.detectionId,
running: false,
detections: [],
timestamp: Date.now(),
}, undefined, undefined);
this.endObjectDetection();
}
async startPipelineAnalysis() {
if (this.detectorRunning)
return;
this.detectorRunning = true;
this.analyzeStop = Date.now() + this.getDetectionDuration();
const newPipeline = this.newPipeline;
let generator: () => Promise<AsyncGenerator<VideoFrame & MediaObject>>;
if (newPipeline === 'Snapshot' && !this.hasMotionType) {
const self = this;
generator = async () => (async function* gen() {
try {
while (self.detectorRunning) {
const now = Date.now();
const sleeper = async () => {
const diff = now + 1100 - Date.now();
if (diff > 0)
await sleep(diff);
};
let image: MediaObject & VideoFrame;
try {
const mo = await self.cameraDevice.takePicture({
reason: 'event',
});
image = await sdk.mediaManager.convertMediaObject(mo, ScryptedMimeTypes.Image);
}
catch (e) {
self.console.error('Video analysis snapshot failed. Will retry in a moment.');
await sleeper();
continue;
}
// self.console.log('yield')
yield image;
// self.console.log('done yield')
await sleeper();
}
}
finally {
self.console.log('Snapshot generation finished.');
}
})();
}
else {
const destination: MediaStreamDestination = this.hasMotionType ? 'low-resolution' : 'local-recorder';
const videoFrameGenerator = systemManager.getDeviceById<VideoFrameGenerator>(newPipeline);
if (!videoFrameGenerator)
throw new Error('invalid VideoFrameGenerator');
const stream = await this.cameraDevice.getVideoStream({
destination,
// ask rebroadcast to mute audio, not needed.
audio: null,
});
generator = async () => videoFrameGenerator.generateVideoFrames(stream, {
resize: this.model?.inputSize ? {
width: this.model.inputSize[0],
height: this.model.inputSize[1],
} : undefined,
format: this.model?.inputFormat,
});
}
try {
let detections = 0;
for await (const detected
of await this.objectDetection.generateObjectDetections(await generator(), {
settings: this.getCurrentSettings(),
})) {
if (!this.detectorRunning) {
break;
}
const now = Date.now();
if (now > this.analyzeStop) {
break;
}
// apply the zones to the detections and get a shallow copy list of detections after
// exclusion zones have applied
const zonedDetections = this.applyZones(detected.detected);
const filteredDetections = zonedDetections
.filter(d => {
if (!d.zones?.length)
return d.score >= this.scoreThreshold;
for (const zone of d.zones || []) {
const zi = this.zoneInfos[zone];
const scoreThreshold = zi?.scoreThreshold || this.scoreThreshold;
if (d.score >= scoreThreshold)
return true;
}
});
detected.detected.detections = filteredDetections;
detections++;
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
if (detected.detected.detectionId) {
const jpeg = await detected.videoFrame.toBuffer({
format: 'jpg',
});
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
this.setDetection(detected.detected, mo);
// this.console.log('image saved', detected.detected.detections);
}
this.reportObjectDetections(detected.detected);
if (this.hasMotionType) {
await sleep(250);
}
// this.handleDetectionEvent(detected.detected);
}
}
catch (e) {
this.console.error('video pipeline ended with error', e);
}
finally {
this.endObjectDetection();
}
}
async startStreamAnalysis() {
if (!this.hasMotionType && this.storageSettings.values.captureMode === 'Snapshot') {
if (this.newPipeline) {
await this.startPipelineAnalysis();
}
else if (!this.hasMotionType && this.storageSettings.values.captureMode === 'Snapshot') {
await this.startSnapshotAnalysis();
}
else {
@@ -487,6 +634,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
await this.objectDetection?.detectObjects(undefined, {
detectionId: this.detectionId,
duration: this.getDetectionDuration(),
settings: this.getCurrentSettings(),
}, this);
}
catch (e) {
@@ -536,6 +684,19 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
}
normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]) {
let [x, y, width, height] = boundingBox;
let x2 = x + width;
let y2 = y + height;
// the zones are point paths in percentage format
x = x * 100 / inputDimensions[0];
y = y * 100 / inputDimensions[1];
x2 = x2 * 100 / inputDimensions[0];
y2 = y2 * 100 / inputDimensions[1];
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
return box;
}
getDetectionDuration() {
// when motion type, the detection interval is a keepalive reset.
// the duration needs to simply be an arbitrarily longer time.
@@ -545,22 +706,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
applyZones(detection: ObjectsDetected) {
// determine zones of the objects, if configured.
if (!detection.detections)
return;
return [];
let copy = detection.detections.slice();
for (const o of detection.detections) {
if (!o.boundingBox)
continue;
o.zones = []
let [x, y, width, height] = o.boundingBox;
let x2 = x + width;
let y2 = y + height;
// the zones are point paths in percentage format
x = x * 100 / detection.inputDimensions[0];
y = y * 100 / detection.inputDimensions[1];
x2 = x2 * 100 / detection.inputDimensions[0];
y2 = y2 * 100 / detection.inputDimensions[1];
const box = [[x, y], [x2, y], [x2, y2], [x, y2]];
const box = this.normalizeBox(o.boundingBox, detection.inputDimensions);
let included: boolean;
for (const [zone, zoneValue] of Object.entries(this.zones)) {
@@ -600,6 +753,14 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
}
// if this is a motion sensor and there are no inclusion zones set up,
// use a default inclusion zone that crops the top and bottom to
// prevents errant motion from the on screen time changing every second.
if (this.hasMotionType && included === undefined) {
const defaultInclusionZone = [[0, 10], [100, 10], [100, 90], [0, 90]];
included = polygonOverlap(box, defaultInclusionZone);
}
// if there are inclusion zones and this object
// was not in any of them, filter it out.
if (included === false)
@@ -641,15 +802,15 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detection);
}
trackObjects(detectionResult: ObjectsDetected, showAll?: boolean) {
async trackObjects(detectionResult: ObjectsDetected, showAll?: boolean) {
// do not denoise
if (this.hasMotionType) {
return;
return detectionResult;
}
if (!detectionResult?.detections) {
// detection session ended.
return;
return detectionResult;
}
const { detections } = detectionResult;
@@ -719,6 +880,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (found.length || showAll) {
this.console.log('current detections:', this.detectionState.previousDetections.map(d => `${d.detection.className} (${d.detection.score}, ${d.detection.boundingBox?.join(', ')})`).join(', '));
}
return detectionResult;
}
setDetection(detection: ObjectsDetected, detectionInput: MediaObject) {
@@ -772,9 +935,29 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
return BUILTIN_MOTION_SENSOR_REPLACE;
}
get newPipeline() {
if (!this.plugin.storageSettings.values.newPipeline)
return;
const newPipeline = this.storageSettings.values.newPipeline;
if (!newPipeline)
return newPipeline;
if (newPipeline === 'Snapshot')
return newPipeline;
const pipelines = getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator));
const found = pipelines.find(p => p.name === newPipeline);
return found?.id || pipelines[0]?.id;
}
async getMixinSettings(): Promise<Setting[]> {
const settings: Setting[] = [];
try {
this.settings = (await this.objectDetection.getDetectionModel(this.getCurrentSettings())).settings;
}
catch (e) {
}
if (this.settings) {
settings.push(...this.settings.map(setting =>
Object.assign({}, setting, {
@@ -785,15 +968,16 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
);
}
settings.push(...await this.storageSettings.getSettings());
this.storageSettings.settings.motionSensorSupplementation.hide = !this.hasMotionType || !this.mixinDeviceInterfaces.includes(ScryptedInterface.MotionSensor);
this.storageSettings.settings.captureMode.hide = this.hasMotionType;
this.storageSettings.settings.captureMode.hide = this.hasMotionType || !!this.plugin.storageSettings.values.newPipeline;
this.storageSettings.settings.newPipeline.hide = this.hasMotionType || !this.plugin.storageSettings.values.newPipeline;
this.storageSettings.settings.detectionDuration.hide = this.hasMotionType;
this.storageSettings.settings.detectionTimeout.hide = this.hasMotionType;
this.storageSettings.settings.motionDuration.hide = !this.hasMotionType;
this.storageSettings.settings.motionAsObjects.hide = !this.hasMotionType;
settings.push(...await this.storageSettings.getSettings());
let hideThreshold = true;
if (!this.hasMotionType) {
let hasInclusionZone = false;
@@ -829,7 +1013,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
settings.push({
subgroup,
key: `zone-${name}`,
title: `Edit Zone`,
title: `Open Zone Editor`,
type: 'clippath',
value: JSON.stringify(value),
});
@@ -987,8 +1171,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements MixinProvider {
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, mixinProviderNativeId: ScryptedNativeId, public model: ObjectDetectionModel) {
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId });
currentMixins = new Set<ObjectDetectionMixin>();
constructor(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState, public plugin: ObjectDetectionPlugin, public model: ObjectDetectionModel) {
super({ mixinDevice, mixinDeviceInterfaces, mixinDeviceState, mixinProviderNativeId: plugin.nativeId });
// trigger mixin creation. todo: fix this to not be stupid hack.
for (const id of Object.keys(systemManager.getSystemState())) {
@@ -1000,55 +1186,93 @@ class ObjectDetectorMixin extends MixinDeviceBase<ObjectDetection> implements Mi
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
// filter out
for (const iface of interfaces) {
if (iface.startsWith(`${ScryptedInterface.ObjectDetection}:`)) {
const deviceMatch = this.mixinDeviceInterfaces.find(miface => miface.startsWith(iface));
if (deviceMatch)
continue;
return null;
}
}
const hasMotionType = this.model.classes.includes('motion');
const prefix = `${objectDetectionPrefix}${hasMotionType}`;
const thisPrefix = `${prefix}:${this.id}`;
const found = interfaces.find(iface => iface.startsWith(prefix) && iface !== thisPrefix);
if (found)
return;
// this.console.log('found', found);
if ((type === ScryptedDeviceType.Camera || type === ScryptedDeviceType.Doorbell) && (interfaces.includes(ScryptedInterface.VideoCamera) || interfaces.includes(ScryptedInterface.Camera))) {
const ret: string[] = [ScryptedInterface.ObjectDetector, ScryptedInterface.Settings];
const ret: string[] = [
ScryptedInterface.ObjectDetector,
ScryptedInterface.Settings,
thisPrefix,
];
const model = await this.mixinDevice.getDetectionModel();
if (model.classes?.includes('motion')) {
// const vamotion = 'mixin:@scrypted/objectdetector:motion';
// if (interfaces.includes(vamotion))
// return;
if (model.classes?.includes('motion')) {
ret.push(
ScryptedInterface.MotionSensor,
// vamotion,
);
}
return ret;
return ret;
}
return null;
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }) {
let objectDetection = systemManager.getDeviceById<ObjectDetection>(this.id);
const group = objectDetection.name.replace('Plugin', '').trim();
const hasMotionType = this.model.classes.includes('motion');
const group = hasMotionType ? 'Motion Detection' : 'Object Detection';
// const group = objectDetection.name.replace('Plugin', '').trim();
const settings = this.model.settings;
return new ObjectDetectionMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model.name, group, hasMotionType, settings);
const ret = new ObjectDetectionMixin(this.plugin, mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.mixinProviderNativeId, objectDetection, this.model, group, hasMotionType, settings);
this.currentMixins.add(ret);
return ret;
}
async releaseMixin(id: string, mixinDevice: any) {
this.currentMixins.delete(mixinDevice);
return mixinDevice.release();
}
}
class ObjectDetectionPlugin extends AutoenableMixinProvider {
class ObjectDetectionPlugin extends AutoenableMixinProvider implements Settings {
currentMixins = new Set<ObjectDetectorMixin>();
storageSettings = new StorageSettings(this, {
newPipeline: {
title: 'New Video Pipeline',
description: 'WARNING! DO NOT ENABLE: Use the new video pipeline. Leave blank to use the legacy pipeline.',
type: 'boolean',
},
activeMotionDetections: {
title: 'Active Motion Detection Sessions',
readonly: true,
mapGet: () => {
return [...this.currentMixins.values()]
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
.reduce((c2, v2) => c2 + (v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
}
},
activeObjectDetections: {
title: 'Active Object Detection Sessions',
readonly: true,
mapGet: () => {
return [...this.currentMixins.values()]
.reduce((c1, v1) => c1 + [...v1.currentMixins.values()]
.reduce((c2, v2) => c2 + (!v2.hasMotionType && v2.detectorRunning ? 1 : 0), 0), 0);
}
}
})
constructor(nativeId?: ScryptedNativeId) {
super(nativeId);
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (!interfaces.includes(ScryptedInterface.ObjectDetection))
return;
@@ -1057,12 +1281,15 @@ class ObjectDetectionPlugin extends AutoenableMixinProvider {
async getMixin(mixinDevice: ObjectDetection, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
const model = await mixinDevice.getDetectionModel();
return new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this.nativeId, model);
const ret = new ObjectDetectorMixin(mixinDevice, mixinDeviceInterfaces, mixinDeviceState, this, model);
this.currentMixins.add(ret);
return ret;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
// what does this mean to make a mixin provider no longer available?
// just ignore it until reboot?
this.currentMixins.delete(mixinDevice);
}
}

View File

@@ -1,3 +1,5 @@
import sdk from '@scrypted/sdk';
export function safeParseJson(value: string) {
try {
return JSON.parse(value);
@@ -5,3 +7,7 @@ export function safeParseJson(value: string) {
catch (e) {
}
}
export function getAllDevices() {
return Object.keys(sdk.systemManager.getSystemState()).map(id => sdk.systemManager.getDeviceById(id));
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/onvif",
"version": "0.0.114",
"version": "0.0.117",
"description": "ONVIF Camera Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,4 +1,4 @@
import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import sdk, { AdoptDevice, Device, DeviceCreatorSettings, DeviceDiscovery, DeviceInformation, DiscoveredDevice, Intercom, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PanTiltZoom, PanTiltZoomCommand, PictureOptions, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, Settings, SettingValue, VideoCamera, VideoCameraConfiguration } from "@scrypted/sdk";
import { AddressInfo } from "net";
import onvif from 'onvif';
import { Stream } from "stream";
@@ -6,6 +6,7 @@ import xml2js from 'xml2js';
import { Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { connectCameraAPI, OnvifCameraAPI, OnvifEvent } from "./onvif-api";
import { OnvifIntercom } from "./onvif-intercom";
import { OnvifPTZMixinProvider } from "./onvif-ptz";
const { mediaManager, systemManager, deviceManager } = sdk;
@@ -393,7 +394,7 @@ class OnvifCamera extends RtspSmartCamera implements ObjectDetector, Intercom, V
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
}
async putSetting(key: string, value: string) {
async putSetting(key: string, value: any) {
this.client = undefined;
this.rtspMediaStreamOptions = undefined;
@@ -429,6 +430,17 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
constructor(nativeId?: string) {
super(nativeId);
process.nextTick(() => {
deviceManager.onDeviceDiscovered({
name: 'ONVIF PTZ',
type: ScryptedDeviceType.Builtin,
nativeId: 'ptz',
interfaces: [
ScryptedInterface.MixinProvider,
]
})
})
onvif.Discovery.on('device', (cam: any, rinfo: AddressInfo, xml: any) => {
// Function will be called as soon as the NVT responses
@@ -511,6 +523,12 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
})
}
async getDevice(nativeId: string) {
if (nativeId === 'ptz')
return new OnvifPTZMixinProvider('ptz');
return super.getDevice(nativeId);
}
getAdditionalInterfaces() {
return [
ScryptedInterface.Camera,
@@ -531,6 +549,7 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
let ptzCapabilities: string[];
if (!skipValidate) {
try {
const api = await connectCameraAPI(httpAddress, username, password, this.console, undefined);
@@ -545,6 +564,13 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
}
settings.newCamera = info.model;
if (api.cam?.services?.find((s: any) => s.namespace === 'http://www.onvif.org/ver20/ptz/wsdl')) {
ptzCapabilities = [
'Pan',
'Tilt',
];
}
}
catch (e) {
this.console.error('Error adding ONVIF camera', e);
@@ -576,6 +602,16 @@ class OnvifProvider extends RtspProvider implements DeviceDiscovery {
intercom.intercomClient?.client.destroy();
}
if (ptzCapabilities) {
try {
const rd = sdk.systemManager.getDeviceById(device.id);
const ptz = await this.getDevice('ptz');
rd.setMixins([...(rd.mixins || []), ptz.id]);
}
catch (e) {
}
}
return nativeId;
}

View File

@@ -0,0 +1,133 @@
import { DeviceState, MixinDeviceBase, MixinDeviceOptions, MixinProvider, PanTiltZoom, PanTiltZoomCommand, PanTiltZoomMovement, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { connectCameraAPI } from "./onvif-api";
import {SettingsMixinDeviceBase, SettingsMixinDeviceOptions} from '../../../common/src/settings-mixin';
export class OnvifPtzMixin extends SettingsMixinDeviceBase<Settings> implements PanTiltZoom, Settings {
storageSettings = new StorageSettings(this, {
ptz: {
title: 'Pan/Tilt/Zoom',
type: 'string',
multiple: true,
choices: [
'Pan',
'Tilt',
'Zoom',
],
persistedDefaultValue: [
'Pan',
'Tilt',
],
onPut: (ov, ptz: string[]) => {
this.ptzCapabilities = {
pan: ptz.includes('Pan'),
tilt: ptz.includes('Tilt'),
zoom: ptz.includes('Zoom'),
}
}
}
});
constructor(options: SettingsMixinDeviceOptions<Settings>) {
super(options);
// force a read to set the state.
this.storageSettings.values.ptz;
}
getMixinSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putMixinSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async ptzCommand(command: PanTiltZoomCommand) {
const client = await this.getClient();
let speed: any;
if (command.speed) {
speed = {
x: command.speed.pan,
y: command.speed.tilt,
zoom: command.speed.zoom
};
}
if (command.movement === PanTiltZoomMovement.Relative) {
return new Promise<void>((r, f) => {
client.cam.relativeMove({
x: command.pan,
y: command.tilt,
zoom: command.zoom,
speed: speed
}, (e, result, xml) => {
if (e)
return f(e);
r();
})
})
}
else if (command.movement === PanTiltZoomMovement.Absolute) {
return new Promise<void>((r, f) => {
client.cam.absoluteMove({
x: command.pan,
y: command.tilt,
zoom: command.zoom,
speed: speed
}, (e, result, xml) => {
if (e)
return f(e);
r();
})
})
}
}
async getClient() {
const creds = await this.getCredentials();
return connectCameraAPI(creds.ipAndPort, creds.username, creds.password, this.console, undefined)
}
async getCredentials() {
const settings = await this.mixinDevice.getSettings();
const username = settings.find(s => s.key === 'username')?.value?.toString();
const password = settings.find(s => s.key === 'password')?.value?.toString();
const ip = settings.find(s => s.key === 'ip')?.value?.toString();
const httpPort = settings.find(s => s.key === 'httpPort')?.value?.toString();
const ipAndPort = `${ip}:${httpPort || 80}`;
return {
ipAndPort,
username,
password,
}
}
}
export class OnvifPTZMixinProvider extends ScryptedDeviceBase implements MixinProvider {
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
if (type !== ScryptedDeviceType.Camera || !interfaces.includes(ScryptedInterface.VideoCamera) || !interfaces.includes(ScryptedInterface.Settings))
return;
return [
ScryptedInterface.PanTiltZoom,
ScryptedInterface.Settings,
];
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: DeviceState): Promise<any> {
return new OnvifPtzMixin({
group: 'ONVIF PTZ',
groupKey: 'ptz',
mixinDevice,
mixinDeviceInterfaces,
mixinDeviceState,
mixinProviderNativeId: this.nativeId,
})
}
}

View File

@@ -9,3 +9,4 @@ dist/*.js
dist/*.txt
__pycache__
all_models
.venv

View File

@@ -16,6 +16,6 @@
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/scrypted_python"
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}

View File

@@ -1,50 +1,52 @@
{
"name": "@scrypted/opencv",
"version": "0.0.64",
"version": "0.0.66",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/opencv",
"version": "0.0.64",
"version": "0.0.66",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.1.17",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.74.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.15"
"typedoc": "^0.23.21"
}
},
"../sdk": {
@@ -59,12 +61,12 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
@@ -72,9 +74,11 @@
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.4.0",
"typedoc": "^0.23.15",
"webpack": "^5.74.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
}

View File

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

View File

@@ -3,6 +3,7 @@ from time import sleep
from detect import DetectionSession, DetectPlugin
from typing import Any, List, Tuple
import numpy as np
import asyncio
import cv2
import imutils
Gst = None
@@ -10,7 +11,7 @@ try:
from gi.repository import Gst
except:
pass
from scrypted_sdk.types import ObjectDetectionModel, ObjectDetectionResult, ObjectsDetected, Setting
from scrypted_sdk.types import ObjectDetectionModel, ObjectDetectionResult, ObjectsDetected, Setting, VideoFrame
from PIL import Image
class OpenCVDetectionSession(DetectionSession):
@@ -93,6 +94,9 @@ class OpenCVPlugin(DetectPlugin):
def get_pixel_format(self):
return self.pixelFormat
def get_input_format(self) -> str:
return 'gray'
def parse_settings(self, settings: Any):
area = defaultArea
@@ -106,7 +110,8 @@ class OpenCVPlugin(DetectPlugin):
blur = int(settings.get('blur', blur))
return area, threshold, interval, blur
def detect(self, detection_session: OpenCVDetectionSession, frame, settings: Any, src_size, convert_to_src_size) -> ObjectsDetected:
def detect(self, detection_session: OpenCVDetectionSession, frame, src_size, convert_to_src_size) -> ObjectsDetected:
settings = detection_session.settings
area, threshold, interval, blur = self.parse_settings(settings)
# see get_detection_input_size on undocumented size requirements for GRAY8
@@ -119,10 +124,15 @@ class OpenCVPlugin(DetectPlugin):
detection_session.curFrame = cv2.GaussianBlur(
gray, (blur, blur), 0, dst=detection_session.curFrame)
detections: List[ObjectDetectionResult] = []
detection_result: ObjectsDetected = {}
detection_result['detections'] = detections
detection_result['inputDimensions'] = src_size
if detection_session.previous_frame is None:
detection_session.previous_frame = detection_session.curFrame
detection_session.curFrame = None
return
return detection_result
detection_session.frameDelta = cv2.absdiff(
detection_session.previous_frame, detection_session.curFrame, dst=detection_session.frameDelta)
@@ -138,10 +148,6 @@ class OpenCVPlugin(DetectPlugin):
detection_session.dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(fcontours)
detections: List[ObjectDetectionResult] = []
detection_result: ObjectsDetected = {}
detection_result['detections'] = detections
detection_result['inputDimensions'] = src_size
for c in contours:
x, y, w, h = cv2.boundingRect(c)
@@ -163,6 +169,9 @@ class OpenCVPlugin(DetectPlugin):
detections.append(detection)
return detection_result
def get_input_details(self) -> Tuple[int, int, int]:
return (300, 300, 1)
def get_detection_input_size(self, src_size):
# The initial implementation of this plugin used BGRA
@@ -197,11 +206,45 @@ class OpenCVPlugin(DetectPlugin):
detection_session.cap = None
return super().end_session(detection_session)
def run_detection_image(self, detection_session: DetectionSession, image: Image.Image, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
async def run_detection_image(self, detection_session: DetectionSession, image: Image.Image, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
# todo
raise Exception('can not run motion detection on image')
async def run_detection_videoframe(self, videoFrame: VideoFrame, detection_session: OpenCVDetectionSession) -> ObjectsDetected:
width = videoFrame.width
height = videoFrame.height
def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
aspectRatio = width / height
# dont bother resizing if its already fairly small
if width <= 640 and height < 640:
scale = 1
resize = None
elif aspectRatio > 1:
scale = height / 300
resize = {
'height': 300,
'width': int(300 * aspectRatio)
}
else:
scale = width / 300
resize = {
'width': 300,
'height': int(300 / aspectRatio)
}
buffer = await videoFrame.toBuffer({
'resize': resize,
})
def convert_to_src_size(point, normalize = False):
return point[0] * scale, point[1] * scale, True
mat = np.ndarray((videoFrame.height, videoFrame.width, self.pixelFormatChannelCount), buffer=buffer, dtype=np.uint8)
detections = self.detect(
detection_session, mat, (width, height), convert_to_src_size)
return detections
async def run_detection_avframe(self, detection_session: DetectionSession, avframe, settings: Any, src_size, convert_to_src_size) -> Tuple[ObjectsDetected, Any]:
if avframe.format.name != 'yuv420p' and avframe.format.name != 'yuvj420p':
mat = avframe.to_ndarray(format='gray8')
else:
@@ -209,11 +252,11 @@ class OpenCVPlugin(DetectPlugin):
detections = self.detect(
detection_session, mat, settings, src_size, convert_to_src_size)
if not detections or not len(detections['detections']):
self.detection_sleep(settings)
await self.detection_sleep(settings)
return None, None
return detections, None
def run_detection_gstsample(self, detection_session: OpenCVDetectionSession, gst_sample, settings: Any, src_size, convert_to_src_size) -> ObjectsDetected:
async def run_detection_gstsample(self, detection_session: OpenCVDetectionSession, gst_sample, settings: Any, src_size, convert_to_src_size) -> ObjectsDetected:
buf = gst_sample.get_buffer()
caps = gst_sample.get_caps()
# can't trust the width value, compute the stride
@@ -230,24 +273,24 @@ class OpenCVPlugin(DetectPlugin):
buffer=info.data,
dtype=np.uint8)
detections = self.detect(
detection_session, mat, settings, src_size, convert_to_src_size)
detection_session, mat, src_size, convert_to_src_size)
# no point in triggering empty events.
finally:
buf.unmap(info)
if not detections or not len(detections['detections']):
self.detection_sleep(settings)
await self.detection_sleep(settings)
return None, None
return detections, None
def create_detection_session(self):
return OpenCVDetectionSession()
def detection_sleep(self, settings: Any):
async def detection_sleep(self, settings: Any):
area, threshold, interval, blur = self.parse_settings(settings)
# it is safe to block here because gstreamer creates a queue thread
sleep(interval / 1000)
await asyncio.sleep(interval / 1000)
def detection_event_notified(self, settings: Any):
self.detection_sleep(settings)
return super().detection_event_notified(settings)
async def detection_event_notified(self, settings: Any):
await self.detection_sleep(settings)
return await super().detection_event_notified(settings)

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/pam-diff",
"version": "0.0.15",
"version": "0.0.16",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/pam-diff",
"version": "0.0.15",
"version": "0.0.16",
"hasInstallScript": true,
"dependencies": {
"@types/node": "^16.6.1",

View File

@@ -43,5 +43,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.15"
"version": "0.0.16"
}

View File

@@ -1,4 +1,4 @@
import { FFmpegInput, MediaObject, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionSession, ObjectsDetected, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes } from '@scrypted/sdk';
import { ObjectDetectionResult, FFmpegInput, MediaObject, ObjectDetection, ObjectDetectionCallbacks, ObjectDetectionModel, ObjectDetectionSession, ObjectsDetected, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes } from '@scrypted/sdk';
import sdk from '@scrypted/sdk';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from "../../../common/src/media-helpers";
@@ -121,22 +121,42 @@ class PamDiff extends ScryptedDeviceBase implements ObjectDetection {
const p2p = new P2P();
const pamDiff = new PD({
difference: 9,
percent: 75,
response: 'percent',
difference: session.settings?.difference || defaultDifference,
percent: session.settings?.percent || defaultPercentage,
response: session?.settings?.motionAsObjects ? 'blobs' : 'percent',
});
pamDiff.on('diff', async (data: any) => {
const trigger = data.trigger[0];
// console.log(trigger.blobs.length);
const { blobs } = trigger;
const detections: ObjectDetectionResult[] = [];
if (blobs?.length) {
for (const blob of blobs) {
detections.push(
{
className: 'motion',
score: trigger.percent / 100,
boundingBox: [blob.minX, blob.minY, blob.maxX - blob.minX, blob.maxY - blob.minY],
}
)
}
}
else {
detections.push(
{
className: 'motion',
score: trigger.percent / 100,
}
)
}
const event: ObjectsDetected = {
timestamp: Date.now(),
running: true,
detectionId: pds.id,
detections: [
{
className: 'motion',
score: data.trigger[0].percent / 100,
}
]
inputDimensions: [640, 360],
detections,
}
if (pds.callbacks) {
pds.callbacks.onDetection(event);
@@ -149,10 +169,13 @@ class PamDiff extends ScryptedDeviceBase implements ObjectDetection {
const console = sdk.deviceManager.getMixinConsole(mediaObject.sourceId, this.nativeId);
pds.pamDiff = pamDiff;
pds.pamDiff.setDifference(session.settings?.difference || defaultDifference).setPercent(session.settings?.percent || defaultPercentage);
pds.pamDiff
.setDifference(session.settings?.difference || defaultDifference)
.setPercent(session.settings?.percent || defaultPercentage)
.setResponse(session?.settings?.motionAsObjects ? 'blobs' : 'percent');;
safePrintFFmpegArguments(console, args);
pds.cp = child_process.spawn(ffmpeg, args, {
stdio:[ 'inherit', 'pipe', 'pipe', 'pipe']
stdio: ['inherit', 'pipe', 'pipe', 'pipe']
});
let pamTimeout: NodeJS.Timeout;
const resetTimeout = () => {

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