Compare commits

..

196 Commits

Author SHA1 Message Date
Koushik Dutta
d9637679bf server: verup 2024-12-30 19:27:57 -08:00
Koushik Dutta
7ea849d357 openvino: massive perf improvements via async api usage 2024-12-29 23:20:17 -08:00
Koushik Dutta
e4f01f10f4 postbeta 2024-12-29 20:02:34 -08:00
Koushik Dutta
bd61e9a5dd server: fix fs.promises. 2024-12-29 20:02:26 -08:00
Koushik Dutta
a2f8504290 postbeta 2024-12-29 19:51:15 -08:00
Koushik Dutta
928683a429 server: more cluster mode hooks 2024-12-29 19:51:05 -08:00
Koushik Dutta
4d6bd61650 postbeta 2024-12-29 14:34:29 -08:00
Koushik Dutta
9321a5e0dd Merge branch 'main' of github.com:koush/scrypted 2024-12-29 14:33:44 -08:00
Koushik Dutta
1622a0be63 server: update launch.json configs 2024-12-29 14:33:40 -08:00
Brett Jia
55cb62cb72 server: use standard-telnetlib for Python 3.13+ (#1669)
* bump portable-python to version with 3.13

* add standard-telnetlib to scrypted requirements for Python 3.13+
2024-12-29 14:22:32 -08:00
Koushik Dutta
11ea37d1c4 wyze: fix performance issues! 2024-12-28 22:22:51 -08:00
Koushik Dutta
8e1dfa8174 core: ensure lxc-docker is updated every boot 2024-12-28 20:35:44 -08:00
Koushik Dutta
0cf4802385 install: fixup missing debugpy 2024-12-28 15:39:26 -08:00
Koushik Dutta
194facb19c Revert "docker: remove pips"
This reverts commit 5f7ecc0410.
2024-12-28 15:37:46 -08:00
Koushik Dutta
6438ad1e3c tensorflow-lite: pipeline pre/post processing 2024-12-28 15:24:28 -08:00
Koushik Dutta
586f78ebc1 lxc: fix auto repair in systemd script 2024-12-28 14:50:57 -08:00
Koushik Dutta
48c5e1a5fe tensorflow-lite: quantization cleanups 2024-12-28 14:24:12 -08:00
Koushik Dutta
a6a986a8ac postbeta 2024-12-28 13:25:08 -08:00
Koushik Dutta
0b04d92131 server: use site packages in python for debugging 2024-12-28 13:24:40 -08:00
Koushik Dutta
05e9627f4a server: add debugpy to install list 2024-12-28 13:09:47 -08:00
Koushik Dutta
381c6de336 install/server: remove psutil 2024-12-28 13:08:22 -08:00
Koushik Dutta
4206ee4686 Merge branch 'main' of github.com:koush/scrypted 2024-12-27 22:40:45 -08:00
Koushik Dutta
e33a793867 tensorflow-lite: use new yolov9s model with separate outputs to fix quantization accuracy loss 2024-12-27 22:40:33 -08:00
Brett Jia
699eebaf14 docker: set default shell to bash (#1667)
* docker: change default shell to bash

* set SHELL
2024-12-26 18:50:49 -08:00
Brett Jia
45a2d5764c docker: dynamically find amdgpu deb package name (#1666) 2024-12-26 18:22:30 -08:00
Koushik Dutta
5f7ecc0410 docker: remove pips 2024-12-26 17:58:35 -08:00
Koushik Dutta
92257e41c1 webrtc: fix media conversion failure 2024-12-25 20:22:53 -08:00
Koushik Dutta
c5a703896c webrtc: improve media to signaling channel conversion 2024-12-25 19:55:16 -08:00
Koushik Dutta
51aa79956a core: publish ui updates 2024-12-25 19:29:12 -08:00
Koushik Dutta
fc1151ce8c Merge branch 'main' of github.com:koush/scrypted 2024-12-25 19:28:33 -08:00
Koushik Dutta
eaa2c37d57 tensorflow-lite: add relu models 2024-12-25 19:28:28 -08:00
Koushik Dutta
162bb7bfab proxmox: docker-compose.sh should repair dpkg first 2024-12-24 19:52:32 -08:00
Koushik Dutta
e467414704 rtp: fix leak if child process fails to spawn 2024-12-23 15:06:30 -08:00
Koushik Dutta
8ec6a25833 rebroadcast: add support for rtsp url using cluster address in case scrypted server address is not set 2024-12-23 11:16:36 -08:00
Koushik Dutta
56bc0d6a26 postbeta 2024-12-22 12:23:13 -08:00
Koushik Dutta
9098426c3b server: add rpc support for shallow serialized arrays 2024-12-22 12:16:10 -08:00
Koushik Dutta
0d9d425ef0 server: fix python search order 2024-12-22 12:05:33 -08:00
Koushik Dutta
4c6ca3b2a5 Merge branch 'main' of github.com:koush/scrypted 2024-12-22 09:01:34 -08:00
Koushik Dutta
762e058ec5 videoanalyis: reduce default confidence for smart motion sensor 2024-12-22 09:01:30 -08:00
Koushik Dutta
f02509152d docker: improve disk setup and add auto remount 2024-12-21 20:30:44 -08:00
Koushik Dutta
9d92031e4c videoanalysis: make some settings immediate 2024-12-20 11:22:02 -08:00
Koushik Dutta
6d0027d3e8 proxmox: onboot 2024-12-20 09:10:45 -08:00
Koushik Dutta
274e043c81 videoanalysis: add separate crop zone 2024-12-19 18:03:58 -08:00
Koushik Dutta
817a6f5a59 videoanalysis: fix zone normalization 2024-12-19 15:22:04 -08:00
Koushik Dutta
cbdf8873e0 Merge branch 'main' of github.com:koush/scrypted 2024-12-19 14:54:28 -08:00
Koushik Dutta
c9c9e106db videoanalaysis: fix zone persistence 2024-12-19 14:54:23 -08:00
Long Zheng
f3d7ebd2a2 Fix Windows install script NPM/Node version clash (#1662)
* Fix NPM/Node version clash

* Update install-scrypted-dependencies-win.ps1
2024-12-19 12:04:23 -08:00
Koushik Dutta
0ea6b13cb9 videoanalysis: smart occupancy sensor 2024-12-19 11:14:27 -08:00
Koushik Dutta
68cbe9a4f9 videoanalysis: smart occupancy sensor 2024-12-19 10:48:03 -08:00
Koushik Dutta
c7ab9085ff core: more ui fixes 2024-12-19 10:36:03 -08:00
Koushik Dutta
45993b3cb9 snapshot: fix nre on toImage format validaiton 2024-12-19 10:24:30 -08:00
Koushik Dutta
82ce08ab53 core: occupancy ui 2024-12-19 09:40:42 -08:00
Koushik Dutta
262fb32085 core: publish ui 2024-12-19 09:33:05 -08:00
Koushik Dutta
919d2dee85 tensorflow-lite: missing files 2024-12-18 09:40:36 -08:00
Koushik Dutta
1bb7df53c7 mqtt: publish 2024-12-18 09:40:10 -08:00
Koushik Dutta
612cf7b520 postbeta 2024-12-17 21:49:13 -08:00
Koushik Dutta
55a80f1898 server: fix env nre 2024-12-17 21:49:04 -08:00
Koushik Dutta
44ab56a888 tensorflow-lite: threshold cleanup 2024-12-17 11:38:12 -08:00
Koushik Dutta
eaae396861 tensorflow-lite: new default model 2024-12-17 09:22:25 -08:00
Koushik Dutta
cff170a508 postbeta 2024-12-16 19:55:06 -08:00
Koushik Dutta
c811109ee9 sdk/core: rebuild with cjs/es fixes 2024-12-16 19:54:04 -08:00
Koushik Dutta
c8e4502d11 sdk/server: more reliable module env detection 2024-12-16 19:51:25 -08:00
Koushik Dutta
b75c0e0ca1 sdk: ensure import.meta is undefined for webpack 2024-12-16 19:23:14 -08:00
Koushik Dutta
f64c9226a1 postbeta 2024-12-16 08:03:44 -08:00
Koushik Dutta
95dd67cd3a server: combine NODE_PATHs 2024-12-16 08:03:24 -08:00
Koushik Dutta
3ac0ca5c7a sdk: remove old node pty 2024-12-16 07:57:08 -08:00
Koushik Dutta
cd68af9796 core: remove old node pty 2024-12-16 07:54:25 -08:00
Koushik Dutta
9c1be5865b openvino: use relu face 2024-12-15 19:37:00 -08:00
Koushik Dutta
675f23235b postbeta 2024-12-15 13:12:00 -08:00
Koushik Dutta
0824136458 server: allow NODE_PATH override 2024-12-15 13:11:45 -08:00
Koushik Dutta
2b1b65d723 core: publish ui fix for extension toggling new devices 2024-12-15 12:16:26 -08:00
Koushik Dutta
16995ed9e8 core: fix aggregate device 2024-12-15 09:34:22 -08:00
Koushik Dutta
c5fb7d20a0 videoanalysis: fix motion reporting from object detector 2024-12-13 23:38:08 -08:00
Koushik Dutta
8c67f1e0ff tapo: update readme 2024-12-13 12:45:59 -08:00
Koushik Dutta
50e2ae83b4 objectdetector: fixup normalizeBox to allow scalar 2024-12-13 08:23:33 -08:00
Koushik Dutta
1eb4f6fd55 core: publish support for editing camera zones via deviceFilter param 2024-12-12 15:48:12 -08:00
Koushik Dutta
9152512679 proxmox: use restore storage for reset as well 2024-12-12 10:57:48 -08:00
Koushik Dutta
7870ed7eeb reolink: fixup probe 2024-12-12 09:11:48 -08:00
Koushik Dutta
40c0dea505 reolink: validate device info 2024-12-11 21:39:27 -08:00
Koushik Dutta
3542d327ea postbeta 2024-12-11 20:09:42 -08:00
Koushik Dutta
2ef87c21b6 server: allow cluster labels to request plugins 2024-12-11 20:09:33 -08:00
Koushik Dutta
4585f43318 openvino: disable npu for recognition with user input 2024-12-11 15:54:16 -08:00
Koushik Dutta
30da19510a openvino: disable npu for recognition 2024-12-11 15:53:40 -08:00
Koushik Dutta
5ea1c9467f proxmox: fix restore prompt install 2024-12-10 15:47:32 -08:00
Koushik Dutta
1d6eabc9e8 proxmox: restore prompt 2024-12-10 15:39:52 -08:00
Koushik Dutta
9ea4b5a29b proxmox: find a default storage device 2024-12-10 15:29:06 -08:00
Koushik Dutta
539692867b openvino: reenable npu 2024-12-10 12:22:13 -08:00
Koushik Dutta
e796404995 core: deprecate lxc 2024-12-10 10:43:37 -08:00
Koushik Dutta
54b21260d1 core: fix cluster worker rename 2024-12-10 10:23:26 -08:00
Koushik Dutta
be35fb2dc2 postbeta 2024-12-10 09:55:43 -08:00
Koushik Dutta
04065a3487 core/client: publish 2024-12-10 09:42:00 -08:00
Koushik Dutta
ac882c723a server: fix cluster dependencies 2024-12-10 09:40:16 -08:00
Koushik Dutta
02cde6382c server: revert tsconfig change 2024-12-10 09:33:04 -08:00
Koushik Dutta
f942a13e90 client: add cluster manager 2024-12-10 09:32:23 -08:00
Koushik Dutta
6299caac20 server: fix typings 2024-12-10 09:30:29 -08:00
Koushik Dutta
6f0501634f server: move cluster manager 2024-12-10 09:30:13 -08:00
Koushik Dutta
575e544c40 core: Fix nre if clusterManager does not exist 2024-12-09 21:45:24 -08:00
Koushik Dutta
ff448e9c7f core: manage cluster through ui 2024-12-09 21:19:10 -08:00
Koushik Dutta
6173d67bb0 postbeta 2024-12-09 14:18:44 -08:00
Koushik Dutta
4431158bfa server: add various controls for server node in cluster mode 2024-12-09 14:04:43 -08:00
Koushik Dutta
822054e888 videoanalysis: hide decoder option if detection provides it 2024-12-09 10:07:42 -08:00
Koushik Dutta
d7e21d1d44 mqtt: fix exports 2024-12-08 16:08:36 -08:00
Koushik Dutta
6e451a1b06 server: npm audit 2024-12-07 18:53:49 -08:00
Koushik Dutta
57eccd4ad7 Merge branch 'main' into cluster 2024-12-07 18:37:30 -08:00
Koushik Dutta
c2d45e4357 cloud: additional tunnel check 2024-12-07 18:37:16 -08:00
Koushik Dutta
698a4a4a4a postbeta 2024-12-06 12:47:06 -08:00
Koushik Dutta
01493e311d server: fix repl in cluster mode 2024-12-06 12:46:49 -08:00
Koushik Dutta
a6d62365dc postbeta 2024-12-06 11:52:53 -08:00
Koushik Dutta
9b504a280f docker: switch to noble base 2024-12-06 11:52:26 -08:00
Koushik Dutta
5df8689236 docker: update intel for legacy + latest install process. 2024-12-06 11:39:08 -08:00
Koushik Dutta
235d408f1f dockker: test noble 2024-12-06 11:27:09 -08:00
Koushik Dutta
9b4547be85 docker: prevent pip upgrade 2024-12-06 11:14:02 -08:00
Koushik Dutta
0b8bc0d0d1 docker: python3.12 2024-12-06 10:54:05 -08:00
Koushik Dutta
134d4be1b7 docker: noble base 2024-12-06 10:47:01 -08:00
Koushik Dutta
e77487ed15 docker: noble base 2024-12-06 10:46:25 -08:00
Koushik Dutta
6e60fe1c09 openvino: disable npu due to openvino bug 2024-12-06 10:07:43 -08:00
Long Zheng
a7424b3546 Update Windows install script workaround npm issue (#1654)
* Update Windows install script workaround npm issue

* Test removing RunAsAdministrator

* Revert "Test removing RunAsAdministrator"

This reverts commit 46c80964ea.

* Test removing npm fix

* Revert "Test removing npm fix"

This reverts commit 0f9adbeae6.

* Test dump daemon logs

* More test

* More tests

* More tests

* Test

* Test

* Test

* Test

* Cleanup

* Add spawn error handler

* Fix event handler

* Remove node version debug
2024-12-06 09:19:01 -08:00
Koushik Dutta
70cf3488ef openvino: add scrypted_yolov9t_relu_int8_320 2024-12-06 08:49:56 -08:00
Koushik Dutta
d74ac6fb8e server: cleanup thread peers 2024-12-05 21:00:07 -08:00
Koushik Dutta
47cad5d747 mqtt: fix break 2024-12-05 12:58:58 -08:00
Koushik Dutta
4ebb7215c0 server: fix HoL in sendStream by using a dedicated cluster connect 2024-12-05 12:24:55 -08:00
Koushik Dutta
1d55830f10 server: sendStream should also be one way 2024-12-05 11:38:40 -08:00
Koushik Dutta
0bb5c79875 postbeta 2024-12-05 11:37:54 -08:00
Koushik Dutta
e3ca09a80b server/sdk: add support for HttpResponse.sendStream 2024-12-05 11:37:27 -08:00
Koushik Dutta
ef53829ccc core: clean up device groups 2024-12-05 09:38:36 -08:00
Koushik Dutta
5f0cf6b6c2 mqtt: remove scrypted-eval fs hack 2024-12-05 09:38:00 -08:00
Koushik Dutta
59e09825ff postbeta 2024-12-05 09:24:47 -08:00
Koushik Dutta
b4aa20b4cd server: remove legacy storage event 2024-12-05 09:24:36 -08:00
Koushik Dutta
e2eba2a227 openvino: beta 2024-12-05 08:46:36 -08:00
Koushik Dutta
9370a163fd videoanalysis: improve low watermark throttling 2024-12-05 08:45:58 -08:00
Koushik Dutta
c65f38f251 postbeta 2024-12-05 08:45:13 -08:00
Koushik Dutta
83bde83a39 server: ensure cluster client service control 2024-12-05 08:45:03 -08:00
Koushik Dutta
786b4b5ed9 postbeta 2024-12-04 21:32:07 -08:00
Koushik Dutta
f9c1d7704a sdk: fix sourcemap 2024-12-04 21:31:52 -08:00
Koushik Dutta
3162d2be34 Update install-amd-graphics.sh 2024-12-04 20:48:34 -08:00
Koushik Dutta
3f0a788a6a sdk/server: add mode to workers 2024-12-04 17:51:36 -08:00
Koushik Dutta
72504286ea postbeta 2024-12-04 15:59:56 -08:00
Koushik Dutta
c664cc3b4d server/sdk: include/check sdk version for plugin forwards/backwards compat. 2024-12-04 15:59:41 -08:00
Koushik Dutta
99853906b9 postbeta 2024-12-04 11:42:23 -08:00
Koushik Dutta
ea873a527b sdk/server: clean up sdk init race conditions to allow side effect imports 2024-12-04 10:54:28 -08:00
Koushik Dutta
df0b13512a videoanalysis: fix sample history tracking to purge before measurement 2024-12-03 13:42:40 -08:00
Koushik Dutta
1651152eec videoanalysis: publish 2024-12-03 10:06:36 -08:00
Koushik Dutta
7b3ab501b2 server: remove cluster cpu tracking 2024-12-03 00:21:46 -08:00
Koushik Dutta
3a77a3398d videoanalysis: use detection fps as measurement of system load 2024-12-03 00:00:37 -08:00
Koushik Dutta
f2ece1270a openvino: update to relu+int8 models 2024-12-02 22:49:58 -08:00
Koushik Dutta
8b6d8aeae6 Merge branch 'main' into cluster 2024-12-02 22:14:01 -08:00
Koushik Dutta
578bba67f8 postbeta 2024-12-02 19:00:55 -08:00
Koushik Dutta
15fb7d86e2 postbeta 2024-12-02 18:58:00 -08:00
Koushik Dutta
9d23caa66d server: downgrade typescript 2024-12-02 18:57:51 -08:00
Koushik Dutta
c46ed2cef5 postbeta 2024-12-02 18:46:17 -08:00
Koushik Dutta
7dcfdaa98e server: prevent crash on missing cpu usage 2024-12-02 18:46:06 -08:00
Koushik Dutta
ee23c93132 postbeta 2024-12-02 15:41:50 -08:00
Koushik Dutta
b1b0dd8997 server: fix non cluster crash 2024-12-02 15:41:42 -08:00
Koushik Dutta
6cc5a0e04c postbeta 2024-12-02 15:10:52 -08:00
Koushik Dutta
a75b263141 server: cluster cpu usage monitoring 2024-12-02 15:08:56 -08:00
Koushik Dutta
d91ec68e6c sdk: find object cluster worker affinity 2024-12-02 11:02:35 -08:00
Koushik Dutta
8ccc7a6c06 postbeta 2024-12-01 20:09:18 -08:00
Koushik Dutta
a6ece48cc3 server: add cluster worker weight 2024-12-01 17:46:14 -08:00
Koushik Dutta
7398f280cc postbeta 2024-11-30 23:19:40 -08:00
Koushik Dutta
eb1d0f647a server: fix es imports on old node 2024-11-30 23:19:30 -08:00
Koushik Dutta
b5a40b27a9 postbeta 2024-11-30 22:52:37 -08:00
Koushik Dutta
84870b444c server: revert createRequire 2024-11-30 22:49:59 -08:00
Koushik Dutta
339c934dda sdk: rollup fixes 2024-11-30 22:46:12 -08:00
Koushik Dutta
4df0eec70a server: esmodule cleanups 2024-11-30 20:00:41 -08:00
Koushik Dutta
6d268ade69 server: formatting 2024-11-30 09:41:30 -08:00
Koushik Dutta
6b040954a0 esmodule: project cleanup 2024-11-30 09:38:15 -08:00
Koushik Dutta
73d2f5b408 esmodule plugins: wip 2024-11-30 09:09:38 -08:00
Koushik Dutta
71bb2ec80a postbeta 2024-11-29 14:06:02 -08:00
Koushik Dutta
92b120886c server: remove log from eval params 2024-11-29 14:05:52 -08:00
Koushik Dutta
9001d996e2 sdk: fix commonjs entry mangling debug 2024-11-28 22:13:11 -08:00
Koushik Dutta
d060a74689 docker: Update install-amd-graphics.sh 2024-11-28 20:09:51 -08:00
Koushik Dutta
8ba4c46576 docker: Update install-intel-npu.sh 2024-11-28 19:56:16 -08:00
Koushik Dutta
a77f82462d openvino: rollback openvino 2024-11-28 19:53:24 -08:00
Koushik Dutta
3a1401afbb openvino: bump openvino 2024-11-28 19:44:47 -08:00
Koushik Dutta
14ae374916 postbeta 2024-11-28 19:37:55 -08:00
Koushik Dutta
52d915cc68 server: dependency updates 2024-11-28 19:37:44 -08:00
Koushik Dutta
cb501e66c6 sdk: publush 2024-11-28 19:14:34 -08:00
Koushik Dutta
053b43128f Merge branch 'cluster' of github.com:koush/scrypted into cluster 2024-11-28 18:47:41 -08:00
Koushik Dutta
702456a40d sdk: rollup support 2024-11-28 18:25:12 -08:00
Koushik Dutta
17ebbb1656 sdk: rollup support 2024-11-28 18:24:46 -08:00
Koushik Dutta
0f79cd88ce sdk: remove dead dep 2024-11-28 12:41:07 -08:00
Koushik Dutta
76c960100c sdk: remove dead dep 2024-11-28 12:39:11 -08:00
Koushik Dutta
6d56e41651 sample: update 2024-11-28 10:56:56 -08:00
Koushik Dutta
640d66474c postbeta 2024-11-28 10:48:19 -08:00
Koushik Dutta
238c82a354 server: add cluster worker info 2024-11-28 10:42:42 -08:00
Koushik Dutta
25a369403c server: add cluster worker info 2024-11-28 10:41:04 -08:00
Koushik Dutta
b1e1f54af5 server: add hooks to get cluster worker controls 2024-11-28 10:02:08 -08:00
Koushik Dutta
8df38dbebe server: add env control 2024-11-28 09:43:22 -08:00
Koushik Dutta
229dcd3174 postbeta 2024-11-28 09:28:34 -08:00
Koushik Dutta
c3d6dcb6a2 server: pass through service control 2024-11-28 09:28:24 -08:00
Koushik Dutta
0c951519e2 postbeta 2024-11-28 08:57:46 -08:00
Koushik Dutta
1f406ae740 server: pass through service control for cluster mode 2024-11-28 08:57:23 -08:00
Koushik Dutta
8d0de7e557 postbeta 2024-11-27 18:39:51 -08:00
Koushik Dutta
e799ada9c9 sdk: fixup clusterWorkerId to be optional 2024-11-26 09:02:01 -08:00
Koushik Dutta
8e888bc6a1 Update docker-compose.yml 2024-11-23 22:05:56 -08:00
Koushik Dutta
c5053008b7 docker: use relative path for volume. 2024-11-23 22:04:33 -08:00
147 changed files with 6499 additions and 6923 deletions

View File

@@ -11,7 +11,7 @@ jobs:
NODE_VERSION: '20'
strategy:
matrix:
BASE: ["jammy"]
BASE: ["noble"]
FLAVOR: ["full", "lite"]
steps:
- name: Check out the repo
@@ -83,7 +83,7 @@ jobs:
runs-on: self-hosted
strategy:
matrix:
BASE: ["jammy"]
BASE: ["noble"]
steps:
- name: Check out the repo
uses: actions/checkout@v3

View File

@@ -20,9 +20,9 @@ jobs:
strategy:
matrix:
BASE: [
["jammy-nvidia", ".s6"],
["jammy-full", ".s6"],
["jammy-lite", ""],
["noble-nvidia", ".s6"],
["noble-full", ".s6"],
["noble-lite", ""],
]
steps:
- name: Check out the repo
@@ -95,15 +95,15 @@ jobs:
push: true
tags: |
${{ format('koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
${{ matrix.BASE[0] == 'jammy-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'koush/scrypted:nvidia' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'koush/scrypted:lite' || '' }}
${{ matrix.BASE[0] == 'noble-full' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-nvidia' && 'koush/scrypted:nvidia' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-full' && 'koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-lite' && 'koush/scrypted:lite' || '' }}
${{ format('ghcr.io/koush/scrypted:v{1}-{0}', matrix.BASE[0], github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
${{ matrix.BASE[0] == 'jammy-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-full' && 'ghcr.io/koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'jammy-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
${{ matrix.BASE[0] == 'noble-full' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-nvidia' && 'ghcr.io/koush/scrypted:nvidia' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-full' && 'ghcr.io/koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE[0] == 'noble-lite' && 'ghcr.io/koush/scrypted:lite' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -63,7 +63,6 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
const allParams = Object.assign({}, params, {
sdk,
fs: require('realfs'),
ScryptedDeviceBase,
MixinDeviceBase,
StorageSettings,

View File

@@ -19,7 +19,7 @@ function isPi(model: string) {
export function isRaspberryPi() {
let cpuInfo: string;
try {
cpuInfo = require('realfs').readFileSync('/proc/cpuinfo', { encoding: 'utf8' });
cpuInfo = require('fs').readFileSync('/proc/cpuinfo', { encoding: 'utf8' });
}
catch (e) {
// if this fails, this is probably not a pi

View File

@@ -51,8 +51,9 @@ RUN apt-get -y install \
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install debugpy typing_extensions psutil
# ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
# RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install debugpy
################################################################
# End section generated from template/Dockerfile.full.header
@@ -85,8 +86,8 @@ RUN add-apt-repository -y ppa:deadsnakes/ppa && \
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3.9 -m pip install --upgrade pip
RUN python3.9 -m pip install debugpy typing_extensions psutil
# RUN python3.9 -m pip install --upgrade pip
RUN python3.9 -m pip install debugpy
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
@@ -94,16 +95,20 @@ RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" |
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
# set default shell to bash
RUN chsh -s /bin/bash
ENV SHELL="/bin/bash"
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.12"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
ENV SCRYPTED_DOCKER_FLAVOR="full"

View File

@@ -22,8 +22,8 @@ ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.10"
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.12"
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
ENV SCRYPTED_DOCKER_FLAVOR="lite"

View File

@@ -75,7 +75,8 @@ services:
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
# Default volume for the Scrypted database. Typically should not be changed.
- ~/.scrypted/volume:/server/volume
# The volume will be placed relative to this docker-compose.yml.
- ./volume:/server/volume
# LXC usage only
# lxc - /var/run/docker.sock:/var/run/docker.sock

View File

@@ -23,7 +23,13 @@ fi
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
FILENAME="amdgpu-install_6.2.60202-1_all.deb"
FILENAME=$(curl -s -L https://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/ | grep -o 'amdgpu-install_[^ ]*' | cut -d'"' -f1)
if [ -z "$FILENAME" ]
then
echo "AMD graphics package can not be installed. Could not find the package name."
exit 1
fi
set -e
mkdir -p /tmp/amd
cd /tmp/amd

View File

@@ -29,27 +29,50 @@ apt-get -y update &&
apt-get -y install intel-media-va-driver-non-free &&
apt-get -y dist-upgrade;
# manual installation
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
# these debs are seemingly ubuntu 22.04 only.
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
apt-get install -y ocl-icd-libopencl1
# very stupid legacy + current install process conflict.
# install 24.35.30872.22 for legacy support. Then install latest.
# https://github.com/intel/compute-runtime/issues/770#issuecomment-2515166915
# https://github.com/intel/compute-runtime/releases/tag/24.35.30872.22
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-core_1.0.17537.20_amd64.deb
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.20/intel-igc-opencl_1.0.17537.20_amd64.deb
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-dbgsym_1.3.30872.22_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1-dbgsym_1.3.30872.22_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu-legacy1_1.3.30872.22_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-level-zero-gpu_1.3.30872.22_amd64.deb
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
#curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-dbgsym_24.35.30872.22_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1-dbgsym_24.35.30872.22_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd-legacy1_24.35.30872.22_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/intel-opencl-icd_24.35.30872.22_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.35.30872.22/libigdgmm12_22.5.0_amd64.deb
dpkg -i *.deb
rm -f *.deb
# https://github.com/intel/compute-runtime/releases/tag/24.45.31740.9
# note that at time of commit, IGC supports ubuntu 24.04 only possibly due to their builder being on 24.04.
IGC_VERSION=2_2.1.12+18087_amd64
COMPUTE_VERSION=24.45.31740.9
ZERO_GPU_VERSION=1.6.31740.9_amd64
LIBIGDGMM_VERSION=22.5.2_amd64
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.1.12/intel-igc-core-$IGC_VERSION.deb
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/v2.1.12/intel-igc-opencl-$IGC_VERSION.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu-dbgsym_$ZERO_GPU_VERSION.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-level-zero-gpu_$ZERO_GPU_VERSION.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd-dbgsym_"$COMPUTE_VERSION"_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/intel-opencl-icd_"$COMPUTE_VERSION"_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/$COMPUTE_VERSION/libigdgmm12_$LIBIGDGMM_VERSION.deb
set +e
dpkg -i *.deb
set -e
# the legacy + latest process says this may be necessary but it does not seem to be in a clean environment.
apt-get install --fix-broken
cd /tmp && rm -rf /tmp/gpu

View File

@@ -38,7 +38,7 @@ set -e
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
# level zero must also be installed
LEVEL_ZERO_VERSION=1.18.5
LEVEL_ZERO_VERSION=1.19.2
# https://github.com/oneapi-src/level-zero
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero_"$LEVEL_ZERO_VERSION"+u$distro.deb
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v"$LEVEL_ZERO_VERSION"/level-zero-devel_"$LEVEL_ZERO_VERSION"+u$distro.deb

View File

@@ -128,7 +128,7 @@ then
set -e
removescryptedfstab
mkdir -p /mnt/scrypted-nvr
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail,noatime 0 0" >> /etc/fstab
echo "UUID=$UUID /mnt/scrypted-nvr ext4 defaults,nofail,noatime,x-systemd.automount 0 0" >> /etc/fstab
mount -a
systemctl daemon-reload
set +e

View File

@@ -26,8 +26,8 @@ RUN add-apt-repository -y ppa:deadsnakes/ppa && \
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3.9 -m pip install --upgrade pip
RUN python3.9 -m pip install debugpy typing_extensions psutil
# RUN python3.9 -m pip install --upgrade pip
RUN python3.9 -m pip install debugpy
# Coral Edge TPU
# https://coral.ai/docs/accelerator/get-started/#runtime-on-linux
@@ -35,16 +35,20 @@ RUN echo "deb https://packages.cloud.google.com/apt coral-edgetpu-stable main" |
RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update && apt-get -y install libedgetpu1-std
# set default shell to bash
RUN chsh -s /bin/bash
ENV SHELL="/bin/bash"
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.12"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
ENV SCRYPTED_PYTHON312_PATH="/usr/bin/python3.12"
ENV SCRYPTED_DOCKER_FLAVOR="full"

View File

@@ -48,8 +48,9 @@ RUN apt-get -y install \
# allow pip to install to system
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install debugpy typing_extensions psutil
# ERROR: Cannot uninstall pip 24.0, RECORD file not found. Hint: The package was installed by debian.
# RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install debugpy
################################################################
# End section generated from template/Dockerfile.full.header

View File

@@ -69,11 +69,14 @@ then
fi
RUN python$PYTHON_VERSION -m pip install --upgrade pip
# besides debugpy, none of these dependencies are needed anymore?
# portable python includes typing and does not need typing_extensions.
# opencv-python-headless has wheels for macos.
if [ "$PYTHON_VERSION" != "3.10" ]
then
RUN python$PYTHON_VERSION -m pip install typing
fi
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python psutil
RUN python$PYTHON_VERSION -m pip install debugpy typing_extensions opencv-python
echo "Installing Scrypted Launch Agent..."

View File

@@ -1,3 +1,5 @@
#Requires -RunAsAdministrator
# Set-PSDebug -Trace 1
# stop existing service if any
@@ -8,7 +10,7 @@ sc.exe stop scrypted.exe
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
# Install node.js
choco upgrade -y nodejs-lts --version=20.11.1
choco upgrade -y nodejs-lts --version=20.18.0
# Install VC Redist, which is necessary for portable python
choco install -y vcredist140
@@ -22,11 +24,19 @@ $SCRYPTED_WINDOWS_PYTHON_VERSION="-3.9"
# Refresh environment variables for py and npx to work
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
# Workaround Windows Node no longer creating %APPDATA%\npm which causes npx to fail
# Fixed in newer versions of NPM but not the one bundled with Node 20
# https://github.com/nodejs/node/issues/53538
npm i -g npm
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install --upgrade pip
# besides debugpy, none of these dependencies are needed anymore?
# portable python includes typing and does not need typing_extensions.
# opencv-python-headless has wheels for windows.
py $SCRYPTED_WINDOWS_PYTHON_VERSION -m pip install debugpy typing_extensions typing opencv-python
$SCRYPTED_INSTALL_VERSION=[System.Environment]::GetEnvironmentVariable("SCRYPTED_INSTALL_VERSION","User")
if ($SCRYPTED_INSTALL_VERSION -eq $null) {
npx -y scrypted@latest install-server
} else {
@@ -41,6 +51,8 @@ npm install --prefix $SCRYPTED_HOME @koush/node-windows --save
$NPX_PATH = (Get-Command npx).Path
# The path needs double quotes to handle spaces in the directory path
$NPX_PATH_ESCAPED = '"' + $NPX_PATH.replace('\', '\\') + '"'
# On newer versions of NPM, the NPX might be a .ps1 file which doesn't work with child_process.spawn, change to .cmd
$NPX_PATH_ESCAPED = $NPX_PATH_ESCAPED.replace('.ps1', '.cmd')
$SERVICE_JS = @"
const fs = require('fs');
@@ -54,6 +66,8 @@ child_process.spawn('$NPX_PATH_ESCAPED', ['-y', 'scrypted', 'serve'], {
stdio: 'inherit',
// allow spawning .cmd https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2
shell: true,
}).on('error', (err) => {
console.error('Error spawning child process', err);
});
"@
@@ -99,6 +113,9 @@ svc.on("install", () => {
svc.on("start", () => {
console.log("Service started");
});
svc.on("error", (err) => {
console.log("Service error", err);
});
svc.install();
"@

18
install/proxmox/docker-compose.sh Normal file → Executable file
View File

@@ -4,21 +4,15 @@ cd /root/.scrypted
# always immediately upgrade everything in case there's a broken update.
# this will also be preferable for troubleshooting via lxc reboot.
export DEBIAN_FRONTEND=noninteractive
(apt -y --fix-broken install && (yes | dpkg --configure -a) && apt -y update && apt -y dist-upgrade) &
yes | dpkg --configure -a
apt -y --fix-broken install && apt -y update && apt -y dist-upgrade
# foreground pull if requested.
if [ -e "volume/.pull" ]
then
rm -rf volume/.pull
PULL="--pull"
(sleep 300 && docker container prune -f && docker image prune -a -f) &
else
# always background pull in case there's a broken image.
(sleep 300 && docker compose pull && docker container prune -f && docker image prune -a -f) &
fi
# force a pull to ensure we have the latest images.
# not using --pull always cause that fails everything on network down
docker compose pull
# do not daemonize, when it exits, systemd will restart it.
# force a recreate as .env may have changed.
# furthermore force recreate gets the container back into a known state
# which is preferable in case the user has made manual changes and then restarts.
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit $PULL
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit

View File

@@ -26,8 +26,7 @@ then
fi
SCRYPTED_BACKUP_VMID=10445
if [ -n "$SCRYPTED_RESTORE" ]
then
function prepareScryptedRestore() {
pct config $VMID 2>&1 > /dev/null
if [ "$?" != "0" ]
then
@@ -43,6 +42,11 @@ then
RESTORE_VMID=$VMID
VMID=$SCRYPTED_BACKUP_VMID
pct destroy $VMID 2>&1 > /dev/null
}
if [ -n "$SCRYPTED_RESTORE" ]
then
prepareScryptedRestore
fi
echo "Downloading scrypted container backup."
@@ -71,31 +75,56 @@ then
echo ""
echo "==============================================================="
echo "Existing container $VMID found."
echo "Please choose from the following options to resolve this error."
echo "==============================================================="
echo ""
echo "1. To reinstall and reset Scrypted, run this script with --force to overwrite the existing container."
echo "THIS WILL WIPE THE EXISTING CONFIGURATION:"
echo ""
echo "VMID=$VMID bash $0 --force"
echo ""
echo "2. To reinstall Scrypted and and retain existing configuration, run this script with the environment variable SCRYPTED_RESTORE=true."
echo "This script can be used ro reinstall Scrypted and reset the container to a factory state."
echo "This preserves existing data. Creating a backup within Scrypted is highly recommended in case the reset fails."
echo "THIS WILL WIPE ADDITIONAL VOLUMES SUCH AS NVR STORAGE. NVR volumes will need to be readded after the restore:"
echo ""
echo "SCRYPTED_RESTORE=true VMID=$VMID bash $0"
echo ""
echo "3. To install and run multiple Scrypted containers, run this script with the environment variable specifying"
echo "the new VMID=<number>. For example, to create a new LXC with VMID 12345:"
echo ""
echo "VMID=12345 bash $0"
readyn "Reinstall Scrypted and and retain existing configuration?"
exit 1
if [ "$yn" != "y" ]
then
echo ""
echo "1. To reinstall and reset Scrypted, run this script with --force to overwrite the existing container."
echo "THIS WILL WIPE THE EXISTING CONFIGURATION:"
echo ""
echo "VMID=$VMID bash $0 --force"
echo ""
echo "2. To reinstall Scrypted and and retain existing configuration, run this script with the environment variable SCRYPTED_RESTORE=true."
echo "This preserves existing data. Creating a backup within Scrypted is highly recommended in case the reset fails."
echo "THIS WILL WIPE ADDITIONAL VOLUMES SUCH AS NVR STORAGE. NVR volumes will need to be readded after the restore:"
echo ""
echo "SCRYPTED_RESTORE=true VMID=$VMID bash $0"
echo ""
echo "3. To install and run multiple Scrypted containers, run this script with the environment variable specifying"
echo "the new VMID=<number>. For example, to create a new LXC with VMID 12345:"
echo ""
echo "VMID=12345 bash $0"
exit 1
fi
SCRYPTED_RESTORE=true
prepareScryptedRestore
fi
fi
if [[ ! "$@" =~ "--storage" ]]
then
HAS_LOCAL_LVM=$(pvesm status | grep local-lvm | grep active)
HAS_LOCAL_ZFS=$(pvesm status | grep local-zfs | grep active)
if [ ! -z "$HAS_LOCAL_LVM" ]
then
RESTORE_STORAGE="--storage local-lvm"
elif [ ! -z "$HAS_LOCAL_ZFS" ]
then
RESTORE_STORAGE="--storage local-zfs"
else
echo "Could not determine a valid storage device. One may need to be specified manually."
fi
fi
pct stop $VMID 2>&1 > /dev/null
pct restore $VMID $SCRYPTED_TAR_ZST $@
pct restore $VMID $SCRYPTED_TAR_ZST $RESTORE_STORAGE $@
if [ "$?" != "0" ]
then
@@ -150,7 +179,7 @@ if [ -n "$SCRYPTED_RESTORE" ]
then
echo ""
echo ""
echo "Running this script will reset the Scrypted container to a factory state while preserving existing data."
echo "This script will reset the Scrypted container to a factory state while preserving existing data."
echo "IT IS RECOMMENDED TO CREATE A BACKUP INSIDE SCRYPTED FIRST."
readyn "Are you sure you want to continue?"
if [ "$yn" != "y" ]
@@ -220,7 +249,7 @@ then
VMID=$RESTORE_VMID
echo "Restoring with reset image..."
pct restore --force 1 $VMID *.tar $@
pct restore --force 1 $VMID *.tar $RESTORE_STORAGE $@
echo "Restoring volumes..."
move_volume $SCRYPTED_BACKUP_VMID $VMID mp0 hide-warning
@@ -233,6 +262,9 @@ then
pct destroy $SCRYPTED_BACKUP_VMID
fi
echo "Enabling startup on boot..."
pct set $VMID -onboot 1
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
if [ "$yn" == "y" ]
then

View File

@@ -1,15 +1,15 @@
{
"name": "@scrypted/client",
"version": "1.3.6",
"version": "1.3.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.6",
"version": "1.3.9",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.3.66",
"@scrypted/types": "^0.3.92",
"engine.io-client": "^6.6.1",
"follow-redirects": "^1.15.9",
"rimraf": "^6.0.1"
@@ -17,6 +17,7 @@
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^22.7.4",
"@types/ws": "^8.5.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
}
@@ -75,9 +76,10 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.66",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.66.tgz",
"integrity": "sha512-POHpVgW6Ce8mnJRaXZRm+2RtvFuPP+ZehsDrhUqkQdxmnV81m8K2+3M6Vhrt+07kNDXmrznAijoj/OzXkdZWgw=="
"version": "0.3.92",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.92.tgz",
"integrity": "sha512-/M1Lg42/yoFWusj5+Lyp2S0JCiWDDWcmsjiUnTf1DahZ6/M2oZ3bwR/0KX3D9vJE79owWST1Gm0+Rdvpxuil9A==",
"license": "ISC"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -126,6 +128,16 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -211,9 +223,10 @@
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.3.6",
"version": "1.3.9",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -14,11 +14,12 @@
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^22.7.4",
"@types/ws": "^8.5.13",
"ts-node": "^10.9.2",
"typescript": "^5.6.2"
},
"dependencies": {
"@scrypted/types": "^0.3.66",
"@scrypted/types": "^0.3.92",
"engine.io-client": "^6.6.1",
"follow-redirects": "^1.15.9",
"rimraf": "^6.0.1"

View File

@@ -700,6 +700,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
deviceManager,
endpointManager,
mediaManager,
clusterManager,
} = scrypted;
console.log('api attached', Date.now() - start);
@@ -859,6 +860,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
connectionType,
admin,
systemManager,
clusterManager,
deviceManager,
endpointManager,
mediaManager,

View File

@@ -56,13 +56,13 @@ Scrypted Cloud automatically creates a login free tunnel for remote access.
The following steps are only necessary if you want to associate the tunnel with your existing Cloudflare account to manage it remotely.
1. Create the Tunnel in the [Cloudflare Zero Trust Dashboard](https://one.dash.cloudflare.com).
2. Copy the token shown for the tunnel shown in the `install [token]` command. For example, if you see `cloudflared service install eyJhI344aA...`, then `eyJhI344aA...` is the token you need to copy.
3. Paste the token into the Cloud Plugin Advanced Settings.
4. Add a `Public Hostname` to the tunnel.
* Choose a (sub)domain.
* Service `Type` is `HTTPS` and `URL` is `localhost:port`. Replace the port with `Forward Port` from Cloud Plugin Settings.
* Expand `Additional Application Settings` -> `TLS` menus and enable `No TLS Verify`.
1. Navigate to the Cloud Plugin's Cloudflare Settings.
2. Enter the Cloudflare subdomain, e.g. `scrypted.example.org`.
3. Open the authorization link printed in the Log in a browser.
4. Log in to Cloudflare if prompted. Then open the authorization link again.
5. Select the domain for the specified the subdomain.
6. Authorization should now be complete.
5. Reload Cloud Plugin.
6. Verify Cloudflare successfully connected by observing the `Console` Logs.
::: info
Visiting the authorization link twice as directed in the above instructions may be necessary. Cloudflare will not prompt a with a list of domains unless the browser session is already logged in.
:::

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,7 @@
]
},
"dependencies": {
"@eneris/push-receiver": "^4.2.0",
"@eneris/push-receiver": "^4.3.0",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"bpmux": "^8.2.1",
@@ -48,10 +48,9 @@
},
"devDependencies": {
"@types/http-proxy": "^1.17.15",
"@types/ip": "^1.1.3",
"@types/nat-upnp": "^1.1.5",
"@types/node": "^22.5.2",
"@types/node": "^22.10.1",
"ts-node": "^10.9.2"
},
"version": "0.2.47"
"version": "0.2.48"
}

View File

@@ -183,6 +183,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.storageSettings.values.cloudflaredTunnelCredentials = undefined;
this.doCloudflaredLogin(nv);
},
console: true,
},
cloudflaredTunnelLoginUrl: {
group: 'Cloudflare',
@@ -1056,6 +1057,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if ((line.includes('Unregistered tunnel connection')
|| line.includes('Connection terminated error')
|| line.includes('Register tunnel error')
|| line.includes('Failed to serve tunnel')
|| line.includes('Failed to get tunnel'))
&& deferred.finished) {
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');

View File

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

View File

@@ -0,0 +1 @@
../../../../install/proxmox/docker-compose.sh

View File

@@ -1,22 +0,0 @@
[Unit]
Description=Scrypted service
After=network.target
[Service]
User=root
Group=root
Type=simple
ExecStart=/usr/bin/npx -y scrypted serve
Restart=always
RestartSec=3
Environment="NODE_OPTIONS=--dns-result-order=ipv4first"
Environment="SCRYPTED_PYTHON_PATH=/usr/bin/python3"
Environment="SCRYPTED_PYTHON39_PATH=/usr/bin/python3.9"
Environment="SCRYPTED_PYTHON310_PATH=/usr/bin/python3.10"
Environment="SCRYPTED_FFMPEG_PATH=/usr/bin/ffmpeg"
Environment="SCRYPTED_INSTALL_ENVIRONMENT=lxc"
StandardOutput=null
StandardError=null
[Install]
WantedBy=multi-user.target

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/core",
"version": "0.3.86",
"version": "0.3.102",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.3.86",
"version": "0.3.102",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -88,21 +88,28 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.63",
"version": "0.3.100",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.24.7",
"adm-zip": "^0.5.14",
"axios": "^1.7.3",
"babel-loader": "^9.1.3",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -115,11 +122,9 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^22.1.0",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.5"
"typedoc": "^0.26.11"
}
},
"node_modules/@scrypted/common": {
@@ -281,23 +286,28 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.24.7",
"@types/node": "^22.1.0",
"@types/stringify-object": "^4.0.5",
"adm-zip": "^0.5.14",
"axios": "^1.7.3",
"babel-loader": "^9.1.3",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"@types/node": "^22.10.1",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"stringify-object": "^3.3.0",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.5",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"tslib": "^2.8.1",
"typedoc": "^0.26.11",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
}
},

View File

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

View File

@@ -1,5 +1,5 @@
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, Readme, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk';
import { AggregateDevice, createAggregateDevice } from './aggregate';
import { AggregateDevice } from './aggregate';
const { deviceManager } = sdk;
export const AggregateCoreNativeId = 'aggregatecore';
@@ -13,24 +13,6 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
this.systemDevice = {
deviceCreator: 'Device Group',
};
for (const nativeId of deviceManager.getNativeIds()) {
if (nativeId?.startsWith('aggregate:')) {
const aggregate = createAggregateDevice(nativeId);
this.aggregate.set(nativeId, aggregate);
this.reportAggregate(nativeId, aggregate.computeInterfaces(), aggregate.providedName);
}
}
sdk.systemManager.listen((eventSource, eventDetails, eventData) => {
if (eventDetails.eventInterface === 'Storage') {
const ids = [...this.aggregate.values()].map(a => a.id);
if (ids.includes(eventSource.id)) {
const aggregate = [...this.aggregate.values()].find(a => a.id === eventSource.id);
this.reportAggregate(aggregate.nativeId, aggregate.computeInterfaces(), aggregate.providedName);
}
}
});
}
async getReadmeMarkdown(): Promise<string> {
@@ -51,7 +33,8 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
const { name } = settings;
const nativeId = `aggregate:${Math.random()}`;
await this.reportAggregate(nativeId, [], name?.toString());
const aggregate = createAggregateDevice(nativeId);
const aggregate = new AggregateDevice(this, nativeId);
aggregate.computeInterfaces();
this.aggregate.set(nativeId, aggregate);
return nativeId;
}
@@ -68,9 +51,17 @@ export class AggregateCore extends ScryptedDeviceBase implements DeviceProvider,
}
async getDevice(nativeId: string) {
return this.aggregate.get(nativeId);
let device = this.aggregate.get(nativeId);
if (device)
return device;
device = new AggregateDevice(this, nativeId);
device.computeInterfaces();
this.aggregate.set(nativeId, device);
return device;
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
const device = this.aggregate.get(nativeId);
device?.release();
}
}

View File

@@ -1,11 +1,8 @@
import sdk, { EventListener, EventListenerRegister, FFmpegInput, LockState, MediaStreamDestination, RequestMediaStreamOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import type { AggregateCore } from "./aggregate-core";
const { systemManager, mediaManager, deviceManager } = sdk;
export interface AggregateDevice extends ScryptedDeviceBase {
computeInterfaces(): string[];
}
interface Aggregator<T> {
(values: T[]): T;
}
@@ -141,143 +138,144 @@ function createVideoCamera(devices: VideoCamera[], console: Console): VideoCamer
}
}
export function createAggregateDevice(nativeId: string): AggregateDevice {
class AggregateDeviceImpl extends ScryptedDeviceBase implements Settings {
listeners: EventListenerRegister[] = [];
storageSettings = new StorageSettings(this, {
deviceInterfaces: {
title: 'Selected Device Interfaces',
description: 'The components of other devices to combine into this device group.',
type: 'interface',
multiple: true,
deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`,
export class AggregateDevice extends ScryptedDeviceBase implements Settings {
listeners: EventListenerRegister[] = [];
storageSettings = new StorageSettings(this, {
deviceInterfaces: {
title: 'Selected Device Interfaces',
description: 'The components of other devices to combine into this device group.',
type: 'interface',
multiple: true,
deviceFilter: `id !== '${this.id}' && deviceInterface !== '${ScryptedInterface.Settings}'`,
onPut: () => {
this.core.reportAggregate(this.nativeId, this.computeInterfaces(), this.providedName);
}
})
constructor() {
super(nativeId);
try {
const data = this.storage.getItem('data');
if (data) {
const { deviceInterfaces } = JSON.parse(data);
this.storageSettings.values.deviceInterfaces = deviceInterfaces;
}
}
catch (e) {
}
this.storage.removeItem('data');
}
})
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
constructor(public core: AggregateCore, nativeId: string) {
super(nativeId);
try {
const data = this.storage.getItem('data');
if (data) {
const { deviceInterfaces } = JSON.parse(data);
this.storageSettings.values.deviceInterfaces = deviceInterfaces;
}
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
catch (e) {
}
this.storage.removeItem('data');
}
makeListener(iface: string, devices: ScryptedDevice[]) {
const aggregator = aggregators.get(iface);
if (!aggregator) {
const ds = deviceManager.getDeviceState(this.nativeId);
// if this device can't be aggregated for whatever reason, pass property through.
for (const device of devices) {
const register = device.listen({
event: iface,
watch: true,
}, (source, details, data) => {
if (details.property)
ds[details.property] = data;
});
this.listeners.push(register);
}
return;
}
const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0];
if (!property) {
this.console.warn('aggregating interface with no property?', iface);
return;
}
const runAggregator = () => {
const values = devices.map(device => device[property]);
(this as any)[property] = aggregator(values);
}
const listener: EventListener = () => runAggregator();
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
makeListener(iface: string, devices: ScryptedDevice[]) {
const aggregator = aggregators.get(iface);
if (!aggregator) {
const ds = deviceManager.getDeviceState(this.nativeId);
// if this device can't be aggregated for whatever reason, pass property through.
for (const device of devices) {
const register = device.listen({
event: iface,
watch: true,
}, listener);
}, (source, details, data) => {
if (details.property)
ds[details.property] = data;
});
this.listeners.push(register);
}
return runAggregator;
return;
}
computeInterfaces(): string[] {
this.listeners.forEach(listener => listener.removeListener());
this.listeners = [];
try {
const interfaces = new Map<string, string[]>();
for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) {
const parts = deviceInterface.split('#');
const id = parts[0];
const iface = parts[1];
if (!interfaces.has(iface))
interfaces.set(iface, []);
interfaces.get(iface).push(id);
}
for (const [iface, ids] of interfaces.entries()) {
const devices = ids.map(id => systemManager.getDeviceById(id));
const runAggregator = this.makeListener(iface, devices);
runAggregator?.();
}
for (const [iface, ids] of interfaces.entries()) {
const devices = ids.map(id => systemManager.getDeviceById(id));
const descriptor = ScryptedInterfaceDescriptors[iface];
if (!descriptor) {
this.console.warn(`descriptor not found for ${iface}, skipping method generation`);
continue;
}
if (iface === ScryptedInterface.VideoCamera) {
const camera = createVideoCamera(devices as any, this.console);
for (const method of descriptor.methods) {
AggregateDeviceImpl.prototype[method] = (...args: any[]) => camera[method](...args);
}
continue;
}
for (const method of descriptor.methods) {
AggregateDeviceImpl.prototype[method] = async function (...args: any[]) {
const ret: Promise<any>[] = [];
for (const device of devices) {
ret.push(device[method](...args));
}
const results = await Promise.all(ret);
return results[0];
}
}
}
return [...interfaces.keys()];
}
catch (e) {
// this.console.error('error loading aggregate device', e);
return [];
}
const property = ScryptedInterfaceDescriptors[iface]?.properties?.[0];
if (!property) {
this.console.warn('aggregating interface with no property?', iface);
return;
}
const runAggregator = () => {
const values = devices.map(device => device[property]);
(this as any)[property] = aggregator(values);
}
const listener: EventListener = () => runAggregator();
for (const device of devices) {
const register = device.listen({
event: iface,
watch: true,
}, listener);
this.listeners.push(register);
}
return runAggregator;
}
const ret = new AggregateDeviceImpl();
ret.computeInterfaces();
return new AggregateDeviceImpl();
}
release() {
this.listeners.forEach(listener => listener.removeListener());
this.listeners = [];
}
computeInterfaces(): string[] {
this.release();
try {
const interfaces = new Map<string, string[]>();
for (const deviceInterface of this.storageSettings.values.deviceInterfaces as string[]) {
const parts = deviceInterface.split('#');
const id = parts[0];
const iface = parts[1];
if (!interfaces.has(iface))
interfaces.set(iface, []);
interfaces.get(iface).push(id);
}
for (const [iface, ids] of interfaces.entries()) {
const devices = ids.map(id => systemManager.getDeviceById(id));
const runAggregator = this.makeListener(iface, devices);
runAggregator?.();
}
for (const [iface, ids] of interfaces.entries()) {
const devices = ids.map(id => systemManager.getDeviceById(id));
const descriptor = ScryptedInterfaceDescriptors[iface];
if (!descriptor) {
this.console.warn(`descriptor not found for ${iface}, skipping method generation`);
continue;
}
if (iface === ScryptedInterface.VideoCamera) {
const camera = createVideoCamera(devices as any, this.console);
for (const method of descriptor.methods) {
this[method] = (...args: any[]) => camera[method](...args);
}
continue;
}
for (const method of descriptor.methods) {
this[method] = async function (...args: any[]) {
const ret: Promise<any>[] = [];
for (const device of devices) {
ret.push(device[method](...args));
}
const results = await Promise.all(ret);
return results[0];
}
}
}
return [...interfaces.keys()];
}
catch (e) {
// this.console.error('error loading aggregate device', e);
return [];
}
}
}

160
plugins/core/src/cluster.ts Normal file
View File

@@ -0,0 +1,160 @@
import { createAsyncQueue } from "@scrypted/common/src/async-queue";
import sdk, { Readme, ScryptedDeviceBase, ScryptedInterface, ScryptedSettings, Setting, Settings } from "@scrypted/sdk";
export const ClusterCoreNativeId = 'clustercore';
export class ClusterCore extends ScryptedDeviceBase implements Settings, Readme, ScryptedSettings {
writeQueue = createAsyncQueue<() => Promise<void>>();
constructor(nativeId: string) {
super(nativeId);
(async () => {
for await (const write of this.writeQueue.queue) {
try {
await write();
}
catch (e) {
this.console.error('error writing settings', e);
}
finally {
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
}
}
})();
}
async getSettings(): Promise<Setting[]> {
const mode = sdk.clusterManager?.getClusterMode?.();
if (!mode)
return [];
const workers = await sdk.clusterManager.getClusterWorkers();
const ret: Setting[] = [];
const clientWorkers = Object.values(workers);
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
for (const worker of clientWorkers) {
const group = `Worker: ${worker.name}`;
const name: Setting = {
key: `${worker.id}:name`,
group,
title: 'Name',
description: 'The friendly name of the worker.',
value: worker.name,
};
ret.push(name);
const mode: Setting = {
key: `${worker.id}:mode`,
group,
title: 'Mode',
description: 'The mode of the worker.',
value: worker.mode,
readonly: true,
};
ret.push(mode);
const envControl = await clusterFork.getEnvControl(worker.id);
// catch in case env is coming from vscode launch.json and no .env actually exists.
const dotEnv: string = await envControl.getDotEnv().catch(() => {});
const dotEnvLines = dotEnv?.split('\n') || worker.labels;
const dotEnvParsed = dotEnvLines.map(line => {
const trimmed = line.trim();
if (trimmed.startsWith('#')) {
return { line };
}
const [key, ...value] = trimmed.split('=');
return { key, value: value.join('='), line };
});
const workerLabels = dotEnvParsed.find(line => line.key === 'SCRYPTED_CLUSTER_LABELS')?.value?.split(',') || [];
const labelChoices = new Set<string>([
...workerLabels,
'storage',
'compute',
'compute.preferred',
'@scrypted/coreml',
'@scrypted/openvino',
'@scrypted/onnx',
'@scrypted/tensorflow-lite',
]);
const labels: Setting = {
key: `${worker.id}:labels`,
group,
title: 'Labels',
description: 'The labels to apply to this worker. Modifying the labels will restart the worker. Some labels, such as the host OS and architecture, cannot be changed.',
multiple: true,
combobox: true,
choices: [...labelChoices],
value: workerLabels,
};
ret.push(labels);
}
return ret;
}
async putSetting(key: string, value: any) {
await this.writeQueue.enqueue(async () => {
const split = key.split(':');
const [workerId, setting] = split;
const workers = await sdk.clusterManager.getClusterWorkers();
const worker = workers[workerId];
if (!worker)
return;
switch (setting) {
case 'name':
case 'labels':
break;
default:
return;
}
const clusterFork = await sdk.systemManager.getComponent('cluster-fork');
const envControl = await clusterFork.getEnvControl(worker.id);
const dotEnv: string = await envControl.getDotEnv().catch(() => {});
const dotEnvLines = dotEnv?.split('\n') || worker.labels;
const dotEnvParsed = dotEnvLines.map(line => {
const trimmed = line.trim();
if (trimmed.startsWith('#')) {
return { line };
}
const [key, ...value] = trimmed.split('=');
return { key, value: value.join('='), line };
});
const updateDotEnv = async (key: string, newValue: string) => {
let entry = dotEnvParsed.find(line => line.key === key);
if (!entry) {
entry = { key, value: '', line: '' };
dotEnvParsed.push(entry);
}
entry.line = `${key}=${newValue}`;
await envControl.setDotEnv(dotEnvParsed.filter(line => line).map(line => line.line).join('\n'));
};
if (setting === 'labels') {
await updateDotEnv('SCRYPTED_CLUSTER_LABELS', value.join(','));
} else if (setting === 'name') {
await updateDotEnv('SCRYPTED_CLUSTER_WORKER_NAME', value);
}
setTimeout(async () => {
const serviceControl = await clusterFork.getServiceControl(worker.id);
await serviceControl.restart().catch(() => { });
}, 10000);
});
}
async getReadmeMarkdown(): Promise<string> {
return `Manage Scrypted's cluster mode. Run storage devices and compute services on separate servers.`;
}
}

View File

@@ -10,11 +10,12 @@ import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
import { LauncherMixin } from './launcher-mixin';
import { MediaCore } from './media-core';
import { checkLxcDependencies } from './platform/lxc';
import { checkLegacyLxc, checkLxc } from './platform/lxc';
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
import { UsersCore, UsersNativeId } from './user';
import { ClusterCore, ClusterCoreNativeId } from './cluster';
const { deviceManager, endpointManager } = sdk;
@@ -27,6 +28,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
publicRouter: any = Router();
mediaCore: MediaCore;
scriptCore: ScriptCore;
clusterCore: ClusterCore;
aggregateCore: AggregateCore;
automationCore: AutomationCore;
users: UsersCore;
@@ -96,12 +98,23 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
settings: "General",
}
checkLxcDependencies();
checkLegacyLxc();
checkLxc();
this.storageSettings.settings.releaseChannel.hide = process.env.SCRYPTED_INSTALL_ENVIRONMENT !== 'lxc-docker';
this.indexHtml = readFileAsString('dist/index.html');
(async () => {
await deviceManager.onDeviceDiscovered(
{
name: 'Cluster',
nativeId: ClusterCoreNativeId,
interfaces: [ScryptedInterface.Settings, ScryptedInterface.Readme, ScryptedInterface.ScryptedSettings],
type: ScryptedDeviceType.Builtin,
},
);
})();
(async () => {
await deviceManager.onDeviceDiscovered(
{
@@ -214,6 +227,8 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Dev
}
async getDevice(nativeId: string) {
if (nativeId === ClusterCoreNativeId)
return this.clusterCore ||= new ClusterCore(ClusterCoreNativeId);
if (nativeId === 'launcher')
return new LauncherMixin('launcher');
if (nativeId === 'mediacore')

View File

@@ -1,121 +1,35 @@
import sdk from '@scrypted/sdk';
import child_process from 'child_process';
import { once } from 'events';
import fs from 'fs';
import os from 'os';
import sdk from '@scrypted/sdk';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC = 'lxc';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER = 'lxc-docker';
export async function checkLxcDependencies() {
export async function checkLegacyLxc() {
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC)
return;
let needRestart = false;
if (!process.version.startsWith('v20.')) {
const cp = child_process.spawn('sh', ['-c', 'apt update -y && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt install -y nodejs']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install Node.js 20.x.');
else
needRestart = true;
}
if (!fs.existsSync('/var/run/avahi-daemon/socket')) {
const cp = child_process.spawn('sh', ['-c', 'apt update -y && apt install -y avahi-daemon && apt upgrade -y']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install avahi-daemon.');
else
needRestart = true;
}
const scryptedService = fs.readFileSync('lxc/scrypted.service').toString();
const installedScryptedService = fs.readFileSync('/etc/systemd/system/scrypted.service').toString();
if (installedScryptedService !== scryptedService) {
fs.writeFileSync('/etc/systemd/system/scrypted.service', scryptedService);
needRestart = true;
const cp = child_process.spawn('systemctl', ['daemon-reload']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to daemon-reload systemd.');
}
try {
const output = await new Promise<string>((r, f) => child_process.exec("sh -c 'apt list --installed | grep level-zero/'", (err, stdout, stderr) => {
if (err && !stdout && !stderr)
f(err);
else
r(stdout + '\n' + stderr);
}));
const cpuModel = os.cpus()[0].model;
if (cpuModel.includes('Core') && cpuModel.includes('Ultra')) {
if (
// apt
!output.includes('level-zero/')
) {
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install intel-driver-compiler-npu.');
else
needRestart = true;
}
}
else {
// level-zero crashes openvino on older CPU due to illegal instruction.
// so ensure it is not installed if this is not a core ultra system with npu.
if (
// apt
output.includes('level-zero/')
) {
const cp = child_process.spawn('apt', ['-y', 'remove', 'level-zero']);
const [exitCode] = await once(cp, 'exit');
console.log('level-zero removed', exitCode);
needRestart = true;
}
}
}
catch (e) {
sdk.log.a('Failed to verify/install intel-driver-compiler-npu.');
}
try {
// intel opencl icd is broken from their official apt repos on kernel versions 6.8, which ships with ubuntu 24.04 and proxmox 8.2.
// the intel apt repo has not been updated yet.
// the current workaround is to install the release manually.
// https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
const output = await new Promise<string>((r, f) => child_process.exec("sh -c 'apt show versions intel-opencl-icd'", (err, stdout, stderr) => {
if (err && !stdout && !stderr)
f(err);
else
r(stdout + '\n' + stderr);
}));
if (
// apt
output.includes('Version: 23')
// was installed via script at some point
|| output.includes('Version: 24.13.29138.7')
|| output.includes('Version: 24.26.30049.6')
|| output.includes('Version: 24.31.30508.7')
// current script version: 24.35.30872.22
) {
const cp = child_process.spawn('sh', ['-c', 'curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install intel-opencl-icd.');
else
needRestart = true;
}
}
catch (e) {
sdk.log.a('Failed to verify/install intel-opencl-icd version.');
}
if (needRestart)
sdk.log.a('A system update is pending. Please restart Scrypted to apply changes.');
sdk.log.a('This system is currently running the legacy LXC installation method and must be migrated to the new LXC manually: https://docs.scrypted.app/installation.html#proxmox-ve-container-reset');
}
const DOCKER_COMPOSE_SH_PATH = '/root/.scrypted/docker-compose.sh';
const LXC_DOCKER_COMPOSE_SH_PATH = 'lxc/docker-compose.sh';
export async function checkLxc() {
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC_DOCKER)
return;
const foundDockerComposeSh = await fs.promises.readFile(DOCKER_COMPOSE_SH_PATH, 'utf8');
const dockerComposeSh = await fs.promises.readFile(LXC_DOCKER_COMPOSE_SH_PATH, 'utf8');
if (foundDockerComposeSh === dockerComposeSh) {
// check if the file is executable
const stats = await fs.promises.stat(DOCKER_COMPOSE_SH_PATH);
if (stats.mode & 0o111)
return;
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
return;
}
await fs.promises.copyFile(LXC_DOCKER_COMPOSE_SH_PATH, DOCKER_COMPOSE_SH_PATH);
await fs.promises.chmod(DOCKER_COMPOSE_SH_PATH, 0o755);
}

View File

@@ -206,14 +206,7 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
if (parsed.interactive) {
let spawn: typeof ptySpawn;
try {
try {
spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
if (!spawn)
throw new Error();
}
catch (e) {
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
}
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
cp = new InteractiveTerminal(cmd, extraPaths, spawn);
}
catch (e) {

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/mqtt",
"version": "0.0.82",
"version": "0.0.86",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/mqtt",
"version": "0.0.82",
"version": "0.0.86",
"dependencies": {
"aedes": "^0.46.1",
"axios": "^0.23.0",

View File

@@ -43,5 +43,5 @@
"@types/node": "^18.4.2",
"@types/nunjucks": "^3.2.0"
},
"version": "0.0.82"
"version": "0.0.86"
}

View File

@@ -1,3 +1,63 @@
import type { MqttClient } from "./mqtt-client";
import type { ScryptedDeviceBase } from "@scrypted/sdk";
import type { MqttClient, MqttEvent, MqttSubscriptions } from "./mqtt-client";
declare const device: ScryptedDeviceBase;
export declare const mqtt: MqttClient;
export function createSensor(options: {
type: string,
topic: string,
when: (message: MqttEvent) => boolean;
set: (value: boolean) => void,
delay?: number;
}) {
const subscriptions: MqttSubscriptions = {};
let timeout: NodeJS.Timeout;
subscriptions[options.topic] = message => {
const detected = options.when(message);
if (!options.delay) {
options.set(detected);
return;
}
if (!detected)
return;
options.set(true);
clearTimeout(timeout);
timeout = setTimeout(() => options.set(false), options.delay * 1000);
};
mqtt.subscribe(subscriptions);
mqtt.handleTypes(options.type);
}
export function createMotionSensor(options: {
topic: string,
when: (message: MqttEvent) => boolean;
delay?: number;
}) {
return createSensor({
type: "MotionSensor",
topic: options.topic,
set: (value: boolean) => device.motionDetected = value,
when: options.when,
delay: options.delay,
})
}
export function createBinarySensor(options: {
topic: string,
when: (message: MqttEvent) => boolean;
delay?: number;
}) {
return createSensor({
type: "BinarySensor",
topic: options.topic,
set: (value: boolean) => device.binaryState = value,
when: options.when,
delay: options.delay,
})
}

View File

@@ -3,7 +3,7 @@ import { ScriptDeviceImpl, scryptedEval as scryptedEvalBase } from "@scrypted/co
const util = require("!!raw-loader!./api/util.ts").default;
const libs = {
util,
util: util.replace('export', ''),
};
export async function scryptedEval(device: ScryptedDeviceBase, script: string, params: { [name: string]: any }) {

View File

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

View File

@@ -13,4 +13,8 @@ benefits to HomeKit, which does its own detection processing.
## Smart Motion Sensors
This plugin can be used to create smart motion sensors that trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This feature requires cameras with hardware or software object detection capability.
This plugin can be used to create smart motion sensors that trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires cameras with hardware or software object detection capability.
## Smart Occupancy Sensors
This plugin can be used to create smart occupancy sensors remains triggered when a specific type of object (vehicle, person, animal, etc) is detected on a camera. Created sensors can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/objectdetector",
"version": "0.1.47",
"version": "0.1.60",
"description": "Scrypted Video Analysis Plugin. Installed alongside a detection service like OpenCV or TensorFlow.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -1,48 +0,0 @@
import { CpuInfo, cpus } from 'os';
function getIdleTotal(cpu: CpuInfo) {
const t = cpu.times;
const total = t.user + t.nice + t.sys + t.idle + t.irq;
const idle = t.idle;
return {
idle,
total,
}
}
export class CpuTimer {
previousSample: ReturnType<typeof cpus>;
maxSpeed = 0;
sample(): number {
const sample = cpus();
const previousSample = this.previousSample;
this.previousSample = sample;
// can cpu count change at runtime, who knows
if (!previousSample || previousSample.length !== sample.length)
return 0;
// cpu may be throttled in low power mode, so observe total speed to scale
let totalSpeed = 0;
const times = sample.map((v, i) => {
totalSpeed += v.speed;
const c = getIdleTotal(v);
const p = getIdleTotal(previousSample[i]);
const total = c.total - p.total;
const idle = c.idle - p.idle;
return 1 - idle / total;
});
this.maxSpeed = Math.max(this.maxSpeed, totalSpeed);
// will return a value between 0 and 1, where 1 is full cpu speed
// the cpu usage is scaled by the clock speed
// so if the cpu is running at 1ghz out of 3ghz, the cpu usage is scaled by 1/3
const clockScale = totalSpeed / this.maxSpeed;
const total = times.reduce((p, c) => p + c, 0);
return total / sample.length * clockScale;
}
}

View File

@@ -1,14 +1,14 @@
import { Deferred } from '@scrypted/common/src/deferred';
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, DeviceState, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator, WritableDeviceState } from '@scrypted/sdk';
import sdk, { Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, EventListenerRegister, MediaObject, MediaStreamDestination, MixinDeviceBase, MixinProvider, MotionSensor, ObjectDetection, ObjectDetectionModel, ObjectDetectionTypes, ObjectDetectionZone, ObjectDetector, ObjectsDetected, Point, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings, VideoCamera, VideoFrame, VideoFrameGenerator, WritableDeviceState } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import crypto from 'crypto';
import { AutoenableMixinProvider } from "../../../common/src/autoenable-mixin-provider";
import { SettingsMixinDeviceBase } from "../../../common/src/settings-mixin";
import { CpuTimer } from './cpu-timer';
import { FFmpegVideoFrameGenerator } from './ffmpeg-videoframes';
import { insidePolygon, normalizeBox, polygonOverlap } from './polygon';
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor, createObjectDetectorStorageSetting } from './smart-motionsensor';
import { fixLegacyClipPath, insidePolygon, normalizeBoxToClipPath, polygonOverlap } from './polygon';
import { SMART_MOTIONSENSOR_PREFIX, SmartMotionSensor } from './smart-motionsensor';
import { SMART_OCCUPANCYSENSOR_PREFIX, SmartOccupancySensor } from './smart-occupancy-sensor';
import { getAllDevices, safeParseJson } from './util';
@@ -20,6 +20,17 @@ const defaultMotionDuration = 30;
const BUILTIN_MOTION_SENSOR_ASSIST = 'Assist';
const BUILTIN_MOTION_SENSOR_REPLACE = 'Replace';
// at 5fps object detection speed, the camera is considered throttled.
// throttling may be due to cpu, gpu, npu or whatever.
// regardless, purging low fps object detection sessions will likely
// restore performance.
const fpsKillWaterMark = 5
const fpsLowWaterMark = 7;
// cameras may have low performance due to low framerate or intensive tasks such as
// LPR and face recognition. if multiple cams are in low performance mode, then
// the system may be struggling.
const lowPerformanceMinThreshold = 2;
const objectDetectionPrefix = `${ScryptedInterface.ObjectDetection}:`;
type ClipPath = Point[];
@@ -85,6 +96,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
...getAllDevices().filter(d => d.interfaces.includes(ScryptedInterface.VideoFrameGenerator)).map(d => d.name),
];
return {
hide: this.model?.decoder,
choices,
}
},
@@ -103,6 +115,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
analyzeStop: number;
detectorSignal = new Deferred<void>().resolve();
released = false;
sampleHistory: number[] = [];
// settings: Setting[];
get detectorRunning() {
@@ -174,6 +187,8 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
}
else {
value = this.storage.getItem(setting.key);
if (setting.type === 'number')
value = parseFloat(value);
}
value ||= setting.value;
@@ -434,13 +449,18 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
break;
}
const now = Date.now();
// stop when analyze period ends.
if (!this.hasMotionType && this.analyzeStop && Date.now() > this.analyzeStop) {
if (!this.hasMotionType && this.analyzeStop && now > this.analyzeStop) {
this.analyzeStop = undefined;
break;
}
if (!longObjectDetectionWarning && !this.hasMotionType && Date.now() - start > 5 * 60 * 1000) {
this.purgeSampleHistory(now);
this.sampleHistory.push(now);
if (!longObjectDetectionWarning && !this.hasMotionType && now - start > 5 * 60 * 1000) {
longObjectDetectionWarning = true;
this.console.warn('Camera has been performing object detection for 5 minutes due to persistent motion. This may adversely affect system performance. Read the Optimizing System Performance guide for tips and tricks. https://github.com/koush/nvr.scrypted.app/wiki/Optimizing-System-Performance')
}
@@ -451,8 +471,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
const zonedDetections = this.applyZones(detected.detected);
detected.detected.detections = zonedDetections;
// this.console.warn('dps', detections / (Date.now() - start) * 1000);
if (!this.hasMotionType) {
this.plugin.trackDetection();
@@ -465,7 +483,6 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
currentDetections.set(d.className, Math.max(currentDetections.get(d.className) || 0, d.score));
}
const now = Date.now();
if (now > lastReport + 10000) {
const found = [...currentDetections.entries()].map(([className, score]) => `${className} (${score})`);
if (!found.length)
@@ -478,23 +495,20 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (detected.detected.detectionId) {
updatePipelineStatus('creating jpeg');
// const start = Date.now();
let { image } = detected.videoFrame;
image = await sdk.connectRPCObject(image);
const jpeg = await image.toBuffer({
format: 'jpg',
});
const mo = await sdk.mediaManager.createMediaObject(jpeg, 'image/jpeg');
// this.console.log('retain took', Date.now() -start);
this.setDetection(detected.detected, mo);
// this.console.log('image saved', detected.detected.detections);
}
const motionFound = this.reportObjectDetections(detected.detected);
if (this.hasMotionType) {
// if motion is detected, stop processing and exit loop allowing it to sleep.
if (motionFound) {
// however, when running in analyze mode, continue to allow viewing motion boxes for test purposes.
if (!this.analyzeStop || Date.now() > this.analyzeStop) {
if (!this.analyzeStop || now > this.analyzeStop) {
this.analyzeStop = undefined;
clearInterval(interval);
return true;
@@ -503,10 +517,25 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
await sleep(250);
}
updatePipelineStatus('waiting result');
// this.handleDetectionEvent(detected.detected);
}
}
purgeSampleHistory(now: number) {
while (this.sampleHistory.length && now - this.sampleHistory[0] > 10000) {
this.sampleHistory.shift();
}
}
get detectionFps() {
const now = Date.now();
this.purgeSampleHistory(now);
const first = this.sampleHistory[0];
// require at least 5 seconds of samples.
if (!first || (now - first) < 8000)
return Infinity;
return this.sampleHistory.length / ((now - first) / 1000);
}
applyZones(detection: ObjectsDetected) {
// determine zones of the objects, if configured.
if (!detection.detections)
@@ -516,7 +545,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (!o.boundingBox)
continue;
const box = normalizeBox(o.boundingBox, detection.inputDimensions);
const box = normalizeBoxToClipPath(o.boundingBox, detection.inputDimensions);
let included: boolean;
// need a way to explicitly include package zone.
@@ -524,12 +553,17 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
included = true;
else
o.zones = [];
for (const [zone, zoneValue] of Object.entries(this.zones)) {
for (let [zone, zoneValue] of Object.entries(this.zones)) {
zoneValue = fixLegacyClipPath(zoneValue);
if (zoneValue.length < 3) {
// this.console.warn(zone, 'Zone is unconfigured, skipping.');
continue;
}
// object detection may report motion, don't filter these at all.
if (!this.hasMotionType && o.className === 'motion')
continue;
const zoneInfo = this.zoneInfos[zone];
const exclusion = zoneInfo?.filterMode ? zoneInfo.filterMode === 'exclude' : zoneInfo?.exclusion;
// track if there are any inclusion zones
@@ -569,7 +603,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
// 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: ClipPath = [[0, 10], [100, 10], [100, 90], [0, 90]];
const defaultInclusionZone: ClipPath = [[0, .1], [1, .1], [1, .9], [0, .9]];
included = polygonOverlap(box, defaultInclusionZone);
}
@@ -817,7 +851,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (key.startsWith('zone-')) {
const zoneName = key.substring('zone-'.length);
if (this.zones[zoneName]) {
this.zones[zoneName] = JSON.parse(vs);
this.zones[zoneName] = Array.isArray(value) ? value : JSON.parse(vs);
this.storage.setItem('zones', JSON.stringify(this.zones));
}
return;
@@ -1007,14 +1041,12 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
},
});
devices = new Map<string, any>();
cpuTimer = new CpuTimer();
cpuUsage = 0;
constructor(nativeId?: ScryptedNativeId) {
super(nativeId, 'v5');
this.systemDevice = {
deviceCreator: 'Smart Motion Sensor',
deviceCreator: 'Smart Sensor',
};
process.nextTick(() => {
@@ -1028,19 +1060,28 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
})
});
// on an interval check to see if system load allows squelched detectors to start up.
setInterval(() => {
this.cpuUsage = this.cpuTimer.sample();
// this.console.log('cpu usage', Math.round(this.cpuUsage * 100));
const runningDetections = this.runningObjectDetections;
// don't allow too many cams to start up at once if resuming from a low performance state.
let allowStart = 2;
// always allow 2 cameras to push past cpu throttling
if (runningDetections.length > 2) {
const cpuPerDetector = this.cpuUsage / runningDetections.length;
allowStart = Math.ceil(1 / cpuPerDetector) - runningDetections.length;
if (allowStart <= 0)
// allow minimum amount of concurrent cameras regardless of system specs
if (runningDetections.length > lowPerformanceMinThreshold) {
// if anything is below the kill threshold, do not start
const killable = runningDetections.filter(o => o.detectionFps < fpsKillWaterMark && !o.analyzeStop);
if (killable.length > lowPerformanceMinThreshold) {
const cameraNames = runningDetections.map(o => `${o.name} ${o.detectionFps}`).join(', ');
const first = killable[0];
first.console.warn(`System at capacity. Ending object detection.`, cameraNames);
first.endObjectDetection();
return;
}
const lowWatermark = runningDetections.filter(o => o.detectionFps < fpsLowWaterMark);
if (lowWatermark.length > lowPerformanceMinThreshold)
allowStart = 1;
}
const idleDetectors = [...this.currentMixins.values()]
@@ -1054,7 +1095,7 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
return;
}
}
}, 10000)
}, 5000)
}
checkHasEnabledMixin(device: ScryptedDevice): boolean {
@@ -1081,26 +1122,28 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
if (runningDetections.find(o => o.id === mixin.id))
return false;
// always allow 2 cameras to push past cpu throttling
if (runningDetections.length < 2)
// allow minimum amount of concurrent cameras regardless of system specs
if (runningDetections.length < lowPerformanceMinThreshold)
return true;
const cpuPerDetector = this.cpuUsage / runningDetections.length;
const cpuPercent = Math.round(this.cpuUsage * 100);
if (cpuPerDetector * (runningDetections.length + 1) > .9) {
const [first] = runningDetections;
// find any cameras struggling with a with low detection fps.
const lowWatermark = runningDetections.filter(o => o.detectionFps < fpsLowWaterMark);
if (lowWatermark.length > lowPerformanceMinThreshold) {
const [first] = lowWatermark;
// if cameras have been detecting enough to catch the activity, kill it for new camera.
const cameraNames = runningDetections.map(o => `${o.name} ${o.detectionFps}`).join(', ');
if (Date.now() - first.detectionStartTime > 30000) {
first.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Ending object detection to process activity on ${mixin.name}.`);
first.console.warn(`System at capacity. Ending object detection to process activity on ${mixin.name}.`, cameraNames);
first.endObjectDetection();
mixin.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Ending object detection on ${first.name} to process activity.`);
mixin.console.warn(`System at capacity. Ending object detection on ${first.name} to process activity.`, cameraNames);
return true;
}
mixin.console.warn(`CPU is at capacity: ${cpuPercent} with ${runningDetections.length} cameras. Not starting object detection to continue processing recent activity on ${first.name}.`);
mixin.console.warn(`System at capacity. Not starting object detection to continue processing recent activity on ${first.name}.`, cameraNames);
return false;
}
// CPU capacity is fine
// System capacity is fine. Start the detection.
return true;
}
@@ -1155,6 +1198,8 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
ret = this.devices.get(nativeId) || new FFmpegVideoFrameGenerator('ffmpeg');
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX))
ret = this.devices.get(nativeId) || new SmartMotionSensor(this, nativeId);
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX))
ret = this.devices.get(nativeId) || new SmartOccupancySensor(this, nativeId);
if (ret)
this.devices.set(nativeId, ret);
@@ -1165,6 +1210,13 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
if (nativeId?.startsWith(SMART_MOTIONSENSOR_PREFIX)) {
const smart = this.devices.get(nativeId) as SmartMotionSensor;
smart?.detectionListener?.removeListener();
smart?.resetMotionTimeout();
}
if (nativeId?.startsWith(SMART_OCCUPANCYSENSOR_PREFIX)) {
const smart = this.devices.get(nativeId) as SmartOccupancySensor;
smart?.detectionListener?.removeListener();
smart?.resetOccupiedTimeout();
smart?.clearOccupancyInterval();
}
}
@@ -1200,32 +1252,71 @@ export class ObjectDetectionPlugin extends AutoenableMixinProvider implements Se
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
createObjectDetectorStorageSetting(),
{
key: 'sensorType',
title: 'Sensor Type',
description: 'Select the type of sensor to create.',
choices: [
'Smart Motion Sensor',
'Smart Occupancy Sensor',
],
},
{
key: 'camera',
title: 'Camera',
description: 'Select a camera or doorbell.',
type: 'device',
deviceFilter: `type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}'`,
},
];
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
const objectDetector = sdk.systemManager.getDeviceById(settings.objectDetector as string);
let name = objectDetector.name || 'New';
name += ' Smart Motion Sensor'
const sensorType = settings.sensorType;
const camera = sdk.systemManager.getDeviceById(settings.camera as string);
if (sensorType === 'Smart Motion Sensor') {
const nativeId = SMART_MOTIONSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
let name = camera.name || 'New';
name += ' Smart Motion Sensor'
const id = await sdk.deviceManager.onDeviceDiscovered({
nativeId,
name,
type: ScryptedDeviceType.Sensor,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.MotionSensor,
ScryptedInterface.Settings,
ScryptedInterface.Readme,
]
});
const id = await sdk.deviceManager.onDeviceDiscovered({
nativeId,
name,
type: ScryptedDeviceType.Sensor,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.MotionSensor,
ScryptedInterface.Settings,
ScryptedInterface.Readme,
]
});
const sensor = new SmartMotionSensor(this, nativeId);
sensor.storageSettings.values.objectDetector = objectDetector?.id;
const sensor = new SmartMotionSensor(this, nativeId);
sensor.storageSettings.values.objectDetector = camera?.id;
return id;
return id;
}
else if (sensorType === 'Smart Occupancy Sensor') {
const nativeId = SMART_OCCUPANCYSENSOR_PREFIX + crypto.randomBytes(8).toString('hex');
let name = camera.name || 'New';
name += ' Smart Occupancy Sensor'
const id = await sdk.deviceManager.onDeviceDiscovered({
nativeId,
name,
type: ScryptedDeviceType.Sensor,
interfaces: [
ScryptedInterface.OccupancySensor,
ScryptedInterface.Settings,
ScryptedInterface.Readme,
]
});
const sensor = new SmartOccupancySensor(this, nativeId);
sensor.storageSettings.values.camera = camera?.id;
return id;
}
}
}

View File

@@ -1,4 +1,4 @@
import { Point } from '@scrypted/sdk';
import type { ClipPath, Point } from '@scrypted/sdk';
import polygonClipping from 'polygon-clipping';
// const polygonOverlap = require('polygon-overlap');
@@ -14,15 +14,36 @@ export function insidePolygon(point: Point, polygon: Point[]) {
return !!intersect.length;
}
export function normalizeBox(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
export function fixLegacyClipPath(clipPath: ClipPath): ClipPath {
if (!clipPath)
return;
// if any value is over abs 2, then divide by 100.
// this is a workaround for the old scrypted bug where the path was not normalized.
// this is a temporary workaround until the path is normalized in the UI.
let needNormalize = false;
for (const p of clipPath) {
for (const c of p) {
if (Math.abs(c) >= 2)
needNormalize = true;
}
}
if (!needNormalize)
return clipPath;
return clipPath.map(p => p.map(c => c / 100)) as ClipPath;
}
export function normalizeBoxToClipPath(boundingBox: [number, number, number, number], inputDimensions: [number, number]): [Point, Point, Point, Point] {
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];
x = x / inputDimensions[0];
y = y / inputDimensions[1];
x2 = x2 / inputDimensions[0];
y2 = y2 / inputDimensions[1];
return [[x, y], [x2, y], [x2, y2], [x, y2]];
}

View File

@@ -1,24 +1,18 @@
import sdk, { Camera, EventListenerRegister, MediaObject, MotionSensor, ObjectDetector, ObjectsDetected, Readme, RequestPictureOptions, ResponsePictureOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
import { StorageSetting, StorageSettings } from "@scrypted/sdk/storage-settings";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { levenshteinDistance } from "./edit-distance";
import type { ObjectDetectionPlugin } from "./main";
export const SMART_MOTIONSENSOR_PREFIX = 'smart-motionsensor-';
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
export function createObjectDetectorStorageSetting(): StorageSetting {
return {
key: 'objectDetector',
title: 'Object Detector',
description: 'Select the camera or doorbell that provides smart detection event.',
type: 'device',
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
};
}
export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, Readme, MotionSensor, Camera {
storageSettings = new StorageSettings(this, {
objectDetector: createObjectDetectorStorageSetting(),
objectDetector: {
title: 'Camera',
description: 'Select a camera or doorbell that provides smart detection events.',
type: 'device',
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.ObjectDetector}')`,
},
detections: {
title: 'Detections',
description: 'The detections that will trigger this smart motion sensor.',
@@ -145,13 +139,13 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
return;
}
resetTrigger() {
resetMotionTimeout() {
clearTimeout(this.timeout);
this.timeout = undefined;
}
trigger() {
this.resetTrigger();
this.resetMotionTimeout();
this.motionDetected = true;
const duration: number = this.storageSettings.values.detectionTimeout;
if (!duration)
@@ -167,7 +161,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
this.detectionListener = undefined;
this.motionListener?.removeListener();
this.motionListener = undefined;
this.resetTrigger();
this.resetMotionTimeout();
const objectDetector: ObjectDetector & MotionSensor & ScryptedDevice = this.storageSettings.values.objectDetector;
@@ -178,8 +172,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
if (!detections?.length)
return;
const console = sdk.deviceManager.getMixinConsole(objectDetector.id, this.nativeId);
this.motionListener = objectDetector.listen({
event: ScryptedInterface.MotionSensor,
watch: true,
@@ -258,7 +250,7 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
if (match) {
if (!this.motionDetected)
console.log('Smart Motion Sensor triggered on', match);
this.console.log('Smart Motion Sensor triggered on', match);
if (detected.detectionId)
this.lastPicture = objectDetector.getDetectionInput(detected.detectionId, details.eventId);
this.trigger();
@@ -278,6 +270,6 @@ export class SmartMotionSensor extends ScryptedDeviceBase implements Settings, R
return `
## Smart Motion Sensor
This Smart Motion Sensor can trigger when a specific type of object (car, person, dog, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
This Smart Motion Sensor can trigger when a specific type of object (vehicle, person, animal, etc) triggers movement on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires a camera with hardware or software object detection capability.`;
}
}

View File

@@ -0,0 +1,316 @@
import sdk, { Camera, ClipPath, EventListenerRegister, Image, ObjectDetection, ObjectDetector, ObjectsDetected, OccupancySensor, Readme, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, SettingValue, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { levenshteinDistance } from "./edit-distance";
import type { ObjectDetectionPlugin } from "./main";
import { normalizeBoxToClipPath, polygonOverlap } from "./polygon";
export const SMART_OCCUPANCYSENSOR_PREFIX = 'smart-occupancysensor-';
const nvrAcceleratedMotionSensorId = sdk.systemManager.getDeviceById('@scrypted/nvr', 'motion')?.id;
export class SmartOccupancySensor extends ScryptedDeviceBase implements Settings, Readme, OccupancySensor {
storageSettings = new StorageSettings(this, {
camera: {
title: 'Camera',
description: 'Select the camera or doorbell image to analyze periodically.',
type: 'device',
deviceFilter: `(type === '${ScryptedDeviceType.Doorbell}' || type === '${ScryptedDeviceType.Camera}') && interfaces.includes('${ScryptedInterface.Camera}')`,
immediate: true,
},
objectDetection: {
title: 'Object Detector',
description: 'Select the object detection plugin to use for detecting objects.',
type: 'device',
deviceFilter: `interfaces.includes('ObjectDetectionPreview') && id !== '${nvrAcceleratedMotionSensorId}'`,
immediate: true,
},
detections: {
title: 'Detections',
description: 'The detections that will trigger this occupancy sensor.',
multiple: true,
choices: [],
},
occupancyInterval: {
title: 'Occupancy Check Interval',
description: 'The interval in minutes that the sensor will check for occupancy.',
type: 'number',
defaultValue: 60,
// save and restore in seconds for consistency.
mapPut(oldValue, newValue) {
return newValue * 60;
},
mapGet(value) {
return value / 60;
},
},
zone: {
title: 'Edit Intersect Zone',
description: 'Optional: Configure the intersect zone for the occupancy check. Objects intersecting this zone will trigger the occupancy sensor.',
type: 'clippath',
},
captureZone: {
title: 'Edit Crop Zone',
description: 'Optional: Configure the capture zone for the occupancy check. The image will be cropped to this zone before detection. Cropping to desired location will improve detection performance.',
type: 'clippath',
},
minScore: {
title: 'Minimum Score',
description: 'The minimum score required for a detection to trigger the occupancy sensor.',
type: 'number',
defaultValue: 0.4,
},
labels: {
group: 'Recognition',
title: 'Labels',
description: 'The labels (license numbers, names) that will trigger this smart occupancy sensor.',
multiple: true,
combobox: true,
choices: [],
},
labelDistance: {
group: 'Recognition',
title: 'Label Distance',
description: 'The maximum edit distance between the detected label and the desired label. Ie, a distance of 1 will match "abcde" to "abcbe" or "abcd".',
type: 'number',
defaultValue: 2,
},
labelScore: {
group: 'Recognition',
title: 'Label Score',
description: 'The minimum score required for a label to trigger the occupancy sensor.',
type: 'number',
defaultValue: 0,
}
});
detectionListener: EventListenerRegister;
occupancyTimeout: NodeJS.Timeout;
occupancyInterval: NodeJS.Timeout;
constructor(public plugin: ObjectDetectionPlugin, nativeId?: ScryptedNativeId) {
super(nativeId);
this.storageSettings.settings.zone.onGet = async () => {
return {
deviceFilter: this.storageSettings.values.camera?.id,
}
};
this.storageSettings.settings.captureZone.onGet = async () => {
return {
deviceFilter: this.storageSettings.values.camera?.id,
}
};
this.storageSettings.settings.detections.onGet = async () => {
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
const choices = (await objectDetection?.getDetectionModel())?.classes || [];
return {
hide: !objectDetection,
choices,
};
};
this.storageSettings.settings.detections.onPut = () => this.rebind();
this.storageSettings.settings.objectDetection.onPut = () => this.rebind();
this.storageSettings.settings.zone.onPut = () => this.rebind();
this.storageSettings.settings.captureZone.onPut = () => this.rebind();
this.rebind();
}
resetOccupiedTimeout() {
clearTimeout(this.occupancyTimeout);
this.occupancyTimeout = undefined;
}
clearOccupancyInterval() {
clearInterval(this.occupancyInterval);
this.occupancyInterval = undefined;
}
trigger() {
this.resetOccupiedTimeout();
this.occupied = true;
const duration: number = this.storageSettings.values.occupancyInterval;
if (!duration)
return;
this.occupancyTimeout = setTimeout(() => {
this.occupied = false;
}, duration * 60000 + 10000);
}
checkDetection(detections: string[], labels: string[], labelDistance: number, labelScore: number, detected: ObjectsDetected) {
const match = detected.detections?.find(d => {
if (d.score && d.score < this.storageSettings.values.minScore)
return false;
if (!detections?.includes(d.className))
return false;
const zone: ClipPath = this.storageSettings.values.zone;
if (zone?.length >= 3) {
if (!d.boundingBox)
return false;
const detectionBoxPath = normalizeBoxToClipPath(d.boundingBox, detected.inputDimensions);
if (!polygonOverlap(detectionBoxPath, zone))
return false;
}
if (!labels?.length)
return true;
if (!d.label)
return false;
for (const label of labels) {
if (label === d.label) {
if (!labelScore || d.labelScore >= labelScore)
return true;
this.console.log('Label score too low.', d.labelScore);
continue;
}
if (!labelDistance)
continue;
if (levenshteinDistance(label, d.label) > labelDistance) {
this.console.log('Label does not match.', label, d.label, d.labelScore);
continue;
}
if (!labelScore || d.labelScore >= labelScore)
return true;
this.console.log('Label score too low.', d.labelScore);
}
return false;
});
if (match) {
if (!this.occupied)
this.console.log('Occupancy Sensor triggered on', match);
this.trigger();
}
}
async runDetection() {
try {
const objectDetection: ObjectDetection = this.storageSettings.values.objectDetection;
if (!objectDetection) {
this.console.error('no object detection plugin selected');
return;
}
const camera: ScryptedDevice & Camera = this.storageSettings.values.camera;
if (!camera) {
this.console.error('no camera selected');
return;
}
const picture = await camera.takePicture({
reason: 'event',
});
const zone: ClipPath = this.storageSettings.values.captureZone;
let detected: ObjectsDetected;
if (zone?.length >= 3) {
const image = await sdk.mediaManager.convertMediaObject<Image>(picture, ScryptedMimeTypes.Image);
let left = image.width;
let top = image.height;
let right = 0;
let bottom = 0;
for (const point of zone) {
left = Math.min(left, point[0]);
top = Math.min(top, point[1]);
right = Math.max(right, point[0]);
bottom = Math.max(bottom, point[1]);
}
left = left * image.width;
top = top * image.height;
right = right * image.width;
bottom = bottom * image.height;
let width = right - left;
let height = bottom - top;
// square it for standard detection
width = height = Math.max(width, height);
// recenter it
left = left + (right - left - width) / 2;
top = top + (bottom - top - height) / 2;
// ensure bounds are within image.
left = Math.max(0, left);
top = Math.max(0, top);
width = Math.min(width, image.width - left);
height = Math.min(height, image.height - top);
const cropped = await image.toImage({
crop: {
left,
top,
width,
height,
},
});
detected = await objectDetection.detectObjects(cropped);
// adjust the origin of the bounding boxes for the crop.
for (const d of detected.detections) {
d.boundingBox[0] += left;
d.boundingBox[1] += top;
}
detected.inputDimensions = [image.width, image.height];
}
else {
detected = await objectDetection.detectObjects(picture);
}
this.checkDetection(this.storageSettings.values.detections, this.storageSettings.values.labels, this.storageSettings.values.labelDistance, this.storageSettings.values.labelScore, detected);
}
catch (e) {
this.console.error('failed to take picture', e);
}
}
rebind() {
this.occupied = false;
this.detectionListener?.removeListener();
this.detectionListener = undefined;
this.resetOccupiedTimeout();
this.clearOccupancyInterval();
this.runDetection();
this.occupancyInterval = setInterval(() => {
this.runDetection();
}, this.storageSettings.values.occupancyInterval * 60000);
// camera may have an object detector that can also be observed for occupancy for free.
const objectDetector: ObjectDetector & ScryptedDevice = this.storageSettings.values.camera;
if (!objectDetector)
return;
const detections: string[] = this.storageSettings.values.detections;
if (!detections?.length)
return;
const { labels, labelDistance, labelScore } = this.storageSettings.values;
this.detectionListener = objectDetector.listen(ScryptedInterface.ObjectDetector, (source, details, data) => {
const detected: ObjectsDetected = data;
this.checkDetection(detections, labels, labelDistance, labelScore, detected);
});
}
async getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async getReadmeMarkdown(): Promise<string> {
return `
## Smart Occupancy Sensor
This Occupancy Sensor remains triggered while specified objects (vehicle, person, animal, etc) are detected on a camera. The sensor can then be synced to other platforms such as HomeKit, Google Home, Alexa, or Home Assistant for use in automations. This Sensor requires an object detector plugin such as Scrypted NVR, OpenVINO, CoreML, ONNX, or Tensorflow-lite.`;
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/openvino",
"version": "0.1.137",
"version": "0.1.148",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/openvino",
"version": "0.1.137",
"version": "0.1.148",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -48,5 +48,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.137"
"version": "0.1.148"
}

View File

@@ -6,9 +6,12 @@ from predict.rectangle import Rectangle
defaultThreshold = .2
def parse_yolov10(results, threshold = defaultThreshold, scale = None, confidence_scale = None):
def parse_yolov10(results, threshold = defaultThreshold, scale = None, confidence_scale = None, threshold_scale = None):
objs: list[Prediction] = []
keep = np.argwhere(results[4:] > threshold)
if not threshold_scale:
keep = np.argwhere(results[4:] > threshold)
else:
keep = np.argwhere(results[4:] > threshold_scale(results[4:]))
for indices in keep:
class_id = indices[0]
index = indices[1]
@@ -52,9 +55,12 @@ def parse_yolo_nas(predictions):
objs.append(obj)
return objs
def parse_yolov9(results, threshold = defaultThreshold, scale = None, confidence_scale = None):
def parse_yolov9(results, threshold = defaultThreshold, scale = None, confidence_scale = None, threshold_scale = None):
objs: list[Prediction] = []
keep = np.argwhere(results[4:] > threshold)
if not threshold_scale:
keep = np.argwhere(results[4:] > threshold)
else:
keep = np.argwhere(threshold_scale(results[4:]) > threshold)
for indices in keep:
class_id = indices[0]
index = indices[1]

View File

@@ -25,12 +25,18 @@ try:
except:
OpenVINOTextRecognition = None
predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "OpenVINO-Predict")
prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "OpenVINO-Prepare")
predictExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="OpenVINO-Predict")
prepareExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="OpenVINO-Prepare")
availableModels = [
"Default",
"scrypted_yolov9c_relu_int8_320",
"scrypted_yolov9s_relu_int8_320",
"scrypted_yolov9t_relu_int8_320",
"scrypted_yolov9c_int8_320",
"scrypted_yolov9m_int8_320",
"scrypted_yolov9s_int8_320",
"scrypted_yolov9t_int8_320",
"scrypted_yolov10m_320",
"scrypted_yolov10s_320",
"scrypted_yolov10n_320",
@@ -42,10 +48,6 @@ availableModels = [
"scrypted_yolov9s_320",
"scrypted_yolov9t_320",
"scrypted_yolov8n_320",
"ssd_mobilenet_v1_coco",
"ssdlite_mobilenet_v2",
"yolo-v3-tiny-tf",
"yolo-v4-tiny-tf",
]
@@ -130,22 +132,26 @@ class OpenVINOPlugin(
gpu = True
except:
pass
# AUTO mode can cause conflicts or hide errors with NPU and GPU
# so try to be explicit and fall back accordingly.
mode = self.storage.getItem("mode") or "Default"
if mode == "Default":
mode = "AUTO"
if npu:
if gpu:
mode = f"AUTO:NPU,GPU,CPU"
else:
mode = f"AUTO:NPU,CPU"
mode = 'NPU'
elif len(dgpus):
mode = f"AUTO:{','.join(dgpus)},CPU"
# forcing GPU can cause crashes on older GPU.
elif gpu:
mode = f"GPU"
# recognition models are not supported on NPU.
self.recognition_mode = mode
if "NPU" in mode:
self.recognition_mode = "AUTO"
mode = mode or "AUTO"
self.mode = mode
@@ -155,14 +161,18 @@ class OpenVINOPlugin(
model = self.storage.getItem("model") or "Default"
if model == "Default" or model not in availableModels:
# relu + int8 wins out by a mile on gpu and npu.
# observation is that silu + float is faster than silu + int8 on gpu.
# possibly due to quantization causing complexity at the activation function?
# however, silu + int8 is faster than silu + float on cpu. (tested on wyse 5070)
if model != "Default":
self.storage.setItem("model", "Default")
if arc or nvidia or npu:
model = "scrypted_yolov9c_320"
model = "scrypted_yolov9c_relu_int8_320"
elif iris_xe:
model = "scrypted_yolov9s_320"
model = "scrypted_yolov9s_relu_int8_320"
else:
model = "scrypted_yolov9t_320"
model = "scrypted_yolov9t_relu_int8_320"
self.yolo = "yolo" in model
self.scrypted_yolov9 = "scrypted_yolov9" in model
self.scrypted_yolov10 = "scrypted_yolov10" in model
@@ -231,6 +241,15 @@ class OpenVINOPlugin(
self.storage.removeItem("precision")
self.requestRestart()
self.infer_queue = ov.AsyncInferQueue(self.compiled_model)
def callback(infer_request, future: asyncio.Future):
try:
output = infer_request.get_output_tensor(0)
self.loop.call_soon_threadsafe(future.set_result, output)
except Exception as e:
self.loop.call_soon_threadsafe(future.set_exception, e)
self.infer_queue.set_callback(callback)
print(
"EXECUTION_DEVICES",
self.compiled_model.get_property("EXECUTION_DEVICES"),
@@ -304,68 +323,38 @@ class OpenVINOPlugin(
return super().get_input_format()
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
def predict(input_tensor):
infer_request = self.compiled_model.create_infer_request()
infer_request.set_input_tensor(input_tensor)
output_tensors = infer_request.infer()
async def predict(input_tensor):
f = asyncio.Future(loop = self.loop)
self.infer_queue.start_async(input_tensor, f)
objs = []
output_tensors = await f
if self.scrypted_yolo:
if self.scrypted_yolov10:
return yolo.parse_yolov10(output_tensors[0][0])
if self.scrypted_yolo_nas:
return yolo.parse_yolo_nas([output_tensors[1], output_tensors[0]])
return yolo.parse_yolov9(output_tensors[0][0])
if not self.yolo:
output = output_tensors
for values in output.data[0][0]:
valid, index, confidence, l, t, r, b = values
if valid == -1:
break
if self.yolo:
# index 2 will always either be 13 or 26
# index 1 may be 13/26 or 255 depending on yolo 3 vs 4
if infer_request.outputs[0].data.shape[2] == 13:
out_blob = infer_request.outputs[0]
else:
out_blob = infer_request.outputs[1]
def torelative(value: float):
return value * self.model_dim
# 13 13
objects = yolo.parse_yolo_region(
out_blob.data,
(input.width, input.height),
(81, 82, 135, 169, 344, 319),
self.sigmoid,
)
l = torelative(l)
t = torelative(t)
r = torelative(r)
b = torelative(b)
for r in objects:
obj = Prediction(
r["classId"],
r["confidence"],
Rectangle(r["xmin"], r["ymin"], r["xmax"], r["ymax"]),
)
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
objs.append(obj)
# what about output[1]?
# 26 26
# objects = yolo.parse_yolo_region(out_blob, (input.width, input.height), (,27, 37,58, 81,82))
return objs
output = infer_request.get_output_tensor(0)
for values in output.data[0][0]:
valid, index, confidence, l, t, r, b = values
if valid == -1:
break
def torelative(value: float):
return value * self.model_dim
l = torelative(l)
t = torelative(t)
r = torelative(r)
b = torelative(b)
obj = Prediction(index - 1, confidence, Rectangle(l, t, r, b))
objs.append(obj)
return objs
output = output_tensors.data
if self.scrypted_yolov10:
return yolo.parse_yolov10(output[0])
if self.scrypted_yolo_nas:
return yolo.parse_yolo_nas([output[1], output[0]])
return yolo.parse_yolov9(output[0])
def prepare():
# the input_tensor can be created with the shared_memory=True parameter,
@@ -400,9 +389,7 @@ class OpenVINOPlugin(
input_tensor = await asyncio.get_event_loop().run_in_executor(
prepareExecutor, lambda: prepare()
)
objs = await asyncio.get_event_loop().run_in_executor(
predictExecutor, lambda: predict(input_tensor)
)
objs = await predict(input_tensor)
except:
traceback.print_exc()

View File

@@ -16,6 +16,10 @@ faceRecognizePrepare, faceRecognizePredict = async_infer.create_executors(
class OpenVINOFaceRecognition(FaceRecognizeDetection):
def __init__(self, plugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
self.prefer_relu = True
def downloadModel(self, model: str):
scrypted_yolov9 = "scrypted_yolov9" in model
ovmodel = "best-converted" if scrypted_yolov9 else "best"
@@ -30,7 +34,7 @@ class OpenVINOFaceRecognition(FaceRecognizeDetection):
f"{model_version}/{model}/{precision}/{ovmodel}.bin",
)
print(xmlFile, binFile)
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
return self.plugin.core.compile_model(xmlFile, self.plugin.recognition_mode)
async def predictDetectModel(self, input: Image.Image):
def predict():

View File

@@ -28,7 +28,7 @@ class OpenVINOTextRecognition(TextRecognition):
f"{model_version}/{model}/{precision}/{ovmodel}.bin",
)
print(xmlFile, binFile)
return self.plugin.core.compile_model(xmlFile, self.plugin.mode)
return self.plugin.core.compile_model(xmlFile, self.plugin.recognition_mode)
async def predictDetectModel(self, input: np.ndarray):
def predict():

View File

@@ -26,6 +26,9 @@ class FaceRecognizeDetection(PredictPlugin):
def __init__(self, plugin: PredictPlugin, nativeId: str):
super().__init__(nativeId=nativeId, plugin=plugin)
if not hasattr(self, "prefer_relu"):
self.prefer_relu = False
self.inputheight = 320
self.inputwidth = 320
@@ -35,7 +38,7 @@ class FaceRecognizeDetection(PredictPlugin):
self.loop = asyncio.get_event_loop()
self.minThreshold = 0.5
self.detectModel = self.downloadModel("scrypted_yolov9t_face_320")
self.detectModel = self.downloadModel("scrypted_yolov9t_relu_face_320" if self.prefer_relu else "scrypted_yolov9t_face_320")
self.faceModel = self.downloadModel("inception_resnet_v1")
def downloadModel(self, model: str):

View File

@@ -1,5 +1,7 @@
# openvino 2024.5.0 crashes NPU. Update: NPU can not be used with AUTO in this version
# openvino 2024.4.0 crashes legacy systems.
# openvino 2024.3.0 crashes on older CPU (J4105 and older) if level-zero is installed via apt.
# openvino 2024.2.0 and older crashes on arc dGPU.
openvino==2024.4.0
openvino==2024.5.0
Pillow==10.3.0
opencv-python-headless==4.10.0.84

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.38",
"version": "0.10.39",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.38",
"version": "0.10.39",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/prebuffer-mixin",
"version": "0.10.38",
"version": "0.10.39",
"description": "Video Stream Rebroadcast, Prebuffer, and Management Plugin for Scrypted.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -2,19 +2,33 @@ import net from 'net';
import sdk from '@scrypted/sdk';
export async function getUrlLocalAdresses(console: Console, url: string) {
let urls: string[];
try {
const addresses = await sdk.endpointManager.getLocalAddresses();
if (!addresses)
return;
const urls = addresses.map(address => {
urls = addresses.map(address => {
const u = new URL(url);
u.hostname = net.isIPv6(address) ? `[${address}]` : address;
return u.toString();
});
return urls;
}
catch (e) {
console.warn('Error determining external addresses. Is Scrypted Server Address configured?', e);
return
}
if (process.env.SCRYPTED_CLUSTER_ADDRESS) {
try {
const clusterUrl = new URL(url);
clusterUrl.hostname = process.env.SCRYPTED_CLUSTER_ADDRESS;
const str = clusterUrl.toString();
if (!urls.includes(str))
urls.push(str);
}
catch (e) {
console.warn('Error determining external addresses. Is Scrypted Cluster Address configured?', e);
}
}
return urls;
}

View File

@@ -27,12 +27,13 @@ export interface DevInfo {
wifi: number;
}
async function getDeviceInfo(host: string, username: string, password: string): Promise<DevInfo> {
async function getDeviceInfoInternal(host: string, parameters: Record<string, string>): Promise<DevInfo> {
const url = new URL(`http://${host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetDevInfo');
params.set('user', username);
params.set('password', password);
for (const [key, value] of Object.entries(parameters)) {
params.set(key, value);
}
const response = await httpFetch({
url,
@@ -43,13 +44,24 @@ async function getDeviceInfo(host: string, username: string, password: string):
if (error)
throw new Error('error during call to getDeviceInfo');
return response.body?.[0]?.value?.DevInfo;
const ret: DevInfo = response.body?.[0]?.value?.DevInfo;
if (!ret?.type && !ret?.model && !ret?.exactType)
throw new Error('device info return unexpected data');
return ret;
}
export async function getDeviceInfo(host: string, username: string, password: string): Promise<DevInfo> {
const parameters = await getLoginParameters(host, username, password);
return getDeviceInfoInternal(host, parameters.parameters);
}
export async function getLoginParameters(host: string, username: string, password: string, forceToken?: boolean) {
if (!forceToken) {
try {
await getDeviceInfo(host, username, password);
await getDeviceInfoInternal(host, {
user: username,
password,
});
return {
parameters: {
user: username,

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/snapshot",
"version": "0.2.55",
"version": "0.2.56",
"description": "Snapshot Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
@@ -34,8 +34,8 @@
]
},
"dependencies": {
"@types/node": "^20.10.6",
"sharp": "^0.33.1",
"@types/node": "^22.10.2",
"sharp": "^0.33.5",
"whatwg-mimetype": "^4.0.0"
},
"devDependencies": {

View File

@@ -117,7 +117,7 @@ export class VipsImage implements Image {
}
async toImage(options: ImageOptions) {
if (options.format)
if (options?.format)
throw new Error('format can only be used with toBuffer');
const newVipsImage = await this.toVipsImage(options);
return createVipsMediaObject(newVipsImage);

View File

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

View File

@@ -2,9 +2,17 @@
This plugin adds two way audio support for Tapo cameras. This plugin does not import cameras into Scrypted. Use the ONVIF plugin to import Tapo cameras, and then use this plugin to add two way audio support.
# Setup
## Tapo Setup
1. Add the Tapo Camera using the ONVIF Plugin.
1. Open the Tapo app on iOS/Android.
2. Click `Me` in the bottom bar.
3. Click `Tapo Lab`.
4. Enable Third Party Compatibility.
## Scrypted Setup
1. Add the Tapo Camera using the ONVIF Plugin. The ONVIF password can be found in the camera's settings in the Tapo app: Settings -> Advanced Settings -> Camera Account -> Account Information.
2. Enable ONVIF Two Way Audio on the camera.
3. Enable the Tapo Two Way Audio extension.
4. Enter your Tapo Cloud password into the Tapo Two Way Audio Settings.
4. Enter your Tapo Cloud password into the Tapo Two Way Audio Settings. This is not the same as the ONVIF password.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/tapo",
"version": "0.0.19",
"version": "0.0.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/tapo",
"version": "0.0.19",
"version": "0.0.20",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
@@ -34,23 +34,29 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.45",
"version": "0.3.100",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
@@ -62,11 +68,9 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@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"
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.11"
}
},
"node_modules/@cspotcode/source-map-support": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/tapo",
"version": "0.0.19",
"version": "0.0.20",
"description": "Tapo Camera Plugin for Scrypted",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",

View File

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

View File

@@ -1,19 +1,7 @@
{
// docker installation
"scrypted.debugHost": "koushik-ubuntuvm",
"scrypted.serverRoot": "/server",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// local checkout
// "scrypted.debugHost": "127.0.0.1",
// "scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "koushik-windows",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"scrypted.debugHost": "127.0.0.1",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]

View File

@@ -1,51 +1,56 @@
{
"name": "@scrypted/tensorflow-lite",
"version": "0.1.68",
"version": "0.1.74",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/tensorflow-lite",
"version": "0.1.68",
"version": "0.1.74",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.39",
"version": "0.3.102",
"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.15.9",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"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",
"webpack-bundle-analyzer": "^4.5.0"
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"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/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.11"
}
},
"../sdk": {
@@ -60,24 +65,29 @@
"@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",
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"@types/node": "^22.10.1",
"adm-zip": "^0.5.16",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"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"
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typedoc": "^0.26.11",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
}
}
}

View File

@@ -58,5 +58,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.68"
"version": "0.1.74"
}

View File

@@ -19,19 +19,24 @@ except Exception as e:
pass
import asyncio
import concurrent.futures
import queue
import re
from typing import Any, Tuple
import scrypted_sdk
import tflite_runtime.interpreter as tflite
from .yolo_separate_outputs import *
from scrypted_sdk.types import Setting, SettingValue
from common import yolo
from predict import PredictPlugin
prepareExecutor = concurrent.futures.ThreadPoolExecutor(thread_name_prefix="TFLite-Prepare")
availableModels = [
"Default",
"scrypted_yolov9s_relu_sep_320",
"scrypted_yolov9t_relu_320",
"scrypted_yolov9s_relu_320",
"ssd_mobilenet_v2_coco_quant_postprocess",
"tf2_ssd_mobilenet_v2_coco17_ptq",
"ssdlite_mobiledet_coco_qat_postprocess",
@@ -49,6 +54,7 @@ availableModels = [
"efficientdet_lite3x_640_ptq",
]
def parse_label_contents(contents: str):
lines = contents.splitlines()
lines = [line for line in lines if line.strip()]
@@ -79,7 +85,7 @@ class TensorFlowLitePlugin(
edge_tpus = None
pass
model_version = "v13"
model_version = "v14"
model = self.storage.getItem("model") or "Default"
if model not in availableModels:
self.storage.setItem("model", "Default")
@@ -94,17 +100,12 @@ class TensorFlowLitePlugin(
nonlocal model
if defaultModel:
model = "scrypted_yolov10n_320"
# if edge_tpus and next(
# (obj for obj in edge_tpus if obj["type"] == "usb"), None
# ):
# model = "ssdlite_mobiledet_coco_qat_postprocess"
# else:
# model = "efficientdet_lite0_320_ptq"
model = "scrypted_yolov9s_relu_sep_320"
self.yolo = "yolo" in model
self.yolov9 = "yolov9" in model
self.scrypted_model = "scrypted" in model
self.scrypted_yolov10 = "scrypted_yolov10" in model
self.scrypted_yolo_sep = "_sep" in model
self.modelName = model
print(f"model: {model}")
@@ -149,7 +150,8 @@ class TensorFlowLitePlugin(
try:
interpreter = make_interpreter(modelFile, ":%s" % idx)
interpreter.allocate_tensors()
_, height, width, channels = interpreter.get_input_details()[0][
self.image_input_details = interpreter.get_input_details()[0]
_, height, width, channels = self.image_input_details[
"shape"
]
self.input_details = int(width), int(height), int(channels)
@@ -171,7 +173,8 @@ class TensorFlowLitePlugin(
modelFile = downloadModel()
interpreter = tflite.Interpreter(model_path=modelFile)
interpreter.allocate_tensors()
_, height, width, channels = interpreter.get_input_details()[0]["shape"]
self.image_input_details = interpreter.get_input_details()[0]
_, height, width, channels = self.image_input_details["shape"]
self.input_details = int(width), int(height), int(channels)
available_interpreters.append(interpreter)
self.interpreter_count = self.interpreter_count + 1
@@ -182,8 +185,7 @@ class TensorFlowLitePlugin(
thread_name = threading.current_thread().name
interpreter = available_interpreters.pop()
self.interpreters[thread_name] = interpreter
print('Interpreter initialized on thread {}'.format(thread_name))
print("Interpreter initialized on thread {}".format(thread_name))
self.executor = concurrent.futures.ThreadPoolExecutor(
initializer=executor_initializer,
@@ -223,61 +225,101 @@ class TensorFlowLitePlugin(
return self.input_details[0:2]
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
def predict():
interpreter = self.interpreters[threading.current_thread().name]
if self.yolo:
tensor_index = input_details(interpreter, "index")
def prepare():
if not self.yolo:
return input
im = np.stack([input])
i = interpreter.get_input_details()[0]
if i["dtype"] == np.int8:
scale, zero_point = i["quantization"]
if scale == 0.003986024297773838 and zero_point == -128:
# fast path for quantization 1/255 = 0.003986024297773838
im = im.view(np.int8)
im -= 128
else:
im = im.astype(np.float32) / (255.0 * scale)
im = (im + zero_point).astype(np.int8) # de-scale
else:
# this code path is unused.
im = im.astype(np.float32) / 255.0
interpreter.set_tensor(tensor_index, im)
interpreter.invoke()
output_details = interpreter.get_output_details()
output = output_details[0]
x = interpreter.get_tensor(output["index"])
input_scale = self.get_input_details()[0]
if x.dtype == np.int8:
scale, zero_point = output["quantization"]
threshold = yolo.defaultThreshold / scale + zero_point
combined_scale = scale * input_scale
if self.scrypted_yolov10:
objs = yolo.parse_yolov10(
x[0],
threshold,
scale=lambda v: (v - zero_point) * combined_scale,
confidence_scale=lambda v: (v - zero_point) * scale,
)
else:
objs = yolo.parse_yolov9(
x[0],
threshold,
scale=lambda v: (v - zero_point) * combined_scale,
confidence_scale=lambda v: (v - zero_point) * scale,
)
else:
# this code path is unused.
objs = yolo.parse_yolov9(x[0], scale=lambda v: v * input_scale)
im = np.stack([input])
# this non-quantized code path is unused but here for reference.
if self.image_input_details["dtype"] != np.int8 and self.image_input_details["dtype"] != np.int16:
im = im.astype(np.float32) / 255.0
return im
scale, zero_point = self.image_input_details["quantization"]
if scale == 0.003986024297773838 and zero_point == -128:
# fast path for quantization 1/255 = 0.003986024297773838
im = im.view(np.int8)
im -= 128
else:
tflite_common.set_input(interpreter, input)
im = im.astype(np.float32) / (255.0 * scale)
im = (im + zero_point).astype(np.int8) # de-scale
return im
def predict(im):
interpreter = self.interpreters[threading.current_thread().name]
if not self.yolo:
tflite_common.set_input(interpreter, im)
interpreter.invoke()
objs = detect.get_objects(
interpreter, score_threshold=0.2, image_scale=(1, 1)
)
return objs
tensor_index = input_details(interpreter, "index")
interpreter.set_tensor(tensor_index, im)
interpreter.invoke()
output_details = interpreter.get_output_details()
output_tensors = [(interpreter.get_tensor(output["index"]), output) for output in output_details]
return output_tensors
def post_process(output_tensors):
if not self.yolo:
return output_tensors
# handle separate outputs for quantization accuracy
if self.scrypted_yolo_sep:
outputs = []
for ot, output in output_tensors:
o = ot.astype(np.float32)
scale, zero_point = output["quantization"]
o -= zero_point
o *= scale
outputs.append(o)
output = yolo_separate_outputs.decode_bbox(outputs, [input.width, input.height])
if self.scrypted_yolov10:
objs = yolo.parse_yolov10(output[0])
else:
objs = yolo.parse_yolov9(output[0])
return objs
# this scale stuff can probably be optimized to dequantize ahead of time...
x, output = output_tensors[0]
input_scale = self.get_input_details()[0]
# this non-quantized code path is unused but here for reference.
if x.dtype != np.int8 and x.dtype != np.int16:
if self.scrypted_yolov10:
objs = yolo.parse_yolov10(x[0], scale=lambda v: v * input_scale)
else:
objs = yolo.parse_yolov9(x[0], scale=lambda v: v * input_scale)
return objs
# this scale stuff can probably be optimized to dequantize ahead of time...
scale, zero_point = output["quantization"]
combined_scale = scale * input_scale
if self.scrypted_yolov10:
objs = yolo.parse_yolov10(
x[0],
scale=lambda v: (v - zero_point) * combined_scale,
confidence_scale=lambda v: (v - zero_point) * scale,
threshold_scale=lambda v: (v - zero_point) * scale,
)
else:
objs = yolo.parse_yolov9(
x[0],
scale=lambda v: (v - zero_point) * combined_scale,
confidence_scale=lambda v: (v - zero_point) * scale,
threshold_scale=lambda v: (v - zero_point) * scale,
)
return objs
objs = await asyncio.get_event_loop().run_in_executor(self.executor, predict)
im = await asyncio.get_event_loop().run_in_executor(prepareExecutor, prepare)
output_tensors = await asyncio.get_event_loop().run_in_executor(self.executor, lambda: predict(im))
objs = await asyncio.get_event_loop().run_in_executor(prepareExecutor, lambda: post_process(output_tensors))
ret = self.create_detection_result(objs, src_size, cvss)
return ret

View File

@@ -0,0 +1,65 @@
import numpy as np
from common.softmax import softmax
class DFL:
def __init__(self, c1=16):
self.c1 = c1
self.conv_weights = np.arange(c1).reshape(1, c1, 1, 1)
def forward(self, x):
b, _, a = x.shape # batch, channels, anchors
x = x.reshape(b, 4, self.c1, a).transpose(0, 2, 1, 3)
x = softmax(x, axis=1)
x = np.sum(self.conv_weights * x, axis=1)
return x.reshape(b, 4, a)
def make_anchors(feats, strides, grid_cell_offset=0.5):
anchor_points, stride_tensor = [], []
assert feats is not None
dtype = feats[0].dtype
for i, stride in enumerate(strides):
_, _, h, w = feats[i].shape
sx = np.arange(w, dtype=dtype) + grid_cell_offset # shift x
sy = np.arange(h, dtype=dtype) + grid_cell_offset # shift y
sy, sx = np.meshgrid(sy, sx, indexing="ij")
anchor_points.append(np.stack((sx, sy), axis=-1).reshape(-1, 2))
stride_tensor.append(np.full((h * w, 1), stride, dtype=dtype))
return np.concatenate(anchor_points), np.concatenate(stride_tensor)
def dist2bbox(distance, anchor_points, xywh=True, dim=-1):
lt, rb = np.split(distance, 2, axis=dim)
anchor_points = anchor_points.transpose(0, 2, 1)
x1y1 = anchor_points - lt
x2y2 = anchor_points + rb
if xywh:
c_xy = (x1y1 + x2y2) / 2
wh = x2y2 - x1y1
return np.concatenate((c_xy, wh), axis=dim) # xywh bbox
return np.concatenate((x1y1, x2y2), axis=dim) # xyxy bbox
def decode_bbox(preds, img_shape):
num_classes = next((o.shape[2] for o in preds if o.shape[2] != 64), -1)
assert num_classes != -1, 'cannot infer postprocessor inputs via output shape if there are 64 classes'
pos = [
i for i, _ in sorted(enumerate(preds),
key=lambda x: (x[1].shape[2] if num_classes > 64 else -x[1].shape[2], -x[1].shape[1]))]
x = np.transpose(
np.concatenate([
np.concatenate([preds[i] for i in pos[:len(pos) // 2]], axis=1),
np.concatenate([preds[i] for i in pos[len(pos) // 2:]], axis=1)], axis=2), (0, 2, 1))
reg_max = (x.shape[1] - num_classes) // 4
dfl = DFL(reg_max) if reg_max > 1 else lambda x: x
img_h, img_w = img_shape[-2], img_shape[-1]
strides = [
int(np.sqrt(img_shape[-2] * img_shape[-1] / preds[p].shape[1])) for p in pos if preds[p].shape[2] != 64]
dims = [(img_h // s, img_w // s) for s in strides]
fake_feats = [np.zeros((1, 1, h, w), dtype=preds[0].dtype) for h, w in dims]
anchors, strides = make_anchors(fake_feats, strides, 0.5)
strides_tensor = strides.transpose(1, 0)
strides_tensor = np.expand_dims(strides_tensor, 0)
dbox = dist2bbox(dfl.forward(x[:, :-num_classes, :]), anchors[None, ...], xywh=True, dim=1) * strides_tensor
return np.concatenate((dbox, 1 / (1 + np.exp(-x[:, -num_classes:, :]))), axis=1)

View File

@@ -9,18 +9,15 @@
"version": "0.0.164",
"license": "Apache",
"dependencies": {
"@koush/unifi-protect": "file:../../external/unifi-protect",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^1.7.8",
"unifi-protect": "^4.16.0",
"ws": "^8.18.0"
"axios": "^1.4.0",
"ws": "^8.13.0"
},
"devDependencies": {
"@types/node": "^22.9.4",
"@types/ws": "^8.5.13"
},
"optionalDependencies": {
"@adobe/fetch": "^4.1.9"
"@types/node": "^20.4.2",
"@types/ws": "^8.5.5"
}
},
"../../common": {
@@ -29,33 +26,58 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^5.0.1",
"typescript": "^5.5.3"
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^20.11.0",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2"
}
},
"../../external/unifi-protect": {
"name": "@koush/unifi-protect",
"version": "3.0.4",
"license": "ISC",
"dependencies": {
"abort-controller": "^3.0.0",
"domexception": "^4.0.0",
"node-fetch": "^3.3.0",
"util": "^0.12.4",
"ws": "^8.5.0"
},
"devDependencies": {
"@types/node": "^17.0.18",
"@types/ws": "^8.2.3",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"rimraf": "^3.0.2",
"typescript": "^4.5.5"
},
"engines": {
"node": ">=12"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.88",
"version": "0.3.31",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.26.0",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^1.6.5",
"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": "^6.0.1",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"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",
@@ -67,28 +89,19 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.10"
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
}
},
"../sdk": {
"extraneous": true
},
"node_modules/@adobe/fetch": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@adobe/fetch/-/fetch-4.1.9.tgz",
"integrity": "sha512-FWIzm4vvl1OtCarTBgWDW6otj4gxrNmMf/DdriqBUw7DjjmckjT3ZQA43b4HzBkzkuAHhajwMy91btp9fWgTEw==",
"dependencies": {
"debug": "4.3.7",
"http-cache-semantics": "4.1.1",
"lru-cache": "7.18.3"
},
"engines": {
"node": ">=14.16"
}
"node_modules/@koush/unifi-protect": {
"resolved": "../../external/unifi-protect",
"link": true
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
@@ -99,18 +112,15 @@
"link": true
},
"node_modules/@types/node": {
"version": "22.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.4.tgz",
"integrity": "sha512-d9RWfoR7JC/87vj7n+PVTzGg9hDyuFjir3RxUHbjFSKNd9mpxbxwMEyaCim/ddCmy4IuW7HjTzF3g9p3EtWEOg==",
"dev": true,
"dependencies": {
"undici-types": "~6.19.8"
}
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
"dev": true
},
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -122,28 +132,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dependencies": {
"follow-redirects": "^1.15.6",
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -155,22 +152,6 @@
"node": ">= 0.8"
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -180,9 +161,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"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",
@@ -211,19 +192,6 @@
"node": ">= 6"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"engines": {
"node": ">=12"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -243,55 +211,15 @@
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"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/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"node_modules/unifi-protect": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/unifi-protect/-/unifi-protect-4.16.0.tgz",
"integrity": "sha512-M8/VUTKhPxlzagIQdpjvXbdUPp4a/3F051CghaLXWT9JfnVuJZGLYC3U1zYOXtKVIfqP+KcTmn6sSTawHTGADQ==",
"dependencies": {
"@adobe/fetch": "4.1.9",
"ws": "8.18.0"
},
"bin": {
"ufp": "dist/util/ufp.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"bufferutil": "4.0.8"
}
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": {
"node": ">=10.0.0"
},
@@ -310,63 +238,68 @@
}
},
"dependencies": {
"@adobe/fetch": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@adobe/fetch/-/fetch-4.1.9.tgz",
"integrity": "sha512-FWIzm4vvl1OtCarTBgWDW6otj4gxrNmMf/DdriqBUw7DjjmckjT3ZQA43b4HzBkzkuAHhajwMy91btp9fWgTEw==",
"@koush/unifi-protect": {
"version": "file:../../external/unifi-protect",
"requires": {
"debug": "4.3.7",
"http-cache-semantics": "4.1.1",
"lru-cache": "7.18.3"
"@types/node": "^17.0.18",
"@types/ws": "^8.2.3",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"abort-controller": "^3.0.0",
"domexception": "^4.0.0",
"eslint": "^8.9.0",
"node-fetch": "^3.3.0",
"rimraf": "^3.0.2",
"typescript": "^4.5.5",
"util": "^0.12.4",
"ws": "^8.5.0"
}
},
"@scrypted/common": {
"version": "file:../../common",
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"monaco-editor": "^0.50.0",
"ts-node": "^10.9.2",
"typescript": "^5.5.3"
"typescript": "^5.3.3"
}
},
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.26.0",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^1.6.5",
"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": "^6.0.1",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.10",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@types/node": {
"version": "22.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.4.tgz",
"integrity": "sha512-d9RWfoR7JC/87vj7n+PVTzGg9hDyuFjir3RxUHbjFSKNd9mpxbxwMEyaCim/ddCmy4IuW7HjTzF3g9p3EtWEOg==",
"dev": true,
"requires": {
"undici-types": "~6.19.8"
}
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
"dev": true
},
"@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -378,24 +311,15 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"requires": {
"follow-redirects": "^1.15.6",
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"optional": true,
"requires": {
"node-gyp-build": "^4.3.0"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -404,23 +328,15 @@
"delayed-stream": "~1.0.0"
}
},
"debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"requires": {
"ms": "^2.1.3"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
},
"form-data": {
"version": "4.0.0",
@@ -432,16 +348,6 @@
"mime-types": "^2.1.12"
}
},
"http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -455,42 +361,15 @@
"mime-db": "1.52.0"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"optional": true
},
"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=="
},
"undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
},
"unifi-protect": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/unifi-protect/-/unifi-protect-4.16.0.tgz",
"integrity": "sha512-M8/VUTKhPxlzagIQdpjvXbdUPp4a/3F051CghaLXWT9JfnVuJZGLYC3U1zYOXtKVIfqP+KcTmn6sSTawHTGADQ==",
"requires": {
"@adobe/fetch": "4.1.9",
"bufferutil": "4.0.8",
"ws": "8.18.0"
}
},
"ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"requires": {}
}
}

View File

@@ -1,6 +1,5 @@
{
"name": "@scrypted/unifi-protect",
"type": "module",
"version": "0.0.164",
"description": "Unifi Protect Plugin for Scrypted",
"author": "Scrypted",
@@ -29,22 +28,20 @@
"DeviceProvider",
"Settings"
],
"babel": true,
"pluginDependencies": [
"@scrypted/prebuffer-mixin"
]
},
"devDependencies": {
"@types/node": "^22.9.4",
"@types/ws": "^8.5.13"
"@types/node": "^20.4.2",
"@types/ws": "^8.5.5"
},
"dependencies": {
"@koush/unifi-protect": "file:../../external/unifi-protect",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^1.7.8",
"unifi-protect": "^4.16.0",
"ws": "^8.18.0"
},
"optionalDependencies": {
"@adobe/fetch": "^4.1.9"
"axios": "^1.4.0",
"ws": "^8.13.0"
}
}

View File

@@ -96,16 +96,16 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
});
const camera = this.findCamera();
const camera = this.findCamera() as any;
await this.protect.api.updateDevice(camera, {
await this.protect.api.updateCamera(camera, {
privacyZones,
} as any);
}
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
const camera = this.findCamera();
await this.protect.api.updateDevice(camera, {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
ispSettings: {
zoomPosition: Math.abs(command.zoom * 100),
}
@@ -113,8 +113,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
async setStatusLight(on: boolean) {
const camera = this.findCamera();
await this.protect.api.updateDevice(camera, {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
ledSettings: {
isEnabled: on,
}
@@ -170,9 +170,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
const camera = this.findCamera();
const endpoint = new URL(this.protect.api.getApiEndpoint("talkback"));
endpoint.searchParams.set('camera', camera.id);
const response = await this.protect.loginFetch(endpoint.toString());
const params = new URLSearchParams({ camera: camera.id });
const response = await this.protect.loginFetch(this.protect.api.wsUrl() + "/talkback?" + params.toString());
const tb = response.data as Record<string, string>;
// Adjust the URL for our address.
@@ -376,7 +375,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
findCamera() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.bootstrap.cameras.find(camera => camera.id === id);
return this.protect.api.cameras.find(camera => camera.id === id);
}
async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
const camera = this.findCamera();
@@ -442,9 +441,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
const sanitizedBitrate = Math.min(channel.maxBitrate, Math.max(channel.minBitrate, bitrate));
this.console.log(channel.name, 'bitrate change requested', bitrate, 'clamped to', sanitizedBitrate);
channel.bitrate = sanitizedBitrate;
const cameraResult = await this.protect.api.updateDevice(camera, {
channels: camera.channels,
});
const cameraResult = await this.protect.api.updateCameraChannels(camera);
if (!cameraResult) {
throw new Error("setVideoStreamOptions failed")
}
@@ -461,7 +458,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
setMotionDetected(motionDetected: boolean) {
this.motionDetected = motionDetected;
if (this.findCamera().featureFlags.hasPackageCamera) {
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
if (deviceManager.getNativeIds().includes(this.packageCameraNativeId)) {
this.ensurePackageCamera();
this.packageCamera.motionDetected = motionDetected;
@@ -483,7 +480,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
text: title.substring(0, 30),
type: 'CUSTOM_MESSAGE',
};
this.protect.api.updateDevice(this.findCamera(), {
this.protect.api.updateCamera(this.findCamera(), {
lcdMessage: payload,
})

View File

@@ -14,23 +14,23 @@ export class UnifiLight extends ScryptedDeviceBase implements OnOff, Brightness,
this.console.log(protectLight);
}
async turnOff(): Promise<void> {
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
if (!result)
this.console.error('turnOff failed.');
}
async turnOn(): Promise<void> {
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
if (!result)
this.console.error('turnOn failed.');
}
async setBrightness(brightness: number): Promise<void> {
const ledLevel = Math.round(((brightness as number) / 20) + 1);
this.protect.api.updateDevice(this.findLight(), { lightDeviceSettings: { ledLevel } });
this.protect.api.updateLight(this.findLight(), { lightDeviceSettings: { ledLevel } });
}
findLight() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.bootstrap.lights.find(light => light.id === id);
return this.protect.api.lights.find(light => light.id === id);
}
updateState(light?: Readonly<ProtectLightConfig>) {

View File

@@ -1,8 +1,9 @@
import { Lock, LockState, ScryptedDeviceBase } from "@scrypted/sdk";
import { UnifiProtect } from "./main";
import { ProtectDoorLockConfig } from "./unifi-protect";
export class UnifiLock extends ScryptedDeviceBase implements Lock {
constructor(public protect: UnifiProtect, nativeId: string, protectLock: any) {
constructor(public protect: UnifiProtect, nativeId: string, protectLock: Readonly<ProtectDoorLockConfig>) {
super(nativeId);
this.updateState(protectLock);
@@ -10,23 +11,23 @@ export class UnifiLock extends ScryptedDeviceBase implements Lock {
}
async lock(): Promise<void> {
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/close`, {
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/close`, {
method: 'POST',
});
}
async unlock(): Promise<void> {
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/open`, {
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/open`, {
method: 'POST',
});
}
findLock() {
const id = this.protect.findId(this.nativeId);
return (this.protect.api.bootstrap.doorlocks as any).find(doorlock => doorlock.id === id);
return this.protect.api.doorlocks.find(doorlock => doorlock.id === id);
}
updateState(lock?: any) {
updateState(lock?: Readonly<ProtectDoorLockConfig>) {
lock = lock || this.findLock();
if (!lock)
return;

View File

@@ -4,12 +4,12 @@ import sdk, { Device, DeviceProvider, ObjectDetectionResult, ObjectsDetected, Sc
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios from "axios";
import { UnifiCamera } from "./camera";
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
import { UnifiLight } from "./light";
import { UnifiLock } from "./lock";
import { debounceFingerprintDetected, debounceMotionDetected } from "./camera-sensors";
import { UnifiSensor } from "./sensor";
import { FeatureFlagsShim } from "./shim";
import { ProtectApi, ProtectCameraConfigInterface, ProtectEventAddInterface, ProtectEventPacket } from "./unifi-protect";
import { FeatureFlagsShim, LastSeenShim } from "./shim";
import { ProtectApi, ProtectApiUpdates, ProtectNvrUpdatePayloadCameraUpdate, ProtectNvrUpdatePayloadEventAdd } from "./unifi-protect";
const { deviceManager } = sdk;
@@ -54,10 +54,10 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return;
}
const device = this.api.bootstrap.cameras?.find(c => c.id === packet.action.id)
|| this.api.bootstrap.lights?.find(c => c.id === packet.action.id)
|| (this.api.bootstrap.doorlocks as any)?.find(c => c.id === packet.action.id)
|| this.api.bootstrap.sensors?.find(c => c.id === packet.action.id);
const device = this.api.cameras?.find(c => c.id === packet.action.id)
|| this.api.lights?.find(c => c.id === packet.action.id)
|| this.api.doorlocks?.find(c => c.id === packet.action.id)
|| this.api.sensors?.find(c => c.id === packet.action.id);
if (!device) {
return;
@@ -100,7 +100,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
})
}
listener(updatePacket: ProtectEventPacket) {
listener(event: Buffer) {
const updatePacket = ProtectApiUpdates.decodeUpdatePacket(this.console, event);
if (!updatePacket)
return;
@@ -108,27 +109,27 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
const unifiDevice = this.handleUpdatePacket(updatePacket);
switch (updatePacket.header.modelKey) {
switch (updatePacket.action.modelKey) {
case "sensor":
case "doorlock":
case "light":
case "camera": {
if (!unifiDevice) {
this.console.log('unknown device, sync needed?', updatePacket.header.id);
this.console.log('unknown device, sync needed?', updatePacket.action.id);
return;
}
if (updatePacket.header.action !== "update") {
unifiDevice.console.log('non update', updatePacket.header.action);
if (updatePacket.action.action !== "update") {
unifiDevice.console.log('non update', updatePacket.action.action);
return;
}
unifiDevice.updateState();
if (updatePacket.header.modelKey === "doorlock")
if (updatePacket.action.modelKey === "doorlock")
return;
const payload = updatePacket.payload as ProtectCameraConfigInterface;
const payload = updatePacket.payload as any as ProtectNvrUpdatePayloadCameraUpdate & LastSeenShim;
if (updatePacket.header.modelKey !== "camera")
if (updatePacket.action.modelKey !== "camera")
return;
const unifiCamera = unifiDevice as UnifiCamera;
@@ -141,19 +142,19 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
break;
}
case "event": {
const payload = updatePacket.payload as ProtectEventAddInterface;
if (updatePacket.header.action !== "add") {
if (payload.end && updatePacket.header.id) {
if (updatePacket.action.action !== "add") {
if ((updatePacket?.payload as any)?.end && updatePacket.action.id) {
// unifi reports the event ended but it seems to take a moment before the snapshot
// is actually ready.
setTimeout(() => {
const running = this.runningEvents.get(updatePacket.header.id);
const running = this.runningEvents.get(updatePacket.action.id);
running?.resolve?.(undefined)
}, 2000);
}
return;
}
const payload = updatePacket.payload as ProtectNvrUpdatePayloadEventAdd;
if (!payload.camera)
return;
const nativeId = this.getNativeId({ id: payload.camera }, false);
@@ -165,7 +166,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
const detectionId = payload.id;
const actionId = updatePacket.header.id;
const actionId = updatePacket.action.id;
let resolve: (value: unknown) => void;
const promise = new Promise(r => resolve = r);
@@ -248,26 +249,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
this.console.log(message, ...parameters);
}
reconnecting = false;
wsTimeout: NodeJS.Timeout;
reconnect(reason: string) {
return async () => {
if (this.reconnecting)
return;
this.reconnecting = true;
this.api?.reset();
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
await sleep(10000);
this.discoverDevices(0);
}
}
async discoverDevices(duration: number) {
this.api?.reset();
this.reconnecting = false;
clearTimeout(this.wsTimeout);
const ip = this.getSetting('ip');
const username = this.getSetting('username');
const password = this.getSetting('password');
@@ -289,8 +271,10 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return
}
this.api?.eventsWs?.removeAllListeners();
this.api?.eventsWs?.close();
if (!this.api) {
this.api = new ProtectApi({
this.api = new ProtectApi(ip, username, password, {
debug() { },
error: (...args) => {
this.console.error(...args);
@@ -300,37 +284,48 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
});
}
let reconnecting = false;
const reconnect = (reason: string) => {
return async () => {
if (reconnecting)
return;
reconnecting = true;
this.api?.eventsWs?.close();
this.api?.eventsWs?.emit('close');
this.api?.eventsWs?.removeAllListeners();
if (this.api.eventsWs) {
this.console.warn('Event Listener failed to close. Requesting plugin restart.');
deviceManager.requestRestart();
}
this.console.error('Event Listener reconnecting in 10 seconds:', reason);
await sleep(10000);
this.discoverDevices(0);
}
}
try {
const loginResult = await this.api.login(ip, username, password);
if (!loginResult) {
this.log.a('Login failed. Check credentials.');
return;
}
if (!await this.api.getBootstrap()) {
this.reconnect('refresh failed')();
if (!await this.api.refreshDevices()) {
reconnect('refresh failed')();
return;
}
let wsTimeout: NodeJS.Timeout;
const resetWsTimeout = () => {
clearTimeout(this.wsTimeout);
this.wsTimeout = setTimeout(() => this.reconnect('timeout'), 5 * 60 * 1000);
clearTimeout(wsTimeout);
wsTimeout = setTimeout(reconnect('timeout'), 5 * 60 * 1000);
};
resetWsTimeout();
this.api.on('message', message => {
this.api.eventsWs?.on('message', (data) => {
resetWsTimeout();
this.listener(message);
})
this.listener(data as Buffer);
});
this.api.eventsWs?.on('close', reconnect('close'));
this.api.eventsWs?.on('error', reconnect('error'));
const devices: Device[] = [];
if (!this.api.bootstrap.cameras.length) {
this.console.warn('no cameras found. is this an admin account? cancelling sync.');
return;
}
for (let camera of this.api.bootstrap.cameras || []) {
for (let camera of this.api.cameras || []) {
if (camera.isAdoptedByOther) {
this.console.log('skipping camera that is adopted by another nvr', camera.id, camera.name);
continue;
@@ -352,9 +347,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
if (needUpdate) {
camera = await this.api.updateDevice(camera, {
channels: camera.channels,
});
camera = await this.api.updateCameraChannels(camera);
if (!camera) {
this.log.a('Unable to enable RTSP and IDR interval on camera. Is this an admin account?');
continue;
@@ -399,7 +392,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
if (camera.featureFlags.hasLcdScreen) {
d.interfaces.push(ScryptedInterface.Notifier);
}
if (camera.featureFlags.hasPackageCamera) {
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
d.interfaces.push(ScryptedInterface.DeviceProvider);
}
if (camera.featureFlags.hasLedStatus) {
@@ -412,7 +405,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const sensor of this.api.bootstrap.sensors || []) {
for (const sensor of this.api.sensors || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: sensor.name,
@@ -420,7 +413,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
info: {
manufacturer: 'Ubiquiti',
model: sensor.type,
ip: (sensor as any).host,
ip: sensor.host,
firmware: sensor.firmwareVersion,
version: sensor.hardwareRevision,
serialNumber: sensor.id,
@@ -440,7 +433,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const light of this.api.bootstrap.lights || []) {
for (const light of this.api.lights || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: light.name,
@@ -465,7 +458,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const lock of (this.api.bootstrap.doorlocks as any) || []) {
for (const lock of this.api.doorlocks || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: lock.name,
@@ -497,12 +490,12 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
// handle package cameras as a sub device
for (const camera of this.api.bootstrap.cameras) {
for (const camera of this.api.cameras) {
const devices: Device[] = [];
const providerNativeId = this.getNativeId(camera, true);
if (camera.featureFlags.hasPackageCamera) {
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
const nativeId = providerNativeId + '-packageCamera';
const d: Device = {
providerNativeId,
@@ -576,25 +569,25 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return this.locks.get(nativeId);
const id = this.findId(nativeId);
const camera = this.api.bootstrap.cameras.find(camera => camera.id === id);
const camera = this.api.cameras.find(camera => camera.id === id);
if (camera) {
const ret = new UnifiCamera(this, nativeId, camera);
this.cameras.set(nativeId, ret);
return ret;
}
const sensor = this.api.bootstrap.sensors.find(sensor => sensor.id === id);
const sensor = this.api.sensors.find(sensor => sensor.id === id);
if (sensor) {
const ret = new UnifiSensor(this, nativeId, sensor);
this.sensors.set(nativeId, ret);
return ret;
}
const light = this.api.bootstrap.lights.find(light => light.id === id);
const light = this.api.lights.find(light => light.id === id);
if (light) {
const ret = new UnifiLight(this, nativeId, light);
this.lights.set(nativeId, ret);
return ret;
}
const lock = (this.api.bootstrap.doorlocks as any)?.find(lock => lock.id === id);
const lock = this.api.doorlocks?.find(lock => lock.id === id);
if (lock) {
const ret = new UnifiLock(this, nativeId, lock);
this.locks.set(nativeId, ret);

View File

@@ -15,7 +15,7 @@ export class UnifiSensor extends ScryptedDeviceBase implements Thermometer, Humi
findSensor() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.bootstrap.sensors.find(sensor => sensor.id === id);
return this.protect.api.sensors.find(sensor => sensor.id === id);
}
async setTemperatureUnit(temperatureUnit: TemperatureUnit): Promise<void> {

View File

@@ -1,8 +1,13 @@
export interface FeatureFlagsShim {
hasPackageCamera: boolean;
hasFingerprintSensor: boolean;
}
export interface LastSeenShim {
lastSeen: number;
}
export interface PrivacyZone {
id: number;
name: string;

View File

@@ -1,3 +1,3 @@
// export * from '@koush/unifi-protect'
export * from 'unifi-protect'
export * from '@koush/unifi-protect/src/index'
// export * from '../../../external/unifi-protect/src/index'

View File

@@ -1,9 +1,9 @@
{
"compilerOptions": {
"module": "es2022",
"module": "Node16",
"target": "ES2021",
"resolveJsonModule": true,
"moduleResolution": "bundler",
"moduleResolution": "Node16",
"esModuleInterop": true,
"sourceMap": true
},

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/webrtc",
"version": "0.2.55",
"version": "0.2.57",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/webrtc",
"version": "0.2.55",
"version": "0.2.57",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/webrtc",
"version": "0.2.55",
"version": "0.2.57",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -295,8 +295,20 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
const console = deviceManager.getMixinConsole(options?.sourceId, this.nativeId);
if (fromMimeType !== ScryptedMimeTypes.FFmpegInput && fromMimeType !== ScryptedMimeTypes.RequestMediaStream) {
try {
const mo = await mediaManager.createMediaObject(data, fromMimeType);
data = await mediaManager.convertMediaObjectToJSON<FFmpegInput>(mo, ScryptedMimeTypes.FFmpegInput);
}
catch (e) {
console.error('failed to create media object:', e);
throw new Error(`@scrypted/webrtc is unable to convert ${fromMimeType} to ${ScryptedMimeTypes.RTCSignalingChannel}`);
}
fromMimeType = ScryptedMimeTypes.FFmpegInput;
}
if (fromMimeType === ScryptedMimeTypes.FFmpegInput) {
const ffmpegInput: FFmpegInput = JSON.parse(data.toString());
const ffmpegInput: FFmpegInput = typeof data === 'object' && !Buffer.isBuffer(data) ? data : JSON.parse(data.toString());
const mo = await mediaManager.createFFmpegMediaObject(ffmpegInput);
class OnDemandSignalingChannel implements RTCSignalingChannel {
@@ -314,27 +326,23 @@ export class WebRTCPlugin extends AutoenableMixinProvider implements DeviceCreat
return new OnDemandSignalingChannel();
}
else if (fromMimeType === ScryptedMimeTypes.RequestMediaStream) {
const rms = data as RequestMediaStream;
const mo = await mediaManager.createMediaObject(rms, ScryptedMimeTypes.RequestMediaStream);
class OnDemandSignalingChannel implements RTCSignalingChannel {
async startRTCSignalingSession(session: RTCSignalingSession): Promise<RTCSessionControl> {
return createRTCPeerConnectionSink(session, console,
undefined,
mo,
plugin.storageSettings.values.requireOpus,
plugin.storageSettings.values.maximumCompatibilityMode,
plugin.getRTCConfiguration(),
await plugin.getWeriftConfiguration(),
);
}
}
return new OnDemandSignalingChannel();
}
else {
throw new Error(`@scrypted/webrtc is unable to convert ${fromMimeType} to ${ScryptedMimeTypes.RTCSignalingChannel}`);
const rms = data as RequestMediaStream;
const mo = await mediaManager.createMediaObject(rms, ScryptedMimeTypes.RequestMediaStream);
class OnDemandSignalingChannel implements RTCSignalingChannel {
async startRTCSignalingSession(session: RTCSignalingSession): Promise<RTCSessionControl> {
return createRTCPeerConnectionSink(session, console,
undefined,
mo,
plugin.storageSettings.values.requireOpus,
plugin.storageSettings.values.maximumCompatibilityMode,
plugin.getRTCConfiguration(),
await plugin.getWeriftConfiguration(),
);
}
}
return new OnDemandSignalingChannel();
}
async convertToRTCConnectionManagement(result: ReturnType<typeof zygote>, data: any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions) {

View File

@@ -157,6 +157,10 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
let rtspSdp: string;
const sdpDeferred = new Deferred<string>();
killDeferred.promise.finally(() => {
if (!sdpDeferred.finished)
sdpDeferred.reject(new Error('killed'));
});
const videoSectionDeferred = new Deferred<MSection>();
const audioSectionDeferred = new Deferred<MSection>();
videoSectionDeferred.promise.then(s => video?.onMSection?.(s));
@@ -279,6 +283,7 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
ffmpegLogInitialOutput(console, cp);
killDeferred.promise.finally(() => safeKillFFmpeg(cp));
cp.on('exit', () => killDeferred.resolve(undefined));
cp.on('error', () => killDeferred.resolve(undefined));
audioPipe = cp.stdio[3] as Writable;
}
@@ -489,6 +494,10 @@ export async function startRtpForwarderProcess(console: Console, ffmpegInput: FF
if (!allowAudioTranscoderExit)
killDeferred.resolve(undefined);
});
cp.on('error', () => {
if (!allowAudioTranscoderExit)
killDeferred.resolve(undefined);
});
if (pipeSdp) {
const pipe = cp.stdio[3] as Writable;
pipe.write(pipeSdp);

View File

@@ -6,7 +6,7 @@
"configurations": [
{
"name": "Scrypted Debugger",
"type": "python",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "${config:scrypted.debugHost}",
@@ -21,7 +21,7 @@
},
{
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
"remoteRoot": "."
},
]
}

View File

@@ -1,18 +1,7 @@
{
// docker installation
// "scrypted.debugHost": "koushik-thin",
// "scrypted.serverRoot": "/server",
"scrypted.debugHost": "scrypted-nvr",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// local checkout
"scrypted.debugHost": "127.0.0.1",
"scrypted.serverRoot": "/server",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/wyze",
"version": "0.0.53",
"version": "0.0.54",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/wyze",
"version": "0.0.53",
"version": "0.0.54",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -33,5 +33,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.53"
"version": "0.0.54"
}

View File

@@ -11,6 +11,7 @@ import sys
import threading
import time
import traceback
import socket
import urllib
import urllib.request
from ctypes import c_int
@@ -189,9 +190,10 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
ffmpeg = await scrypted_sdk.mediaManager.getFFmpegPath()
loop = asyncio.get_event_loop()
class RFC4571Writer:
class RFC4571Writer(asyncio.DatagramProtocol):
def connection_made(self, transport):
pass
sock = transport.get_extra_info('socket')
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 * 1024 * 1024)
def datagram_received(self, data, addr):
l = len(data)
@@ -221,7 +223,7 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
"rtp",
"-payload_type",
"96",
f"rtp://127.0.0.1:{vport}?pkt_size=1300",
f"rtp://127.0.0.1:{vport}?pkt_size=64000",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
@@ -258,7 +260,7 @@ class WyzeCamera(scrypted_sdk.ScryptedDeviceBase, VideoCamera, Settings, PanTilt
"rtp",
"-payload_type",
"97",
f"rtp://127.0.0.1:{aport}?pkt_size=1300",
f"rtp://127.0.0.1:{aport}?pkt_size=64000",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,

View File

@@ -56,7 +56,7 @@ exports.deploy = function (debugHost, noRebind) {
return new Promise((resolve, reject) => {
var out;
if (process.env.NODE_ENV == 'production')
if (process.env.NODE_ENV === 'production')
out = path.resolve(process.cwd(), 'dist');
else
out = path.resolve(process.cwd(), 'out');

View File

@@ -15,14 +15,16 @@ const os = require('os');
const rimraf = require('rimraf');
const webpack = require('webpack');
const tmp = require('tmp');
const child_process = require('child_process');
const { once } = require('events');
let out;
if (process.env.NODE_ENV === 'production')
out = path.resolve(cwd, 'dist');
else
out = path.resolve(cwd, 'out');
if (fs.existsSync(path.resolve(cwd, 'src/main.py'))) {
let out;
if (process.env.NODE_ENV == 'production')
out = path.resolve(cwd, 'dist');
else
out = path.resolve(cwd, 'out');
const resolved = path.resolve(cwd, 'src');
const zip = new AdmZip();
@@ -40,171 +42,193 @@ if (fs.existsSync(path.resolve(cwd, 'src/main.py'))) {
if (fs.existsSync(zipfs))
zip.addLocalFolder(zipfs, 'fs');
zip.writeZip(path.join(out, 'plugin.zip'));
return;
}
const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json').toString()));
const optionalDependencies = Object.keys(packageJson.optionalDependencies || {});
if (packageJson.scrypted.babel) {
process.env.SCRYPTED_WEBPACK_BABEL = 'true';
}
const defaultMainNodeJs = 'main.nodejs.js';
const entries = [];
if (packageJson.exports) {
for (const [key, value] of Object.entries(packageJson.exports)) {
entries.push({
filename: key,
output: value,
});
}
}
else {
const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json').toString()));
const optionalDependencies = Object.keys(packageJson.optionalDependencies || {});
if (packageJson.scrypted.babel) {
process.env.SCRYPTED_WEBPACK_BABEL = 'true';
}
const defaultMainNodeJs = 'main.nodejs.js';
const entries = [];
if (packageJson.exports) {
for (const [key, value] of Object.entries(packageJson.exports)) {
for (const search of ['src/main.js', 'src/main.ts']) {
const resolved = path.resolve(cwd, search);
if (fs.existsSync(resolved)) {
entries.push({
filename: key,
output: value,
filename: search,
output: defaultMainNodeJs,
});
break;
}
}
else {
for (const search of ['src/main.js', 'src/main.ts']) {
const resolved = path.resolve(cwd, search);
if (fs.existsSync(resolved)) {
entries.push({
filename: search,
output: defaultMainNodeJs,
});
break;
}
}
}
const nodeWebpackConfig = 'webpack.nodejs.config.js';
if (!entries?.length) {
console.warn('unable to locate src/main.js or src/main.ts');
console.warn('if a custom webpack config is used, will fall back to an entry configured there');
entries.push(undefined);
}
const zip = new AdmZip();
const readme = path.join(cwd, 'README.md');
if (fs.existsSync(readme)) {
let readmeText = fs.readFileSync(readme).toString();;
const changelog = path.join(cwd, 'CHANGELOG.md');
if (fs.existsSync(changelog)) {
readmeText += '\n\n\n<br/><br/>' + fs.readFileSync(changelog).toString();
}
zip.addFile('README.md', Buffer.from(readmeText));
}
const nodeWebpackConfig = 'webpack.nodejs.config.js';
const NODE_PATH = path.resolve(__dirname, '..', 'node_modules');
let out;
if (process.env.NODE_ENV == 'production')
out = path.resolve(cwd, 'dist');
else
out = path.resolve(cwd, 'out');
// hack to override NODE_PATH dynamically.
// otherwise webpack plugins are not found.
process.env.NODE_PATH = NODE_PATH;
require('module').Module._initPaths();
if (!entries?.length) {
console.warn('unable to locate src/main.js or src/main.ts');
console.warn('if a custom webpack config is used, will fall back to an entry configured there');
entries.push(undefined);
}
async function rollup() {
if (out)
rimraf.sync(out);
let webpackCmd = path.resolve(cwd, 'node_modules/.bin/webpack-cli');
if (!fs.existsSync(webpackCmd)) {
webpackCmd = path.resolve(cwd, 'node_modules/@scrypted/sdk/node_modules/.bin/webpack-cli')
let rollupCmd = path.resolve(cwd, 'node_modules/.bin/rollup');
if (!fs.existsSync(rollupCmd)) {
rollupCmd = path.resolve(cwd, 'node_modules/@scrypted/sdk/node_modules/.bin/rollup')
}
if (os.platform().startsWith('win')) {
webpackCmd += '.cmd';
rollupCmd += '.cmd';
}
const zip = new AdmZip();
const readme = path.join(cwd, 'README.md');
if (fs.existsSync(readme)) {
let readmeText = fs.readFileSync(readme).toString();;
const changelog = path.join(cwd, 'CHANGELOG.md');
if (fs.existsSync(changelog)) {
readmeText += '\n\n\n<br/><br/>' + fs.readFileSync(changelog).toString();
const cp = child_process.spawn(rollupCmd, [
'--config', path.resolve(__dirname, '../rollup.nodejs.config.mjs'),
], {
stdio: 'inherit',
});
await once(cp, 'exit');
if (cp.exitCode)
throw new Error('rollup failed');
finishZip();
}
async function pack() {
if (out)
rimraf.sync(out);
await new Promise((resolve, reject) => {
let webpackConfig;
const customWebpackConfig = path.resolve(cwd, nodeWebpackConfig);
const defaultWebpackConfig = path.resolve(__dirname, '..', nodeWebpackConfig);
if (fs.existsSync(customWebpackConfig)) {
webpackConfig = customWebpackConfig;
}
zip.addFile('README.md', Buffer.from(readmeText));
}
const NODE_PATH = path.resolve(__dirname, '..', 'node_modules');
// hack to override NODE_PATH dynamically.
// otherwise webpack plugins are not found.
process.env.NODE_PATH = NODE_PATH;
require('module').Module._initPaths();
async function pack() {
if (out)
rimraf.sync(out);
await new Promise((resolve, reject) => {
let webpackConfig;
const customWebpackConfig = path.resolve(cwd, nodeWebpackConfig);
const defaultWebpackConfig = path.resolve(__dirname, '..', nodeWebpackConfig);
if (fs.existsSync(customWebpackConfig)) {
webpackConfig = customWebpackConfig;
}
else {
webpackConfig = defaultWebpackConfig;
}
process.env.SCRYPTED_DEFAULT_WEBPACK_CONFIG = defaultWebpackConfig;
const webpackEntries = {};
const config = require(webpackConfig);
for (let entry of entries) {
entry ||= {
filename: config?.entry?.main,
output: defaultMainNodeJs,
};
if (!entry?.filename) {
console.error("no main.ts or main.js was found, and webpack config does not supply an entry file.");
console.error(entry?.filename);
throw new Error();
}
const main = path.resolve(cwd, entry.filename);
if (!fs.existsSync(main)) {
console.error("entry file specified in webpack config does not exist");
throw new Error();
}
webpackEntries[entry?.output] = main;
}
config.entry = webpackEntries;
config.output.filename = '[name]';
config.output.path = out;
for (const opt of optionalDependencies) {
const t = tmp.tmpNameSync({
postfix: '.js',
});
fs.writeFileSync(t, `
const e = __non_webpack_require__('${opt}');
module.exports = e;
`);
config.resolve.alias[opt] = t;
}
webpack(config, (err, stats) => {
if (err)
return reject(err);
if (stats.hasErrors()) {
console.error(stats.toJson().errors);
return reject(new Error('webpack failed'));
}
resolve();
})
});
// create a zip that has a main.nodejs.js in the root, and an fs folder containing a read only virtual file system.
// todo: read write file system? seems like a potential sandbox and backup nightmare to do a real fs. scripts should
// use localStorage, etc?
const jsFiles = fs.readdirSync(out, {
withFileTypes: true
}).filter(ft => ft.isFile() && ft.name.endsWith('.js')).map(ft => ft.name);
for (const js of jsFiles) {
zip.addLocalFile(path.join(out, js));
const sourcemap = path.join(out, js + '.map');
if (fs.existsSync(sourcemap))
zip.addLocalFile(sourcemap);
console.log(js);
else {
webpackConfig = defaultWebpackConfig;
}
const zipfs = path.join(cwd, 'fs');
if (fs.existsSync(zipfs))
zip.addLocalFolder(zipfs, 'fs');
zip.writeZip(path.join(out, 'plugin.zip'));
process.env.SCRYPTED_DEFAULT_WEBPACK_CONFIG = defaultWebpackConfig;
const webpackEntries = {};
const config = require(webpackConfig);
for (let entry of entries) {
entry ||= {
filename: config?.entry?.main,
output: defaultMainNodeJs,
};
if (!entry?.filename) {
console.error("no main.ts or main.js was found, and webpack config does not supply an entry file.");
console.error(entry?.filename);
throw new Error();
}
const main = path.resolve(cwd, entry.filename);
if (!fs.existsSync(main)) {
console.error("entry file specified in webpack config does not exist");
throw new Error();
}
webpackEntries[entry?.output] = main;
}
config.entry = webpackEntries;
config.output.filename = '[name]';
config.output.path = out;
for (const opt of optionalDependencies) {
const t = tmp.tmpNameSync({
postfix: '.js',
});
fs.writeFileSync(t, `
const e = __non_webpack_require__('${opt}');
module.exports = e;
`);
config.resolve.alias[opt] = t;
}
webpack(config, (err, stats) => {
if (err)
return reject(err);
if (stats.hasErrors()) {
console.error(stats.toJson().errors);
return reject(new Error('webpack failed'));
}
resolve();
})
});
finishZip();
}
function finishZip() {
// create a zip that has a main.nodejs.js in the root, and an fs folder containing a read only virtual file system.
// todo: read write file system? seems like a potential sandbox and backup nightmare to do a real fs. scripts should
// use localStorage, etc?
const jsFiles = fs.readdirSync(out, {
withFileTypes: true
}).filter(ft => ft.isFile() && ft.name.endsWith('.js')).map(ft => ft.name);
for (const js of jsFiles) {
zip.addLocalFile(path.join(out, js));
const sourcemap = path.join(out, js + '.map');
if (fs.existsSync(sourcemap))
zip.addLocalFile(sourcemap);
console.log(js);
}
pack()
.catch(e => process.nextTick(() => {
console.error(e);
throw new Error(e);
}));
const sdkVersion = require(path.join(__dirname, '../package.json')).version;
zip.addFile('sdk.json', Buffer.from(JSON.stringify({
version: sdkVersion,
})));
}
const zipfs = path.join(cwd, 'fs');
if (fs.existsSync(zipfs))
zip.addLocalFolder(zipfs, 'fs');
zip.writeZip(path.join(out, 'plugin.zip'));
}
(packageJson.scrypted.rollup ? rollup : pack)()
.catch(e => process.nextTick(() => {
console.error(e);
throw new Error(e);
}));

4771
sdk/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/sdk",
"version": "0.3.88",
"version": "0.3.102",
"description": "",
"main": "dist/src/index.js",
"exports": {
@@ -29,25 +29,30 @@
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.26.0",
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@rollup/plugin-virtual": "^3.0.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"axios": "^1.7.8",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.1",
"rollup": "^4.27.4",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"tslib": "^2.8.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2"
},
"devDependencies": {
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.10"
"typedoc": "^0.26.11"
},
"types": "dist/src/index.d.ts"
}

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