Compare commits

..

248 Commits

Author SHA1 Message Date
Koushik Dutta
ea628a7130 wip: unifi 2024-11-28 08:47:11 -08:00
Koushik Dutta
ed498ae418 server/sdk: make worker disposable.
todo: implement python resource pattern?
2024-11-25 11:01:07 -08:00
Koushik Dutta
5b46036b2d sdk: add clusterWorkerId option to generateVideoFrames 2024-11-25 10:53:43 -08:00
Koushik Dutta
7bd4f4053d predict: publish 2024-11-23 21:50:38 -08:00
Koushik Dutta
f83cbfa5e7 predict: use new cluster worker labels 2024-11-23 18:39:29 -08:00
Koushik Dutta
8480713ec6 postbeta 2024-11-23 08:05:26 -08:00
Koushik Dutta
dcae7ce367 server: remove debug code causing crashes 2024-11-23 08:05:17 -08:00
Koushik Dutta
101d362260 openvino: beta 2024-11-22 21:20:09 -08:00
Koushik Dutta
73bdca1be6 postbeta 2024-11-22 21:18:51 -08:00
Koushik Dutta
c407fa0b9f server: ensure alert log goes to console as well 2024-11-22 21:10:35 -08:00
Koushik Dutta
d26c595fd6 server: clean up clustering lifecycle management 2024-11-22 20:57:34 -08:00
Koushik Dutta
38c00f5b9b openvino: fix cluster label to require x64 2024-11-22 19:01:20 -08:00
Koushik Dutta
ab4738973d predict: cluster for should enforce compute/darwin label 2024-11-22 17:08:43 -08:00
Koushik Dutta
ea065f506c predict: fix load balancer 2024-11-21 21:53:09 -08:00
Koushik Dutta
4b7b66c96b postbeta 2024-11-21 21:08:22 -08:00
Koushik Dutta
0462ad228b server: fix non cluster crash 2024-11-21 21:08:13 -08:00
Koushik Dutta
45ac7f2f4e postbeta 2024-11-21 20:54:29 -08:00
Koushik Dutta
6618129e1d server: cluster load balancing 2024-11-21 20:54:20 -08:00
Koushik Dutta
1a72eddcc8 predict: formatting 2024-11-21 15:22:35 -08:00
Koushik Dutta
1c9037dc35 predict: fix worker startup on primary server 2024-11-21 15:22:16 -08:00
Koushik Dutta
2c5b79291f server: python formatting 2024-11-21 14:53:16 -08:00
Koushik Dutta
cd0ab104ea predict: formatting 2024-11-21 14:52:01 -08:00
Koushik Dutta
c6f4c1a669 onnx: implement clustering. and cleanup coreml/openvino. 2024-11-21 14:51:31 -08:00
Koushik Dutta
9f28e38716 openvino: cluster support 2024-11-21 14:37:49 -08:00
Koushik Dutta
ba8f25fde3 postbeta 2024-11-21 14:30:14 -08:00
Koushik Dutta
9f27b2f382 Merge branch 'main' into cluster 2024-11-21 11:20:49 -08:00
Koushik Dutta
96fa6af0fc reolink: fix missing debug setting 2024-11-21 11:20:44 -08:00
Koushik Dutta
eca5fbecdc Merge branch 'main' into cluster 2024-11-21 10:04:47 -08:00
Koushik Dutta
8e0e2854e9 dev: update npm-install.sh 2024-11-21 10:04:42 -08:00
Koushik Dutta
1eb9d938e7 core: publish 2024-11-21 10:04:14 -08:00
Koushik Dutta
095f80e1f9 dev: update npm-install.sh 2024-11-21 10:04:03 -08:00
Koushik Dutta
da1f6118c8 predict: wip coreml clustering across multiple macs 2024-11-20 22:27:20 -08:00
Koushik Dutta
5060748e9d server: implement clustered plugin debugging 2024-11-20 20:53:23 -08:00
Koushik Dutta
25df4f8376 server: fix cluster logging 2024-11-20 20:51:45 -08:00
Koushik Dutta
56fdff3545 server: cluster client error logging 2024-11-20 18:30:28 -08:00
Koushik Dutta
2fbfe2cb65 server: more logging 2024-11-20 18:28:12 -08:00
Koushik Dutta
32ede1f7fe postbeta 2024-11-20 18:16:09 -08:00
Koushik Dutta
1a2ec8ab4e server: log startup 2024-11-20 18:16:01 -08:00
Koushik Dutta
53cab91b02 server: refactor runtime worker creation 2024-11-20 14:53:58 -08:00
Koushik Dutta
02a46a9202 postbeta 2024-11-20 12:38:06 -08:00
Koushik Dutta
69f4de66e9 server: exit hooks for python fork 2024-11-20 12:18:25 -08:00
Koushik Dutta
aed6e0c446 server: wip cluster mode load balancing 2024-11-20 11:39:21 -08:00
Koushik Dutta
432c178f29 sdk: more cluster fixes for python 2024-11-20 10:58:10 -08:00
Koushik Dutta
bcf698daa3 sdk: expose ClusterManager to python 2024-11-20 10:13:54 -08:00
Koushik Dutta
347a957cd3 sdk: add cluster manager 2024-11-20 10:10:47 -08:00
Koushik Dutta
459b95a0e2 server: python cluster worker routing 2024-11-18 21:33:49 -08:00
Koushik Dutta
1c18129449 postbeta 2024-11-18 21:05:37 -08:00
Koushik Dutta
21274df881 server: fix cluster check nre 2024-11-18 21:05:28 -08:00
Koushik Dutta
ca243e79bb server: apply default runtime for cluster fork 2024-11-18 21:03:00 -08:00
Koushik Dutta
924394d365 postbeta 2024-11-18 19:50:15 -08:00
Koushik Dutta
23167da88b server: fork by clusterWorkerId 2024-11-18 19:29:07 -08:00
Koushik Dutta
c1d48e1c6b sdk: add cluster worker request to fork 2024-11-18 15:47:31 -08:00
Koushik Dutta
7a22e17d84 Merge branch 'main' into cluster 2024-11-18 13:46:27 -08:00
Koushik Dutta
75b2ff22ce openvino: regenerate models 2024-11-18 13:46:19 -08:00
Koushik Dutta
edde093140 Merge branch 'main' into cluster 2024-11-18 13:03:14 -08:00
Koushik Dutta
8622934c8b coreml: cluster support 2024-11-18 13:00:24 -08:00
Koushik Dutta
153cc3ed94 server: python cleanup 2024-11-18 12:26:46 -08:00
Koushik Dutta
2ae6113750 server: wip python cluster fork 2024-11-18 11:31:52 -08:00
Koushik Dutta
4ec001c2a2 server: assign workers ids 2024-11-18 10:11:33 -08:00
Koushik Dutta
794ac6c8d2 postbeta 2024-11-18 09:38:13 -08:00
Koushik Dutta
8422ffe55a postbeta 2024-11-17 22:20:51 -08:00
Koushik Dutta
285c07e33e postbeta 2024-11-17 20:43:46 -08:00
Koushik Dutta
ef04398a79 openvino: 9c relu int8 2024-11-17 19:15:43 -08:00
Koushik Dutta
6429ea718a postbeta 2024-11-17 15:38:52 -08:00
Koushik Dutta
5f9147e720 server: fix node cluster ping 2024-11-17 15:38:42 -08:00
Koushik Dutta
ea4922d8e5 onnx: fix provider detection 2024-11-17 10:24:09 -08:00
Koushik Dutta
70293ca827 server: cluster affinity 2024-11-16 19:35:40 -08:00
Koushik Dutta
878487180e postbeta 2024-11-16 19:07:05 -08:00
Koushik Dutta
f094903ed9 server: fix cluster fork liveness leak 2024-11-16 19:06:53 -08:00
Koushik Dutta
5117e217cf core: publish 2024-11-16 11:48:13 -08:00
Koushik Dutta
07c3f2832a postbeta 2024-11-16 11:35:50 -08:00
Koushik Dutta
977666bc3c server: add worker ids 2024-11-16 11:35:39 -08:00
Koushik Dutta
2ec6760308 nvidia: notes 2024-11-16 11:07:51 -08:00
Koushik Dutta
0dbe556835 diagnostics: use a better defaults for detection plugin verification 2024-11-16 09:40:34 -08:00
Koushik Dutta
c4f76df255 onnx: fix execution provider reporting 2024-11-16 09:32:55 -08:00
Koushik Dutta
0bf0ec08ab server: plugin init cleanups 2024-11-15 23:40:38 -08:00
Koushik Dutta
1a2216a7de postbeta 2024-11-15 22:43:29 -08:00
Koushik Dutta
d868c3b3bb server: remove stats checker 2024-11-15 22:43:19 -08:00
Koushik Dutta
882709ea51 server: cluster fork tracking 2024-11-15 22:00:51 -08:00
Koushik Dutta
df249c554c common: persistent fork service 2024-11-15 21:41:07 -08:00
Koushik Dutta
fcbaeb1d1d postbeta 2024-11-15 19:54:12 -08:00
Koushik Dutta
c6dda05fa4 server: force ipv4 on cluster connect 2024-11-15 19:54:01 -08:00
Koushik Dutta
953b7812c5 server: shuffle python cluster code 2024-11-15 19:53:16 -08:00
Koushik Dutta
7434a5c4ba postbeta 2024-11-15 15:06:09 -08:00
Koushik Dutta
b240a17bb0 server: support hostnames in clustering, and auto detection of client cluster addresses for easy image cloning. 2024-11-15 15:03:32 -08:00
Koushik Dutta
aab0507805 Merge branch 'main' into cluster 2024-11-15 11:37:39 -08:00
Koushik Dutta
3e091623a8 docker: add amd kfd device to install script 2024-11-15 11:37:35 -08:00
Koushik Dutta
0871898385 videoanalysis: log filtered detections 2024-11-15 10:06:38 -08:00
Koushik Dutta
eea7e4be32 beta 2024-11-15 10:02:13 -08:00
Koushik Dutta
8167ca85bb postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
a09291114f server: possible fix for electron mac startup hang 2024-11-15 10:02:13 -08:00
Koushik Dutta
39efe0d994 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
21f56216b0 server: fix unnecessary peer creation 2024-11-15 10:02:13 -08:00
Koushik Dutta
8820bac571 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
47a683e385 server: fix ping by providing pong 2024-11-15 10:02:13 -08:00
Koushik Dutta
17f367a373 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
fad0a520ca server: simplify pong 2024-11-15 10:02:13 -08:00
Koushik Dutta
1f0d5dc3b9 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
a965f9b569 server: simplify pong 2024-11-15 10:02:13 -08:00
Koushik Dutta
eb1a388f69 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
b2cf5ac3c7 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
ce10a49f0f server: fix ping/pong 2024-11-15 10:02:13 -08:00
Koushik Dutta
5e31a0db96 server: python cleanup 2024-11-15 10:02:13 -08:00
Koushik Dutta
8f1a673db5 server: refactor cluster 2024-11-15 10:02:13 -08:00
Koushik Dutta
7405476556 server: cluster env var sanitization 2024-11-15 10:02:13 -08:00
Koushik Dutta
7dc86f59bf postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
2d7cef600d server: fix cluster connect logging 2024-11-15 10:02:13 -08:00
Koushik Dutta
5de0f8937b postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
8e8d333ea2 server: cluster logging 2024-11-15 10:02:13 -08:00
Koushik Dutta
d66a6317de zwave: cluster support 2024-11-15 10:02:13 -08:00
Koushik Dutta
49e3fc1438 predict: fix cluster labels 2024-11-15 10:02:13 -08:00
Koushik Dutta
fbe3daa072 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
670216135b server: fix python stats updater 2024-11-15 10:02:13 -08:00
Koushik Dutta
ff903fa891 rebroadcast: fix external urls with ipv6 2024-11-15 10:02:13 -08:00
Koushik Dutta
ebc6ede275 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
4de91d0673 server: fix zipapi rpc bug 2024-11-15 10:02:13 -08:00
Koushik Dutta
3da1d00f6f postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
4ff00a7753 server: fix cluster any label 2024-11-15 10:02:13 -08:00
Koushik Dutta
f245fb257d postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
b1a21a6037 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
0b9f3309a2 postbeta 2024-11-15 10:02:13 -08:00
Koushik Dutta
09b9b33bac server: working python clustering 2024-11-15 10:02:13 -08:00
Koushik Dutta
21d020919a client: update des 2024-11-15 10:02:13 -08:00
Koushik Dutta
02d090cb94 server: fix python cluster server loop missing 2024-11-15 10:02:13 -08:00
Koushik Dutta
817db34357 server: refactor cluster connect, remove dead code 2024-11-15 10:02:13 -08:00
Koushik Dutta
a3eda8cfba server: cleanup fork envs 2024-11-15 10:02:13 -08:00
Koushik Dutta
5a62fdc06b server/sdk: new cluster label format 2024-11-15 10:02:13 -08:00
Koushik Dutta
5ff8a65c4a predict: add cluster label settings to package.json 2024-11-15 10:02:13 -08:00
Koushik Dutta
719dfd2f24 server: plugin device deletion crash fix 2024-11-15 10:02:13 -08:00
Koushik Dutta
7d28d1d9d4 server: wip python clustering 2024-11-15 10:02:13 -08:00
Koushik Dutta
aaa924b9b4 core: publish 2024-11-15 10:02:13 -08:00
Koushik Dutta
f69b93c9fa server: fix consoles in clustered environment 2024-11-15 10:02:13 -08:00
Koushik Dutta
12be06adad rebroadcast: cleanup 2024-11-15 10:02:13 -08:00
Koushik Dutta
f6fa28b584 server: fix cluster host volumes 2024-11-15 10:02:13 -08:00
Koushik Dutta
fc1e5210a5 server: cleanup 2024-11-15 10:02:13 -08:00
Koushik Dutta
7601b8f0d0 server: fixup cluster clients from other addresses 2024-11-15 10:02:13 -08:00
Koushik Dutta
b0557704b2 cleanup 2024-11-15 10:02:13 -08:00
Koushik Dutta
572883ed98 server: functional cluster console 2024-11-15 10:02:13 -08:00
Koushik Dutta
92927c8b93 server: working node cluster fork 2024-11-15 10:02:13 -08:00
Koushik Dutta
11ae57b185 server: wip cluster 2024-11-15 10:02:13 -08:00
Koushik Dutta
9f55f0b32a rpc: add peer const 2024-11-15 10:02:13 -08:00
Koushik Dutta
ef52e0a723 server: cleanup import 2024-11-15 10:02:13 -08:00
Koushik Dutta
3df6af1fcd server: add tls listener 2024-11-15 10:02:13 -08:00
Koushik Dutta
a283cfb429 server: remove legacy socket rpc channel 2024-11-15 10:02:13 -08:00
Koushik Dutta
3ae2dd769a sdk: fork labels 2024-11-15 10:02:13 -08:00
Koushik Dutta
3b916e7e20 server: wip cluster 2024-11-15 10:02:13 -08:00
Koushik Dutta
d93f05a228 server: wip cluster 2024-11-15 10:02:13 -08:00
Koushik Dutta
68183775db reolink: publish 2024-11-15 10:02:02 -08:00
Koushik Dutta
a8db883661 unifi-protect: fix fingerprint on new protect 2024-11-15 10:01:49 -08:00
apocaliss92
4a51caa281 reolink: Add zoom RTSP streams to trackmix cameras (#1635)
* Add zoom RTSP streams to trackmix cameras

* update rtmp streams for POE TrackMix (#1)

* update rtmp streams for POE TrackMix

Fixing resolution of main streams too for Trackmix.

* leave in legacy bcs stream

* flv streams removed

* autotrack bcs stream restored

* additional rtsp streams added only to channel 0

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
Co-authored-by: Joshua Seidel <29733828+JoshuaSeidel@users.noreply.github.com>
2024-11-14 22:08:20 -08:00
Koushik Dutta
c3148b8ed9 server: disable nan serialization completely in python 2024-11-10 12:30:05 -08:00
Koushik Dutta
bc95a15f89 Revert "server: do not serialize python nan in rpc protocol."
This reverts commit e9d73c6faa.
2024-11-10 12:29:26 -08:00
Koushik Dutta
8954de3c93 predict: beta 2024-11-10 10:16:03 -08:00
Koushik Dutta
cbfad097db predict: sanitzation 2024-11-10 10:15:12 -08:00
Koushik Dutta
c9e83c496c openvino: rollback openvino off nightly 2024-11-10 08:51:07 -08:00
Koushik Dutta
442e1883c5 openvino: quantize test 2024-11-10 08:47:51 -08:00
Koushik Dutta
f819e6d29c unifi-protect: send fingerprint events + user id as object detections 2024-11-08 13:03:33 -08:00
Koushik Dutta
261c07f330 docker: remove gst alsa 2024-11-08 10:02:22 -08:00
Koushik Dutta
2328c9dd75 docker: remove build utils 2024-11-08 09:59:22 -08:00
Koushik Dutta
15639052c3 install: update/trim intel binaries 2024-11-08 09:59:02 -08:00
Koushik Dutta
d91c7d89b2 docker: remove pillow simd deps 2024-11-08 09:43:41 -08:00
Koushik Dutta
fbdefbe06a python-codecs: remove pillow simd 2024-11-08 09:42:52 -08:00
Koushik Dutta
832ee0180c postbeta 2024-11-08 09:35:13 -08:00
Koushik Dutta
a616e95c0e detect: use opencv-headless 2024-11-08 09:14:11 -08:00
Koushik Dutta
7ab9208203 tensorflow-lite: update project files 2024-11-08 09:14:02 -08:00
Koushik Dutta
9db6808e85 coreml: bump coremltools, use opencv headless 2024-11-08 09:13:36 -08:00
Koushik Dutta
5d48760fd8 core: publish 2024-11-08 09:13:19 -08:00
Koushik Dutta
6ce2166e0a unifi-protect: Fix unstable ids on camera sensors 2024-11-08 08:24:27 -08:00
Koushik Dutta
201dc30650 onnx: fixup project files 2024-11-07 20:57:33 -08:00
Koushik Dutta
84bb7865fe unifi-protect: fingerprint sensor fixes 2024-11-07 09:23:23 -08:00
Koushik Dutta
ab1cd379a9 unifi-protect: fingerprint sensor beta 2024-11-07 09:07:00 -08:00
Koushik Dutta
9208ca9566 Merge branch 'main' of github.com:koush/scrypted 2024-11-07 08:56:59 -08:00
Koushik Dutta
e62897e14c unifi-protect: build fixes 2024-11-07 08:56:56 -08:00
Koushik Dutta
65559e6685 install: prevent amd graphics on non x86 2024-11-06 17:37:52 -08:00
Koushik Dutta
611b7c50bf docker: add intel npu, amd gpu 2024-11-06 17:28:56 -08:00
Koushik Dutta
e983526455 install: suppress tar delete error 2024-11-06 11:09:11 -08:00
Koushik Dutta
10c167d4a3 install: Update install-intel-npu.sh 2024-11-06 11:08:10 -08:00
Koushik Dutta
0c641ccf6c common: createAsyncQueueFromGenerator should not read as fast as possible. 2024-11-06 10:07:03 -08:00
Koushik Dutta
5899ad866a openvino: invalid device recovery 2024-11-05 08:38:52 -08:00
Koushik Dutta
17ecb56259 openvino: initial yuv support 2024-11-04 12:04:18 -08:00
Koushik Dutta
ffeade08ca postbeta 2024-11-04 10:32:08 -08:00
apocaliss92
49a567fb51 reolink: floodlights (#1633)
* Reolink floodlight support

* Fix on state check

* Flashlight interval removed

* Code cleanup

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-11-03 08:49:30 -08:00
Koushik Dutta
aac104f386 install: doc amdgpu-install 2024-11-01 21:46:59 -07:00
Koushik Dutta
b4aff117ce install: add kfd for amd support 2024-11-01 21:44:39 -07:00
Koushik Dutta
13d4519a35 install: amd cleanup 2024-11-01 21:42:09 -07:00
Koushik Dutta
743102c965 install: amd graphics 2024-11-01 21:41:40 -07:00
Koushik Dutta
315e5bb6e6 proxmox: add support for explicit directories in disk setup script 2024-11-01 14:08:49 -07:00
Koushik Dutta
6ddef853ad predict: publish 2024-11-01 10:52:30 -07:00
Koushik Dutta
5848cf1e5e predict: Fix nans in payloads causing plugin crash, add support for yuv models 2024-11-01 10:34:29 -07:00
Koushik Dutta
f00f650b4f docker: add libvulkan1 2024-10-31 18:22:17 -07:00
Koushik Dutta
e9d73c6faa server: do not serialize python nan in rpc protocol.
This causes protocol failure and plugin to be killed. Javascript behavior is to convert NaN to null.
Mimicing this behavior ensures stability though all JSON dicts are recursively inspected.
2024-10-31 10:44:08 -07:00
Koushik Dutta
b6d601ebc4 sdk: add boolean to SerializableType 2024-10-31 09:20:14 -07:00
Koushik Dutta
1b58b0dd9b sdk: Image ffmpegFormats flag 2024-10-31 09:18:05 -07:00
Koushik Dutta
1b5ef3103e sdk: publish 2024-10-29 12:27:02 -07:00
Koushik Dutta
78236a54b8 postrelease 2024-10-29 12:22:35 -07:00
Koushik Dutta
ec2e4d64fd rebroadcast: set online/offline state without prebuffer requirement 2024-10-29 12:21:40 -07:00
Koushik Dutta
44644448f5 postbeta 2024-10-29 12:07:01 -07:00
Koushik Dutta
0a86d5c4ea server: disable auto transferable Buffers 2024-10-29 12:06:48 -07:00
Koushik Dutta
20282e05ea Merge branch 'main' of github.com:koush/scrypted 2024-10-29 12:05:53 -07:00
Koushik Dutta
9d6b405fa9 postbeta 2024-10-29 12:05:45 -07:00
Koushik Dutta
b82ce5ff45 server: disable auto transferable Buffers 2024-10-29 12:05:33 -07:00
apocaliss92
f461198e1e reolink: battery params supported (#1621)
* Reolink battery params supported

* Style restored

* Battery check interval incresed

* Fix battery checks

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-28 18:36:17 -07:00
Jacob McSwain
7505e6907a feat(sdk): add support for notification channel in NotifierOptions (#1625)
* feat(sdk): add support for notification channel in NotifierOptions

This change spins off from conversation at https://github.com/scryptedapp/homeassistant/pull/1 and allows consumers of the `Notifier` interface to specify a channel for notifications to be sent to. Android platforms can use this to send notifications to a specific channel, allowing the user to have fine-grained control over the audio and priority of the notifications they receive.

* chore(sdk): place Android notification channel under the android object

* fix(automation-actions): remove channel from UI
2024-10-27 01:38:56 -05:00
apocaliss92
c1046d5706 HomeKit: Add flag to not autoenable devices on creation (#1626)
* Add flag to not autoenable devices on creation

* Invert naming logic

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-27 01:38:24 -05:00
Koushik Dutta
a61c06b607 remove dev site 2024-10-26 16:19:52 -05:00
Koushik Dutta
d3df5742e6 update index 2024-10-26 15:35:44 -05:00
Koushik Dutta
68ac42ca46 sdk: new dev site 2024-10-26 11:51:20 -05:00
apocaliss92
bb7c6ef8b9 reolink: Fix check for reolink nvrs (#1624)
Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-23 09:35:01 -05:00
apocaliss92
446e8ed61e reolink: add nvr flag (#1620)
* Reolink - Fix name/model fetching if homehub

* Checks removed

* style change removed

* Change logic for earlier returns

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-21 14:02:20 -07:00
Koushik Dutta
80372b35f2 npu: update drivers 2024-10-21 11:29:08 -07:00
Koushik Dutta
57eff2f296 Merge branch 'main' of github.com:koush/scrypted 2024-10-21 10:39:44 -07:00
Koushik Dutta
d996088041 sdk: fix deprecation on getCloudEndpoint 2024-10-21 10:39:40 -07:00
apocaliss92
04be70019b Reolink - Fix get ability (#1616)
* Reolink - Fix get ability

* getAbility current behavior kept

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-20 15:20:26 -07:00
Koushik Dutta
51732d0dcd proxmox: preserve hostname 2024-10-19 21:48:19 -07:00
Koushik Dutta
e40bc3ddee proxmox: preserve hostname 2024-10-19 21:38:07 -07:00
Koushik Dutta
3f4409e1c3 Merge branch 'main' of github.com:koush/scrypted 2024-10-19 21:26:23 -07:00
Koushik Dutta
63b7616ab3 proxmox: preserve mac address 2024-10-19 21:26:17 -07:00
apocaliss92
29059691ce Alexa: add option to not auto enable devices (#1615)
Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-19 16:10:42 -07:00
apocaliss92
531a9d28dc reolink: Reolink only-token client added (#1614)
* Reolink only-token client added

* Reolink auth fixes

---------

Co-authored-by: Gianluca Ruocco <gianluca.ruocco@xarvio.com>
2024-10-19 15:06:35 -07:00
Koushik Dutta
3314b4d9ca reolink: publish 2024-10-18 20:30:40 -07:00
Koushik Dutta
37df9810c8 diagnostics: deprecate electron-core 2024-10-18 10:39:10 -07:00
Koushik Dutta
47c1cbba3c lxc-docker: shuffle volumes for faster resets 2024-10-18 09:53:52 -07:00
Koushik Dutta
ded7e549bb lxc-docker: link to docs for mount point specifics 2024-10-18 09:25:17 -07:00
Koushik Dutta
abb2b85cec lxc-docker: fix mp restore 2024-10-18 09:16:50 -07:00
Koushik Dutta
7d157d2882 lxc-docker: fix message line length 2024-10-18 09:14:01 -07:00
Koushik Dutta
c6c0a225dd lxc-docker: update messaging 2024-10-18 09:12:22 -07:00
Koushik Dutta
276fc386ec Merge branch 'main' of github.com:koush/scrypted 2024-10-18 09:08:15 -07:00
Koushik Dutta
0b21afd193 lxc-docker: handle moving volumes in migration 2024-10-18 09:08:10 -07:00
Koushik Dutta
1032e58e3b Update config.yaml 2024-10-17 21:06:52 -07:00
Koushik Dutta
4987b01167 Update package.json 2024-10-17 19:26:55 -07:00
Koushik Dutta
28bb8c5b3c proxmox: clarify docs 2024-10-17 14:15:44 -07:00
Koushik Dutta
2160170c3a proxmox: bump to lxc-docker 2024-10-17 14:06:21 -07:00
Koushik Dutta
c0eac9053b proxmox: restore warning 2024-10-17 13:58:46 -07:00
Koushik Dutta
d57501dd42 proxmox: fix restore 2024-10-17 13:53:34 -07:00
Koushik Dutta
264cb0404f proxmox: dont show existing container warning on restore 2024-10-17 13:41:40 -07:00
Koushik Dutta
dc9f4b39a8 proxmox: storage setup docs 2024-10-17 13:38:50 -07:00
Koushik Dutta
653eeceaf2 proxmox: stop container prior to restore. 2024-10-17 13:35:57 -07:00
Koushik Dutta
3d8711947a proxmox: detect force for better error handling 2024-10-17 13:33:02 -07:00
Koushik Dutta
38038d5f30 proxmox: script language cleanup 2024-10-17 13:28:59 -07:00
Koushik Dutta
e21d9c3a0c proxmox: additional installation options if container exists 2024-10-17 13:24:53 -07:00
Koushik Dutta
7b8c014b3b mac: remove libvips 2024-10-17 12:31:06 -07:00
Koushik Dutta
55a30864fd Revert "mac: update python to use whatever is latest"
This reverts commit 4f419ff75c.
2024-10-17 12:30:50 -07:00
Koushik Dutta
4f419ff75c mac: update python to use whatever is latest 2024-10-17 12:26:25 -07:00
Koushik Dutta
638a4f28ad postbeta 2024-10-17 12:09:53 -07:00
Koushik Dutta
8970154b8f lxc-docker: restore prune if pull is requested 2024-10-17 12:05:37 -07:00
Koushik Dutta
c96debaaed lxc-docker: wait 5 minutes prior to pruning 2024-10-17 12:04:46 -07:00
Koushik Dutta
fe7b479235 lxc-docker: use docker compose --pull as appropriate 2024-10-17 11:53:09 -07:00
Koushik Dutta
aa1486e641 postrelease 2024-10-17 11:15:41 -07:00
166 changed files with 6259 additions and 3565 deletions

3
.gitmodules vendored
View File

@@ -11,9 +11,6 @@
[submodule "external/werift"]
path = external/werift
url = ../../koush/werift-webrtc
[submodule "sdk/developer.scrypted.app"]
path = sdk/developer.scrypted.app
url = ../../koush/developer.scrypted.app
[submodule "plugins/sample-cameraprovider"]
path = plugins/sample-cameraprovider
url = ../../koush/scrypted-sample-cameraprovider

View File

@@ -160,7 +160,7 @@ export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
(async() => {
try {
for await (const i of generator) {
q.submit(i);
await q.enqueue(i);
}
}
catch (e) {

View File

@@ -4,6 +4,41 @@ import os from 'os';
export type Zygote<T> = () => PluginFork<T>;
export function createService<T, V>(options: ForkOptions, create: (t: Promise<T>) => Promise<V>): {
getResult: () => Promise<V>,
terminate: () => void,
} {
let killed = false;
let currentResult: Promise<V>;
let currentFork: ReturnType<typeof sdk.fork<T>>;
return {
getResult() {
if (killed)
throw new Error('service terminated');
if (currentResult)
return currentResult;
currentFork = sdk.fork<T>(options);
currentFork.worker.on('exit', () => currentResult = undefined);
currentResult = create(currentFork.result);
currentResult.catch(() => currentResult = undefined);
return currentResult;
},
terminate() {
if (killed)
return;
killed = true;
currentFork.worker.terminate();
currentFork = undefined;
currentResult = undefined;
}
}
}
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
let zygote = sdk.fork<T>(options);
function* next() {

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "v0.118.0-jammy-full"
version: "v0.120.0-jammy-full"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"

View File

@@ -14,12 +14,7 @@ ENV DEBIAN_FRONTEND=noninteractive
# base tools and development stuff
RUN apt-get update && apt-get -y install \
curl software-properties-common apt-utils \
build-essential \
cmake \
ffmpeg \
gcc \
libcairo2-dev \
libgirepository1.0-dev \
pkg-config && \
apt-get -y update && \
apt-get -y upgrade
@@ -40,16 +35,12 @@ RUN apt-get -y install \
python3-setuptools \
python3-wheel
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN echo "Installing pillow-simd dependencies."
RUN apt-get -y install \
libjpeg-dev zlib1g-dev
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN echo "Installing gstreamer."
# python-codecs pygobject dependencies
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
RUN apt-get -y install \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
gstreamer1.0-vaapi
# python3 gstreamer bindings
@@ -71,11 +62,17 @@ RUN python3 -m pip install debugpy typing_extensions psutil
################################################################
FROM header as base
# intel opencl gpu and npu for openvino
# vulkan
RUN apt -y install libvulkan1
# intel opencl for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
# Disable NPU on docker, because level-zero crashes openvino on older systems.
# RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# NPU driver will SIGILL on openvino prior to 2024.5.0
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# amd opencl
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite

View File

@@ -98,6 +98,9 @@ services:
# hardware accelerated video decoding, opencl, etc.
# "/dev/dri:/dev/dri",
# AMD GPU
# "/dev/kfd:/dev/kfd",
# uncomment below as necessary.
# zwave usb serial device

View File

@@ -0,0 +1,36 @@
if [ "$(uname -m)" != "x86_64" ]
then
echo "AMD graphics will not be installed on this architecture."
exit 0
fi
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
# needs either ubuntu 22.0.4 or 24.04
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
echo "AMD graphics package can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
exit 1
fi
if [ -n "$UBUNTU_22_04" ]
then
distro="jammy"
else
distro="noble"
fi
# https://amdgpu-install.readthedocs.io/en/latest/install-prereq.html#installing-the-installer-package
FILENAME="amdgpu-install_6.2.60202-1_all.deb"
set -e
mkdir -p /tmp/amd
cd /tmp/amd
curl -O -L http://repo.radeon.com/amdgpu-install/latest/ubuntu/$distro/$FILENAME
apt -y install rsync
dpkg -i $FILENAME
amdgpu-install --usecase=opencl --no-dkms -y --accept-eula
cd /tmp
rm -rf /tmp/amd

View File

@@ -30,8 +30,8 @@ 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.13.29138.7
# 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
@@ -39,12 +39,12 @@ apt-get install -y ocl-icd-libopencl1
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

View File

@@ -7,7 +7,7 @@ fi
UBUNTU_22_04=$(lsb_release -r | grep "22.04")
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
if [ -z "$UBUNTU_22_04" ]
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
if [ -d "/etc/pve" ]
@@ -23,6 +23,13 @@ then
exit 0
fi
if [ -n "$UBUNTU_22_04" ]
then
distro="22.04_amd64"
else
distro="24.04_amd64"
fi
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
# no errors beyond this point
@@ -30,27 +37,23 @@ set -e
rm -rf /tmp/npu && mkdir -p /tmp/npu && cd /tmp/npu
# different npu downloads for ubuntu versions
if [ -n "$UBUNTU_22_04" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-driver-compiler-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
# firmware can only be installed on host. will cause problems inside container.
if [ -n "$INTEL_FW_NPU" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-fw-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-level-zero-npu_1.8.0.20240916-10885588273_ubuntu22.04_amd64.deb
else
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-driver-compiler-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
if [ -n "$INTEL_FW_NPU" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-fw-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.8.0/intel-level-zero-npu_1.8.0.20240916-10885588273_ubuntu24.04_amd64.deb
fi
# level zero must also be installed
LEVEL_ZERO_VERSION=1.18.5
# 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
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero_1.17.6+u22.04_amd64.deb
curl -O -L https://github.com/oneapi-src/level-zero/releases/download/v1.17.6/level-zero-devel_1.17.6+u22.04_amd64.deb
# npu driver
# https://github.com/intel/linux-npu-driver
NPU_VERSION=1.10.0
NPU_VERSION_DATE=20241107-11729849322
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-driver-compiler-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
# firmware can only be installed on host. will cause problems inside container.
if [ -n "$INTEL_FW_NPU" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-fw-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v"$NPU_VERSION"/intel-level-zero-npu_$NPU_VERSION."$NPU_VERSION_DATE"_ubuntu$distro.deb
apt -y update
apt -y install libtbb12

View File

@@ -2,9 +2,13 @@ UBUNTU_22_04=$(lsb_release -r | grep "22.04")
UBUNTU_24_04=$(lsb_release -r | grep "24.04")
set -e
# Install CUDA for 22.04
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
# need this apt for nvidia-utils
# needs either ubuntu 22.0.4 or 24.04
# Install CUDA for 24.04
# https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=24.04&target_type=deb_network
# Do not apt install nvidia-open, must use cuda-drivers.
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
echo "NVIDIA container toolkit can not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."

View File

@@ -75,10 +75,14 @@ echo "Created $DOCKER_COMPOSE_YML"
if [ -z "$SCRYPTED_LXC" ]
then
if [ -d /dev/dri ]
if [ -e /dev/dri ]
then
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
fi
if [ -e /dev/kfd ]
then
sed -i 's/'#' "\/dev\/kfd/"\/dev\/kfd/g' $DOCKER_COMPOSE_YML
fi
else
# uncomment lxc specific stuff
sed -i 's/'#' lxc //g' $DOCKER_COMPOSE_YML

View File

@@ -3,11 +3,17 @@
################################################################
FROM header as base
# intel opencl gpu and npu for openvino
# vulkan
RUN apt -y install libvulkan1
# intel opencl for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
# Disable NPU on docker, because level-zero crashes openvino on older systems.
# RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# NPU driver will SIGILL on openvino prior to 2024.5.0
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# amd opencl
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-amd-graphics.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite

View File

@@ -11,12 +11,7 @@ ENV DEBIAN_FRONTEND=noninteractive
# base tools and development stuff
RUN apt-get update && apt-get -y install \
curl software-properties-common apt-utils \
build-essential \
cmake \
ffmpeg \
gcc \
libcairo2-dev \
libgirepository1.0-dev \
pkg-config && \
apt-get -y update && \
apt-get -y upgrade
@@ -37,16 +32,12 @@ RUN apt-get -y install \
python3-setuptools \
python3-wheel
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN echo "Installing pillow-simd dependencies."
RUN apt-get -y install \
libjpeg-dev zlib1g-dev
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN echo "Installing gstreamer."
# python-codecs pygobject dependencies
RUN apt-get -y install libcairo2-dev libgirepository1.0-dev
RUN apt-get -y install \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav \
gstreamer1.0-vaapi
# python3 gstreamer bindings

View File

@@ -40,8 +40,6 @@ echo "Installing Scrypted dependencies..."
RUN_IGNORE xcode-select --install
RUN brew update
RUN_IGNORE brew install node@20
# snapshot plugin and others
RUN brew install libvips
# dlib
RUN brew install cmake

View File

@@ -10,14 +10,15 @@ export DEBIAN_FRONTEND=noninteractive
if [ -e "volume/.pull" ]
then
rm -rf volume/.pull
docker compose pull && docker container prune -f && docker image prune -a -f
PULL="--pull"
(sleep 300 && docker container prune -f && docker image prune -a -f) &
else
# always background pull in case there's a broken image.
(docker compose pull && docker container prune -f && docker image prune -a -f) &
(sleep 300 && docker compose pull && docker container prune -f && docker image prune -a -f) &
fi
# 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
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum | head -c 32) docker compose up --force-recreate --abort-on-container-exit $PULL

View File

@@ -18,17 +18,30 @@ function readyn() {
}
cd /tmp
SCRYPTED_VERSION=v0.116.0
SCRYPTED_VERSION=v0.120.0
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
if [ -z "$VMID" ]
then
VMID=10443
fi
SCRYPTED_BACKUP_VMID=10445
if [ -n "$SCRYPTED_RESTORE" ]
then
pct config $VMID 2>&1 > /dev/null
if [ "$?" != "0" ]
then
echo "VMID $VMID not found."
exit 1
fi
# append existing mac address.
HWADDR=",hwaddr=$(pct config $VMID | grep -oE 'hwaddr=[A-Z0-9:]+' | cut -d '=' -f 2)"
RESTORE_HOSTNAME=$(pct config $VMID | grep -oE 'hostname: [^[:space:]]+' | cut -d ':' -f 2- | tr -d ' ')
pct destroy $SCRYPTED_BACKUP_VMID 2>&1 > /dev/null
RESTORE_VMID=$VMID
VMID=10444
VMID=$SCRYPTED_BACKUP_VMID
pct destroy $VMID 2>&1 > /dev/null
fi
@@ -39,18 +52,49 @@ then
mv scrypted.tar.zst $SCRYPTED_TAR_ZST
fi
echo "Checking for existing container."
pct config $VMID
if [ "$?" == "0" ]
if [[ "$@" =~ "--force" ]]
then
echo ""
echo "Existing container $VMID found. Run this script with --force to overwrite the existing container."
echo "This will wipe all existing data. Clone the existing container to retain the data, then reassign the owner of the scrypted volume after installation is complete."
echo ""
echo "bash $0 --force"
echo ""
IGNORE_EXISTING=true
fi
if [ -n "$SCRYPTED_RESTORE" ]
then
IGNORE_EXISTING=true
fi
if [ -z "$IGNORE_EXISTING" ]
then
echo "Checking for existing container."
pct config $VMID
if [ "$?" == "0" ]
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 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
fi
pct stop $VMID 2>&1 > /dev/null
pct restore $VMID $SCRYPTED_TAR_ZST $@
if [ "$?" != "0" ]
@@ -73,7 +117,7 @@ then
exit 1
fi
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto
pct set $VMID -net0 name=eth0,bridge=vmbr0,ip=dhcp,ip6=auto$HWADDR
if [ "$?" != "0" ]
then
echo ""
@@ -82,6 +126,18 @@ then
echo "Ignoring... Please verify your container's network settings."
fi
if [ -n "$RESTORE_HOSTNAME" ]
then
pct set $VMID --hostname $RESTORE_HOSTNAME
if [ "$?" != "0" ]
then
echo ""
echo "pct hostname restore failed"
echo ""
echo "Ignoring... Please verify your container's dns settings."
fi
fi
CONF=/etc/pve/lxc/$VMID.conf
if [ -f "$CONF" ]
then
@@ -92,23 +148,89 @@ fi
if [ -n "$SCRYPTED_RESTORE" ]
then
readyn "Running this script will reset Scrypted to a factory state while preserving existing data. IT IS RECOMMENDED TO CREATE A BACKUP FIRST. Are you sure you want to continue?"
echo ""
echo ""
echo "Running 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" ]
then
exit 1
fi
echo "Stopping scrypted..."
pct stop $RESTORE_VMID 2>&1 > /dev/null
echo "Preparing rootfs reset..."
# this copies the
pct set 10444 --delete mp0 && pct set 10444 --delete unused0 && pct move-volume $RESTORE_VMID mp0 --target-vmid 10444 --target-volume mp0
rm *.tar
vzdump 10444 --dumpdir /tmp
# remove the empty data volume from the downloaded image.
pct set $SCRYPTED_BACKUP_VMID --delete mp0 && pct set $SCRYPTED_BACKUP_VMID --delete unused0
if [ "$?" != "0" ]
then
echo "Failed to remove data volume from image."
exit 1
fi
# create a backup that contains only the root disk.
rm -f *.tar
vzdump $SCRYPTED_BACKUP_VMID --dumpdir /tmp
# this moves the data volume from the current scrypted instance to the backup target to preserve it during
# the restore.
pct move-volume $RESTORE_VMID mp0 --target-vmid $SCRYPTED_BACKUP_VMID --target-volume mp0
if [ "$?" != "0" ]
then
echo "Failed to move data volume to backup."
exit 1
fi
# arguments: from to mp hide-warning
function move_volume() {
HAS_VOLUME=$(pct config $1 | grep $3:)
if [ -n "$HAS_VOLUME" ]
then
echo "Moving $3..."
# this may error and there may be recording loss. bailing at ths point is already too late.
pct move-volume $1 $3 --target-vmid $2 --target-volume $3
# volume must be inside /mnt to get into docker container
INSIDE_MNT=$(echo $HAS_VOLUME | grep /mnt)
if [ -z "$INSIDE_MNT" -a -z "$4" ]
then
echo "##################################################################"
echo "The following mount point is not visible to the"
echo "Scrypted docker container within the LXC:"
echo ""
echo "$HAS_VOLUME"
echo ""
echo "This recordings directory will be unavailable."
echo "The mount point must be updated to a path within /mnt."
echo "https://docs.scrypted.app/scrypted-nvr/recording-storage.html#proxmox-ve-mount-point"
echo "##################################################################"
fi
fi
}
# try moving 5 volumes, any more than that seems unlikely
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp1 hide-warning
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp2 hide-warning
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp3 hide-warning
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp4 hide-warning
move_volume $RESTORE_VMID $SCRYPTED_BACKUP_VMID mp5 hide-warning
VMID=$RESTORE_VMID
echo "Moving data volume to backup..."
pct restore $VMID *.tar $@
echo "Restoring with reset image..."
pct restore --force 1 $VMID *.tar $@
pct destroy 10444
echo "Restoring volumes..."
move_volume $SCRYPTED_BACKUP_VMID $VMID mp0 hide-warning
move_volume $SCRYPTED_BACKUP_VMID $VMID mp1
move_volume $SCRYPTED_BACKUP_VMID $VMID mp2
move_volume $SCRYPTED_BACKUP_VMID $VMID mp3
move_volume $SCRYPTED_BACKUP_VMID $VMID mp4
move_volume $SCRYPTED_BACKUP_VMID $VMID mp5
pct destroy $SCRYPTED_BACKUP_VMID
fi
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
@@ -117,6 +239,7 @@ then
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"drm\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"kfd\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"accel\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
@@ -143,4 +266,5 @@ then
fi
echo "Scrypted setup is complete and the container resources can be started."
echo "Scrypted NVR users should provide at least 4 cores and 16GB RAM prior to starting."
echo ""
echo "Scrypted NVR servers should run the disk setup script in the documentation to add storage prior to starting the container."

View File

@@ -1,6 +1,7 @@
#!/bin/bash
NVR_STORAGE=$1
NVR_STORAGE_DIRECTORY=$2
DISK_TYPE="large"
if [ ! -z "$FAST_DISK" ]
@@ -10,9 +11,9 @@ fi
if [ -z "$NVR_STORAGE" ]; then
echo ""
echo "Error: Proxmox Directory Disk not provided. Usage:"
echo "Error: Directory name not provided. Usage:"
echo ""
echo "bash $0 <proxmox-directory-disk>"
echo "bash $0 directory-name [/optional/path/to/storage]"
echo ""
exit 1
fi
@@ -30,20 +31,30 @@ if [ ! -f "$FILE" ]; then
exit 1
fi
STORAGE="/mnt/pve/$NVR_STORAGE"
if [ ! -d "$STORAGE" ]
if [ ! -z "$NVR_STORAGE_DIRECTORY" ]
then
echo "Error: $STORAGE not found."
echo "The Proxmox Directory Storage must be created using the UI prior to running this script."
exit 1
if [ ! -d "$NVR_STORAGE_DIRECTORY" ]
then
echo ""
echo "Error: $NVR_STORAGE_DIRECTORY directory not found."
echo ""
exit 1
fi
else
STORAGE="/mnt/pve/$NVR_STORAGE"
if [ ! -d "$STORAGE" ]
then
echo "Error: $STORAGE not found."
echo "The Proxmox Directory Storage must be created using the UI prior to running this script."
exit 1
fi
# use subdirectory doesn't conflict with Proxmox storage of backups etc.
NVR_STORAGE_DIRECTORY="$STORAGE/mounts/scrypted-nvr"
fi
# use subdirectory doesn't conflict with Proxmox storage of backups etc.
STORAGE="$STORAGE/mounts/scrypted-nvr"
# create the hidden folder that can be used as a marker.
mkdir -p $STORAGE/.nvr
chmod 0777 $STORAGE
mkdir -p $NVR_STORAGE_DIRECTORY/.nvr
chmod 0777 $NVR_STORAGE_DIRECTORY
echo "Stopping Scrypted..."
pct stop "$VMID"
@@ -57,7 +68,7 @@ then
fi
echo "Adding new $DISK_TYPE lxc.mount.entry."
echo "lxc.mount.entry: $STORAGE mnt/nvr/$DISK_TYPE/$NVR_STORAGE none bind,optional,create=dir" >> "$FILE"
echo "lxc.mount.entry: $NVR_STORAGE_DIRECTORY mnt/nvr/$DISK_TYPE/$NVR_STORAGE none bind,optional,create=dir" >> "$FILE"
echo "Starting Scrypted..."
pct start $VMID

View File

@@ -27,7 +27,7 @@ echo "external/werift > npm install"
npm install
popd
for directory in ffmpeg-camera rtsp amcrest onvif hikvision unifi-protect webrtc homekit
for directory in rtsp amcrest onvif hikvision reolink unifi-protect webrtc homekit
do
echo "$directory > npm install"
pushd plugins/$directory

View File

@@ -9,7 +9,7 @@
"version": "1.3.6",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.3.60",
"@scrypted/types": "^0.3.66",
"engine.io-client": "^6.6.1",
"follow-redirects": "^1.15.9",
"rimraf": "^6.0.1"
@@ -75,9 +75,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.60",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.60.tgz",
"integrity": "sha512-oapFYQvyHLp0odCSx//USNnGNegS9ZL6a1HFIZzjDdMj2MNszTqiucAcu/wAlBwqjgURlP4/8xeLGVHEa4S2uQ=="
"version": "0.3.66",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.66.tgz",
"integrity": "sha512-POHpVgW6Ce8mnJRaXZRm+2RtvFuPP+ZehsDrhUqkQdxmnV81m8K2+3M6Vhrt+07kNDXmrznAijoj/OzXkdZWgw=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",

View File

@@ -18,7 +18,7 @@
"typescript": "^5.6.2"
},
"dependencies": {
"@scrypted/types": "^0.3.60",
"@scrypted/types": "^0.3.66",
"engine.io-client": "^6.6.1",
"follow-redirects": "^1.15.9",
"rimraf": "^6.0.1"

View File

@@ -1,12 +1,11 @@
{
"name": "@scrypted/alexa",
"version": "0.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.3.3",
"version": "0.3.4",
"dependencies": {
"axios": "^1.3.4",
"uuid": "^9.0.0"
@@ -203,4 +202,4 @@
}
}
}
}
}

View File

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

View File

@@ -53,6 +53,12 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
title: "Pairing Key",
description: "The pairing key used to validate requests from Alexa. Clear this key or delete the plugin to allow pairing with a different Alexa login.",
},
disableAutoAdd: {
title: "Disable auto add",
description: "Disable automatic enablement of devices.",
type: 'boolean',
defaultValue: false,
},
});
accessToken: Promise<string>;
@@ -116,6 +122,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
if (!supportedTypes.has(device.type))
return DeviceMixinStatus.NotSupported;
if (this.storageSettings.values.disableAutoAdd) {
return DeviceMixinStatus.Skip;
}
mixins.push(this.id);
const plugins = await systemManager.getComponent('plugins');
@@ -671,7 +681,8 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
enum DeviceMixinStatus {
NotSupported = 0,
Setup = 1,
AlreadySetup = 2
AlreadySetup = 2,
Skip = 3,
}
class HttpResponseLoggingImpl implements AlexaHttpResponse {

View File

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

View File

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

View File

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

View File

@@ -17,9 +17,13 @@ export class PluginSocketService extends ScryptedDeviceBase implements StreamSer
throw new Error('must provide pluginId');
const plugins = await sdk.systemManager.getComponent('plugins');
const replPort: number = await plugins.getRemoteServicePort(pluginId, this.serviceName);
const servicePort = await plugins.getRemoteServicePort(pluginId, this.serviceName) as number | [number, string];
const [port, host] = Array.isArray(servicePort) ? servicePort : [servicePort, undefined];
const socket = net.connect(replPort);
const socket = net.connect({
port,
host,
});
await once(socket, 'connect');
const queue = createAsyncQueue<Buffer>();

View File

@@ -1,6 +1,6 @@
{
"scrypted.debugHost": "127.0.0.1",
"scrypted.debugHost": "scrypted-nvr",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]

View File

@@ -1,36 +1,35 @@
{
"name": "@scrypted/coreml",
"version": "0.1.70",
"version": "0.1.76",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.70",
"version": "0.1.76",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.31",
"version": "0.3.77",
"dev": true,
"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",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"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",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
@@ -42,11 +41,11 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
"ts-node": "^10.9.2",
"typedoc": "^0.26.10"
}
},
"../sdk": {
@@ -61,25 +60,24 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^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",
"@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",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"rimraf": "^6.0.1",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
"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"
}
}
}

View File

@@ -35,12 +35,18 @@
"interfaces": [
"Settings",
"DeviceProvider",
"ClusterForkInterface",
"ObjectDetection",
"ObjectDetectionPreview"
]
],
"labels": {
"require": [
"@scrypted/coreml"
]
}
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.70"
"version": "0.1.76"
}

View File

@@ -69,9 +69,13 @@ def parse_labels(userDefined):
return parse_label_contents(classes)
class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
class CoreMLPlugin(
PredictPlugin,
scrypted_sdk.Settings,
scrypted_sdk.DeviceProvider,
):
def __init__(self, nativeId: str | None = None, forked: bool = False):
super().__init__(nativeId=nativeId, forked=forked)
model = self.storage.getItem("model") or "Default"
if model == "Default" or model not in availableModels:
@@ -139,7 +143,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
self.faceDevice = None
self.textDevice = None
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
if not self.forked:
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
async def prepareRecognitionModels(self):
try:
@@ -148,6 +154,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "CoreML Face Recognition",
@@ -160,6 +167,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "CoreML Text Recognition",
@@ -176,10 +184,10 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
async def getDevice(self, nativeId: str) -> Any:
if nativeId == "facerecognition":
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(nativeId)
self.faceDevice = self.faceDevice or CoreMLFaceRecognition(self, nativeId)
return self.faceDevice
if nativeId == "textrecognition":
self.textDevice = self.textDevice or CoreMLTextRecognition(nativeId)
self.textDevice = self.textDevice or CoreMLTextRecognition(self, nativeId)
return self.textDevice
raise Exception("unknown device")
@@ -227,7 +235,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
return ret
if self.scrypted_yolo_nas:
predictions = list(out_dict.values())
predictions = list(out_dict.values())
objs = yolo.parse_yolo_nas(predictions)
ret = self.create_detection_result(objs, src_size, cvss)
return ret
@@ -250,13 +258,13 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
for r in objects:
obj = Prediction(
r["classId"].astype(float),
r["confidence"].astype(float),
r["classId"],
r["confidence"],
Rectangle(
r["xmin"].astype(float),
r["ymin"].astype(float),
r["xmax"].astype(float),
r["ymax"].astype(float),
r["xmin"],
r["ymin"],
r["xmax"],
r["ymax"],
),
)
objs.append(obj)
@@ -275,9 +283,9 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProv
),
)
coordinatesList = out_dict["coordinates"].astype(float)
coordinatesList = out_dict["coordinates"]
for index, confidenceList in enumerate(out_dict["confidence"].astype(float)):
for index, confidenceList in enumerate(out_dict["confidence"]):
values = confidenceList
maxConfidenceIndex = max(range(len(values)), key=values.__getitem__)
maxConfidence = confidenceList[maxConfidenceIndex]

View File

@@ -29,8 +29,8 @@ def cosine_similarity(vector_a, vector_b):
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
class CoreMLFaceRecognition(FaceRecognizeDetection):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def __init__(self, plugin, nativeId: str):
super().__init__(plugin, nativeId)
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-face")
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-face")

View File

@@ -13,8 +13,8 @@ from predict.text_recognize import TextRecognition
class CoreMLTextRecognition(TextRecognition):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def __init__(self, plugin, nativeId: str):
super().__init__(plugin, nativeId)
self.detectExecutor = concurrent.futures.ThreadPoolExecutor(1, "detect-text")
self.recogExecutor = concurrent.futures.ThreadPoolExecutor(1, "recog-text")

View File

@@ -1,4 +1,8 @@
from coreml import CoreMLPlugin
import predict
def create_scrypted_plugin():
return CoreMLPlugin()
async def fork():
return predict.Fork(CoreMLPlugin)

View File

@@ -1,5 +1,3 @@
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
numpy==1.26.4
coremltools==7.2
coremltools==8.0
Pillow==10.3.0
opencv-python==4.10.0.84
opencv-python-headless==4.10.0.84

View File

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

View File

@@ -1,10 +1,12 @@
{
"name": "@scrypted/diagnostics",
"version": "0.0.19",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/diagnostics",
"version": "0.0.19",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
@@ -12,8 +14,7 @@
},
"devDependencies": {
"@types/node": "^22.5.4"
},
"version": "0.0.17"
}
},
"../../common": {
"name": "@scrypted/common",
@@ -32,13 +33,13 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.62",
"version": "0.3.69",
"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",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
@@ -46,7 +47,7 @@
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
@@ -59,11 +60,11 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^22.1.0",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"ts-node": "^10.9.2",
"typedoc": "^0.26.5"
"typedoc": "^0.26.10"
}
},
"node_modules/@emnapi/runtime": {
@@ -719,12 +720,12 @@
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.24.7",
"@types/node": "^22.1.0",
"@babel/preset-typescript": "^7.26.0",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"adm-zip": "^0.5.14",
"axios": "^1.7.3",
"babel-loader": "^9.1.3",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"babel-loader": "^9.2.1",
"babel-plugin-const-enum": "^1.2.0",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
@@ -733,9 +734,9 @@
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typedoc": "^0.26.5",
"typedoc": "^0.26.10",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
}
},
@@ -843,6 +844,5 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true
}
},
"version": "0.0.17"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/diagnostics",
"version": "0.0.17",
"version": "0.0.19",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -128,6 +128,9 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
await device.sendNotification('Scrypted Diagnostics', {
body: 'Body',
subtitle: 'Subtitle',
android: {
channel: 'diagnostics',
}
}, mo);
this.warnStep(console, 'Check the device for the notification.');
@@ -291,6 +294,8 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
const nvrPlugin = sdk.systemManager.getDeviceById('@scrypted/nvr');
const cloudPlugin = sdk.systemManager.getDeviceById('@scrypted/cloud');
const hasCUDA = process.env.NVIDIA_VISIBLE_DEVICES && process.env.NVIDIA_DRIVER_CAPABILITIES;
const onnxPlugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>('@scrypted/onnx');
const openvinoPlugin = sdk.systemManager.getDeviceById<Settings & ObjectDetection>('@scrypted/openvino');
await this.validate(this.console, 'Scrypted Installation', async () => {
@@ -364,10 +369,14 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
});
if (process.platform === 'linux' && nvrPlugin) {
// ensure /dev/dri/renderD128 is available
// ensure /dev/dri/renderD128 or /dev/dri/renderD129 is available
await this.validate(this.console, 'GPU Passthrough', async () => {
if (!fs.existsSync('/dev/dri/renderD128'))
throw new Error('GPU device unvailable or not passed through to container.');
if (!fs.existsSync('/dev/dri/renderD128') && !fs.existsSync('/dev/dri/renderD129'))
throw new Error('GPU device unvailable or not passed through to container. (/dev/dri/renderD128, /dev/dri/renderD129)');
// also check /dev/kfd for AMD CPU
const amdCPU = os.cpus().find(c => c.model.includes('AMD'));
if (amdCPU && !fs.existsSync('/dev/kfd'))
throw new Error('GPU device unvailable or not passed through to container. (/dev/kfd)');
});
}
@@ -403,7 +412,22 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
throw new Error('Invalid response received from short lived URL.');
});
if (openvinoPlugin) {
if ((hasCUDA || process.platform === 'win32') && onnxPlugin) {
await this.validate(this.console, 'ONNX Plugin', async () => {
const settings = await onnxPlugin.getSettings();
const executionDevice = settings.find(s => s.key === 'execution_device');
if (executionDevice?.value?.toString().includes('CPU'))
this.warnStep(this.console, 'GPU device unvailable or not passed through to container.');
const zidane = await sdk.mediaManager.createMediaObjectFromUrl('https://docs.scrypted.app/img/scrypted-nvr/troubleshooting/zidane.jpg');
const detected = await onnxPlugin.detectObjects(zidane);
const personFound = detected.detections!.find(d => d.className === 'person' && d.score > .9);
if (!personFound)
throw new Error('Person not detected in test image.');
});
}
if (!hasCUDA && openvinoPlugin && (process.platform !== 'win32' || !onnxPlugin)) {
await this.validate(this.console, 'OpenVINO Plugin', async () => {
const settings = await openvinoPlugin.getSettings();
const availbleDevices = settings.find(s => s.key === 'available_devices');
@@ -512,6 +536,7 @@ class DiagnosticsPlugin extends ScryptedDeviceBase implements Settings {
await this.validate(this.console, 'Deprecated Plugins', async () => {
const defunctPlugins = [
'@scrypted/electron-core',
'@scrypted/opencv',
'@scrypted/python-codecs',
'@scrypted/pam-diff',

View File

@@ -111,6 +111,12 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
hide: true,
description: 'The last home hub to request a recording. Internally used to determine if a streaming request is coming from remote wifi.',
},
autoAdd: {
title: "Auto enable",
description: "Automatically enable this mixin on new devices.",
type: 'boolean',
defaultValue: true,
},
});
mergedDevices = new Set<string>();
@@ -218,7 +224,8 @@ export class HomeKitPlugin extends ScryptedDeviceBase implements MixinProvider,
try {
const mixins = (device.mixins || []).slice();
if (!mixins.includes(this.id)) {
const autoAdd = this.storageSettings.values.autoAdd ?? true;
if (!mixins.includes(this.id) && autoAdd) {
// don't sync this by default, as it's solely for automations
if (device.type === ScryptedDeviceType.Notifier)
continue;

View File

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

View File

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

View File

@@ -162,7 +162,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
getCurrentSettings() {
const settings = this.model.settings;
if (!settings)
return { id : this.id };
return { id: this.id };
const ret: { [key: string]: any } = {};
for (const setting of settings) {
@@ -338,7 +338,7 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (this.model.decoder) {
if (!options?.suppress)
this.console.log(this.objectDetection.name, '(with builtin decoder)');
this.console.log(this.objectDetection.name, '(with builtin decoder)');
return stream;
}
@@ -456,10 +456,10 @@ class ObjectDetectionMixin extends SettingsMixinDeviceBase<VideoCamera & Camera
if (!this.hasMotionType) {
this.plugin.trackDetection();
// const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
// const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
// if (numZonedDetections !== numOriginalDetections)
// this.console.log('Zone filtered detections:', numZonedDetections - numOriginalDetections);
const numZonedDetections = zonedDetections.filter(d => d.className !== 'motion').length;
const numOriginalDetections = originalDetections.filter(d => d.className !== 'motion').length;
if (numZonedDetections !== numOriginalDetections)
currentDetections.set('filtered', (currentDetections.get('filtered') || 0) + 1);
for (const d of detected.detected.detections) {
currentDetections.set(d.className, Math.max(currentDetections.get(d.className) || 0, d.score));

View File

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

View File

@@ -1,12 +1,8 @@
{
// docker installation
"scrypted.debugHost": "koushik-ubuntuvm",
"scrypted.debugHost": "koushik-winvm",
"scrypted.serverRoot": "/server",
// lxc
// "scrypted.debugHost": "scrypted-server",
// "scrypted.serverRoot": "/root/.scrypted",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
@@ -18,7 +14,6 @@
// "scrypted.debugHost": "koushik-winvm",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"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/onnx",
"version": "0.1.113",
"version": "0.1.119",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/onnx",
"version": "0.1.113",
"version": "0.1.119",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -35,12 +35,18 @@
"interfaces": [
"DeviceProvider",
"Settings",
"ClusterForkInterface",
"ObjectDetection",
"ObjectDetectionPreview"
]
],
"labels": {
"require": [
"@scrypted/onnx"
]
}
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.113"
"version": "0.1.119"
}

View File

@@ -1,4 +1,8 @@
from ort import ONNXPlugin
import predict
def create_scrypted_plugin():
return ONNXPlugin()
async def fork():
return predict.Fork(ONNXPlugin)

View File

@@ -40,6 +40,7 @@ availableModels = [
"scrypted_yolov8n_320",
]
def parse_labels(names):
j = ast.literal_eval(names)
ret = {}
@@ -47,11 +48,15 @@ def parse_labels(names):
ret[int(k)] = v
return ret
class ONNXPlugin(
PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider
PredictPlugin,
scrypted_sdk.BufferConverter,
scrypted_sdk.Settings,
scrypted_sdk.DeviceProvider,
):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def __init__(self, nativeId: str | None = None, forked: bool = False):
super().__init__(nativeId=nativeId, forked=forked)
model = self.storage.getItem("model") or "Default"
if model == "Default" or model not in availableModels:
@@ -67,7 +72,11 @@ class ONNXPlugin(
print(f"model {model}")
onnxmodel = model if self.scrypted_yolo_nas else "best" if self.scrypted_model else model
onnxmodel = (
model
if self.scrypted_yolo_nas
else "best" if self.scrypted_model else model
)
model_version = "v3"
onnxfile = self.downloadFile(
@@ -83,30 +92,37 @@ class ONNXPlugin(
deviceIds = ["0"]
self.deviceIds = deviceIds
compiled_models = []
self.compiled_models = {}
compiled_models: list[onnxruntime.InferenceSession] = []
self.compiled_models: dict[str, onnxruntime.InferenceSession] = {}
self.provider = "Unknown"
try:
for deviceId in deviceIds:
sess_options = onnxruntime.SessionOptions()
providers: list[str] = []
if sys.platform == 'darwin':
if sys.platform == "darwin":
providers.append("CoreMLExecutionProvider")
if ('linux' in sys.platform or 'win' in sys.platform) and (platform.machine() == 'x86_64' or platform.machine() == 'AMD64'):
if ("linux" in sys.platform or "win" in sys.platform) and (
platform.machine() == "x86_64" or platform.machine() == "AMD64"
):
deviceId = int(deviceId)
providers.append(("CUDAExecutionProvider", { "device_id": deviceId }))
providers.append(("CUDAExecutionProvider", {"device_id": deviceId}))
providers.append('CPUExecutionProvider')
providers.append("CPUExecutionProvider")
compiled_model = onnxruntime.InferenceSession(onnxfile, sess_options=sess_options, providers=providers)
compiled_model = onnxruntime.InferenceSession(
onnxfile, sess_options=sess_options, providers=providers
)
compiled_models.append(compiled_model)
input = compiled_model.get_inputs()[0]
self.model_dim = input.shape[2]
self.input_name = input.name
self.labels = parse_labels(compiled_model.get_modelmeta().custom_metadata_map['names'])
self.labels = parse_labels(
compiled_model.get_modelmeta().custom_metadata_map["names"]
)
except:
import traceback
@@ -121,7 +137,15 @@ class ONNXPlugin(
thread_name = threading.current_thread().name
interpreter = compiled_models.pop()
self.compiled_models[thread_name] = interpreter
print('Runtime initialized on thread {}'.format(thread_name))
# remove CPUExecutionProider from providers
providers = interpreter.get_providers()
if not len(providers):
providers = ["CPUExecutionProvider"]
if "CPUExecutionProvider" in providers:
providers.remove("CPUExecutionProvider")
# join the remaining providers string
self.provider = ", ".join(providers)
print("Runtime initialized on thread {}".format(thread_name))
self.executor = concurrent.futures.ThreadPoolExecutor(
initializer=executor_initializer,
@@ -134,9 +158,13 @@ class ONNXPlugin(
thread_name_prefix="onnx-prepare",
)
self.executor.submit(lambda: None)
self.faceDevice = None
self.textDevice = None
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
if not self.forked:
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
async def prepareRecognitionModels(self):
try:
@@ -145,6 +173,7 @@ class ONNXPlugin(
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "ONNX Face Recognition",
@@ -157,6 +186,7 @@ class ONNXPlugin(
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "ONNX Text Recognition",
@@ -206,12 +236,12 @@ class ONNXPlugin(
"key": "execution_device",
"title": "Execution Device",
"readonly": True,
"value": onnxruntime.get_device(),
}
"value": self.provider,
},
]
async def putSetting(self, key: str, value: SettingValue):
if (key == 'deviceIds'):
if key == "deviceIds":
value = json.dumps(value)
self.storage.setItem(key, value)
await self.onDeviceEvent(scrypted_sdk.ScryptedInterface.Settings.value, None)
@@ -225,7 +255,7 @@ class ONNXPlugin(
return [self.model_dim, self.model_dim]
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
def prepare():
def prepare():
im = np.array(input)
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
@@ -235,7 +265,7 @@ class ONNXPlugin(
def predict(input_tensor):
compiled_model = self.compiled_models[threading.current_thread().name]
output_tensors = compiled_model.run(None, { self.input_name: input_tensor })
output_tensors = compiled_model.run(None, {self.input_name: input_tensor})
if self.scrypted_yolov10:
return yolo.parse_yolov10(output_tensors[0][0])
if self.scrypted_yolo_nas:

View File

@@ -14,11 +14,6 @@ from predict.face_recognize import FaceRecognizeDetection
class ONNXFaceRecognition(FaceRecognizeDetection):
def __init__(self, plugin, nativeId: str | None = None):
self.plugin = plugin
super().__init__(nativeId=nativeId)
def downloadModel(self, model: str):
onnxmodel = "best" if "scrypted" in model else model
model_version = "v1"

View File

@@ -14,11 +14,6 @@ from predict.text_recognize import TextRecognition
class ONNXTextRecognition(TextRecognition):
def __init__(self, plugin, nativeId: str | None = None):
self.plugin = plugin
super().__init__(nativeId=nativeId)
def downloadModel(self, model: str):
onnxmodel = model
model_version = "v4"

View File

@@ -1,6 +1,3 @@
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
numpy==1.26.4
# uncomment to require cuda 12, but most stuff is still targetting cuda 11.
# however, stuff targetted for cuda 11 can still run on cuda 12.
# --extra-index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/
@@ -11,4 +8,4 @@ onnxruntime; 'darwin' in sys_platform or platform_machine == 'aarch64'
# ort-nightly-gpu==1.17.3.dev20240409002
Pillow==10.3.0
opencv-python==4.10.0.84
opencv-python-headless==4.10.0.84

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

@@ -5,16 +5,15 @@
// "scrypted.serverRoot": "/home/pi/.scrypted",
// docker installation
// "scrypted.debugHost": "koushik-ubuntu",
// "scrypted.serverRoot": "/server",
"scrypted.debugHost": "scrypted-nvr",
"scrypted.serverRoot": "/server",
// local checkout
"scrypted.debugHost": "127.0.0.1",
"scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "127.0.0.1",
// "scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "koushik-windows",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"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/opencv",
"version": "0.0.91",
"version": "0.0.92",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/opencv",
"version": "0.0.91",
"version": "0.0.92",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -37,5 +37,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.91"
"version": "0.0.92"
}

View File

@@ -1,5 +1,3 @@
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
numpy==1.26.4
imutils>=0.5.0
opencv-python==4.10.0.82
opencv-python-headless==4.10.0.84
Pillow==10.3.0

View File

@@ -1,24 +1,6 @@
{
// docker installation
// "scrypted.debugHost": "scrypted-demo",
// "scrypted.serverRoot": "/server",
// proxmox installation
"scrypted.debugHost": "scrypted-nvr",
"scrypted.serverRoot": "/root/.scrypted",
// 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-winvm",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]

View File

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

View File

@@ -35,12 +35,18 @@
"interfaces": [
"DeviceProvider",
"Settings",
"ClusterForkInterface",
"ObjectDetection",
"ObjectDetectionPreview"
]
],
"labels": {
"require": [
"@scrypted/openvino"
]
}
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.118"
"version": "0.1.137"
}

View File

@@ -33,4 +33,22 @@ async def ensureRGBAData(data: bytes, size: Tuple[int, int], format: str):
return rgb.convert('RGBA')
finally:
rgb.close()
return await to_thread(convert)
return await to_thread(convert)
async def ensureYCbCrAData(data: bytes, size: Tuple[int, int], format: str):
# if the format is already yuvj444p, just return the data as is.
if format == 'yuvj444p':
# return RGB as a hack to indicate the data is already yuv planar.
return Image.frombuffer('RGB', size, data)
def convert():
if format == 'rgb':
tmp = Image.frombuffer('RGB', size, data)
else:
tmp = Image.frombuffer('RGBA', size, data)
try:
return tmp.convert('YCbCr')
finally:
tmp.close()
return await to_thread(convert)

View File

@@ -12,11 +12,11 @@ def parse_yolov10(results, threshold = defaultThreshold, scale = None, confidenc
for indices in keep:
class_id = indices[0]
index = indices[1]
confidence = results[class_id + 4, index].astype(float)
l = results[0][index].astype(float)
t = results[1][index].astype(float)
r = results[2][index].astype(float)
b = results[3][index].astype(float)
confidence = results[class_id + 4, index]
l = results[0][index]
t = results[1][index]
r = results[2][index]
b = results[3][index]
if scale:
l = scale(l)
t = scale(t)
@@ -47,7 +47,7 @@ def parse_yolo_nas(predictions):
pred_cls_label = j[:]
for box, conf, label in zip(pred_bboxes, pred_cls_conf, pred_cls_label):
obj = Prediction(
int(label), conf.astype(float), Rectangle(box[0].astype(float), box[1].astype(float), box[2].astype(float), box[3].astype(float))
int(label), conf, Rectangle(box[0], box[1], box[2], box[3])
)
objs.append(obj)
return objs
@@ -58,11 +58,11 @@ def parse_yolov9(results, threshold = defaultThreshold, scale = None, confidence
for indices in keep:
class_id = indices[0]
index = indices[1]
confidence = results[class_id + 4, index].astype(float)
x = results[0][index].astype(float)
y = results[1][index].astype(float)
w = results[2][index].astype(float)
h = results[3][index].astype(float)
confidence = results[class_id + 4, index]
x = results[0][index]
y = results[1][index]
w = results[2][index]
h = results[3][index]
if scale:
x = scale(x)
y = scale(y)
@@ -190,12 +190,12 @@ def parse_yolo_region(blob, original_im_shape, anchors, sigmoid = True):
ymax = y + height /2
objects.append(
{
'xmin': xmin.astype(float),
'xmax': xmax.astype(float),
'ymin': ymin.astype(float),
'ymax': ymax.astype(float),
'confidence': confidence.astype(float),
'classId': class_id.astype(float),
'xmin': xmin,
'xmax': xmax,
'ymin': ymin,
'ymax': ymax,
'confidence': confidence,
'classId': class_id,
}
)

View File

@@ -4,13 +4,22 @@ import asyncio
from typing import Any, Tuple
import scrypted_sdk
from scrypted_sdk.types import (MediaObject, ObjectDetection,
ObjectDetectionGeneratorSession,
ObjectDetectionModel, ObjectDetectionSession,
ObjectsDetected, ScryptedMimeTypes, Setting)
from scrypted_sdk.types import (
MediaObject,
ObjectDetection,
ObjectDetectionGeneratorSession,
ObjectDetectionModel,
ObjectDetectionSession,
ObjectsDetected,
ScryptedMimeTypes,
Setting,
)
class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
class DetectPlugin(
scrypted_sdk.ScryptedDeviceBase,
ObjectDetection,
):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
self.loop = asyncio.get_event_loop()
@@ -33,47 +42,55 @@ class DetectPlugin(scrypted_sdk.ScryptedDeviceBase, ObjectDetection):
async def getDetectionModel(self, settings: Any = None) -> ObjectDetectionModel:
d: ObjectDetectionModel = {
'name': self.modelName,
'classes': self.getClasses(),
'triggerClasses': self.getTriggerClasses(),
'inputSize': self.get_input_details(),
'inputFormat': self.get_input_format(),
'settings': [],
"name": self.modelName,
"classes": self.getClasses(),
"triggerClasses": self.getTriggerClasses(),
"inputSize": self.get_input_details(),
"inputFormat": self.get_input_format(),
"settings": [],
}
d['settings'] += self.getModelSettings(settings)
d["settings"] += self.getModelSettings(settings)
return d
def get_detection_input_size(self, src_size):
pass
async def run_detection_image(self, videoFrame: scrypted_sdk.Image, detection_session: ObjectDetectionSession) -> ObjectsDetected:
async def run_detection_image(
self, videoFrame: scrypted_sdk.Image, detection_session: ObjectDetectionSession
) -> ObjectsDetected:
pass
async def generateObjectDetections(self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None) -> Any:
async def generateObjectDetections(
self, videoFrames: Any, session: ObjectDetectionGeneratorSession = None
) -> Any:
try:
videoFrames = await scrypted_sdk.sdk.connectRPCObject(videoFrames)
videoFrame: scrypted_sdk.VideoFrame
async for videoFrame in videoFrames:
image = await scrypted_sdk.sdk.connectRPCObject(videoFrame['image'])
detected = await self.run_detection_image(image, session)
yield {
'__json_copy_serialize_children': True,
'detected': detected,
'videoFrame': videoFrame,
}
image = await scrypted_sdk.sdk.connectRPCObject(videoFrame["image"])
detected = await self.run_detection_image(image, session)
yield {
"__json_copy_serialize_children": True,
"detected": detected,
"videoFrame": videoFrame,
}
finally:
try:
await videoFrames.aclose()
except:
pass
async def detectObjects(self, mediaObject: MediaObject, session: ObjectDetectionSession = None) -> ObjectsDetected:
async def detectObjects(
self, mediaObject: MediaObject, session: ObjectDetectionSession = None
) -> ObjectsDetected:
image: scrypted_sdk.Image
if mediaObject.mimeType == ScryptedMimeTypes.Image.value:
image = await scrypted_sdk.sdk.connectRPCObject(mediaObject)
else:
image = await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(mediaObject, ScryptedMimeTypes.Image.value)
image = await scrypted_sdk.mediaManager.convertMediaObjectToBuffer(
mediaObject, ScryptedMimeTypes.Image.value
)
return await self.run_detection_image(image, session)

View File

@@ -1,4 +1,8 @@
from ov import OpenVINOPlugin
import predict
def create_scrypted_plugin():
return OpenVINOPlugin()
async def fork():
return predict.Fork(OpenVINOPlugin)

View File

@@ -30,6 +30,7 @@ prepareExecutor = concurrent.futures.ThreadPoolExecutor(1, "OpenVINO-Prepare")
availableModels = [
"Default",
"scrypted_yolov9c_relu_int8_320",
"scrypted_yolov10m_320",
"scrypted_yolov10s_320",
"scrypted_yolov10n_320",
@@ -37,6 +38,7 @@ availableModels = [
"scrypted_yolov6n_320",
"scrypted_yolov6s_320",
"scrypted_yolov9c_320",
"scrypted_yolov9m_320",
"scrypted_yolov9s_320",
"scrypted_yolov9t_320",
"scrypted_yolov8n_320",
@@ -46,6 +48,7 @@ availableModels = [
"yolo-v4-tiny-tf",
]
def parse_label_contents(contents: str):
lines = contents.splitlines()
lines = [line for line in lines if line.strip()]
@@ -87,10 +90,13 @@ def dump_device_properties(core):
class OpenVINOPlugin(
PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider
PredictPlugin,
scrypted_sdk.BufferConverter,
scrypted_sdk.Settings,
scrypted_sdk.DeviceProvider,
):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def __init__(self, nativeId: str | None = None, forked: bool = False):
super().__init__(nativeId=nativeId, forked=forked)
self.core = ov.Core()
dump_device_properties(self.core)
@@ -158,16 +164,22 @@ class OpenVINOPlugin(
else:
model = "scrypted_yolov9t_320"
self.yolo = "yolo" in model
self.scrypted_yolov9 = "scrypted_yolov9" in model
self.scrypted_yolov10 = "scrypted_yolov10" in model
self.scrypted_yolo_nas = "scrypted_yolo_nas" in model
self.scrypted_yolo = "scrypted_yolo" in model
self.scrypted_model = "scrypted" in model
self.scrypted_yuv = "yuv" in model
self.sigmoid = model == "yolo-v4-tiny-tf"
self.modelName = model
ovmodel = "best" if self.scrypted_model else model
ovmodel = (
"best-converted"
if self.scrypted_yolov9
else "best" if self.scrypted_model else model
)
model_version = "v5"
model_version = "v7"
xmlFile = self.downloadFile(
f"https://github.com/koush/openvino-models/raw/main/{model}/{precision}/{ovmodel}.xml",
f"{model_version}/{model}/{precision}/{ovmodel}.xml",
@@ -203,11 +215,12 @@ class OpenVINOPlugin(
self.compiled_model = self.core.compile_model(xmlFile, mode)
except:
import traceback
traceback.print_exc()
if mode == "GPU":
if "GPU" in mode:
try:
print("GPU mode failed, reverting to AUTO.")
print(f"{mode} mode failed, reverting to AUTO.")
mode = "AUTO"
self.mode = mode
self.compiled_model = self.core.compile_model(xmlFile, mode)
@@ -222,7 +235,7 @@ class OpenVINOPlugin(
"EXECUTION_DEVICES",
self.compiled_model.get_property("EXECUTION_DEVICES"),
)
print(f"model/mode/precision: {model}/{mode}/{precision}")
print(f"model/mode: {model}/{mode}")
# mobilenet 1,300,300,3
# yolov3/4 1,416,416,3
@@ -235,7 +248,9 @@ class OpenVINOPlugin(
self.faceDevice = None
self.textDevice = None
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
if not self.forked:
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
async def getSettings(self) -> list[Setting]:
mode = self.storage.getItem("mode") or "Default"
@@ -283,6 +298,11 @@ class OpenVINOPlugin(
def get_input_size(self) -> Tuple[int, int]:
return [self.model_dim, self.model_dim]
def get_input_format(self):
if self.scrypted_yuv:
return "yuvj444p"
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()
@@ -329,7 +349,7 @@ class OpenVINOPlugin(
return objs
output = infer_request.get_output_tensor(0)
for values in output.data[0][0].astype(float):
for values in output.data[0][0]:
valid, index, confidence, l, t, r, b = values
if valid == -1:
break
@@ -347,14 +367,24 @@ class OpenVINOPlugin(
return objs
def prepare():
def prepare():
# the input_tensor can be created with the shared_memory=True parameter,
# but that seems to cause issues on some platforms.
if self.scrypted_yolo:
im = np.array(input)
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
if not self.scrypted_yuv:
im = np.expand_dims(input, axis=0)
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
else:
# when a yuv image is requested, it may be either planar or interleaved
# as as hack, the input will come as RGB if already planar.
if input.mode != "RGB":
im = np.array(input)
im = im.reshape((1, self.model_dim, self.model_dim, 3))
im = im.transpose((0, 3, 1, 2)) # BHWC to BCHW, (n, 3, h, w)
else:
im = np.array(input)
im = im.reshape((1, 3, self.model_dim, self.model_dim))
im = im.astype(np.float32) / 255.0
im = np.ascontiguousarray(im) # contiguous
input_tensor = ov.Tensor(array=im)
@@ -388,6 +418,7 @@ class OpenVINOPlugin(
"nativeId": "facerecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "OpenVINO Face Recognition",
@@ -400,6 +431,7 @@ class OpenVINOPlugin(
"nativeId": "textrecognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ClusterForkInterface.value,
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "OpenVINO Text Recognition",

View File

@@ -16,15 +16,11 @@ faceRecognizePrepare, faceRecognizePredict = async_infer.create_executors(
class OpenVINOFaceRecognition(FaceRecognizeDetection):
def __init__(self, plugin, nativeId: str | None = None):
self.plugin = plugin
super().__init__(nativeId=nativeId)
def downloadModel(self, model: str):
ovmodel = "best"
scrypted_yolov9 = "scrypted_yolov9" in model
ovmodel = "best-converted" if scrypted_yolov9 else "best"
precision = self.plugin.precision
model_version = "v5"
model_version = "v7"
xmlFile = self.downloadFile(
f"https://github.com/koush/openvino-models/raw/main/{model}/{precision}/{ovmodel}.xml",
f"{model_version}/{model}/{precision}/{ovmodel}.xml",

View File

@@ -15,11 +15,6 @@ textRecognizePrepare, textRecognizePredict = async_infer.create_executors(
class OpenVINOTextRecognition(TextRecognition):
def __init__(self, plugin, nativeId: str | None = None):
self.plugin = plugin
super().__init__(nativeId=nativeId)
def downloadModel(self, model: str):
ovmodel = "best"
precision = self.plugin.precision

View File

@@ -2,48 +2,77 @@ from __future__ import annotations
import asyncio
import os
import re
import math
import traceback
import urllib.request
from typing import Any, List, Tuple
from typing import Any, List, Tuple, Mapping
import scrypted_sdk
from PIL import Image
from scrypted_sdk.types import (ObjectDetectionResult, ObjectDetectionSession,
ObjectsDetected, Setting)
from scrypted_sdk.types import (
ObjectDetectionResult,
ObjectDetectionSession,
ObjectsDetected,
Setting,
)
import common.colors
from detect import DetectPlugin
from predict.rectangle import Rectangle
class Prediction:
def __init__(self, id: int, score: float, bbox: Tuple[float, float, float, float], embedding: str = None):
self.id = id
self.score = score
self.bbox = bbox
def __init__(self, id: int, score: float, bbox: Rectangle, embedding: str = None):
# these may be numpy values. sanitize them.
self.id = int(id)
self.score = float(score)
# ensure all floats from numpy
self.bbox = Rectangle(
float(bbox.xmin),
float(bbox.ymin),
float(bbox.xmax),
float(bbox.ymax),
)
self.embedding = embedding
class PredictPlugin(DetectPlugin):
class PredictPlugin(DetectPlugin, scrypted_sdk.ClusterForkInterface):
labels: dict
def __init__(self, nativeId: str | None = None):
def __init__(
self,
plugin: PredictPlugin = None,
nativeId: str | None = None,
forked: bool = False,
):
super().__init__(nativeId=nativeId)
self.plugin = plugin
# self.clusterIndex = 0
# periodic restart of main plugin because there seems to be leaks in tflite or coral API.
if not nativeId:
loop = asyncio.get_event_loop()
loop.call_later(4 * 60 * 60, lambda: self.requestRestart())
self.batch: List[Tuple[Any, asyncio.Future]] = []
self.batching = 0
self.batch_flush = None
self.forked = forked
if not self.forked:
self.forks: Mapping[str, scrypted_sdk.PluginFork] = {}
if not self.plugin and not self.forked:
asyncio.ensure_future(self.startCluster(), loop=self.loop)
def downloadFile(self, url: str, filename: str):
try:
filesPath = os.path.join(os.environ['SCRYPTED_PLUGIN_VOLUME'], 'files')
filesPath = os.path.join(os.environ["SCRYPTED_PLUGIN_VOLUME"], "files")
fullpath = os.path.join(filesPath, filename)
if os.path.isfile(fullpath):
return fullpath
tmp = fullpath + '.tmp'
tmp = fullpath + ".tmp"
print("Creating directory for", tmp)
os.makedirs(os.path.dirname(fullpath), exist_ok=True)
print("Downloading", url)
@@ -64,6 +93,7 @@ class PredictPlugin(DetectPlugin):
except:
print("Error downloading", url)
import traceback
traceback.print_exc()
raise
@@ -71,7 +101,7 @@ class PredictPlugin(DetectPlugin):
return list(self.labels.values())
def getTriggerClasses(self) -> list[str]:
return ['motion']
return ["motion"]
def requestRestart(self):
asyncio.ensure_future(scrypted_sdk.deviceManager.requestRestart())
@@ -84,35 +114,47 @@ class PredictPlugin(DetectPlugin):
return []
def get_input_format(self) -> str:
return 'rgb'
return "rgb"
def create_detection_result(self, objs: List[Prediction], size, convert_to_src_size=None) -> ObjectsDetected:
def create_detection_result(
self, objs: List[Prediction], size, convert_to_src_size=None
) -> ObjectsDetected:
detections: List[ObjectDetectionResult] = []
detection_result: ObjectsDetected = {}
detection_result['detections'] = detections
detection_result['inputDimensions'] = size
detection_result["detections"] = detections
detection_result["inputDimensions"] = size
for obj in objs:
className = self.labels.get(obj.id, obj.id)
detection: ObjectDetectionResult = {}
detection['boundingBox'] = (
obj.bbox.xmin, obj.bbox.ymin, obj.bbox.xmax - obj.bbox.xmin, obj.bbox.ymax - obj.bbox.ymin)
detection['className'] = className
detection['score'] = obj.score
if hasattr(obj, 'embedding') and obj.embedding is not None:
detection['embedding'] = obj.embedding
detection["boundingBox"] = (
obj.bbox.xmin,
obj.bbox.ymin,
obj.bbox.xmax - obj.bbox.xmin,
obj.bbox.ymax - obj.bbox.ymin,
)
# check bounding box for nan
if any(map(lambda x: not math.isfinite(x), detection["boundingBox"])):
print("unexpected nan detected", obj.bbox)
continue
detection["className"] = className
detection["score"] = obj.score
if hasattr(obj, "embedding") and obj.embedding is not None:
detection["embedding"] = obj.embedding
detections.append(detection)
if convert_to_src_size:
detections = detection_result['detections']
detection_result['detections'] = []
detections = detection_result["detections"]
detection_result["detections"] = []
for detection in detections:
bb = detection['boundingBox']
bb = detection["boundingBox"]
x, y = convert_to_src_size((bb[0], bb[1]))
x2, y2 = convert_to_src_size(
(bb[0] + bb[2], bb[1] + bb[3]))
detection['boundingBox'] = (x, y, x2 - x + 1, y2 - y + 1)
detection_result['detections'].append(detection)
x2, y2 = convert_to_src_size((bb[0] + bb[2], bb[1] + bb[3]))
detection["boundingBox"] = (x, y, x2 - x + 1, y2 - y + 1)
if any(map(lambda x: not math.isfinite(x), detection["boundingBox"])):
print("unexpected nan detected", obj.bbox)
continue
detection_result["detections"].append(detection)
# print(detection_result)
return detection_result
@@ -127,7 +169,9 @@ class PredictPlugin(DetectPlugin):
def get_input_size(self) -> Tuple[int, int]:
pass
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
async def detect_once(
self, input: Image.Image, settings: Any, src_size, cvss
) -> ObjectsDetected:
pass
async def detect_batch(self, inputs: List[Any]) -> List[Any]:
@@ -153,33 +197,62 @@ class PredictPlugin(DetectPlugin):
await self.run_batch()
async def queue_batch(self, input: Any) -> List[Any]:
future = asyncio.Future(loop = asyncio.get_event_loop())
future = asyncio.Future(loop=asyncio.get_event_loop())
self.batch.append((input, future))
if self.batching:
self.batching = self.batching - 1
if self.batching:
# if there is any sort of error or backlog, .
if not self.batch_flush:
self.batch_flush = self.loop.call_later(.5, lambda: asyncio.ensure_future(self.flush_batch()))
self.batch_flush = self.loop.call_later(
0.5, lambda: asyncio.ensure_future(self.flush_batch())
)
return await future
await self.run_batch()
return await future
async def safe_detect_once(self, input: Image.Image, settings: Any, src_size, cvss) -> ObjectsDetected:
async def safe_detect_once(
self, input: Image.Image, settings: Any, src_size, cvss
) -> ObjectsDetected:
try:
f = self.detect_once(input, settings, src_size, cvss)
return await asyncio.wait_for(f, 60)
except:
traceback.print_exc()
print(
"encountered an error while detecting. requesting plugin restart."
)
print("encountered an error while detecting. requesting plugin restart.")
self.requestRestart()
raise
async def run_detection_image(self, image: scrypted_sdk.Image, detection_session: ObjectDetectionSession) -> ObjectsDetected:
settings = detection_session and detection_session.get('settings')
batch = (detection_session and detection_session.get('batch')) or 0
# async def detectObjects(
# self, mediaObject: scrypted_sdk.MediaObject, session: ObjectDetectionSession = None
# ) -> ObjectsDetected:
# # main plugin can dispatch
# plugin: PredictPlugin = None
# if scrypted_sdk.clusterManager and scrypted_sdk.clusterManager.getClusterMode() and not self.forked:
# if session:
# del session['batch']
# if len(self.forks):
# totalWorkers = len(self.forks)
# if not self.forked:
# totalWorkers += 1
# self.clusterIndex += 1
# self.clusterIndex %= totalWorkers
# if len(self.forks) != self.clusterIndex:
# fork = list(self.forks.values())[self.clusterIndex]
# result = await fork.result
# plugin = await result.getPlugin()
# if not plugin:
# return await super().detectObjects(mediaObject, session)
# return await plugin.detectObjects(mediaObject, session)
async def run_detection_image(
self, image: scrypted_sdk.Image, detection_session: ObjectDetectionSession
) -> ObjectsDetected:
settings = detection_session and detection_session.get("settings")
batch = (detection_session and detection_session.get("batch")) or 0
self.batching += batch
iw, ih = image.width, image.height
@@ -189,34 +262,142 @@ class PredictPlugin(DetectPlugin):
resize = None
w = image.width
h = image.height
def cvss(point):
return point
else:
resize = None
xs = w / iw
ys = h / ih
def cvss(point):
return point[0] / xs, point[1] / ys
if iw != w or ih != h:
resize = {
'width': w,
'height': h,
"width": w,
"height": h,
}
format = image.format or self.get_input_format()
b = await image.toBuffer({
'resize': resize,
'format': format,
})
if self.get_input_format() == 'rgb':
# if the model requires yuvj444p, convert the image to yuvj444p directly
# if possible, otherwise use whatever is available and convert in the detection plugin
if self.get_input_format() == "yuvj444p":
if image.ffmpegFormats != True:
format = image.format or "rgb"
b = await image.toBuffer(
{
"resize": resize,
"format": format,
}
)
if self.get_input_format() == "rgb":
data = await common.colors.ensureRGBData(b, (w, h), format)
elif self.get_input_format() == 'rgba':
elif self.get_input_format() == "rgba":
data = await common.colors.ensureRGBAData(b, (w, h), format)
elif self.get_input_format() == "yuvj444p":
data = await common.colors.ensureYCbCrAData(b, (w, h), format)
else:
raise Exception("unsupported format")
try:
ret = await self.safe_detect_once(data, settings, (iw, ih), cvss)
return ret
finally:
data.close()
async def forkInterfaceInternal(self, options: dict):
if self.plugin:
return await self.plugin.forkInterfaceInternal(options)
clusterWorkerId = options.get("clusterWorkerId", None)
if not clusterWorkerId:
raise Exception("clusterWorkerId required")
if self.forked:
raise Exception("cannot fork a fork")
forked = self.forks.get(clusterWorkerId, None)
if not forked:
forked = scrypted_sdk.fork(
{"labels": {"require": [self.pluginId]}, **(options or {})}
)
def clusterWorkerExit(result):
print("cluster worker exit", clusterWorkerId)
self.forks.pop(clusterWorkerId)
forked.exit.add_done_callback(clusterWorkerExit)
self.forks[clusterWorkerId] = forked
result = await forked.result
return result
async def forkInterface(self, forkInterface, options: dict = None):
if forkInterface != scrypted_sdk.ScryptedInterface.ObjectDetection.value:
raise Exception("unsupported fork interface")
result = await self.forkInterfaceInternal(options)
if not self.nativeId:
ret = await result.getPlugin()
elif self.nativeId == "textrecognition":
ret = await result.getTextRecognition()
elif self.nativeId == "facerecognition":
ret = await result.getFaceRecognition()
return ret
async def startCluster(self):
try:
clusterManager = scrypted_sdk.clusterManager
if not clusterManager:
return
workers = await clusterManager.getClusterWorkers()
thisClusterWorkerId = clusterManager.getClusterWorkerId()
except:
traceback.print_exc()
return
for cwid in workers:
if cwid == thisClusterWorkerId:
selfFork = Fork(None)
selfFork.plugin = self
pf = scrypted_sdk.PluginFork()
pf.result = asyncio.Future(loop=self.loop)
pf.result.set_result(selfFork)
self.forks[cwid] = pf
continue
async def startClusterWorker(clusterWorkerId=cwid):
print("starting cluster worker", clusterWorkerId)
try:
await self.forkInterfaceInternal(
{"clusterWorkerId": clusterWorkerId}
)
except:
# traceback.print_exc()
pass
asyncio.ensure_future(startClusterWorker(), loop=self.loop)
class Fork:
def __init__(self, PluginType: Any):
if PluginType:
self.plugin = PluginType(forked=True)
else:
self.plugin = None
async def getPlugin(self):
return self.plugin
async def getTextRecognition(self):
return await self.plugin.getDevice("textrecognition")
async def getFaceRecognition(self):
return await self.plugin.getDevice("facerecognition")

View File

@@ -23,8 +23,8 @@ def cosine_similarity(vector_a, vector_b):
return similarity
class FaceRecognizeDetection(PredictPlugin):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def __init__(self, plugin: PredictPlugin, nativeId: str):
super().__init__(nativeId=nativeId, plugin=plugin)
self.inputheight = 320
self.inputwidth = 320

View File

@@ -23,8 +23,8 @@ predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "TextDetect")
class TextRecognition(PredictPlugin):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def __init__(self, plugin: PredictPlugin, nativeId: str):
super().__init__(plugin=plugin, nativeId=nativeId)
self.inputheight = 640
self.inputwidth = 640

View File

@@ -1,7 +1,5 @@
# must ensure numpy is pinned to prevent dependencies with an unpinned numpy from pulling numpy>=2.0.
numpy==1.26.4
# 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
Pillow==10.3.0
opencv-python==4.10.0.84
opencv-python-headless==4.10.0.84

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
import { createActivityTimeout } from '@scrypted/common/src/activity-timeout';
import { cloneDeep } from '@scrypted/common/src/clone-deep';
import { Deferred } from "@scrypted/common/src/deferred";
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
import { createActivityTimeout } from '@scrypted/common/src/activity-timeout';
import { createRtspParser } from "@scrypted/common/src/rtsp-server";
import { parseSdp } from "@scrypted/common/src/sdp-utils";
import { StreamChunk, StreamParser } from '@scrypted/common/src/stream-parser';
import sdk, { FFmpegInput, RequestMediaStreamOptions, ResponseMediaStreamOptions } from "@scrypted/sdk";
import child_process, { ChildProcess, StdioOptions } from 'child_process';

View File

@@ -1,25 +1,20 @@
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) {
const [address] = addresses;
if (address) {
const u = new URL(url);
u.hostname = address;
url = u.toString();
}
urls = addresses.map(address => {
const u = new URL(url);
u.hostname = address;
return u.toString();
});
}
if (!addresses)
return;
const 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
}
return urls;
}

View File

@@ -3,7 +3,7 @@ import { getDebugModeH264EncoderArgs, getH264EncoderArgs } from '@scrypted/commo
import { addVideoFilterArguments } from '@scrypted/common/src/ffmpeg-helpers';
import { ListenZeroSingleClientTimeoutError, closeQuiet, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { readLength } from '@scrypted/common/src/read-stream';
import { H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_B, H265_NAL_TYPE_SPS, RtspServer, RtspTrack, createRtspParser, findH264NaluType, findH265NaluType, getNaluTypes, listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
import { H264_NAL_TYPE_FU_B, H264_NAL_TYPE_IDR, H264_NAL_TYPE_MTAP16, H264_NAL_TYPE_MTAP32, H264_NAL_TYPE_RESERVED0, H264_NAL_TYPE_RESERVED30, H264_NAL_TYPE_RESERVED31, H264_NAL_TYPE_SEI, H264_NAL_TYPE_SPS, H264_NAL_TYPE_STAP_B, RtspServer, RtspTrack, createRtspParser, findH264NaluType, getNaluTypes, listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, getSpsPps, parseSdp } from '@scrypted/common/src/sdp-utils';
import { SettingsMixinDeviceBase, SettingsMixinDeviceOptions } from "@scrypted/common/src/settings-mixin";
import { sleep } from '@scrypted/common/src/sleep';
@@ -15,9 +15,7 @@ import { once } from 'events';
import { parse as h264SpsParse } from "h264-sps-parser";
import net, { AddressInfo } from 'net';
import path from 'path';
import semver from 'semver';
import { Duplex } from 'stream';
import { Worker } from 'worker_threads';
import { ParserOptions, ParserSession, startParserSession } from './ffmpeg-rebroadcast';
import { FileRtspServer } from './file-rtsp-server';
import { getUrlLocalAdresses } from './local-addresses';
@@ -196,13 +194,33 @@ class PrebufferSession {
return;
this.console.log(this.streamName, 'prebuffer session started');
this.parserSessionPromise = this.startPrebufferSession();
this.parserSessionPromise.then(pso => pso.killed.finally(() => {
this.console.error(this.streamName, 'prebuffer session ended');
this.parserSessionPromise = undefined;
}))
let active = false;
this.parserSessionPromise.then(pso => {
pso.once('rtsp', () => {
active = true;
if (!this.mixin.online)
this.mixin.online = true;
});
pso.killed.finally(() => {
this.console.error(this.streamName, 'prebuffer session ended');
this.parserSessionPromise = undefined;
});
})
.catch(e => {
this.console.error(this.streamName, 'prebuffer session ended with error', e);
this.parserSessionPromise = undefined;
if (!active) {
// find sessions that arent this one, and check their prebuffers to see if any data has been received.
// if there's no data, then consider this camera offline.
const others = [...this.mixin.sessions.values()].filter(s => s !== this);
if (others.length) {
const hasData = others.some(s => s.rtspPrebuffer.length);
if (!hasData && this.mixin.online)
this.mixin.online = false;
}
}
});
}
@@ -1415,11 +1433,6 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
}
}
if (!enabledIds.length)
this.online = true;
let active = 0;
// figure out the default stream and streams that may have been removed due to
// a config change.
const toRemove = new Set(this.sessions.keys());
@@ -1462,23 +1475,13 @@ class PrebufferMixin extends SettingsMixinDeviceBase<VideoCamera> implements Vid
}
session.ensurePrebufferSession();
let wasActive = false;
try {
this.console.log(name, 'prebuffer session starting');
const ps = await session.parserSessionPromise;
active++;
wasActive = true;
this.online = !!active;
await ps.killed;
}
catch (e) {
}
finally {
if (wasActive)
active--;
wasActive = false;
this.online = !!active;
}
this.console.log(this.name, 'restarting prebuffer session in 5 seconds');
await new Promise(resolve => setTimeout(resolve, 5000));
}

View File

@@ -21,21 +21,10 @@
},
{
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
"remoteRoot": "."
},
]
},
{
"name": "Python: Test",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/src/test.py",
"console": "internalConsole",
"justMyCode": true,
"env": {
"GST_PLUGIN_PATH": "/opt/homebrew/lib/gstreamer-1.0"
}
}
]
}

View File

@@ -1,31 +1,20 @@
{
// docker installation
// "scrypted.debugHost": "scrypted-server",
// "scrypted.serverRoot": "/server",
// lxc installation
// "scrypted.debugHost": "scrypted-server",
// "scrypted.serverRoot": "/root/.scrypted",
// windows installation
// "scrypted.debugHost": "koushik-windows",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"scrypted.debugHost": "scrypted-nvr",
"scrypted.serverRoot": "/server",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// local checkout
"scrypted.debugHost": "127.0.0.1",
"scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "127.0.0.1",
// "scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "koushik-winvm",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
],
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
]
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.96",
"version": "0.1.97",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/python-codecs",
"version": "0.1.96",
"version": "0.1.97",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/python-codecs",
"version": "0.1.96",
"version": "0.1.97",
"description": "Python Codecs for Scrypted",
"keywords": [
"scrypted",

View File

@@ -3,7 +3,4 @@ numpy>=1.16.2
av>=10.0.0
# in case pyvips fails to load, use a pillow fallback.
# pillow for anything not intel linux, pillow-simd is available on x64 linux
Pillow>=5.4.1; 'linux' not in sys_platform or platform_machine != 'x86_64'
pillow-simd; 'linux' in sys_platform and platform_machine == 'x86_64'
Pillow>=5.4.1

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/reolink",
"version": "0.0.96",
"version": "0.0.100",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/reolink",
"version": "0.0.96",
"version": "0.0.100",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",
@@ -35,7 +35,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.65",
"version": "0.3.67",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/reolink",
"version": "0.0.97",
"version": "0.0.100",
"description": "Reolink Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -1,5 +1,5 @@
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
import sdk, { Brightness, Camera, Device, DeviceCreatorSettings, DeviceInformation, DeviceProvider, Intercom, MediaObject, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestPictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from "@scrypted/sdk";
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { EventEmitter } from "stream";
import { createRtspMediaStreamOptions, Destroyable, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
@@ -50,13 +50,44 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
}
}
class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brightness {
constructor(public camera: ReolinkCamera, nativeId: string) {
super(nativeId);
this.on = false;
}
async setBrightness(brightness: number): Promise<void> {
this.brightness = brightness;
await this.setFloodlight(undefined, brightness);
}
async turnOff() {
this.on = false;
await this.setFloodlight(false);
}
async turnOn() {
this.on = true;
await this.setFloodlight(true);
}
private async setFloodlight(on?: boolean, brightness?: number) {
const api = this.camera.getClientWithToken();
await api.setWhiteLedState(on, brightness);
}
}
class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, Reboot, Intercom, ObjectDetector, PanTiltZoom {
client: ReolinkCameraClient;
clientWithToken: ReolinkCameraClient;
onvifClient: OnvifCameraAPI;
onvifIntercom = new OnvifIntercom(this);
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
motionTimeout: NodeJS.Timeout;
siren: ReolinkCameraSiren;
floodlight: ReolinkCameraFloodlight;
batteryTimeout: NodeJS.Timeout;
storageSettings = new StorageSettings(this, {
doorbell: {
@@ -180,18 +211,11 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
}
const api = this.getClient();
const deviceInfo = await api.getDeviceInfo();
this.console.log('deviceInfo', JSON.stringify(deviceInfo));
this.storageSettings.values.deviceInfo = deviceInfo;
await this.updateAbilities();
await this.updateDevice();
if (this.hasSiren()) {
this.reportSirenDevice();
}
else {
sdk.deviceManager.onDevicesChanged({
providerNativeId: this.nativeId,
devices: []
});
}
await this.reportDevices();
})()
.catch(e => {
this.console.log('device refresh failed', e);
@@ -217,7 +241,13 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
async updateAbilities() {
const api = this.getClient();
const abilities = await api.getAbility();
const apiWithToken = this.getClientWithToken();
let abilities;
try {
abilities = await api.getAbility();
} catch (e) {
abilities = await apiWithToken.getAbility();
}
this.storageSettings.values.abilities = abilities;
this.console.log('getAbility', JSON.stringify(abilities));
}
@@ -286,6 +316,26 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
&& this.storageSettings.values.abilities?.value?.Ability?.supportAudioAlarm?.ver !== 0;
}
hasFloodlight() {
const channel = this.getRtspChannel();
const channelData = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[channel];
if (channelData) {
const floodLightConfigVer = channelData.floodLight?.ver ?? 0;
const supportFLswitchConfigVer = channelData.supportFLswitch?.ver ?? 0;
const supportFLBrightnessConfigVer = channelData.supportFLBrightness?.ver ?? 0;
return floodLightConfigVer > 0 || supportFLswitchConfigVer > 0 || supportFLBrightnessConfigVer > 0;
}
return false;
}
hasBattery() {
const batteryConfigVer = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[this.getRtspChannel()]?.battery?.ver ?? 0;
return batteryConfigVer > 0;
}
async updateDevice() {
const interfaces = this.provider.getInterfaces();
let type = ScryptedDeviceType.Camera;
@@ -309,10 +359,33 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
if (this.storageSettings.values.hasObjectDetector) {
interfaces.push(ScryptedInterface.ObjectDetector);
}
if (this.hasSiren())
if (this.hasSiren() || this.hasFloodlight())
interfaces.push(ScryptedInterface.DeviceProvider);
if (this.hasBattery()) {
interfaces.push(ScryptedInterface.Battery, ScryptedInterface.Online);
this.startBatteryCheckInterval();
}
await this.provider.updateDevice(this.nativeId, name, interfaces, type);
await this.provider.updateDevice(this.nativeId, this.name ?? name, interfaces, type);
}
startBatteryCheckInterval() {
if (this.batteryTimeout) {
clearInterval(this.batteryTimeout);
}
this.batteryTimeout = setInterval(async () => {
const api = this.getClientWithToken();
try {
const { batteryPercent, sleep } = await api.getBatteryInfo();
this.batteryLevel = batteryPercent;
this.online = !sleep;
}
catch (e) {
this.console.log('Error in getting battery info', e);
}
}, 1000 * 60 * 30);
}
async reboot() {
@@ -341,6 +414,12 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
return this.client;
}
getClientWithToken() {
if (!this.clientWithToken)
this.clientWithToken = new ReolinkCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.getRtspChannel(), this.console, true);
return this.clientWithToken;
}
async getOnvifClient() {
if (!this.onvifClient)
this.onvifClient = await this.createOnvifClient();
@@ -567,7 +646,8 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
this.console.error("Codec query failed. Falling back to known defaults.", e);
}
const channel = (this.getRtspChannel() + 1).toString().padStart(2, '0');
const rtspChannel = this.getRtspChannel();
const channel = (rtspChannel + 1).toString().padStart(2, '0');
const streams: UrlMediaStreamOptions[] = [
{
@@ -612,7 +692,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
// 1: support main/extern/sub stream
// 2: support main/sub stream
const live = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[0].live?.ver;
const live = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[rtspChannel].live?.ver;
const [rtmpMain, rtmpExt, rtmpSub, rtspMain, rtspSub] = streams;
streams.splice(0, streams.length);
@@ -621,7 +701,7 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
// 1: main stream enc type is H265
// anecdotally, encoders of type h265 do not have a working RTMP main stream.
const mainEncType = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[0].mainEncType?.ver;
const mainEncType = this.storageSettings.values.abilities?.value?.Ability?.abilityChn?.[rtspChannel].mainEncType?.ver;
if (live === 2) {
if (mainEncType === 1) {
@@ -639,7 +719,14 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
}
if (deviceInfo?.model == "Reolink TrackMix PoE") {
// https://github.com/starkillerOG/reolink_aio/blob/main/reolink_aio/api.py#L93C1-L97C2
// single motion models have 2*2 RTSP channels
if (deviceInfo?.model &&
[
"Reolink TrackMix PoE",
"Reolink TrackMix WiFi",
"RLC-81MA"
].includes(deviceInfo?.model)) {
streams.push({
name: '',
id: 'autotrack.bcs',
@@ -647,14 +734,30 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
video: { width: 896, height: 512 },
url: '',
});
if (rtspChannel === 0) {
streams.push({
name: '',
id: `h264Preview_02_main`,
container: 'rtsp',
video: { codec: 'h264', width: 3840, height: 2160 },
url: ''
}, {
name: '',
id: `h264Preview_02_sub`,
container: 'rtsp',
video: { codec: 'h264', width: 640, height: 480 },
url: ''
})
}
}
for (const stream of streams) {
var streamUrl;
if (stream.container === 'rtmp') {
streamUrl = new URL(`rtmp://${this.getRtmpAddress()}/bcs/channel${this.getRtspChannel()}_${stream.id}`)
streamUrl = new URL(`rtmp://${this.getRtmpAddress()}/bcs/channel${rtspChannel}_${stream.id}`)
const params = streamUrl.searchParams;
params.set("channel", this.getRtspChannel().toString())
params.set("channel", rtspChannel.toString())
params.set("stream", '0')
stream.url = streamUrl.toString();
stream.name = `RTMP ${stream.id}`;
@@ -715,48 +818,83 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
];
}
getOtherSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
async getOtherSettings(): Promise<Setting[]> {
const ret = await super.getOtherSettings();
return [
...await this.storageSettings.getSettings(),
...ret,
];
}
getRtmpAddress() {
return `${this.getIPAddress()}:${this.storage.getItem('rtmpPort') || 1935}`;
}
reportSirenDevice() {
const sirenNativeId = `${this.nativeId}-siren`;
const sirenDevice: Device = {
providerNativeId: this.nativeId,
name: 'Reolink Siren',
nativeId: sirenNativeId,
info: {
...this.info,
},
interfaces: [
ScryptedInterface.OnOff
],
type: ScryptedDeviceType.Siren,
};
async reportDevices() {
const hasSiren = this.hasSiren();
const hasFloodlight = this.hasFloodlight();
const devices: Device[] = [];
if (hasSiren) {
const sirenNativeId = `${this.nativeId}-siren`;
const sirenDevice: Device = {
providerNativeId: this.nativeId,
name: `${this.name} Siren`,
nativeId: sirenNativeId,
info: {
...this.info,
},
interfaces: [
ScryptedInterface.OnOff
],
type: ScryptedDeviceType.Siren,
};
devices.push(sirenDevice);
}
if (hasFloodlight) {
const floodlightNativeId = `${this.nativeId}-floodlight`;
const floodlightDevice: Device = {
providerNativeId: this.nativeId,
name: `${this.name} Floodlight`,
nativeId: floodlightNativeId,
info: {
...this.info,
},
interfaces: [
ScryptedInterface.OnOff
],
type: ScryptedDeviceType.Light,
};
devices.push(floodlightDevice);
}
sdk.deviceManager.onDevicesChanged({
providerNativeId: this.nativeId,
devices: [sirenDevice]
devices
});
return sirenNativeId;
}
async getDevice(nativeId: string): Promise<any> {
if (nativeId.endsWith('-siren')) {
this.siren ||= new ReolinkCameraSiren(this, nativeId);
return this.siren;
} else if (nativeId.endsWith('-floodlight')) {
this.floodlight ||= new ReolinkCameraFloodlight(this, nativeId);
return this.floodlight;
}
}
async releaseDevice(id: string, nativeId: string) {
if (nativeId.endsWith('-siren')) {
delete this.siren;
}
} else
if (nativeId.endsWith('-floodlight')) {
delete this.floodlight;
}
}
}
@@ -790,6 +928,7 @@ class ReolinkProvider extends RtspProvider {
const rtspChannel = parseInt(settings.rtspChannel?.toString()) || 0;
if (!skipValidate) {
const api = new ReolinkCameraClient(httpAddress, username, password, rtspChannel, this.console);
const apiWithToken = new ReolinkCameraClient(httpAddress, username, password, rtspChannel, this.console, true);
try {
await api.jpegSnapshot();
}
@@ -803,7 +942,11 @@ class ReolinkProvider extends RtspProvider {
doorbell = deviceInfo.type === 'BELL';
name = deviceInfo.name ?? 'Reolink Camera';
ai = await api.getAiState();
abilities = await api.getAbility();
try {
abilities = await api.getAbility();
} catch (e) {
abilities = await apiWithToken.getAbility();
}
}
catch (e) {
this.console.error('Reolink camera does not support AI events', e);

View File

@@ -46,25 +46,27 @@ async function getDeviceInfo(host: string, username: string, password: string):
return response.body?.[0]?.value?.DevInfo;
}
export async function getLoginParameters(host: string, username: string, password: string) {
try {
await getDeviceInfo(host, username, password);
return {
parameters: {
user: username,
password,
},
leaseTimeSeconds: Infinity,
export async function getLoginParameters(host: string, username: string, password: string, forceToken?: boolean) {
if (!forceToken) {
try {
await getDeviceInfo(host, username, password);
return {
parameters: {
user: username,
password,
},
leaseTimeSeconds: Infinity,
}
}
catch (e) {
}
}
catch (e) {
}
try {
const url = new URL(`http://${host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'Login');
const response = await httpFetch({
url,
method: 'POST',
@@ -83,7 +85,7 @@ export async function getLoginParameters(host: string, username: string, passwor
},
],
});
const token = response.body?.[0]?.value?.Token?.name || response.body?.value?.Token?.name;
if (!token)
throw new Error('unable to login');

View File

@@ -49,7 +49,7 @@ export class ReolinkCameraClient {
parameters: Record<string, string>;
tokenLease: number;
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console) {
constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console, public readonly forceToken?: boolean) {
this.credential = {
username,
password,
@@ -80,7 +80,7 @@ export class ReolinkCameraClient {
this.console.log(`token expired at ${this.tokenLease}, renewing...`);
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password);
const { parameters, leaseTimeSeconds } = await getLoginParameters(this.host, this.username, this.password, this.forceToken);
this.parameters = parameters
this.tokenLease = Date.now() + 1000 * leaseTimeSeconds;
}
@@ -153,15 +153,37 @@ export class ReolinkCameraClient {
const params = url.searchParams;
params.set('cmd', 'GetAbility');
params.set('channel', this.channelId.toString());
const response = await this.requestWithLogin({
let response = await this.requestWithLogin({
url,
responseType: 'json',
});
const error = response.body?.[0]?.error;
let error = response.body?.[0]?.error;
if (error) {
this.console.error('error during call to getAbility', error);
throw new Error('error during call to getAbility');
this.console.error('error during call to getAbility GET, Trying with POST', error);
url.search = '';
const body = [
{
cmd: "GetAbility",
action: 0,
param: { User: { userName: this.username } }
}
];
response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
error = response.body?.[0]?.error;
if (error) {
this.console.error('error during call to getAbility GET, Trying with POST', error);
throw new Error('error during call to getAbility');
}
}
return {
value: response.body?.[0]?.value || response.body?.value,
data: response.body,
@@ -210,7 +232,46 @@ export class ReolinkCameraClient {
this.console.error('error during call to getDeviceInfo', error);
throw new Error('error during call to getDeviceInfo');
}
return response.body?.[0]?.value?.DevInfo;
const deviceInfo: DevInfo = await response.body?.[0]?.value?.DevInfo;
// Will need to check if it's valid for NVR and NVR_WIFI
if (!['HOMEHUB', 'NVR', 'NVR_WIFI'].includes(deviceInfo.exactType)) {
return deviceInfo;
}
// If the device is listed as homehub, fetch the channel specific information
url.search = '';
const body = [
{ cmd: "GetChnTypeInfo", action: 0, param: { channel: this.channelId } },
{ cmd: "GetChannelstatus", action: 0, param: {} },
]
const additionalInfoResponse = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json'
}, this.createReadable(body));
const chnTypeInfo = additionalInfoResponse?.body?.find(elem => elem.cmd === 'GetChnTypeInfo');
const chnStatus = additionalInfoResponse?.body?.find(elem => elem.cmd === 'GetChannelstatus');
if (chnTypeInfo?.value) {
deviceInfo.firmVer = chnTypeInfo.value.firmVer;
deviceInfo.model = chnTypeInfo.value.typeInfo;
deviceInfo.pakSuffix = chnTypeInfo.value.pakSuffix;
}
if (chnStatus?.value) {
const specificChannelStatus = chnStatus.value?.status?.find(elem => elem.channel === this.channelId);
if (specificChannelStatus) {
deviceInfo.name = specificChannelStatus.name;
}
}
return deviceInfo;
}
async getPtzPresets(): Promise<PtzPreset[]> {
@@ -369,4 +430,69 @@ export class ReolinkCameraClient {
data: response.body,
};
}
async setWhiteLedState(on?: boolean, brightness?: number) {
const url = new URL(`http://${this.host}/api.cgi`);
const settings: any = { channel: this.channelId };
if (on !== undefined) {
settings.state = on ? 1 : 0;
}
if (brightness !== undefined) {
settings.bright = brightness;
}
const body = [{
cmd: 'SetWhiteLed',
param: { WhiteLed: settings }
}];
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
}, this.createReadable(body));
const error = response.body?.[0]?.error;
if (error) {
this.console.error('error during call to setWhiteLedState', JSON.stringify(body), error);
}
}
async getBatteryInfo() {
const url = new URL(`http://${this.host}/api.cgi`);
const body = [
{
cmd: "GetBatteryInfo",
action: 0,
param: { channel: this.channelId }
},
{
cmd: "GetChannelstatus",
}
];
const response = await this.requestWithLogin({
url,
responseType: 'json',
method: 'POST',
}, this.createReadable(body));
const error = response.body?.find(elem => elem.error)?.error;
if (error) {
this.console.error('error during call to getBatteryInfo', error);
}
const batteryInfoEntry = response.body.find(entry => entry.cmd === 'GetBatteryInfo')?.value?.Battery;
const channelStatusEntry = response.body.find(entry => entry.cmd === 'GetChannelstatus')?.value?.status
?.find(chStatus => chStatus.channel === this.channelId)
return {
batteryPercent: batteryInfoEntry?.batteryPercent,
sleep: channelStatusEntry?.sleep === 1,
}
}
}

View File

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

View File

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

View File

@@ -1,24 +1,19 @@
{
// docker installation
// "scrypted.debugHost": "koushik-ubuntu",
// "scrypted.serverRoot": "/server",
"scrypted.debugHost": "koushik-ubuntuvm",
"scrypted.serverRoot": "/server",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// lxc installation
// "scrypted.debugHost": "scrypted-test",
// "scrypted.serverRoot": "/root/.scrypted",
// local checkout
"scrypted.debugHost": "127.0.0.1",
"scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "127.0.0.1",
// "scrypted.serverRoot": "/Users/koush/.scrypted",
// "scrypted.debugHost": "koushik-windows",
// "scrypted.serverRoot": "C:\\Users\\koush\\.scrypted",
"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/tensorflow-lite",
"version": "0.1.65",
"version": "0.1.68",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/tensorflow-lite",
"version": "0.1.65",
"version": "0.1.68",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -48,10 +48,15 @@
"Settings",
"ObjectDetection",
"ObjectDetectionPreview"
]
],
"labels": {
"require": [
"@scrypted/tensorflow-lite"
]
}
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.65"
"version": "0.1.68"
}

View File

@@ -1,23 +1,26 @@
{
"name": "@scrypted/unifi-protect",
"version": "0.0.156",
"version": "0.0.164",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/unifi-protect",
"version": "0.0.156",
"version": "0.0.164",
"license": "Apache",
"dependencies": {
"@koush/unifi-protect": "file:../../external/unifi-protect",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^1.4.0",
"ws": "^8.13.0"
"axios": "^1.7.8",
"unifi-protect": "^4.16.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/node": "^20.4.2",
"@types/ws": "^8.5.5"
"@types/node": "^22.9.4",
"@types/ws": "^8.5.13"
},
"optionalDependencies": {
"@adobe/fetch": "^4.1.9"
}
},
"../../common": {
@@ -26,58 +29,33 @@
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
"typescript": "^5.5.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.31",
"version": "0.3.88",
"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",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"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",
"tmp": "^0.2.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"webpack": "^5.95.0",
"webpack-bundle-analyzer": "^4.10.2"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
@@ -89,19 +67,28 @@
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"@types/node": "^22.8.1",
"@types/stringify-object": "^4.0.5",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
"ts-node": "^10.9.2",
"typedoc": "^0.26.10"
}
},
"../sdk": {
"extraneous": true
},
"node_modules/@koush/unifi-protect": {
"resolved": "../../external/unifi-protect",
"link": 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/@scrypted/common": {
"resolved": "../../common",
@@ -112,15 +99,18 @@
"link": true
},
"node_modules/@types/node": {
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
"dev": true
"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"
}
},
"node_modules/@types/ws": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -132,15 +122,28 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
"dependencies": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.6",
"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",
@@ -152,6 +155,22 @@
"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",
@@ -161,9 +180,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
@@ -192,6 +211,19 @@
"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",
@@ -211,15 +243,55 @@
"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.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"engines": {
"node": ">=10.0.0"
},
@@ -238,68 +310,63 @@
}
},
"dependencies": {
"@koush/unifi-protect": {
"version": "file:../../external/unifi-protect",
"@adobe/fetch": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@adobe/fetch/-/fetch-4.1.9.tgz",
"integrity": "sha512-FWIzm4vvl1OtCarTBgWDW6otj4gxrNmMf/DdriqBUw7DjjmckjT3ZQA43b4HzBkzkuAHhajwMy91btp9fWgTEw==",
"requires": {
"@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"
"debug": "4.3.7",
"http-cache-semantics": "4.1.1",
"lru-cache": "7.18.3"
}
},
"@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.3.3"
"typescript": "^5.5.3"
}
},
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^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",
"@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",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"rimraf": "^6.0.1",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
"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"
}
},
"@types/node": {
"version": "20.4.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
"integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==",
"dev": true
"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"
}
},
"@types/ws": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==",
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
"dev": true,
"requires": {
"@types/node": "*"
@@ -311,15 +378,24 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"version": "1.7.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
"requires": {
"follow-redirects": "^1.15.0",
"follow-redirects": "^1.15.6",
"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",
@@ -328,15 +404,23 @@
"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.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"form-data": {
"version": "4.0.0",
@@ -348,6 +432,16 @@
"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",
@@ -361,15 +455,42 @@
"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.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"requires": {}
}
}

View File

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

View File

@@ -0,0 +1,28 @@
export const MOTION_SENSOR_TIMEOUT = 25000;
export const FINGERPRINT_SENSOR_TIMEOUT = 5000;
export interface UnifiMotionDevice {
motionTimeout: NodeJS.Timeout;
setMotionDetected(motionDetected: boolean): void;
}
export interface UnifiFingerprintDevice {
fingerprintTimeout: NodeJS.Timeout;
setFingerprintDetected(fingerprintDetected: boolean): void;
}
export function debounceMotionDetected(device: UnifiMotionDevice) {
device.setMotionDetected(true);
clearTimeout(device.motionTimeout);
device.motionTimeout = setTimeout(() => {
device.setMotionDetected(false);
}, MOTION_SENSOR_TIMEOUT);
}
export function debounceFingerprintDetected(device: UnifiFingerprintDevice) {
device.setFingerprintDetected(true);
clearTimeout(device.fingerprintTimeout);
device.fingerprintTimeout = setTimeout(() => {
device.setFingerprintDetected(false);
}, FINGERPRINT_SENSOR_TIMEOUT);
}

View File

@@ -1,24 +1,23 @@
import { ffmpegLogInitialOutput, safeKillFFmpeg } from '@scrypted/common/src/media-helpers';
import { readLength } from '@scrypted/common/src/read-stream';
import { fitHeightToWidth } from "@scrypted/common/src/resolution-utils";
import sdk, { Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, Online, PanTiltZoom, PanTiltZoomCommand, PictureOptions, PrivacyMasks, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration, VideoCameraMask } from "@scrypted/sdk";
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamConfiguration, MediaStreamOptions, MediaStreamUrl, MotionSensor, Notifier, NotifierOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, Online, PanTiltZoom, PanTiltZoomCommand, PictureOptions, PrivacyMasks, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, VideoCamera, VideoCameraConfiguration, VideoCameraMask } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { once } from "events";
import { PassThrough, Readable } from "stream";
import { Readable } from "stream";
import WS from 'ws';
import { UnifiProtect } from "./main";
import { MOTION_SENSOR_TIMEOUT, UnifiMotionDevice, debounceMotionDetected } from './motion';
import { MOTION_SENSOR_TIMEOUT, UnifiFingerprintDevice, UnifiMotionDevice, debounceMotionDetected } from './camera-sensors';
import { FeatureFlagsShim, PrivacyZone } from "./shim";
import { ProtectCameraChannelConfig, ProtectCameraConfigInterface, ProtectCameraLcdMessagePayload } from "./unifi-protect";
import { readLength } from '@scrypted/common/src/read-stream';
const { log, deviceManager, mediaManager } = sdk;
const { deviceManager, mediaManager } = sdk;
export const defaultSensorTimeout = 30;
export class UnifiPackageCamera extends ScryptedDeviceBase implements Camera, VideoCamera, MotionSensor {
constructor(public protectCamera: UnifiCamera, nativeId: string) {
super(nativeId);
this.console.log(protectCamera);
}
async takePicture(options?: PictureOptions): Promise<MediaObject> {
const buffer = await this.protectCamera.getSnapshot(options, 'package-snapshot?');
@@ -40,8 +39,13 @@ export class UnifiPackageCamera extends ScryptedDeviceBase implements Camera, Vi
return [options[options.length - 1]];
}
}
export class UnifiFingerprintSensor extends ScryptedDeviceBase implements BinarySensor {
constructor(public protectCamera: UnifiCamera, nativeId: string) {
super(nativeId);
}
}
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff, PanTiltZoom, Online, UnifiMotionDevice, VideoCameraMask {
export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Intercom, Camera, VideoCamera, VideoCameraConfiguration, MotionSensor, Settings, ObjectDetector, DeviceProvider, OnOff, PanTiltZoom, Online, UnifiMotionDevice, VideoCameraMask, UnifiFingerprintDevice {
motionTimeout: NodeJS.Timeout;
detectionTimeout: NodeJS.Timeout;
ringTimeout: NodeJS.Timeout;
@@ -49,6 +53,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
lastSeen: number;
intercomProcess?: ChildProcess;
packageCamera?: UnifiPackageCamera;
fingerprintSensor?: UnifiFingerprintSensor;
fingerprintTimeout: NodeJS.Timeout;
constructor(public protect: UnifiProtect, nativeId: string, protectCamera: Readonly<ProtectCameraConfigInterface>) {
super(nativeId);
@@ -90,16 +96,16 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
});
const camera = this.findCamera() as any;
const camera = this.findCamera();
await this.protect.api.updateCamera(camera, {
await this.protect.api.updateDevice(camera, {
privacyZones,
} as any);
}
async ptzCommand(command: PanTiltZoomCommand): Promise<void> {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
const camera = this.findCamera();
await this.protect.api.updateDevice(camera, {
ispSettings: {
zoomPosition: Math.abs(command.zoom * 100),
}
@@ -107,8 +113,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
async setStatusLight(on: boolean) {
const camera = this.findCamera() as any;
await this.protect.api.updateCamera(camera, {
const camera = this.findCamera();
await this.protect.api.updateDevice(camera, {
ledSettings: {
isEnabled: on,
}
@@ -127,15 +133,33 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
return this.nativeId + '-packageCamera';
}
get fingerprintSensorNativeId() {
return this.nativeId + '-fingerprintSensor';
}
ensurePackageCamera() {
if (!this.packageCamera) {
this.packageCamera = new UnifiPackageCamera(this, this.packageCameraNativeId);
}
}
async getDevice(nativeId: string) {
this.ensurePackageCamera();
return this.packageCamera;
ensureFingerprintSensor() {
if (!this.fingerprintSensor) {
this.fingerprintSensor = new UnifiFingerprintSensor(this, this.fingerprintSensorNativeId);
}
}
async getDevice(nativeId: string) {
if (nativeId === this.packageCameraNativeId) {
this.ensurePackageCamera();
return this.packageCamera;
}
if (nativeId === this.fingerprintSensorNativeId) {
this.ensureFingerprintSensor();
return this.fingerprintSensor;
}
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
@@ -146,8 +170,9 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
const camera = this.findCamera();
const params = new URLSearchParams({ camera: camera.id });
const response = await this.protect.loginFetch(this.protect.api.wsUrl() + "/talkback?" + params.toString());
const endpoint = new URL(this.protect.api.getApiEndpoint("talkback"));
endpoint.searchParams.set('camera', camera.id);
const response = await this.protect.loginFetch(endpoint.toString());
const tb = response.data as Record<string, string>;
// Adjust the URL for our address.
@@ -251,6 +276,8 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
classes.push('ring');
if (this.interfaces.includes(ScryptedInterface.ObjectDetector))
classes.push(...this.findCamera().featureFlags.smartDetectTypes);
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor)
classes.push('fingerprintIdentified');
return {
classes,
};
@@ -349,7 +376,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
findCamera() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.cameras.find(camera => camera.id === id);
return this.protect.api.bootstrap.cameras.find(camera => camera.id === id);
}
async getVideoStream(options?: MediaStreamOptions): Promise<MediaObject> {
const camera = this.findCamera();
@@ -382,7 +409,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
minBitrate: channel.minBitrate,
maxBitrate: channel.maxBitrate,
fps: channel.fps,
idrIntervalMillis: channel.idrInterval * 1000,
keyframeInterval: channel.idrInterval * channel.fps,
},
audio: {
codec: 'aac',
@@ -404,7 +431,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
return vsos;
}
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
async setVideoStreamOptions(options: MediaStreamOptions): Promise<MediaStreamConfiguration> {
const bitrate = options?.video?.bitrate;
if (!bitrate)
return;
@@ -415,7 +442,9 @@ 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.updateCameraChannels(camera);
const cameraResult = await this.protect.api.updateDevice(camera, {
channels: camera.channels,
});
if (!cameraResult) {
throw new Error("setVideoStreamOptions failed")
}
@@ -432,7 +461,7 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
setMotionDetected(motionDetected: boolean) {
this.motionDetected = motionDetected;
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
if (this.findCamera().featureFlags.hasPackageCamera) {
if (deviceManager.getNativeIds().includes(this.packageCameraNativeId)) {
this.ensurePackageCamera();
this.packageCamera.motionDetected = motionDetected;
@@ -440,12 +469,21 @@ export class UnifiCamera extends ScryptedDeviceBase implements Notifier, Interco
}
}
setFingerprintDetected(fingerprintDetected: boolean) {
if ((this.findCamera().featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
if (deviceManager.getNativeIds().includes(this.fingerprintSensorNativeId)) {
this.ensureFingerprintSensor();
this.fingerprintSensor.binaryState = fingerprintDetected;
}
}
}
async sendNotification(title: string, options?: NotifierOptions, media?: MediaObject | string, icon?: MediaObject | string) {
const payload: ProtectCameraLcdMessagePayload = {
text: title.substring(0, 30),
type: 'CUSTOM_MESSAGE',
};
this.protect.api.updateCamera(this.findCamera(), {
this.protect.api.updateDevice(this.findCamera(), {
lcdMessage: payload,
})

View File

@@ -1,6 +1,6 @@
import { Brightness, MotionSensor, OnOff, ScryptedDeviceBase, TemperatureUnit } from "@scrypted/sdk";
import { UnifiProtect } from "./main";
import { UnifiMotionDevice, debounceMotionDetected } from "./motion";
import { UnifiMotionDevice, debounceMotionDetected } from "./camera-sensors";
import { ProtectLightConfig } from "./unifi-protect";
export class UnifiLight extends ScryptedDeviceBase implements OnOff, Brightness, MotionSensor, UnifiMotionDevice {
@@ -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.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
const result = await this.protect.api.updateDevice(this.findLight(), { lightOnSettings: { isLedForceOn: false } });
if (!result)
this.console.error('turnOff failed.');
}
async turnOn(): Promise<void> {
const result = await this.protect.api.updateLight(this.findLight(), { lightOnSettings: { isLedForceOn: true } });
const result = await this.protect.api.updateDevice(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.updateLight(this.findLight(), { lightDeviceSettings: { ledLevel } });
this.protect.api.updateDevice(this.findLight(), { lightDeviceSettings: { ledLevel } });
}
findLight() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.lights.find(light => light.id === id);
return this.protect.api.bootstrap.lights.find(light => light.id === id);
}
updateState(light?: Readonly<ProtectLightConfig>) {

View File

@@ -1,9 +1,8 @@
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: Readonly<ProtectDoorLockConfig>) {
constructor(public protect: UnifiProtect, nativeId: string, protectLock: any) {
super(nativeId);
this.updateState(protectLock);
@@ -11,23 +10,23 @@ export class UnifiLock extends ScryptedDeviceBase implements Lock {
}
async lock(): Promise<void> {
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/close`, {
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/close`, {
method: 'POST',
});
}
async unlock(): Promise<void> {
await this.protect.loginFetch(this.protect.api.doorlocksUrl() + `/${this.findLock().id}/open`, {
await this.protect.loginFetch(this.protect.api.getApiEndpoint('doorlocks') + `/${this.findLock().id}/open`, {
method: 'POST',
});
}
findLock() {
const id = this.protect.findId(this.nativeId);
return this.protect.api.doorlocks.find(doorlock => doorlock.id === id);
return (this.protect.api.bootstrap.doorlocks as any).find(doorlock => doorlock.id === id);
}
updateState(lock?: Readonly<ProtectDoorLockConfig>) {
updateState(lock?: any) {
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 { debounceMotionDetected } from "./motion";
import { UnifiSensor } from "./sensor";
import { FeatureFlagsShim, LastSeenShim } from "./shim";
import { ProtectApi, ProtectApiUpdates, ProtectNvrUpdatePayloadCameraUpdate, ProtectNvrUpdatePayloadEventAdd } from "./unifi-protect";
import { FeatureFlagsShim } from "./shim";
import { ProtectApi, ProtectCameraConfigInterface, ProtectEventAddInterface, ProtectEventPacket } from "./unifi-protect";
const { deviceManager } = sdk;
@@ -54,10 +54,10 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return;
}
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);
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);
if (!device) {
return;
@@ -100,8 +100,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
})
}
listener(event: Buffer) {
const updatePacket = ProtectApiUpdates.decodeUpdatePacket(this.console, event);
listener(updatePacket: ProtectEventPacket) {
if (!updatePacket)
return;
@@ -109,27 +108,27 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
const unifiDevice = this.handleUpdatePacket(updatePacket);
switch (updatePacket.action.modelKey) {
switch (updatePacket.header.modelKey) {
case "sensor":
case "doorlock":
case "light":
case "camera": {
if (!unifiDevice) {
this.console.log('unknown device, sync needed?', updatePacket.action.id);
this.console.log('unknown device, sync needed?', updatePacket.header.id);
return;
}
if (updatePacket.action.action !== "update") {
unifiDevice.console.log('non update', updatePacket.action.action);
if (updatePacket.header.action !== "update") {
unifiDevice.console.log('non update', updatePacket.header.action);
return;
}
unifiDevice.updateState();
if (updatePacket.action.modelKey === "doorlock")
if (updatePacket.header.modelKey === "doorlock")
return;
const payload = updatePacket.payload as any as ProtectNvrUpdatePayloadCameraUpdate & LastSeenShim;
const payload = updatePacket.payload as ProtectCameraConfigInterface;
if (updatePacket.action.modelKey !== "camera")
if (updatePacket.header.modelKey !== "camera")
return;
const unifiCamera = unifiDevice as UnifiCamera;
@@ -142,19 +141,19 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
break;
}
case "event": {
if (updatePacket.action.action !== "add") {
if ((updatePacket?.payload as any)?.end && updatePacket.action.id) {
const payload = updatePacket.payload as ProtectEventAddInterface;
if (updatePacket.header.action !== "add") {
if (payload.end && updatePacket.header.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.action.id);
const running = this.runningEvents.get(updatePacket.header.id);
running?.resolve?.(undefined)
}, 2000);
}
return;
}
const payload = updatePacket.payload as ProtectNvrUpdatePayloadEventAdd;
if (!payload.camera)
return;
const nativeId = this.getNativeId({ id: payload.camera }, false);
@@ -166,7 +165,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
const detectionId = payload.id;
const actionId = updatePacket.action.id;
const actionId = updatePacket.header.id;
let resolve: (value: unknown) => void;
const promise = new Promise(r => resolve = r);
@@ -220,6 +219,15 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
else if (payload.type === 'motion') {
debounceMotionDetected(unifiCamera);
}
else if (payload.type === 'fingerprintIdentified') {
const anypay = payload as any;
const userId: string = anypay.metadata?.fingerprint?.userId || anypay.metadata?.fingerprint?.ulpId;
if (userId) {
debounceFingerprintDetected(unifiCamera);
detections[0].label = userId;
detections[0].labelScore = 1;
}
}
}
const detection: ObjectsDetected = {
@@ -240,7 +248,26 @@ 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');
@@ -262,10 +289,8 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return
}
this.api?.eventsWs?.removeAllListeners();
this.api?.eventsWs?.close();
if (!this.api) {
this.api = new ProtectApi(ip, username, password, {
this.api = new ProtectApi({
debug() { },
error: (...args) => {
this.console.error(...args);
@@ -275,48 +300,37 @@ 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 {
if (!await this.api.refreshDevices()) {
reconnect('refresh failed')();
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')();
return;
}
let wsTimeout: NodeJS.Timeout;
const resetWsTimeout = () => {
clearTimeout(wsTimeout);
wsTimeout = setTimeout(reconnect('timeout'), 5 * 60 * 1000);
clearTimeout(this.wsTimeout);
this.wsTimeout = setTimeout(() => this.reconnect('timeout'), 5 * 60 * 1000);
};
resetWsTimeout();
this.api.eventsWs?.on('message', (data) => {
this.api.on('message', message => {
resetWsTimeout();
this.listener(data as Buffer);
});
this.api.eventsWs?.on('close', reconnect('close'));
this.api.eventsWs?.on('error', reconnect('error'));
this.listener(message);
})
const devices: Device[] = [];
for (let camera of this.api.cameras || []) {
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 || []) {
if (camera.isAdoptedByOther) {
this.console.log('skipping camera that is adopted by another nvr', camera.id, camera.name);
continue;
@@ -338,7 +352,9 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
if (needUpdate) {
camera = await this.api.updateCameraChannels(camera);
camera = await this.api.updateDevice(camera, {
channels: camera.channels,
});
if (!camera) {
this.log.a('Unable to enable RTSP and IDR interval on camera. Is this an admin account?');
continue;
@@ -383,7 +399,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
if (camera.featureFlags.hasLcdScreen) {
d.interfaces.push(ScryptedInterface.Notifier);
}
if ((camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera) {
if (camera.featureFlags.hasPackageCamera) {
d.interfaces.push(ScryptedInterface.DeviceProvider);
}
if (camera.featureFlags.hasLedStatus) {
@@ -396,7 +412,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const sensor of this.api.sensors || []) {
for (const sensor of this.api.bootstrap.sensors || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: sensor.name,
@@ -404,7 +420,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
info: {
manufacturer: 'Ubiquiti',
model: sensor.type,
ip: sensor.host,
ip: (sensor as any).host,
firmware: sensor.firmwareVersion,
version: sensor.hardwareRevision,
serialNumber: sensor.id,
@@ -424,7 +440,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const light of this.api.lights || []) {
for (const light of this.api.bootstrap.lights || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: light.name,
@@ -449,7 +465,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
devices.push(d);
}
for (const lock of this.api.doorlocks || []) {
for (const lock of (this.api.bootstrap.doorlocks as any) || []) {
const d: Device = {
providerNativeId: this.nativeId,
name: lock.name,
@@ -481,32 +497,61 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
// handle package cameras as a sub device
for (const camera of this.api.cameras) {
if (!(camera.featureFlags as any as FeatureFlagsShim).hasPackageCamera)
for (const camera of this.api.bootstrap.cameras) {
const devices: Device[] = [];
const providerNativeId = this.getNativeId(camera, true);
if (camera.featureFlags.hasPackageCamera) {
const nativeId = providerNativeId + '-packageCamera';
const d: Device = {
providerNativeId,
name: camera.name + ' Package Camera',
nativeId,
info: {
manufacturer: 'Ubiquiti',
model: camera.type,
firmware: camera.firmwareVersion,
version: camera.hardwareRevision,
serialNumber: camera.id,
},
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.VideoCamera,
ScryptedInterface.MotionSensor,
],
type: ScryptedDeviceType.Camera,
};
devices.push(d);
}
if ((camera.featureFlags as any as FeatureFlagsShim).hasFingerprintSensor) {
const nativeId = providerNativeId + '-fingerprintSensor';
const d: Device = {
providerNativeId,
name: camera.name + ' Fingerprint Sensor',
nativeId,
info: {
manufacturer: 'Ubiquiti',
model: camera.type,
firmware: camera.firmwareVersion,
version: camera.hardwareRevision,
serialNumber: camera.id,
},
interfaces: [
ScryptedInterface.BinarySensor,
],
type: ScryptedDeviceType.Sensor,
};
devices.push(d);
}
if (!devices.length)
continue;
const nativeId = camera.id + '-packageCamera';
const d: Device = {
providerNativeId: this.getNativeId(camera, true),
name: camera.name + ' Package Camera',
nativeId,
info: {
manufacturer: 'Ubiquiti',
model: camera.type,
firmware: camera.firmwareVersion,
version: camera.hardwareRevision,
serialNumber: camera.id,
},
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.VideoCamera,
ScryptedInterface.MotionSensor,
],
type: ScryptedDeviceType.Camera,
};
await deviceManager.onDevicesChanged({
providerNativeId: this.getNativeId(camera, true),
devices: [d],
devices,
});
}
}
@@ -531,25 +576,25 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
return this.locks.get(nativeId);
const id = this.findId(nativeId);
const camera = this.api.cameras.find(camera => camera.id === id);
const camera = this.api.bootstrap.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.sensors.find(sensor => sensor.id === id);
const sensor = this.api.bootstrap.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.lights.find(light => light.id === id);
const light = this.api.bootstrap.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.doorlocks?.find(lock => lock.id === id);
const lock = (this.api.bootstrap.doorlocks as any)?.find(lock => lock.id === id);
if (lock) {
const ret = new UnifiLock(this, nativeId, lock);
this.locks.set(nativeId, ret);
@@ -613,7 +658,7 @@ export class UnifiProtect extends ScryptedDeviceBase implements Settings, Device
}
getNativeId(device: { id?: string, mac?: string; anonymousDeviceId?: string, host?: string }, update: boolean) {
const { id, mac, anonymousDeviceId,host } = device;
const { id, mac, anonymousDeviceId, host } = device;
const idMaps = this.storageSettings.values.idMaps;
// try to find an existing nativeId given the mac and anonymous device id

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