Compare commits

...

462 Commits

Author SHA1 Message Date
Koushik Dutta
e33cacd15d postbeta 2024-09-06 10:54:31 -07:00
Koushik Dutta
e225b746c4 server: update deps 2024-09-06 10:54:24 -07:00
Koushik Dutta
1e1fda6b9a server: findPluginDevice is not optional 2024-09-06 10:50:06 -07:00
Koushik Dutta
e23c9c22dc onnx: fix platform check 2024-09-05 10:34:22 -07:00
Koushik Dutta
28f1ce9d4e postbeta 2024-09-04 12:19:33 -07:00
Koushik Dutta
5f4e2793ff server: possibly fix bug where rpc object may not be found 2024-09-04 10:36:56 -07:00
Koushik Dutta
1e1755fa7e server: cluster cleanups 2024-09-04 10:17:30 -07:00
Koushik Dutta
ebd56b86e4 postbeta 2024-09-04 09:30:27 -07:00
Koushik Dutta
4607bec07c server: improve fork/thread logging 2024-09-04 09:30:19 -07:00
Koushik Dutta
a26cdfea2a postbeta 2024-09-03 22:45:04 -07:00
Koushik Dutta
6d0da449ad server: simplify convoluted peer key 2024-09-03 22:31:51 -07:00
Koushik Dutta
ad15fe3324 cloud: update deps 2024-09-03 17:20:39 -07:00
Koushik Dutta
fbe3e83884 cameras: move autoconfiguration button so its less accidentally clickable 2024-09-03 16:01:11 -07:00
Koushik Dutta
0d4cf34930 reolink: move some device creation stuff into advanced tab 2024-09-03 15:54:36 -07:00
Koushik Dutta
3a3e15cd74 server: allow main/child thread ipc 2024-09-03 15:26:21 -07:00
Koushik Dutta
8b9cfebbfa cloud: handle mux carrier errors 2024-09-03 08:43:06 -07:00
Koushik Dutta
b3087058a7 google-home/alexa: republish with new sdk for media converter 2024-09-02 08:40:03 -07:00
Koushik Dutta
4e923a78da cloud: fix short lived regression 2024-09-01 20:42:32 -07:00
Koushik Dutta
b5593d6251 cloud: cleanups 2024-09-01 20:14:47 -07:00
Koushik Dutta
40b7b621a0 openvino: update 2024-09-01 08:17:54 -07:00
Koushik Dutta
7104ad6378 openvino: improve device selection 2024-08-29 21:15:19 -07:00
Koushik Dutta
51f893ef63 install: update proxmox and ha 2024-08-29 20:58:54 -07:00
Koushik Dutta
29c5dfd73b core: install npu drivers in lxc 2024-08-29 20:37:42 -07:00
Koushik Dutta
1615f79a0b install: npu driver update 2024-08-29 20:32:35 -07:00
Koushik Dutta
dc5a6126b9 sdk: add id to recorded event on notifications 2024-08-29 08:02:33 -07:00
Koushik Dutta
0fbe3e4686 cloud: upnp no longer default 2024-08-28 13:23:52 -07:00
Koushik Dutta
c9641568f8 core: publish 2024-08-27 21:54:36 -07:00
Koushik Dutta
dceea38eb8 core: publish 2024-08-27 21:31:22 -07:00
Koushik Dutta
cd1fce71e2 postrelease 2024-08-27 21:15:09 -07:00
Koushik Dutta
3b6454f107 cli: add support for version launch hint 2024-08-27 18:30:33 -07:00
Koushik Dutta
d238d8d4ba onnx: fix broken arm64 2024-08-27 11:16:16 -07:00
Koushik Dutta
a24d46f3d2 postbeta 2024-08-27 09:36:56 -07:00
Koushik Dutta
df7deef4aa server/sdk: forks can be associated with specific device/mixin 2024-08-27 09:36:47 -07:00
Koushik Dutta
e94cea0236 Revert "server: revert port contention change"
This reverts commit 57439634e5.
2024-08-27 08:58:22 -07:00
Koushik Dutta
4794a6dbf3 postbeta 2024-08-27 08:50:28 -07:00
Koushik Dutta
57439634e5 server: revert port contention change 2024-08-27 08:50:19 -07:00
Koushik Dutta
89e6e50b12 rebroadcast: reduce logging 2024-08-26 23:04:51 -07:00
Koushik Dutta
c21ef069bd Merge branch 'main' of github.com:koush/scrypted 2024-08-26 14:18:15 -07:00
Koushik Dutta
5d41bb38da postbeta 2024-08-26 14:18:11 -07:00
Koushik Dutta
b289024083 server: fix fork debugging port contention 2024-08-26 14:18:04 -07:00
Brett Jia
56572bcec9 onnx: fix install on linux arm64 (#1564) 2024-08-26 09:29:51 -07:00
Koushik Dutta
3e78209817 postbeta 2024-08-26 08:55:27 -07:00
Koushik Dutta
299204313f server: use constants for plugin markers 2024-08-26 08:55:19 -07:00
Koushik Dutta
88b134f4b9 postbeta 2024-08-26 08:35:02 -07:00
Koushik Dutta
e8bd72a329 server: fix child process flag markers 2024-08-26 08:34:54 -07:00
Koushik Dutta
214dbc8153 postbeta 2024-08-26 08:12:33 -07:00
Koushik Dutta
3f0c706154 server: improve process markers 2024-08-26 08:12:26 -07:00
Koushik Dutta
013131e816 postbeta 2024-08-25 12:36:57 -07:00
Koushik Dutta
0342bf91f6 server: add rimraf 2024-08-25 12:36:46 -07:00
Koushik Dutta
6fb98c7e84 server: remove legacy deps 2024-08-25 12:36:28 -07:00
Koushik Dutta
f4168ff4eb postbeta 2024-08-25 09:58:02 -07:00
Koushik Dutta
a5febd7ca0 server: update deps 2024-08-25 09:57:55 -07:00
Koushik Dutta
31428b4c28 postbeta 2024-08-24 18:21:21 -07:00
Koushik Dutta
3e0dfc6bda install: remove electron 2024-08-24 18:12:44 -07:00
Koushik Dutta
4290eb0abb server: remove electron 2024-08-24 18:12:00 -07:00
Koushik Dutta
fd2d7e9485 server: electron angle setup 2024-08-24 18:10:05 -07:00
Koushik Dutta
a57cf3b1e6 sdk: improve multi entry build 2024-08-24 18:09:50 -07:00
Koushik Dutta
fad485c0d7 Merge branch 'main' of github.com:koush/scrypted 2024-08-23 08:22:41 -07:00
Koushik Dutta
9e3cf83b07 onnx: fix broken onnxruntime-gpu cudnn9 2024-08-23 08:22:35 -07:00
Koushik Dutta
ebe0b6ea7f install: remove intel stuff from lite builds 2024-08-22 16:22:17 -07:00
Koushik Dutta
4ce0ecaaa2 onvif: fix autoconfig with no audio 2024-08-22 12:33:42 -07:00
Koushik Dutta
44264fb50b onnx: make cuda available to windows 2024-08-22 10:27:12 -07:00
Koushik Dutta
ab188bfe80 postbeta 2024-08-21 14:34:05 -07:00
Koushik Dutta
83d32da7f1 server: improve electron enable-features handling 2024-08-21 14:33:57 -07:00
Koushik Dutta
e68b3f401f postbeta 2024-08-21 14:02:44 -07:00
Koushik Dutta
43a73c6d89 server: fix electron bin in npm package 2024-08-21 14:02:12 -07:00
Koushik Dutta
6c6613d841 server: fixup electron bin dir path per platform/version 2024-08-21 14:00:57 -07:00
Koushik Dutta
479ef136a6 postbeta 2024-08-21 13:41:41 -07:00
Koushik Dutta
b42abf377b server: use @electron/get as production dependency 2024-08-21 13:41:05 -07:00
Koushik Dutta
5713935ccc server: break out electron runtimes 2024-08-21 12:51:23 -07:00
Koushik Dutta
09fc609c7f postbeta 2024-08-21 12:22:36 -07:00
Koushik Dutta
e44ba222b8 server: fix convertMediaObjectToJSON back compat 2024-08-21 12:18:30 -07:00
Koushik Dutta
e34a5a7c3d snapshot: fix perma broken nvr snapshots 2024-08-21 12:03:29 -07:00
Koushik Dutta
e490225c4a webrtc: simplify convert paths 2024-08-21 11:50:25 -07:00
Koushik Dutta
e769e8ea98 sdk: remove unnecessary param in debug request 2024-08-21 11:50:00 -07:00
Koushik Dutta
ec5c164552 webrtc: fix type erasure 2024-08-21 11:31:54 -07:00
Koushik Dutta
64a213424d postbeta 2024-08-21 11:10:53 -07:00
Koushik Dutta
e81c454c1e server: fix node thread worker removing handlers that should not be 2024-08-21 11:10:21 -07:00
Koushik Dutta
776307bcbc server: enable SharedArrayBuffer and other stuff for electron 2024-08-21 11:09:31 -07:00
Koushik Dutta
95c97e3eb2 server: fix plugin load causing server crash, fix unhandled rejections caused by plugin kill 2024-08-21 10:59:27 -07:00
Koushik Dutta
08926a35a9 server: use standard debug port for electron 2024-08-21 10:43:51 -07:00
Koushik Dutta
c037548ffb postbeta 2024-08-21 09:52:45 -07:00
Koushik Dutta
462189efc2 server: implement electron debugging 2024-08-21 09:50:22 -07:00
Koushik Dutta
a4fed9c7ad postbeta 2024-08-20 23:20:13 -07:00
Koushik Dutta
3c8f94ab2f server: Fix electron display var 2024-08-20 23:20:04 -07:00
Koushik Dutta
fe3391c89c docker: fix xvfb dep 2024-08-20 22:51:24 -07:00
Koushik Dutta
270b43bed6 postbeta 2024-08-20 22:23:07 -07:00
Koushik Dutta
c0fe12827f server: hide electron window 2024-08-20 22:22:59 -07:00
Koushik Dutta
3bbda53107 postbeta 2024-08-20 22:21:23 -07:00
Koushik Dutta
0d7ee00485 server: electron wip 2024-08-20 22:21:09 -07:00
Koushik Dutta
c605c3ddb5 server: update deps 2024-08-20 20:50:02 -07:00
Koushik Dutta
099ba4f081 server: organize 2024-08-20 20:49:48 -07:00
Koushik Dutta
c14487ac27 server: remove deno 2024-08-20 20:49:23 -07:00
Koushik Dutta
991c31dda8 install: electron deps 2024-08-20 20:35:26 -07:00
Koushik Dutta
3865efd1f7 postbeta 2024-08-19 17:54:18 -07:00
Koushik Dutta
9c5ce45c1e server: catch unhandled iterator teardown throwups 2024-08-19 17:54:08 -07:00
Koushik Dutta
4ab7bc1298 postbeta 2024-08-19 16:04:47 -07:00
Koushik Dutta
0ebbc5ea8f server: sketchy check to determine if buffer is pool buffer 2024-08-19 16:04:38 -07:00
Koushik Dutta
86dcb66e6a postbeta 2024-08-19 15:51:34 -07:00
Koushik Dutta
11831e5d87 server: rpc should handle transport serialization failures and attempt to send plain objects with the error 2024-08-19 15:50:32 -07:00
Koushik Dutta
6b3dc8c1ae postbeta 2024-08-19 13:24:42 -07:00
Koushik Dutta
a49256f073 server: already connected ipc object fast path 2024-08-19 13:24:35 -07:00
Koushik Dutta
ea408377ec postbeta 2024-08-19 13:20:28 -07:00
Koushik Dutta
2da762dfc2 server: working transferible buffers 2024-08-19 13:20:21 -07:00
Koushik Dutta
bfbc6ba6ce rebroadcast: fix build 2024-08-19 12:02:45 -07:00
Koushik Dutta
c2747c80dc server: consolidate peer thread setup 2024-08-19 11:43:10 -07:00
Koushik Dutta
2b58de196e server: use ipc for node threads 2024-08-19 11:07:13 -07:00
Koushik Dutta
731744afbc server: warn on base64 serialization 2024-08-19 09:47:51 -07:00
Koushik Dutta
f4d88493b1 postbeta 2024-08-18 12:47:35 -07:00
Koushik Dutta
265fc4b481 Merge branch 'main' of github.com:koush/scrypted 2024-08-18 12:47:06 -07:00
Koushik Dutta
40a300cff1 server: update console logging location to accomodate vscode filters 2024-08-18 12:47:00 -07:00
Brett Jia
c410907c58 webrtc: remove missing var (#1562) 2024-08-17 23:21:16 -07:00
Brett Jia
199f333fc1 server: allow all application/* mime types (#1561) 2024-08-17 23:21:04 -07:00
Brett Jia
e39bc8c5e6 core: terminal paths via TTYSettings + add TTY interface (#1559) 2024-08-17 16:39:00 -07:00
Brett Jia
a55099de12 webrtc: convert RTCSignalingChannel to FFmpegInput + consolidate all converters (#1558)
* webrtc: add converter from RTCSignalingChannel to FFmpegInput

* consolidate buffer converters to WebRTCBridge as MediaConverter

* consolidate all converters into base plugin device

* remove unused
2024-08-17 10:31:15 -07:00
Koushik Dutta
17900f0589 sdk: fix excessive zipping 2024-08-16 20:32:08 -07:00
Koushik Dutta
075d8bc4ab snapshot: log prebuffer unavailable error 2024-08-16 19:25:41 -07:00
Koushik Dutta
28fce1bb8b Merge branch 'main' of github.com:koush/scrypted 2024-08-16 08:57:22 -07:00
Koushik Dutta
14a2790825 sdk: parallelize build 2024-08-16 08:57:18 -07:00
Roman Sokolov
c2cd491c96 Hikvision Doorbell (#1557)
* Fixed reconnection bugs

* Updated documentation

* hikvision-doorbell version up

* Added support for contact sensor (if it installed on door)

* Refactoring doorbell api

* Fixed some bugs in hikvision doorbell
2024-08-15 15:24:46 -07:00
Koushik Dutta
6301089620 postbeta 2024-08-14 20:28:40 -07:00
Koushik Dutta
c591a87015 server: scope deno into module package 2024-08-14 20:28:33 -07:00
Koushik Dutta
544700ac12 postbeta 2024-08-14 20:22:17 -07:00
Koushik Dutta
2c54dc07ae server: use js for deno-plugin-remote 2024-08-14 20:22:11 -07:00
Koushik Dutta
0dad6eaa76 postbeta 2024-08-14 19:33:11 -07:00
Koushik Dutta
d18b8e0694 server: fix deno stdout 2024-08-14 19:33:03 -07:00
Koushik Dutta
a4130ed047 coreml: update project files 2024-08-14 19:31:52 -07:00
Koushik Dutta
1f89dcb34c common: fix zygote signature 2024-08-14 19:31:31 -07:00
Koushik Dutta
e86ed47533 sdk: fix node import 2024-08-14 13:21:14 -07:00
Koushik Dutta
5fb75e351d postbeta 2024-08-14 12:11:32 -07:00
Koushik Dutta
8ef3fe7a24 server: add support for cross language fork 2024-08-14 11:14:13 -07:00
Koushik Dutta
5c8f034d7c postbeta 2024-08-14 08:43:02 -07:00
Koushik Dutta
561852bc15 server: fixup python remote debugging, fs chdir locations 2024-08-14 08:42:55 -07:00
Koushik Dutta
d14c592d55 vscode-python: proper remote debug fix 2024-08-14 08:33:30 -07:00
Koushik Dutta
bc439d6b7c postbeta 2024-08-14 08:22:55 -07:00
Koushik Dutta
aedb4212fe postbeta 2024-08-14 08:16:43 -07:00
Koushik Dutta
7b780a0eb9 postbeta 2024-08-13 23:21:12 -07:00
Koushik Dutta
6aaaccaece server: revert back to original change that conditionally added unzipped path 2024-08-13 23:21:05 -07:00
Koushik Dutta
0e42b71e4b server: fixup python requirements calc 2024-08-13 23:14:26 -07:00
Koushik Dutta
71a6f9d6a6 vscode-python: revert 2024-08-13 23:03:54 -07:00
Koushik Dutta
68c77aaca4 postbeta 2024-08-13 22:51:41 -07:00
Koushik Dutta
4b73c168b3 vscode-python: update sample with normalized paths 2024-08-13 22:51:18 -07:00
Brett Jia
8f4b67dc5c server: add unzipped path to python module lookup path (#1553)
* server: add unzipped path to python module lookup path

* remove zipPath from sys.path
2024-08-13 22:46:57 -07:00
Koushik Dutta
8a8bee33c1 server: normalize fs path 2024-08-13 22:45:51 -07:00
Koushik Dutta
2b353cf4c8 Merge branch 'main' of github.com:koush/scrypted 2024-08-13 20:13:11 -07:00
Koushik Dutta
6d9cd45936 rebroadcast: add global audio soft mute 2024-08-13 20:13:03 -07:00
Brett Jia
0bc6fadb23 server: allow plugins to customize python installs (#1542)
* server: allow plugins to customize python installs

* move options inside pythonVersion
2024-08-13 18:18:39 -07:00
Koushik Dutta
407f573a29 server: better ipv6 filtering 2024-08-13 12:03:24 -07:00
Koushik Dutta
39674ef9b6 server: fix backup 2024-08-13 10:58:11 -07:00
Koushik Dutta
ffaed01dc3 server: update types 2024-08-13 10:40:49 -07:00
Koushik Dutta
9a144a9a05 postbeta 2024-08-13 10:39:47 -07:00
Koushik Dutta
76e63120f0 server: remove ip dependency for custom implementation 2024-08-13 10:39:37 -07:00
Koushik Dutta
154bc6fef7 postbeta 2024-08-13 10:38:56 -07:00
Koushik Dutta
8302c564c2 cameras: fix autoconfigure math 2024-08-13 09:36:54 -07:00
Koushik Dutta
affda9e94f server: fix anonymous auth + backup/deploy 2024-08-13 09:07:42 -07:00
Koushik Dutta
30148f453c postbeta 2024-08-13 09:07:22 -07:00
Koushik Dutta
259313c454 tensorflow-lite: default model 2024-08-12 21:18:59 -07:00
Koushik Dutta
4f82d49f15 tflite: yolov10 2024-08-12 20:51:06 -07:00
Koushik Dutta
a0b7fc54de cameras: autoconfiguration menu fixes 2024-08-12 17:59:00 -07:00
Koushik Dutta
5501093ff9 core: publish 2024-08-12 12:29:17 -07:00
Koushik Dutta
b2622d92f2 cameras: fix auto config error reporting 2024-08-12 12:26:02 -07:00
Koushik Dutta
957bf742d9 hikvision: auto configure should handle model without audio 2024-08-12 07:50:09 -07:00
Koushik Dutta
b06a3ac55f cameras: dont run configuration in parallel due to race conditions in camera side 2024-08-11 22:20:59 -07:00
Koushik Dutta
9c195594ea webrtc: betas 2024-08-11 21:54:30 -07:00
Koushik Dutta
d32efd6500 amcrest: ad410 autoconfigure fixes 2024-08-11 19:31:01 -07:00
Koushik Dutta
03f1957739 hikvision: ignore vca resource failure 2024-08-11 18:59:59 -07:00
Koushik Dutta
94f564a218 onvif: fix tapo 2024-08-11 18:20:38 -07:00
Koushik Dutta
286aa61a18 hikvision: autoconfigure should disable vca resource 2024-08-11 17:58:44 -07:00
Koushik Dutta
43d30a91f9 cameras: publish autoconfiguration 2024-08-11 17:34:18 -07:00
Koushik Dutta
7be81ab7e2 cameras: more configuration cleanups 2024-08-11 17:31:48 -07:00
Koushik Dutta
31c7c9d86a hikvision: fixup fps calc 2024-08-11 14:26:57 -07:00
Koushik Dutta
820c66311d cameras: autoconfiguration notes 2024-08-11 13:59:27 -07:00
Koushik Dutta
84cbe28a47 Merge branch 'main' of github.com:koush/scrypted 2024-08-11 13:54:40 -07:00
Koushik Dutta
cad1cdd5ce core: publish pending ui changes 2024-08-11 13:54:36 -07:00
Koushik Dutta
2a624a95e5 server: import type for acl for peer dependencies 2024-08-11 13:11:07 -07:00
Koushik Dutta
c6d8333402 reolink: add onvif two way option 2024-08-11 12:25:27 -07:00
Koushik Dutta
96dbb1776c Merge branch 'main' of github.com:koush/scrypted 2024-08-11 11:40:12 -07:00
Koushik Dutta
b43a002650 rebroadcast: fix content base handling when it is a relative path 2024-08-11 11:40:07 -07:00
Koushik Dutta
a58d66d484 docker: use noatime for nvr mounts 2024-08-11 07:59:51 -07:00
Koushik Dutta
bcc9be62e9 reolink: update stream config with docs info and publish 2024-08-11 00:58:34 -07:00
Koushik Dutta
38dd9e2ee2 amcrest: motion reset/setup 2024-08-10 23:56:21 -07:00
Koushik Dutta
3033cd9626 cameras: add autoconfiguration alert 2024-08-10 23:10:05 -07:00
Koushik Dutta
12273b7d1e reolink: fix buikd 2024-08-10 22:46:32 -07:00
Koushik Dutta
afe1421c39 cameras: remove unnecessary codec calls 2024-08-10 22:45:00 -07:00
Koushik Dutta
a9fdb71402 onvif: audio autoconfiguration 2024-08-10 20:26:42 -07:00
Koushik Dutta
9cac4cfd18 hikvision: audio codec configuration 2024-08-10 18:41:20 -07:00
Koushik Dutta
fbaf9b97aa amcrest: auto configure audio 2024-08-10 17:58:56 -07:00
Koushik Dutta
0a55732919 server: fix basic auth handling for demo site 2024-08-10 17:37:30 -07:00
Koushik Dutta
ece0ecbd8f postbeta 2024-08-10 17:37:21 -07:00
Koushik Dutta
d2743465c3 Merge branch 'main' of github.com:koush/scrypted 2024-08-10 15:19:02 -07:00
Koushik Dutta
e41af930c5 server: fix basic auth handling for demo site 2024-08-10 15:18:59 -07:00
Koushik Dutta
582b5182e6 postbeta 2024-08-10 15:18:50 -07:00
Koushik Dutta
2b85aa0f27 intel: fix derp 2024-08-10 14:42:59 -07:00
Koushik Dutta
e74e0b7e21 common: missing file for mp3 handling 2024-08-10 14:17:51 -07:00
Koushik Dutta
70d6813938 rebroadcast/webrtc: fix mp3 handling 2024-08-10 14:17:07 -07:00
Koushik Dutta
c1d84d6e08 amcrest: wip auto configure 2024-08-10 11:06:12 -07:00
Koushik Dutta
5c69c70013 amcrest: wip auto configure 2024-08-10 10:36:26 -07:00
Koushik Dutta
6379aa89ef hikvision: wip autoconfigure 2024-08-09 21:39:43 -07:00
Koushik Dutta
c2756a3a4a hikvision: wip codec configuration 2024-08-09 13:32:22 -07:00
Koushik Dutta
303ced735a cameras: wip codec configuration 2024-08-09 13:15:07 -07:00
Koushik Dutta
bc70803cc0 cameras: wip codec configuration 2024-08-09 10:35:09 -07:00
Koushik Dutta
171b04f267 sdk: Update video stream configuration return type 2024-08-09 09:58:11 -07:00
Koushik Dutta
1cd5c194cc cameras: wip codec configuration 2024-08-08 23:06:50 -07:00
Koushik Dutta
c15fe4281e common: fix auth http body 2024-08-08 21:56:58 -07:00
Koushik Dutta
1bd7f37041 cloud: update push library 2024-08-08 21:56:43 -07:00
Koushik Dutta
170bc5f6ad Merge branch 'main' of github.com:koush/scrypted 2024-08-08 21:30:27 -07:00
Koushik Dutta
8daf74e6db cloud: fix cloudflare unregister 2024-08-08 21:30:22 -07:00
Koushik Dutta
b84adf514e docker: fix hw decode 2024-08-08 17:35:40 -07:00
Roman Sokolov
947aa151a5 Fixed some bugs (#1547)
* Fixed reconnection bugs

* Updated documentation

* hikvision-doorbell version up

---------

Co-authored-by: Roman Sokolov <calm@adguard.com>
2024-08-08 09:24:06 -07:00
Koushik Dutta
12c734fe1b cloud: update cloudflared 2024-08-06 20:44:41 -07:00
Koushik Dutta
724b9332f4 Merge branch 'main' of github.com:koush/scrypted 2024-08-06 20:20:59 -07:00
Koushik Dutta
a1f90607af webrtc: publish with werift race fixes 2024-08-06 20:20:54 -07:00
Koushik Dutta
1a954cc232 deferred: publish 2024-08-06 18:11:29 -07:00
Brett Jia
22cb23a075 rknn: show model name + clean up detection classes (#1545) 2024-08-06 14:13:08 -07:00
Koushik Dutta
608f82cf81 detect: proper model name 2024-08-06 13:33:22 -07:00
Koushik Dutta
2f4608e697 Merge branch 'main' of github.com:koush/scrypted 2024-08-06 10:21:10 -07:00
Koushik Dutta
c08ce3115a webrtc: fix non-trickle negotiation 2024-08-06 10:21:02 -07:00
Koushik Dutta
470c28d6ef homekit: reduce debug hksv duration because people keep filling their disks. 2024-08-06 08:25:44 -07:00
David Montgomery
7ea1d8e85d fix typo (#1544) 2024-08-06 07:08:32 -07:00
Koushik Dutta
1669438312 webrtc: publish 2024-08-05 18:48:25 -07:00
Koushik Dutta
d4b77cac66 proxmox: wip restore rootfs 2024-08-05 18:48:22 -07:00
Koushik Dutta
f1bebd0612 proxmox: wip reset/restore preserving data 2024-08-05 00:33:54 -07:00
Koushik Dutta
003e1f79f0 Revert "proxmox: fix --force hint"
This reverts commit 35cf9f9352.
2024-08-04 23:41:23 -07:00
Koushik Dutta
97702da9ef Merge branch 'main' of github.com:koush/scrypted 2024-08-04 23:39:49 -07:00
Koushik Dutta
35cf9f9352 proxmox: fix --force hint 2024-08-04 23:39:45 -07:00
Koushik Dutta
5225823e8b webrtc: publish 2024-08-04 22:40:46 -07:00
Brett Jia
2569e7c823 sdk: add TTYSettings (#1540)
* sdk: add TerminalSettings

* update to TTYSettings
2024-08-04 21:01:34 -07:00
Koushik Dutta
aa5c4d5064 webrtc: update deps, update zygote invocation. 2024-08-04 21:00:39 -07:00
Koushik Dutta
40ff2a8315 postbeta 2024-08-04 13:45:04 -07:00
Koushik Dutta
bf150712a0 proxmox: flowerbox reboot 2024-08-04 13:37:04 -07:00
Koushik Dutta
92531ff675 proxmox: fix detection 2024-08-04 13:36:05 -07:00
Koushik Dutta
6919c26311 proxmox/docker: host reboot notice 2024-08-04 13:08:43 -07:00
Koushik Dutta
f19c09f239 proxmox: uninstall previous npu per intel docs 2024-08-04 12:47:59 -07:00
Koushik Dutta
7cadb8ffac proxmox: npu driver install support 2024-08-04 12:39:47 -07:00
Koushik Dutta
ea204b24a6 docker/lxc: allow installing the npu fw with flag 2024-08-04 12:32:14 -07:00
Koushik Dutta
45799362ce docker: install npu 2024-08-04 12:20:22 -07:00
Koushik Dutta
2836e10262 docker: npu fw should not be installed in container 2024-08-04 12:18:03 -07:00
Koushik Dutta
a04d463e0f docker/proxmox: add/update intel gpu and npu scripts 2024-08-04 12:17:30 -07:00
Koushik Dutta
3ce8a57daa install: update intel graphics 2024-08-04 11:59:12 -07:00
Koushik Dutta
1647c73375 sdk: fix python generation, publish 2024-08-04 09:29:19 -07:00
Koushik Dutta
b7980b7cbf server: worker pid cleanup 2024-08-04 00:28:42 -07:00
Koushik Dutta
0a6114cc60 server: fix deno kill 2024-08-04 00:26:23 -07:00
Koushik Dutta
149675cfb3 server: fix closure capture 2024-08-04 00:24:57 -07:00
Koushik Dutta
b8eec159bc server: revert and use a global shim 2024-08-04 00:14:59 -07:00
Koushik Dutta
ddb93b28cd Revert "server: use function constructor rather than eval"
This reverts commit 4ed6d1a9fd.
2024-08-04 00:02:13 -07:00
Koushik Dutta
4ed6d1a9fd server: use function constructor rather than eval 2024-08-03 23:42:53 -07:00
Koushik Dutta
bd60e39b24 server: fix deno console 2024-08-03 20:42:43 -07:00
Koushik Dutta
b6636b10f0 sdk: update 2024-08-03 17:26:14 -07:00
Koushik Dutta
8c8beea2eb server: add deno flags 2024-08-03 17:14:50 -07:00
Koushik Dutta
3592a98f2a sdk: update typescript 2024-08-03 17:14:36 -07:00
Koushik Dutta
56f8418d13 postbeta 2024-08-03 10:34:16 -07:00
Koushik Dutta
5bb8ea0f86 server: deno runtime 2024-08-03 10:30:31 -07:00
Koushik Dutta
daddf10035 server: organize imports 2024-08-03 10:29:49 -07:00
Koushik Dutta
2ac8e1509d server: use globalThis rather than global 2024-08-03 10:25:55 -07:00
Koushik Dutta
3cd3208183 server: cpuUsage not available on deno 2024-08-03 10:25:00 -07:00
Koushik Dutta
be217021a2 rpc: remove usage of vm module, embed filename using sourceURL 2024-08-03 10:22:24 -07:00
Koushik Dutta
a2bbb67670 rpc: use globalThis rather than global 2024-08-03 10:21:49 -07:00
Koushik Dutta
465189f4b8 proxmox: add npu 2024-08-03 00:16:37 -07:00
Koushik Dutta
173f7fa4f6 core: publish 2024-08-02 13:56:43 -07:00
Koushik Dutta
d405232d81 server: fix wacky import 2024-08-02 13:45:14 -07:00
Koushik Dutta
673fd95bbd common: remove import 2024-08-01 13:37:06 -07:00
Koushik Dutta
25b2a663c8 server: fix nre in fork 2024-08-01 13:37:01 -07:00
Koushik Dutta
962ecf2cae common: fix zygote opts 2024-08-01 13:28:00 -07:00
Koushik Dutta
4c3f1c43fa server: finish fork filename implementation; 2024-08-01 13:27:46 -07:00
Koushik Dutta
82dfa96699 sdk: support multiple entry points for faster forks 2024-08-01 13:27:31 -07:00
Koushik Dutta
83d3add41a reolink: publish 2024-08-01 12:06:38 -07:00
Koushik Dutta
54db7dc64e server: clustering comments 2024-07-31 22:56:38 -07:00
Koushik Dutta
4c04e9e403 server: implement multi server clustering 2024-07-31 22:51:56 -07:00
Koushik Dutta
de44217f65 core: Fix notificaiton automation 2024-07-31 21:06:22 -07:00
Koushik Dutta
3ae6079615 rebroadcast: publish 2024-07-31 20:29:31 -07:00
Koushik Dutta
3f3409ef1b cloud: publish 2024-07-31 20:27:22 -07:00
Koushik Dutta
fd90b8d5f0 cloud: new cloudflare login flow 2024-07-31 14:14:24 -07:00
Koushik Dutta
782c9930da Merge branch 'main' of github.com:koush/scrypted 2024-07-30 22:49:44 -07:00
Koushik Dutta
d0b46c35a9 server/sdk: wip support for alternative fork main, fork names. add initial workerData message channel. 2024-07-30 22:48:52 -07:00
Koushik Dutta
6c7671dc21 server: fix plugin/thread startup messages 2024-07-30 22:34:59 -07:00
Koushik Dutta
bdc3c204d4 reolink: fix speed 2024-07-30 20:51:57 -07:00
Koushik Dutta
2013830677 reolink: enable speed arg 2024-07-30 16:00:33 -07:00
Koushik Dutta
95906a9ed5 reolink: enable speed arg 2024-07-30 15:59:52 -07:00
Koushik Dutta
6fdd4bd0f4 core: publish ui changes 2024-07-30 11:06:50 -07:00
Koushik Dutta
37cf7aada0 common: remove main thread limitation on zygote 2024-07-30 09:11:07 -07:00
Koushik Dutta
dfdc41626b server: properly handle worker errors 2024-07-30 09:10:42 -07:00
Koushik Dutta
237b3ce0d9 server: cleanup message port references and errors 2024-07-30 08:49:21 -07:00
Koushik Dutta
05745bf3c5 server: use node worker message channel so fork can create forks. 2024-07-30 08:40:46 -07:00
Koushik Dutta
8a6eaa5389 postbeta 2024-07-29 21:38:29 -07:00
Koushik Dutta
7ed298139d coreml: revert changes, require min macos version 2024-07-29 19:17:25 -07:00
Koushik Dutta
82908b82c0 server: enable stable cluster proxyIds 2024-07-29 18:34:18 -07:00
Koushik Dutta
946d88236c rebroadcast: beta 2024-07-29 14:25:30 -07:00
Koushik Dutta
1aa1df885d server: fix connectRPCObject gc race condition 2024-07-29 13:43:52 -07:00
Koushik Dutta
7c94ed9b50 coreml: remove ocr on intel 2024-07-28 22:24:44 -07:00
Koushik Dutta
f7dbff4753 coreml: fix intel mac on catalina 2024-07-28 19:05:42 -07:00
Koushik Dutta
00b5e762a7 webrtc: publish beta 2024-07-28 15:43:54 -07:00
Koushik Dutta
e1440eb76f openvino: use smarter defaults based on detected gpu 2024-07-28 12:19:22 -07:00
Brett Jia
4adb8e4202 server: implement python WritableDeviceState (#1537) 2024-07-28 11:35:27 -07:00
Koushik Dutta
e870370d0c webrtc: sync upstream 2024-07-27 20:35:04 -07:00
Koushik Dutta
f944b76c1f core: admin note on users 2024-07-27 10:44:01 -07:00
Koushik Dutta
62543bdfcf webrtc: fix build 2024-07-26 17:46:25 -07:00
Koushik Dutta
447a321eb7 client: admin flag 2024-07-26 17:45:41 -07:00
Koushik Dutta
d094934bd9 tapo: publish http auth fix 2024-07-26 11:08:39 -07:00
Koushik Dutta
c4f8959072 core: publish 2024-07-26 10:32:55 -07:00
Koushik Dutta
d682bd2ebb reolink: reset siren state 2024-07-26 08:28:53 -07:00
Koushik Dutta
437ab70cd9 homekit: update hap, remove siren from auto sync. 2024-07-26 08:28:25 -07:00
Koushik Dutta
15031cde1f reolink: fix comment 2024-07-26 08:01:13 -07:00
Koushik Dutta
e88008552c reolink: make siren timeout mimic hardware 2024-07-26 08:00:38 -07:00
Koushik Dutta
fd49deefb8 postbeta 2024-07-25 21:56:15 -07:00
Koushik Dutta
1f2973abd2 server: fix plugin deploy when SCRYPTED_DEFAULT_AUTHENTICATION is used 2024-07-25 21:56:03 -07:00
Koushik Dutta
317cd7671f server: fix build 2024-07-25 21:45:44 -07:00
Koushik Dutta
9556efc224 server: update deps, print entire stack on uncaught error 2024-07-25 21:44:10 -07:00
Koushik Dutta
2f9db83868 security: make default listen occur on loopback 2024-07-25 21:06:35 -07:00
Koushik Dutta
c627832ebd security: require explicit address on all server listens 2024-07-25 20:54:47 -07:00
Koushik Dutta
7d2df3af42 security: require explicit address on all server listens 2024-07-25 18:33:16 -07:00
Koushik Dutta
e9288bd4a1 core: publish 2024-07-25 18:32:49 -07:00
Koushik Dutta
191620b55b Merge branch 'main' of github.com:koush/scrypted 2024-07-23 09:22:19 -07:00
Koushik Dutta
90b6fc1e49 core/mqtt: scripting fixes 2024-07-23 09:22:15 -07:00
Koushik Dutta
cd3c748dd0 core: automation builder fixes 2024-07-23 09:18:15 -07:00
Koushik Dutta
34dbc7930e tapo: fix 2 way 2024-07-22 17:40:33 -07:00
Koushik Dutta
112633a776 core: various ui fixes 2024-07-22 14:57:13 -07:00
Koushik Dutta
56416109b1 core: more automation actions 2024-07-21 21:45:32 -07:00
Koushik Dutta
a889abae98 core: notifier support 2024-07-21 15:09:38 -07:00
Koushik Dutta
dcb50ba3ff ring: fix h264 assumption 2024-07-21 10:55:33 -07:00
Koushik Dutta
3c61ddb806 core: publish autoupdate fixes 2024-07-21 10:30:42 -07:00
Koushik Dutta
2a9aba1df8 plugins: publish various 2024-07-21 09:56:32 -07:00
Koushik Dutta
450acbdcb1 Merge branch 'main' of github.com:koush/scrypted 2024-07-21 08:25:42 -07:00
Koushik Dutta
80aff3199a webhook: readme 2024-07-21 08:25:37 -07:00
Brett Jia
834eff20c7 sdk, rebroadcast: support specifying custom ffmpeg path (#1535) 2024-07-21 07:31:26 -07:00
Koushik Dutta
6314f5e45a core: publish 2024-07-20 22:37:48 -07:00
Koushik Dutta
5b3793e810 Merge branch 'main' of github.com:koush/scrypted 2024-07-20 22:20:09 -07:00
Koushik Dutta
9a2bff48c5 core: publish 2024-07-20 22:19:41 -07:00
Brett Jia
aa9903b328 sdk, rebroadcast: don't automatically prebuffer synthetic streams (#1534) 2024-07-20 21:05:47 -07:00
Koushik Dutta
c1f4ae96fa core/rtsp: fix settings bugs 2024-07-20 21:01:17 -07:00
Koushik Dutta
d5995d93e2 Merge branch 'main' of github.com:koush/scrypted 2024-07-20 20:56:04 -07:00
Koushik Dutta
f13844cf3e homekit: publish 2024-07-20 20:56:00 -07:00
Brett Jia
6d7add8272 rebroadcast: support env vars + upstream encoder args (#1533)
* rebroadcast: support env vars + upstream encoder args

* make ffmpegInput override process.env

* make h264EncoderArguments safer

* reorder

* update per feedback
2024-07-20 19:53:22 -07:00
Koushik Dutta
983a683d54 ring: clip support 2024-07-20 15:11:55 -07:00
Koushik Dutta
f3e5cf2a8b core: publish new ui 2024-07-20 14:26:09 -07:00
Koushik Dutta
40db551799 Merge branch 'main' of github.com:koush/scrypted 2024-07-20 14:17:11 -07:00
Koushik Dutta
fdb9e03656 core: publish new ui 2024-07-20 14:17:07 -07:00
Brett Jia
48976b2947 sdk: add env to FFmpegInput + fix python parameterized class generator (#1532)
* sdk: add env to FFmpegInput + fix python parameterized class generator

* downgrade node for gh actions
2024-07-20 13:29:45 -07:00
Koushik Dutta
3ee022c2be core: beta 2024-07-19 23:16:55 -07:00
Koushik Dutta
ab24a61fd3 cameras: update with fixed http auth 2024-07-19 19:53:42 -07:00
Koushik Dutta
8d2237b26f Merge branch 'main' of github.com:koush/scrypted 2024-07-19 17:52:12 -07:00
Koushik Dutta
8ad05bbd5b snapshot: ensure hwaccel none for single frame decodes 2024-07-19 17:52:07 -07:00
Koushik Dutta
7499e79dc7 server: suppress WWW-authenticate 2024-07-19 17:11:24 -07:00
Koushik Dutta
7132278204 common: improve stream end errors 2024-07-19 15:18:13 -07:00
Koushik Dutta
60fa494ed0 Merge branch 'main' of github.com:koush/scrypted 2024-07-19 13:17:57 -07:00
Koushik Dutta
815f204136 core: beta 2024-07-19 10:15:10 -07:00
slyoldfox
fe80fab811 Add more meaningful device DeviceInformation for new UI (#1531) 2024-07-19 09:45:11 -07:00
Koushik Dutta
2699ecd93d core: fix empty automation, rebuild ui 2024-07-18 19:01:23 -07:00
Koushik Dutta
5d634f5876 core: new management console beta 2024-07-18 11:13:17 -07:00
Koushik Dutta
9979789e08 zwave: remove legacy dep 2024-07-18 10:39:51 -07:00
Koushik Dutta
79d2d4e366 zwave: remove legacy dep 2024-07-18 10:37:55 -07:00
Koushik Dutta
90b4bcfec9 actions: fixup static sites workflow 2024-07-18 10:27:37 -07:00
Koushik Dutta
49df286cfa Create static.yml 2024-07-18 10:24:51 -07:00
Koushik Dutta
20edf8a622 move static sites 2024-07-18 10:24:34 -07:00
Koushik Dutta
bb76102171 cloud: fix bug where port reservation failures causes total failure 2024-07-16 23:34:15 -07:00
Koushik Dutta
e71f9b585c cloud: cors for manage.scrypted.app 2024-07-16 13:17:15 -07:00
Koushik Dutta
1effc45f18 sdk: add cloudHref to launcher 2024-07-16 11:27:00 -07:00
Koushik Dutta
2e5e5b7be0 common/core: automation/build fixes for new management site 2024-07-16 09:33:09 -07:00
Koushik Dutta
1df5cfefd0 working scheduler 2024-07-15 20:57:09 -07:00
Koushik Dutta
5d1e2663b8 rtp: fixup promise reject race condition 2024-07-15 14:26:25 -07:00
Koushik Dutta
59441c414b common: revert/fix deferred 2024-07-15 14:26:05 -07:00
Koushik Dutta
419d007445 rtsp: fix unhandled rejection if rtsp client is already destroyed 2024-07-15 13:53:55 -07:00
Koushik Dutta
90bca27bde http auth: improve status code handling 2024-07-15 11:56:39 -07:00
Koushik Dutta
0050624880 http digest auth: fix invalid status code not cleaning up http connections 2024-07-15 11:20:27 -07:00
Koushik Dutta
c4ea7938d1 core: wip automation settings 2024-07-14 23:02:12 -07:00
Koushik Dutta
b5d58455b6 core: transition to settings based automations 2024-07-14 20:11:23 -07:00
Koushik Dutta
82993df715 common: socket failure cleanups 2024-07-14 12:24:09 -07:00
Koushik Dutta
555a688c16 core: use provided nativeid in terminal service 2024-07-14 09:52:44 -07:00
Koushik Dutta
0241a5fb93 core: update sdk and fix build 2024-07-14 09:50:18 -07:00
Koushik Dutta
db7351e7d4 sdk: fix StreamService template args 2024-07-14 09:26:22 -07:00
Koushik Dutta
891e9792f8 sdk: uadd immediate setting flag 2024-07-14 09:17:17 -07:00
Koushik Dutta
97e7333415 core: dead import 2024-07-14 08:17:31 -07:00
Koushik Dutta
dc4dd07ced cloud: cleanup 2024-07-14 07:56:19 -07:00
Koushik Dutta
937f615c8c cloud: earlier unregistration fix for cloudflare 2024-07-14 07:55:38 -07:00
Koushik Dutta
7578cf092e amcrest: use VideoMotionInfo state to reset motion 2024-07-13 20:42:39 -07:00
Koushik Dutta
3041207177 zwave: update to latest zwave-js 2024-07-13 15:45:50 -07:00
Koushik Dutta
46d66122aa Merge branch 'main' of github.com:koush/scrypted 2024-07-12 18:05:58 -07:00
Koushik Dutta
d05e3a92f3 homekit: fix erroneous new subdevice reload 2024-07-12 18:05:53 -07:00
Brett Jia
4a4b077132 sdk: add generic params to StreamService + generate AsyncGenerator type hints in Python (#1527) 2024-07-12 08:08:28 -07:00
Koushik Dutta
cf5e010faf homekit: add codes to readme 2024-07-11 21:47:31 -07:00
Koushik Dutta
46616467f4 common: async queue wrapper for generator 2024-07-11 21:26:13 -07:00
Koushik Dutta
3dcb36adf9 Merge branch 'main' of github.com:koush/scrypted 2024-07-11 20:16:30 -07:00
Roman Sokolov
855940fb03 Hikvision Doorbell Plugin with inheritance (#1400)
* separate plugin

* added SIP "imitation". It almost works

* Refactoring

* Fixed some bugs. Added more UI

* Added descriptions and some fixes

* Stage before beta

* First beta done

* reset common settings

* Adding inheritance from Hikvision Camera plugin

* Added exposing tamper alert device

---------

Co-authored-by: Roman Sokolov <calm@adguard.com>
2024-07-11 16:56:07 -07:00
Brett Jia
1f25e1a308 core: allow specifying command via options (#1522) 2024-07-11 15:38:58 -07:00
Brett Jia
232298d7f4 server: fix python connectRPCObject sha256 (#1525) 2024-07-11 15:38:40 -07:00
Koushik Dutta
fa9b4f1a1c sdk: publish 2024-07-11 13:22:57 -07:00
Brett Jia
355c2719fd sdk: add TTY interface (#1526) 2024-07-11 13:22:18 -07:00
Brett Jia
dfb18ce882 server: fix crash fix on web-based connectRPCObject (#1524) 2024-07-11 09:56:31 -07:00
Koushik Dutta
07187d058b sdk: clippath setting support 2024-07-10 23:59:13 -07:00
Koushik Dutta
5060b5f8c7 homekit: show in scrypted settings 2024-07-10 21:08:37 -07:00
Koushik Dutta
50c628a25e Merge branch 'main' of github.com:koush/scrypted 2024-07-10 19:17:02 -07:00
Koushik Dutta
7bf4609d3d homekit: use html setting 2024-07-10 19:16:57 -07:00
Koushik Dutta
548a8eb321 sdk: update deps 2024-07-10 18:39:57 -07:00
Koushik Dutta
627f9e7a0a sdk: publish 2024-07-10 18:38:16 -07:00
Koushik Dutta
4faf85c988 sdk: update deps 2024-07-10 18:07:36 -07:00
Koushik Dutta
259c6434da Merge branch 'main' of github.com:koush/scrypted 2024-07-10 16:09:34 -07:00
Brett Jia
321d5b364f server: fix python systemManager.getDeviceByName (#1523) 2024-07-10 15:42:24 -07:00
Koushik Dutta
e56491ec27 common: refactor monaco hooks 2024-07-10 14:05:12 -07:00
Koushik Dutta
8fe5d1bace common: monaco typings 2024-07-10 13:17:05 -07:00
Koushik Dutta
4efa58ee8b sdk: add script setting type 2024-07-10 12:45:18 -07:00
Koushik Dutta
8249a5efa1 server: fixup engine.io typings 2024-07-10 11:58:29 -07:00
Koushik Dutta
08b0717407 rpc: move eval into separate file 2024-07-10 11:57:50 -07:00
Koushik Dutta
c277833332 sdk: publish 2024-07-10 11:39:05 -07:00
Koushik Dutta
37d9f2870d Merge branch 'main' of github.com:koush/scrypted 2024-07-10 11:38:15 -07:00
Koushik Dutta
cc71d1292b sdk: add support for day setting 2024-07-10 11:38:10 -07:00
slyoldfox
3ca6841ea2 Support v2 UI for sip and bticino plugins (#1521)
* Fix an undefined error that might occur when sip debug is off
Slight cleanup
Add support for v2 UI

* Decouple voicemail lock device from camera device and only add it for c300x models (c100x doesn't have voicemail)
Add support for v2 UI
Allow changing devaddr setting from UI

* Fix an undefined error that might occur when sip debug is off
Slight cleanup
Add support for v2 UI
2024-07-10 09:31:26 -07:00
Koushik Dutta
c81cdd0df1 server: update deps 2024-07-09 11:19:47 -07:00
Koushik Dutta
bd0cbe5e97 Revert "common: readd server dep"
This reverts commit fdd4eebd96.
2024-07-09 10:48:02 -07:00
Koushik Dutta
fdd4eebd96 common: readd server dep 2024-07-09 10:36:32 -07:00
Koushik Dutta
34eeaf5cce common: use relative import 2024-07-09 17:32:45 +00:00
Koushik Dutta
09c38e427a actions: Update build-sdk.yml 2024-07-09 10:28:07 -07:00
Koushik Dutta
fca2773282 remove wildcard export 2024-07-09 10:06:33 -07:00
Koushik Dutta
c138cc81c0 core: add core to new scrypted settings 2024-07-09 09:22:58 -07:00
Koushik Dutta
91be95e158 core: add support for system device creator 2024-07-09 09:03:53 -07:00
Koushik Dutta
e172b45047 cli: add support for session file path 2024-07-08 15:31:01 -07:00
Koushik Dutta
0a6c07551f cli: missing file 2024-07-08 15:26:56 -07:00
Koushik Dutta
fa33f850f7 cli: rtsp to mp4 converter 2024-07-08 15:25:21 -07:00
Koushik Dutta
605513d165 common: export sleep as relative file due to rollup quirks 2024-07-08 14:04:53 -07:00
Koushik Dutta
d635ab8662 common: bump ts 2024-07-08 14:01:27 -07:00
Koushik Dutta
4862705dcd common: fix sleep export 2024-07-08 13:51:05 -07:00
Koushik Dutta
4d471eb285 sdk/plugins/client/server: add new ScryptedDeviceCreator ui hooks 2024-07-08 09:43:30 -07:00
Koushik Dutta
470d315eaf client: build 2024-07-07 22:53:59 -07:00
Koushik Dutta
4e267e3de9 sdk: publish 2024-07-07 22:45:05 -07:00
Koushik Dutta
bd28cd1766 sdk: update with createdDevice string 2024-07-07 22:28:42 -07:00
Koushik Dutta
f5c324bd68 reolink: fallback if token exchange fails 2024-07-06 15:05:18 -07:00
Koushik Dutta
b7bf995303 mqtt: fix up device creation 2024-07-06 11:48:14 -07:00
Koushik Dutta
68516817aa cloud: fix tunnel registration failure hang 2024-07-06 00:08:43 -07:00
Koushik Dutta
9dc5f2a063 cloud: prefer cloudflared address if available 2024-07-05 17:57:08 -07:00
Koushik Dutta
d2564efe46 cloud: send custom domain header if its in use 2024-07-05 17:47:17 -07:00
Koushik Dutta
696e97914d common: formatting 2024-07-04 20:18:53 -07:00
Koushik Dutta
cafc5da8bf common: remove wildcard sleep export 2024-07-04 19:38:46 -07:00
Koushik Dutta
a24b6432c2 various: fixup strictness 2024-07-04 17:13:30 -07:00
Koushik Dutta
68668c1b91 Merge branch 'main' of github.com:koush/scrypted 2024-07-04 16:53:14 -07:00
Koushik Dutta
460441abd2 common: remove sleep 2024-07-04 16:53:09 -07:00
Koushik Dutta
3875afd002 client: publish 2024-07-04 16:52:47 -07:00
Koushik Dutta
f769c1fbec reolink: add more hw info 2024-07-04 14:50:15 -07:00
Koushik Dutta
644df95f21 reolink: add more hw info 2024-07-04 14:49:05 -07:00
Koushik Dutta
95f1e618f9 unifi-protect: ips 2024-07-04 14:44:15 -07:00
Koushik Dutta
03e6cf1070 Merge branch 'main' of github.com:koush/scrypted 2024-07-02 12:39:00 -07:00
Koushik Dutta
f01a207166 common: export sleep from server 2024-07-02 12:38:55 -07:00
Long Zheng
1795996825 windows: Fix plugin NPM dependencies failing to install on Windows with path containing space (#1517) 2024-06-30 23:15:32 -07:00
Koushik Dutta
375f7bcc09 Merge branch 'main' of github.com:koush/scrypted 2024-06-29 19:29:25 -07:00
Koushik Dutta
76f10ced5f ha: verup 2024-06-29 19:29:21 -07:00
Koushik Dutta
37c791f147 predict: update opencv dep, make required 2024-06-29 15:22:02 -07:00
Koushik Dutta
9a8034eb4c h264: fix bug when stapa packet overflows packetizing a large p frame 2024-06-28 20:00:50 -07:00
Koushik Dutta
ff70ed301e webrtc/homekit: publish mtu changes 2024-06-28 13:14:46 -07:00
Koushik Dutta
3f66594821 reolink: use username and password if it works, fallback to token otherwise 2024-06-28 12:32:14 -07:00
Koushik Dutta
f2cd0218fd reolink: switch to mainEncType marker 2024-06-28 11:12:01 -07:00
Koushik Dutta
028d601674 reolink: automatically remove nonfunctional streams using opaque getAbility api results lol 2024-06-28 10:37:58 -07:00
Koushik Dutta
e06d012875 Revert "Breaks snapshot on doorbell. Not sure why."
This reverts commit 91fbc2fdf8.
2024-06-28 10:35:18 -07:00
Koushik Dutta
5995400414 reolink: log startup error 2024-06-28 09:53:34 -07:00
Koushik Dutta
91fbc2fdf8 Breaks snapshot on doorbell. Not sure why.
Revert "reolink: use Login json api to get around URL escaping limitations with some firmware (#1509)"

This reverts commit fc93a85e21.
2024-06-28 09:42:59 -07:00
Koushik Dutta
6b00324c74 webrtc/homekit: reduce mtu, prep for adaptive bitrate 2024-06-28 09:37:53 -07:00
Koushik Dutta
1369197a11 sdk: revert mtu callback 2024-06-28 09:32:11 -07:00
Koushik Dutta
a30580f3b8 openvino: lock to f16. todo remove multiple precisions. 2024-06-28 09:21:47 -07:00
George Talusan
fc93a85e21 reolink: use Login json api to get around URL escaping limitations with some firmware (#1509) 2024-06-27 21:54:04 -07:00
jstef16
5351d869d4 homekit: fix window covering target position binding (#1512)
Co-authored-by: Jordan Steffan <jordansteffan@Jordans-Mini.localdomain>
2024-06-27 21:50:55 -07:00
Koushik Dutta
a61d9af25c sdk: add hook to set mtu and listen for mtu change requests. 2024-06-27 15:45:36 -07:00
Koushik Dutta
2111413704 reolink: remove debug code 2024-06-27 12:56:54 -07:00
Koushik Dutta
a2781f9af2 reolink/common: siren fixes 2024-06-27 10:51:16 -07:00
Koushik Dutta
09eeae3802 reolink: unhide the doorbell checkbox 2024-06-27 10:24:38 -07:00
Koushik Dutta
0408b7e23d reolink: fix broken doorbell. 2024-06-27 10:15:49 -07:00
Koushik Dutta
ea606de22f postrelease 2024-06-26 09:43:48 -07:00
472 changed files with 22765 additions and 46272 deletions

View File

@@ -7,7 +7,7 @@ on:
pull_request:
paths: ["sdk/**"]
workflow_dispatch:
jobs:
build:
name: Build
@@ -15,11 +15,11 @@ jobs:
defaults:
run:
working-directory: ./sdk
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22.4.1
- run: npm ci
- run: npm run build

44
.github/workflows/static-sites.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
paths: ["sites/static/**", ".github/workflows/static-sites.yml"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: './sites/static'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

6
.gitmodules vendored
View File

@@ -1,9 +1,6 @@
[submodule "plugins/unifi-protect/src/unifi-protect"]
path = external/unifi-protect
url = ../../koush/unifi-protect.git
[submodule "plugins/myq/src/myq"]
path = plugins/myq/src/myq
url = ../../koush/myq.git
[submodule "external/ring-client-api"]
path = external/ring-client-api
url = ../../koush/ring
@@ -14,9 +11,6 @@
[submodule "external/werift"]
path = external/werift
url = ../../koush/werift-webrtc
[submodule "plugins/zwave/file-stream-rotator"]
path = plugins/zwave/file-stream-rotator
url = ../../koush/file-stream-rotator.git
[submodule "sdk/developer.scrypted.app"]
path = sdk/developer.scrypted.app
url = ../../koush/developer.scrypted.app

108
common/package-lock.json generated
View File

@@ -10,12 +10,12 @@
"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"
}
},
@@ -74,7 +74,7 @@
},
"../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.29",
"version": "0.3.45",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -111,39 +111,40 @@
},
"../server": {
"name": "@scrypted/server",
"version": "0.106.0",
"version": "0.115.0",
"extraneous": true,
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"@scrypted/ffmpeg-static": "^6.1.0-build1",
"@scrypted/node-pty": "^1.0.10",
"@scrypted/types": "^0.3.28",
"adm-zip": "^0.5.12",
"@scrypted/node-pty": "^1.0.18",
"@scrypted/types": "^0.3.33",
"adm-zip": "^0.5.14",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"engine.io": "^6.5.4",
"engine.io": "^6.6.0",
"express": "^4.19.2",
"follow-redirects": "^1.15.6",
"http-auth": "^4.2.0",
"ip": "^2.0.1",
"level": "^8.0.1",
"lodash": "^4.17.21",
"nan": "^2.19.0",
"nan": "^2.20.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^10.1.0",
"py": "npm:@bjia56/portable-python@^0.1.31",
"py": "npm:@bjia56/portable-python@^0.1.54",
"router": "^1.3.8",
"semver": "^7.6.2",
"sharp": "^0.33.3",
"sharp": "^0.33.4",
"source-map-support": "^0.5.21",
"tar": "^7.1.0",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"tar": "^7.4.0",
"tslib": "^2.6.3",
"typescript": "^5.5.3",
"whatwg-mimetype": "^4.0.0",
"ws": "^8.17.0"
"ws": "^8.18.0"
},
"bin": {
"scrypted-serve": "bin/scrypted-serve"
@@ -155,7 +156,7 @@
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/ip": "^1.1.3",
"@types/lodash": "^4.17.1",
"@types/lodash": "^4.17.6",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/semver": "^7.5.8",
@@ -205,10 +206,6 @@
"resolved": "../sdk",
"link": true
},
"node_modules/@scrypted/server": {
"resolved": "../server",
"link": true
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -301,6 +298,12 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/monaco-editor": {
"version": "0.50.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
"dev": true
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -345,9 +348,9 @@
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -442,53 +445,6 @@
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@scrypted/server": {
"version": "file:../server",
"requires": {
"@mapbox/node-pre-gyp": "^1.0.11",
"@scrypted/ffmpeg-static": "^6.1.0-build1",
"@scrypted/node-pty": "^1.0.10",
"@scrypted/types": "^0.3.28",
"@types/adm-zip": "^0.5.5",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/follow-redirects": "^1.14.4",
"@types/http-auth": "^4.1.4",
"@types/ip": "^1.1.3",
"@types/lodash": "^4.17.1",
"@types/node-dijkstra": "^2.5.6",
"@types/node-forge": "^1.3.11",
"@types/semver": "^7.5.8",
"@types/source-map-support": "^0.5.10",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.5.10",
"adm-zip": "^0.5.12",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"engine.io": "^6.5.4",
"express": "^4.19.2",
"follow-redirects": "^1.15.6",
"http-auth": "^4.2.0",
"ip": "^2.0.1",
"level": "^8.0.1",
"lodash": "^4.17.21",
"nan": "^2.19.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^10.1.0",
"py": "npm:@bjia56/portable-python@^0.1.31",
"router": "^1.3.8",
"semver": "^7.6.2",
"sharp": "^0.33.3",
"source-map-support": "^0.5.21",
"tar": "^7.1.0",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"whatwg-mimetype": "^4.0.0",
"ws": "^8.17.0"
}
},
"@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
@@ -566,6 +522,12 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"monaco-editor": {
"version": "0.50.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz",
"integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==",
"dev": true
},
"ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -588,9 +550,9 @@
}
},
"typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw=="
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ=="
},
"undici-types": {
"version": "5.26.5",

View File

@@ -12,12 +12,12 @@
"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"
}
}

View File

@@ -94,7 +94,7 @@ export function createAsyncQueue<T>() {
}
catch (e) {
// the yield above may raise an error, and the queue should be ended.
end(e);
end(e as Error);
if (e instanceof EndError)
return;
throw e;
@@ -155,6 +155,23 @@ export function createAsyncQueue<T>() {
}
}
export function createAsyncQueueFromGenerator<T>(generator: AsyncGenerator<T>) {
const q = createAsyncQueue<T>();
(async() => {
try {
for await (const i of generator) {
q.submit(i);
}
}
catch (e) {
q.end(e as Error);
}
q.end();
})();
return q;
}
// async function testSlowEnqueue() {
// const asyncQueue = createAsyncQueue<number>();

View File

@@ -0,0 +1,209 @@
import sdk, { AudioStreamOptions, MediaStreamConfiguration, MediaStreamDestination, MediaStreamOptions, ScryptedDeviceBase, Setting } from "@scrypted/sdk";
export const automaticallyConfigureSettings: Setting = {
key: 'autoconfigure',
title: 'Automatically Configure Settings',
description: 'Automatically configure and valdiate the camera codecs and other settings for optimal Scrypted performance. Some settings will require manual configuration via the camera web admin.',
type: 'boolean',
value: true,
};
export const onvifAutoConfigureSettings: Setting = {
key: 'onvif-autoconfigure',
type: 'html',
value: 'ONVIF autoconfiguration will configure the camera codecs. <b>The camera motion sensor must still be <a target="_blank" href="https://docs.scrypted.app/camera-preparation.html#motion-sensor-setup">configured manually</a>.</b>',
};
const MEGABIT = 1024 * 1000;
function getBitrateForResolution(resolution: number) {
if (resolution >= 3840 * 2160)
return 8 * MEGABIT;
if (resolution >= 2688 * 1520)
return 3 * MEGABIT;
if (resolution >= 1920 * 1080)
return 2 * MEGABIT;
if (resolution >= 1280 * 720)
return MEGABIT;
if (resolution >= 640 * 480)
return MEGABIT / 2;
return MEGABIT / 4;
}
export async function checkPluginNeedsAutoConfigure(plugin: ScryptedDeviceBase, extraDevices = 0) {
if (plugin.storage.getItem('autoconfigure') === 'true')
return;
plugin.storage.setItem('autoconfigure', 'true');
if (sdk.deviceManager.getNativeIds().length <= 1 + extraDevices)
return;
plugin.log.a(`${plugin.name} now has support for automatic camera configuration for optimal performance. Cameras can be autoconfigured in their respective settings.`);
}
export async function autoconfigureCodecs(
getCodecs: () => Promise<MediaStreamOptions[]>,
configureCodecs: (options: MediaStreamOptions) => Promise<MediaStreamConfiguration>,
audioOptions?: AudioStreamOptions,
) {
audioOptions ||= {
codec: 'pcm_mulaw',
bitrate: 64000,
sampleRate: 8000,
};
const codecs = await getCodecs();
const configurable: MediaStreamConfiguration[] = [];
for (const codec of codecs) {
const config = await configureCodecs({
id: codec.id,
});
configurable.push(config);
}
const used: MediaStreamConfiguration[] = [];
for (const _ of ['local', 'remote', 'low-resolution'] as MediaStreamDestination[]) {
// find stream with the highest configurable resolution.
let highest: [MediaStreamConfiguration, number] = [undefined, 0];
for (const codec of configurable) {
if (used.includes(codec))
continue;
for (const resolution of codec.video.resolutions) {
if (resolution[0] * resolution[1] > highest[1]) {
highest = [codec, resolution[0] * resolution[1]];
}
}
}
const config = highest[0];
if (!config)
break;
used.push(config);
}
const findResolutionTarget = (config: MediaStreamConfiguration, width: number, height: number) => {
let diff = 999999999;
let ret: [number, number];
const targetArea = width * height;
for (const res of config.video.resolutions) {
const actualArea = res[0] * res[1];
const diffArea = Math.abs(targetArea - actualArea);
if (diffArea < diff) {
diff = diffArea;
ret = res;
}
}
return ret;
}
// find the highest resolution
const l = used[0];
const resolution = findResolutionTarget(l, 8192, 8192);
// get the fps of 20 or highest available
let fps = Math.min(20, Math.max(...l.video.fpsRange));
let errors = '';
const logConfigureCodecs = async (config: MediaStreamConfiguration) => {
try {
await configureCodecs(config);
}
catch (e) {
errors += e;
}
}
await logConfigureCodecs({
id: l.id,
video: {
width: resolution[0],
height: resolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: getBitrateForResolution(resolution[0] * resolution[1]),
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
audio: audioOptions,
});
if (used.length === 3) {
// find remote and low
const r = used[1];
const l = used[2];
const rResolution = findResolutionTarget(r, 1280, 720);
const lResolution = findResolutionTarget(l, 640, 360);
fps = Math.min(20, Math.max(...r.video.fpsRange));
await logConfigureCodecs({
id: r.id,
video: {
width: rResolution[0],
height: rResolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: 1 * MEGABIT,
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
audio: audioOptions,
});
fps = Math.min(20, Math.max(...l.video.fpsRange));
await logConfigureCodecs({
id: l.id,
video: {
width: lResolution[0],
height: lResolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: MEGABIT / 2,
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
audio: audioOptions,
});
}
else if (used.length == 2) {
let target: [number, number];
if (resolution[0] * resolution[1] > 1920 * 1080)
target = [1280, 720];
else
target = [640, 360];
const rResolution = findResolutionTarget(used[1], target[0], target[1]);
const fps = Math.min(20, Math.max(...used[1].video.fpsRange));
await logConfigureCodecs({
id: used[1].id,
video: {
width: rResolution[0],
height: rResolution[1],
bitrateControl: 'variable',
codec: 'h264',
bitrate: getBitrateForResolution(rResolution[0] * rResolution[1]),
fps,
keyframeInterval: fps * 4,
quality: 5,
profile: 'main',
},
audio: audioOptions,
});
}
else if (used.length === 1) {
// no nop
}
if (errors)
throw new Error(errors);
}

View File

@@ -1,17 +0,0 @@
export class Deferred<T> {
finished = false;
resolve!: (value: T|PromiseLike<T>) => this;
reject!: (error: Error) => this;
promise: Promise<T> = new Promise((resolve, reject) => {
this.resolve = v => {
this.finished = true;
resolve(v);
return this;
};
this.reject = e => {
this.finished = true;
reject(e);
return this;
};
});
}

1
common/src/deferred.ts Symbolic link
View File

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

View File

@@ -0,0 +1,96 @@
import type * as monacoEditor from 'monaco-editor';
export interface StandardLibs {
'@types/node/globals.d.ts': string,
'@types/node/buffer.d.ts': string,
'@types/node/process.d.ts': string,
'@types/node/events.d.ts': string,
'@types/node/stream.d.ts': string,
'@types/node/fs.d.ts': string,
'@types/node/net.d.ts': string,
'@types/node/child_process.d.ts': string,
}
export interface ScryptedLibs {
'@types/sdk/settings-mixin.d.ts': string,
'@types/sdk/storage-settings.d.ts': string,
'@types/sdk/types.d.ts': string,
'@types/sdk/index.d.ts': string,
}
export function createMonacoEvalDefaultsWithLibs(standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
// const libs = Object.assign(scryptedLibs, extraLibs);
function monacoEvalDefaultsFunction(monaco: typeof monacoEditor, standardLibs: StandardLibs, scryptedLibs: ScryptedLibs, extraLibs: { [lib: string]: string }) {
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
Object.assign(
{},
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
{
diagnosticCodesToIgnore: [1108, 1375, 1378],
}
)
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
Object.assign(
{},
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
{
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
}
)
);
const libs: any = {
...scryptedLibs,
...extraLibs,
};
const catLibs = Object.values(libs).join('\n');
const catlibsNoExport = Object.keys(libs)
.map(lib => libs[lib]).map(lib =>
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
.join('\n');
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
${catLibs}
declare global {
${catlibsNoExport}
const log: Logger;
const deviceManager: DeviceManager;
const endpointManager: EndpointManager;
const mediaManager: MediaManager;
const systemManager: SystemManager;
const eventSource: ScryptedDevice;
const eventDetails: EventDetails;
const eventData: any;
}
`,
"node_modules/@types/scrypted__sdk/types/index.d.ts"
);
for (const lib of Object.keys(standardLibs)) {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
standardLibs[lib as keyof StandardLibs],
lib,
);
}
}
return `(function() {
const standardLibs = ${JSON.stringify(standardLibs)};
const scryptedLibs = ${JSON.stringify(scryptedLibs)};
const extraLibs = ${JSON.stringify(extraLibs)};
return (monaco) => {
(${monacoEvalDefaultsFunction})(monaco, standardLibs, scryptedLibs, extraLibs);
}
})();
`;
}

View File

@@ -1,3 +1,5 @@
import type { ScryptedDeviceBase } from "@scrypted/sdk";
export interface ScriptDevice {
/**
* @deprecated Use the default export to specify the device handler.
@@ -6,3 +8,5 @@ export interface ScriptDevice {
handle<T>(handler?: T & object): void;
handleTypes(...interfaces: string[]): void;
}
export declare const device: ScryptedDeviceBase & ScriptDevice;

View File

@@ -1,9 +1,10 @@
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
import { SettingsMixinDeviceBase } from "@scrypted/sdk/settings-mixin";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import fs from 'fs';
import type { TranspileOptions } from "typescript";
import vm from "vm";
import { createMonacoEvalDefaultsWithLibs, ScryptedLibs, StandardLibs } from "./monaco-libs";
import { ScriptDevice } from "./monaco/script-device";
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
@@ -28,22 +29,18 @@ export function readFileAsString(f: string) {
return fs.readFileSync(f).toString();;
}
function getTypeDefs() {
const settingsMixinDefs = readFileAsString('@types/sdk/settings-mixin.d.ts');
const storageSettingsDefs = readFileAsString('@types/sdk/storage-settings.d.ts');
const scryptedTypesDefs = readFileAsString('@types/sdk/types.d.ts');
const scryptedIndexDefs = readFileAsString('@types/sdk/index.d.ts');
function getScryptedLibs(): ScryptedLibs {
return {
settingsMixinDefs,
storageSettingsDefs,
scryptedIndexDefs,
scryptedTypesDefs,
};
"@types/sdk/index.d.ts": readFileAsString('@types/sdk/index.d.ts'),
"@types/sdk/settings-mixin.d.ts": readFileAsString('@types/sdk/settings-mixin.d.ts'),
"@types/sdk/storage-settings.d.ts": readFileAsString('@types/sdk/storage-settings.d.ts'),
"@types/sdk/types.d.ts": readFileAsString('@types/sdk/types.d.ts'),
}
}
export async function scryptedEval(device: ScryptedDeviceBase, script: string, extraLibs: { [lib: string]: string }, params: { [name: string]: any }) {
const libs = Object.assign({
types: getTypeDefs().scryptedTypesDefs,
types: getScryptedLibs()['@types/sdk/types.d.ts'],
}, extraLibs);
const allScripts = Object.values(libs).join('\n').toString() + script;
let compiled: string;
@@ -117,102 +114,18 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
}
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
const safeLibs: any = {};
const standardlibs: StandardLibs = {
"@types/node/globals.d.ts": readFileAsString('@types/node/globals.d.ts'),
"@types/node/buffer.d.ts": readFileAsString('@types/node/buffer.d.ts'),
"@types/node/process.d.ts": readFileAsString('@types/node/process.d.ts'),
"@types/node/events.d.ts": readFileAsString('@types/node/events.d.ts'),
"@types/node/stream.d.ts": readFileAsString('@types/node/stream.d.ts'),
"@types/node/fs.d.ts": readFileAsString('@types/node/fs.d.ts'),
"@types/node/net.d.ts": readFileAsString('@types/node/net.d.ts'),
"@types/node/child_process.d.ts": readFileAsString('@types/node/child_process.d.ts'),
};
for (const safeLib of [
'@types/node/globals.d.ts',
'@types/node/buffer.d.ts',
'@types/node/process.d.ts',
'@types/node/events.d.ts',
'@types/node/stream.d.ts',
'@types/node/fs.d.ts',
'@types/node/net.d.ts',
'@types/node/child_process.d.ts',
]) {
safeLibs[`node_modules/${safeLib}`] = readFileAsString(safeLib)
}
const libs = Object.assign(getTypeDefs(), extraLibs);
function monacoEvalDefaultsFunction(monaco: any, safeLibs: any, libs: any) {
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(
Object.assign(
{},
monaco.languages.typescript.typescriptDefaults.getDiagnosticsOptions(),
{
diagnosticCodesToIgnore: [1108, 1375, 1378],
}
)
);
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
Object.assign(
{},
monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
{
moduleResolution:
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
}
)
);
const catLibs = Object.values(libs).join('\n');
const catlibsNoExport = Object.keys(libs).filter(lib => lib !== 'sdk')
.map(lib => libs[lib]).map(lib =>
lib.toString().replace(/export /g, '').replace(/import.*?/g, ''))
.join('\n');
monaco.languages.typescript.typescriptDefaults.addExtraLib(`
${catLibs}
declare global {
${catlibsNoExport}
const log: Logger;
const deviceManager: DeviceManager;
const endpointManager: EndpointManager;
const mediaManager: MediaManager;
const systemManager: SystemManager;
const mqtt: MqttClient;
const device: ScryptedDeviceBase & { pathname : string };
}
`,
"node_modules/@types/scrypted__sdk/types/index.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['settingsMixin'],
"node_modules/@types/scrypted__sdk/settings-mixin.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['storageSettings'],
"node_modules/@types/scrypted__sdk/storage-settings.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['sdk'],
"node_modules/@types/scrypted__sdk/index.d.ts"
);
for (const lib of Object.keys(safeLibs)) {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
safeLibs[lib],
lib,
);
}
}
return `(function() {
const safeLibs = ${JSON.stringify(safeLibs)};
const libs = ${JSON.stringify(libs)};
return (monaco) => {
(${monacoEvalDefaultsFunction})(monaco, safeLibs, libs);
}
})();
`;
return createMonacoEvalDefaultsWithLibs(standardlibs, getScryptedLibs(), extraLibs);
}
export interface ScriptDeviceImpl extends ScriptDevice {

View File

@@ -79,4 +79,4 @@ export async function bind(server: dgram.Socket, port: number) {
}
}
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
export { ListenZeroSingleClientTimeoutError, listenZero, listenZeroSingleClient } from "../../server/src/listen-zero";

View File

@@ -1 +1 @@
export * from '@scrypted/server/src/media-helpers';
export { safeKillFFmpeg, ffmpegLogInitialOutput, safePrintFFmpegArguments } from '../../server/src/media-helpers';

View File

@@ -54,18 +54,18 @@ export async function read16BELengthLoop(readable: Readable, options: {
readable.on('readable', read);
await once(readable, 'end');
throw new Error('stream ended');
throw new StreamEndError('read16BELengthLoop');
}
export class StreamEndError extends Error {
constructor() {
super('stream ended');
constructor(where: string) {
super(`stream ended: ${where}`);
}
}
export async function readLength(readable: Readable, length: number): Promise<Buffer> {
if (readable.readableEnded || readable.destroyed)
throw new StreamEndError();
throw new StreamEndError('readLength start');
if (!length) {
return Buffer.alloc(0);
@@ -88,12 +88,12 @@ export async function readLength(readable: Readable, length: number): Promise<Bu
}
if (readable.readableEnded || readable.destroyed)
reject(new Error("stream ended during read"));
reject(new StreamEndError('readLength readable'));
};
const e = () => {
cleanup();
reject(new StreamEndError())
reject(new StreamEndError('readLength end'));
};
const cleanup = () => {

View File

@@ -1,5 +1,5 @@
import { RpcPeer } from "@scrypted/server/src/rpc";
import { createRpcSerializer } from "@scrypted/server/src/rpc-serializer";
import { RpcPeer } from "../../server/src/rpc";
import { createRpcSerializer } from "../../server/src/rpc-serializer";
import type { RTCSignalingSession } from "@scrypted/sdk";
export async function createBrowserSignalingSession(ws: WebSocket, localName: string, remoteName: string) {

View File

@@ -41,15 +41,15 @@ export function isPeerConnectionClosed(pc: RTCPeerConnection) {
|| pc.iceConnectionState === 'closed';
}
function silence() {
let ctx = new AudioContext(), oscillator = ctx.createOscillator();
const dest = ctx.createMediaStreamDestination();
oscillator.connect(dest);
oscillator.start();
const ret = dest.stream.getAudioTracks()[0];
ret.enabled = false;
return ret;
}
// function silence() {
// let ctx = new AudioContext(), oscillator = ctx.createOscillator();
// const dest = ctx.createMediaStreamDestination();
// oscillator.connect(dest);
// oscillator.start();
// const ret = dest.stream.getAudioTracks()[0];
// ret.enabled = false;
// return ret;
// }
function createOptions() {
const options: RTCSignalingOptions = {

View File

@@ -506,7 +506,7 @@ export class RtspClient extends RtspBase {
}
}
catch (e) {
this.client.destroy(e);
this.client.destroy(e as Error);
throw e;
}
}
@@ -572,7 +572,8 @@ export class RtspClient extends RtspBase {
}
}
catch (e) {
deferred.reject(e);
if (!deferred.finished)
deferred.reject(e as Error);
this.client.destroy();
}
};
@@ -725,7 +726,10 @@ export class RtspClient extends RtspBase {
Accept: 'application/sdp',
});
this.contentBase = response.headers['content-base'] || response.headers['content-location'];;
this.contentBase = response.headers['content-base'] || response.headers['content-location'];
// content base may be a relative path? seems odd.
if (this.contentBase)
this.contentBase = new URL(this.contentBase, this.url).toString();
return response;
}
@@ -1123,7 +1127,7 @@ export class RtspServer {
}
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
hostname?: string,
hostname: string,
pathToken?: string,
createServer?(duplex: Duplex): T,
}) {

View File

@@ -227,6 +227,10 @@ export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string
codec = 'pcm_alaw';
ffmpegEncoder = 'pcm_alaw';
}
else if (mline.payloadTypes?.includes(14)) {
codec = 'mp3';
ffmpegEncoder = 'mp3';
}
else {
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
// is this the default?

View File

@@ -1 +1 @@
export * from "@scrypted/server/src/sleep"
export { sleep } from "../../server/src/sleep";

View File

@@ -1,19 +1,15 @@
import sdk, { PluginFork } from '@scrypted/sdk';
import worker_threads from 'worker_threads';
import sdk, { ForkOptions, PluginFork } from '@scrypted/sdk';
import { createAsyncQueue } from './async-queue';
import os from 'os';
export type Zygote<T> = () => PluginFork<T>;
export function createZygote<T>(): Zygote<T> {
if (!worker_threads.isMainThread)
return;
let zygote = sdk.fork<T>();
export function createZygote<T>(options?: ForkOptions): Zygote<T> {
let zygote = sdk.fork<T>(options);
function* next() {
while (true) {
const cur = zygote;
zygote = sdk.fork<T>();
zygote = sdk.fork<T>(options);
yield cur;
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>

Before

Width:  |  Height:  |  Size: 667 B

View File

@@ -1,26 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Scrypted Management Console</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<noscript>
<strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,45 +0,0 @@
{
"name": "Scrypted Management Console",
"short_name": "Scrypted",
"icons": [
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "https://koush.github.io/scrypted/plugins/core/ui/img/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#424242"
}

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow:

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "v0.111.0-jammy-full"
version: "v0.116.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

@@ -7,8 +7,7 @@
# install script.
################################################################
ARG BASE="jammy"
ARG REPO="ubuntu"
FROM ${REPO}:${BASE} as header
FROM ubuntu:${BASE} as header
ENV DEBIAN_FRONTEND=noninteractive
@@ -72,8 +71,9 @@ RUN python3 -m pip install debugpy typing_extensions psutil
################################################################
FROM header as base
# intel opencl gpu for openvino
# intel opencl gpu and npu for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite

View File

@@ -17,9 +17,6 @@ RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg -
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
# intel opencl gpu for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"

View File

@@ -1,38 +1,52 @@
if [ "$(uname -m)" = "x86_64" ]
if [ "$(uname -m)" != "x86_64" ]
then
# this script previvously apt install intel-media-va-driver-non-free, but that seems to no longer be necessary.
# the intel provided script is disabled since it does not work with the 6.8 kernel in Ubuntu 24.04 or Proxmox 8.2.
# manual installation of the Intel graphics stuff is required.
# echo "Installing Intel graphics packages."
# apt-get update && apt-get install -y gpg-agent &&
# rm -f /usr/share/keyrings/intel-graphics.gpg &&
# curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
# echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
# apt-get -y update &&
# apt-get -y install intel-opencl-icd &&
# apt-get -y dist-upgrade;
# manual installation
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
rm -rf /tmp/neo && mkdir -p /tmp/neo && cd /tmp/neo &&
apt-get install -y ocl-icd-libopencl1 &&
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.16695.4/intel-igc-core_1.0.16695.4_amd64.deb &&
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.16695.4/intel-igc-opencl_1.0.16695.4_amd64.deb &&
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-level-zero-gpu-dbgsym_1.3.29377.6_amd64.ddeb &&
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-level-zero-gpu_1.3.29377.6_amd64.deb &&
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-opencl-icd-dbgsym_24.17.29377.6_amd64.ddeb &&
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/intel-opencl-icd_24.17.29377.6_amd64.deb &&
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.17.29377.6/libigdgmm12_22.3.19_amd64.deb &&
dpkg -i *.deb &&
cd /tmp && rm -rf /tmp/neo &&
apt-get -y dist-upgrade;
exit $?
else
echo "Intel graphics will not be installed on this architecture."
exit 0
fi
exit 0
# no errors beyond this point
set -e
# the intel provided script is disabled since it does not work with the 6.8 kernel in Ubuntu 24.04 or Proxmox 8.2.
# manual installation of the Intel graphics stuff is required.
# echo "Installing Intel graphics packages."
# apt-get update && apt-get install -y gpg-agent &&
# rm -f /usr/share/keyrings/intel-graphics.gpg &&
# curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
# echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
# apt-get -y update &&
# apt-get -y install intel-opencl-icd &&
# apt-get -y dist-upgrade;
# need intel-media-va-driver-non-free, but all the other intel packages are installed from Intel github.
echo "Installing Intel graphics packages."
apt-get update && apt-get install -y gpg-agent &&
rm -f /usr/share/keyrings/intel-graphics.gpg &&
curl -L https://repositories.intel.com/graphics/intel-graphics.key | gpg --dearmor --yes --output /usr/share/keyrings/intel-graphics.gpg &&
echo 'deb [arch=amd64,i386 signed-by=/usr/share/keyrings/intel-graphics.gpg] https://repositories.intel.com/graphics/ubuntu jammy arc' | tee /etc/apt/sources.list.d/intel.gpu.jammy.list &&
apt-get -y update &&
apt-get -y install intel-media-va-driver-non-free &&
apt-get -y dist-upgrade;
# manual installation
# https://github.com/intel/compute-runtime/releases/tag/24.13.29138.7
rm -rf /tmp/gpu && mkdir -p /tmp/gpu && cd /tmp/gpu
apt-get install -y ocl-icd-libopencl1
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb
curl -O -L https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu-dbgsym_1.3.30049.6_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-level-zero-gpu_1.3.30049.6_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb
curl -O -L https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb
dpkg -i *.deb
cd /tmp && rm -rf /tmp/gpu
apt-get -y dist-upgrade

View File

@@ -0,0 +1,69 @@
if [ "$(uname -m)" != "x86_64" ]
then
echo "Intel NPU 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")
if [ -z "$UBUNTU_22_04" ]
then
# proxmox is compatible with ubuntu 22.04, check for /etc/pve directory
if [ -d "/etc/pve" ]
then
UBUNTU_22_04=true
fi
fi
# needs either ubuntu 22.0.4 or 24.04
if [ -z "$UBUNTU_22_04" ] && [ -z "$UBUNTU_24_04" ]
then
echo "Intel NPU will not be installed. Ubuntu version could not be detected when checking lsb-release and /etc/os-release."
exit 0
fi
dpkg --purge --force-remove-reinstreq intel-driver-compiler-npu intel-fw-npu intel-level-zero-npu
# no errors beyond this point
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.6.0/intel-driver-compiler-npu_1.6.0.20240814-10390978568_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.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu22.04_amd64.deb
else
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.5.1/intel-driver-compiler-npu_1.5.1.20240708-9842236399_ubuntu24.04_amd64.deb
if [ -n "$INTEL_FW_NPU" ]
then
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-fw-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
fi
curl -O -L https://github.com/intel/linux-npu-driver/releases/download/v1.6.0/intel-level-zero-npu_1.6.0.20240814-10390978568_ubuntu24.04_amd64.deb
fi
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
apt -y update
apt -y install libtbb12
dpkg -i *.deb
cd /tmp && rm -rf /tmp/npu
apt-get -y dist-upgrade
if [ -n "$INTEL_FW_NPU" ]
then
echo
echo "###############################################################################"
echo "Intel NPU firmware was installed. Reboot the host to complete the installation."
echo "###############################################################################"
fi

View File

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

View File

@@ -3,8 +3,9 @@
################################################################
FROM header as base
# intel opencl gpu for openvino
# intel opencl gpu and npu for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-npu.sh | bash
# python 3.9 from ppa.
# 3.9 is the version with prebuilt support for tensorflow lite

View File

@@ -10,13 +10,20 @@ function readyn() {
}
cd /tmp
SCRYPTED_VERSION=v0.96.0
SCRYPTED_VERSION=v0.116.0
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
if [ -z "$VMID" ]
then
VMID=10443
fi
if [ -n "$SCRYPTED_RESTORE" ]
then
RESTORE_VMID=$VMID
VMID=10444
pct destroy $VMID 2>&1 > /dev/null
fi
echo "Downloading scrypted container backup."
if [ ! -f "$SCRYPTED_TAR_ZST" ]
then
@@ -75,6 +82,27 @@ else
echo "$CONF not found? Start on boot must be enabled manually."
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?"
if [ "$yn" != "y" ]
then
exit 1
fi
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
VMID=$RESTORE_VMID
echo "Moving data volume to backup..."
pct restore $VMID *.tar $@
pct destroy 10444
fi
echo "Adding udev rule: /etc/udev/rules.d/65-scrypted.rules"
readyn "Add udev rule for hardware acceleration? This may conflict with existing rules."
if [ "$yn" == "y" ]
@@ -82,6 +110,7 @@ then
sh -c "echo 'SUBSYSTEM==\"apex\", MODE=\"0666\"' > /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'KERNEL==\"renderD128\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'KERNEL==\"card0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'KERNEL==\"accel0\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1a6e\", ATTRS{idProduct}==\"089a\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
sh -c "echo 'SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"18d1\", ATTRS{idProduct}==\"9302\", MODE=\"0666\"' >> /etc/udev/rules.d/65-scrypted.rules"
udevadm control --reload-rules && udevadm trigger

View File

@@ -84,15 +84,33 @@ export function createAuthFetch<B, M>(
if (initialHeader && !hasHeader(headers, 'Authorization'))
setHeader(headers, 'Authorization', initialHeader);
const controller = new AbortController();
options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
const initialResponse = await h({
...options,
ignoreStatusCode: true,
signal: controller.signal,
// need to intercept the status code to check for 401.
// all other status codes will be handled according to the initial request options.
checkStatusCode(statusCode) {
// can handle a 401 if an credential is provided.
// however, not providing a credential is also valid, and should
// fall through to the normal response handling which may be interested
// in the 401 response.
if (statusCode === 401 && options.credential)
return true;
if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
return checker(statusCode);
}
return true;
},
responseType: 'readable',
});
if (initialResponse.statusCode !== 401 || !options.credential) {
if (!options?.ignoreStatusCode)
checkStatus(initialResponse.statusCode);
// if it's not a 401, just return the response.
if (initialResponse.statusCode !== 401) {
return {
...initialResponse,
body: await parser(initialResponse.body, options.responseType),

View File

@@ -21,7 +21,7 @@
],
"preLaunchTask": "npm: build",
"args": [
"login",
"serve",
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,12 +1,12 @@
{
"name": "scrypted",
"version": "1.3.16",
"version": "1.3.20",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.3.16",
"version": "1.3.20",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.3.3",

View File

@@ -1,6 +1,6 @@
{
"name": "scrypted",
"version": "1.3.16",
"version": "1.3.20",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"bin": {

View File

@@ -10,6 +10,7 @@ import semver from 'semver';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import { installServe, serveMain } from './service';
import { connectShell } from './shell';
import { convertRtspToMp4, printRtspUsage } from './rtsp-file';
if (!semver.gte(process.version, '16.0.0')) {
throw new Error('"node" version out of date. Please update node to v16 or higher.')
@@ -173,6 +174,14 @@ async function main() {
});
sdk.disconnect();
}
else if (process.argv[2] === 'rtsp') {
if (!process.argv[3]) {
printRtspUsage();
process.exit(1);
}
await convertRtspToMp4(process.argv[3], process.argv[4]);
}
else if (process.argv[2] === 'create-cert-json' && process.argv.length === 5) {
const key = fs.readFileSync(process.argv[3]).toString();
const cert = fs.readFileSync(process.argv[4]).toString();

View File

@@ -0,0 +1,72 @@
import child_process from 'child_process';
import fs from 'fs';
import path from 'path';
import { listenSingleRtspClient } from '../../../common/src/rtsp-server';
import { parseSdp } from '../../../common/src/sdp-utils';
import { once } from 'events';
export async function convertRtspToMp4(rtspFile: string, sessionFile?: string) {
// rtsp file will be in roughly:
// /nvr/scrypted-[id]/[session-timestamp]/[hour-timestamp]/[segment-timestamp].rtsp
// sdp can be found in
// /nvr/scrypted-[id]/[session-timestamp]/session.json
// or legacy:
// /nvr/scrypted-[id]/[session-timestamp]/session.sdp
const sessionDir = path.dirname(path.dirname(rtspFile));
let sdp: string;
let sessionJson = path.join(sessionDir, 'session.json');
if (!fs.existsSync(sessionJson) && sessionFile)
sessionJson = sessionFile.endsWith('.json') && sessionFile;
let sessionSdp = path.join(sessionDir, 'session.sdp');
if (!fs.existsSync(sessionSdp) && sessionFile)
sessionSdp = sessionFile.endsWith('.sdp') && sessionFile;
if (fs.existsSync(sessionJson)) {
sdp = JSON.parse(fs.readFileSync(sessionJson).toString()).sdp;
}
else if (fs.existsSync(sessionSdp)) {
sdp = fs.readFileSync(sessionSdp).toString();
}
else {
console.error('Could not find session sdp. Ensure the rtsp directory structure is intact or specify the path to the session file.');
console.error();
printRtspUsage();
process.exit(1);
}
const parsedSdp = parseSdp(sdp);
const hasAudio = parsedSdp.msections.some(msection => msection.type === 'audio');
const rtspContents = fs.readFileSync(rtspFile);
const clientPromise = await listenSingleRtspClient();
clientPromise.rtspServerPromise.then(async rtspServer => {
rtspServer.sdp = sdp;
await rtspServer.handlePlayback();
console.log('playing')
rtspServer.client.write(rtspContents);
rtspServer.client.end();
});
const mp4 = rtspFile + '.mp4';
const cp = child_process.spawn('ffmpeg', [
'-y',
'-i', clientPromise.url,
'-vcodec', 'copy',
...(hasAudio ? ['-acodec', 'aac'] : []),
mp4,
], {
stdio: 'inherit',
});
await once(cp, 'exit');
console.log('mp4 written to:', mp4);
}
export function printRtspUsage() {
console.log('usage: npx rtsp /path/to/nvr/file.rtsp [/path/to/nvr/session.json | /path/to/nvr/session.sdp]');
}

View File

@@ -12,6 +12,7 @@ async function sleep(ms: number) {
const EXIT_FILE = '.exit';
const UPDATE_FILE = '.update';
const VERSION_FILE = '.version';
async function runCommand(command: string, ...args: string[]) {
if (os.platform() === 'win32') {
@@ -117,6 +118,15 @@ export async function installServe(installVersion: string, ignoreError?: boolean
}
export async function serveMain(installVersion?: string) {
const { installDir, volume } = cwdInstallDir();
if (!installVersion) {
try {
installVersion = fs.readFileSync(path.join(volume, VERSION_FILE)).toString().trim();
}
catch (e) {
}
}
const options = ((): { install: true; version: string } | { install: false } => {
if (installVersion) {
console.log(`Installing @scrypted/server@${installVersion}`);
@@ -139,7 +149,6 @@ export async function serveMain(installVersion?: string) {
}
})();
const { installDir, volume } = cwdInstallDir();
if (options.install) {
await installServe(options.version, true);

View File

@@ -9,8 +9,10 @@
"inlineSources": true,
"declaration": true,
"moduleResolution": "Node16",
"strict": true
},
"strict": true,
"strictPropertyInitialization": false,
"strictNullChecks": false,
},
"include": [
"src/**/*"
],

View File

@@ -1,15 +1,15 @@
{
"name": "@scrypted/client",
"version": "1.3.5",
"version": "1.3.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.5",
"version": "1.3.6",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.3.27",
"@scrypted/types": "^0.3.40",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.6",
"rimraf": "^5.0.5"
@@ -84,9 +84,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.27",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.27.tgz",
"integrity": "sha512-XNtlqzqt6rHyNYwWrz3iiickh1h9ACwcLC3rfwxUbFk/Vq/UbDZgp0kGyj9UW6eLVNHzWFSE2dKqyyDS6V2KAg=="
"version": "0.3.40",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.40.tgz",
"integrity": "sha512-NBjNEfoLp7zL5Tf0odzf191oReDh4FEmZexDmMj1JbKDUMB9S8xJys3vbhcFadU/aUrUkyK/FSbkXv1z87bxSw=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -268,14 +268,14 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/engine.io-client": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.0.tgz",
"integrity": "sha512-iBtCdW5Tk3CnMAnC44VO4LwxXnl+RIq9ua1qHvxf5KSq2rzFgQFdfCSSl6Yuz2hl899SWTkfaT3c+WZQ42dJ8A==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
}
},
@@ -738,15 +738,15 @@
}
},
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.3.5",
"version": "1.3.6",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -18,7 +18,7 @@
"typescript": "^5.4.3"
},
"dependencies": {
"@scrypted/types": "^0.3.27",
"@scrypted/types": "^0.3.40",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.6",
"rimraf": "^5.0.5"

View File

@@ -57,9 +57,9 @@ export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
export interface ScryptedClientStatic extends ScryptedStatic {
userId?: string;
username?: string;
admin: boolean;
disconnect(): void;
onClose?: Function;
version: string;
rtcConnectionManagement?: RTCConnectionManagement;
browserSignalingSession?: BrowserSignalingSession;
address?: string;
@@ -163,6 +163,7 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
token: body.token as string,
addresses: body.addresses as string[],
externalAddresses: body.externalAddresses as string[],
hostname: body.hostname,
// the cloud plugin will include this header.
// should maybe move this into the cloud server itself.
scryptedCloud: response.headers.get('x-scrypted-cloud') === 'true',
@@ -225,6 +226,7 @@ export interface ScryptedClientLoginResult {
scryptedCloud: boolean;
directAddress: string;
cloudAddress: string;
hostname: string;
}
export class ScryptedClientLoginError extends Error {
@@ -270,6 +272,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
let scryptedCloud: boolean;
let directAddress: string;
let cloudAddress: string;
let hostname: string;
let token: string;
console.log('@scrypted/client', packageJson.version);
@@ -295,6 +298,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
authorization = loginResult.authorization;
queryToken = loginResult.queryToken;
token = loginResult.token;
hostname = loginResult.hostname;
console.log('login result', Date.now() - start, loginResult);
}
else {
@@ -367,6 +371,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
authorization = loginCheck.authorization;
queryToken = loginCheck.queryToken;
token = loginCheck.token;
hostname = loginCheck.hostname;
console.log('login checked', Date.now() - start, loginCheck);
}
@@ -518,7 +523,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e);
reject?.(e as Error);
}
});
@@ -532,9 +537,10 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
});
serializer.setupRpcPeer(upgradingPeer);
const readyClose = new Promise<RpcPeer>((resolve, reject) => {
check.on('close', () => reject(new Error('closed')))
})
// is this an issue?
// const readyClose = new Promise<RpcPeer>((resolve, reject) => {
// check.on('close', () => reject(new Error('closed')))
// })
upgradingPeer.params['session'] = session;
@@ -565,7 +571,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
dcSerializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e);
reject?.(e as Error);
pc.close();
}
});
@@ -666,7 +672,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e);
reject?.(e as Error);
}
});
socket.on('message', data => {
@@ -708,16 +714,16 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
});
}
const [version, rtcConnectionManagement] = await Promise.all([
const [admin, rtcConnectionManagement] = await Promise.all([
(async () => {
let version = 'unknown';
try {
// info is
const info = await systemManager.getComponent('info');
version = await info.getVersion();
return !!info;
}
catch (e) {
}
return version;
return false;
})(),
(async () => {
let rtcConnectionManagement: RTCConnectionManagement;
@@ -734,7 +740,6 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
]);
console.log('api initialized', Date.now() - start);
console.log('api queried, version:', version);
const userDevice = Object.keys(systemManager.getSystemState())
.map(id => systemManager.getDeviceById(id))
@@ -781,7 +786,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
serializer.sendMessage(message, reject, serializationContext);
}
catch (e) {
reject?.(e);
reject?.(e as Error);
}
});
serializer.setupRpcPeer(clusterPeer);
@@ -846,7 +851,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
pluginRemoteAPI: undefined,
address,
connectionType,
version,
admin,
systemManager,
deviceManager,
endpointManager,
@@ -868,6 +873,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
queryToken,
authorization,
cloudAddress,
hostname,
},
connectRPCObject,
fork: undefined,

View File

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

View File

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

View File

@@ -1,6 +1,11 @@
<details>
<summary>Changelog</summary>
### 0.3.2
alexa: fix syncedDevices being undefined
### 0.3.1
alexa/google-home: fix potential vulnerability. do not allow local network control using cloud tokens belonging to a different user. the plugins are now locked to a specific scrypted cloud account once paired.

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/alexa",
"version": "0.3.2",
"version": "0.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.3.2",
"version": "0.3.3",
"dependencies": {
"axios": "^1.3.4",
"uuid": "^9.0.0"

View File

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

View File

@@ -0,0 +1,82 @@
{
"CfgRuleId": 1,
"Class": "FaceDetection",
"CountInGroup": 2,
"DetectRegion": null,
"EventID": 10360,
"EventSeq": 6,
"Faces": [
{
"BoundingBox": [
1504,
2336,
1728,
2704
],
"Center": [
1616,
2520
],
"ObjectID": 94,
"ObjectType": "HumanFace",
"RelativeID": 0
}
],
"FrameSequence": 8251212,
"GroupID": 6,
"Mark": 0,
"Name": "FaceDetection",
"Object": {
"Action": "Appear",
"BoundingBox": [
1504,
2336,
1728,
2704
],
"Center": [
1616,
2520
],
"Confidence": 19,
"FrameSequence": 8251212,
"ObjectID": 94,
"ObjectType": "HumanFace",
"RelativeID": 0,
"SerialUUID": "",
"Source": 0.0,
"Speed": 0,
"SpeedTypeInternal": 0
},
"Objects": [
{
"Action": "Appear",
"BoundingBox": [
1504,
2336,
1728,
2704
],
"Center": [
1616,
2520
],
"Confidence": 19,
"FrameSequence": 8251212,
"ObjectID": 94,
"ObjectType": "HumanFace",
"RelativeID": 0,
"SerialUUID": "",
"Source": 0.0,
"Speed": 0,
"SpeedTypeInternal": 0
}
],
"PTS": 43774941350.0,
"Priority": 0,
"RuleID": 1,
"RuleId": 1,
"Source": -1280470024.0,
"UTC": 947510337,
"UTCMS": 0
}

View File

@@ -0,0 +1,62 @@
{
"Action": "Cross",
"Class": "Normal",
"CountInGroup": 1,
"DetectRegion": [
[
455,
260
],
[
3586,
260
],
[
3768,
7580
],
[
382,
7451
]
],
"Direction": "Enter",
"EventID": 10181,
"GroupID": 0,
"Name": "Rule1",
"Object": {
"Action": "Appear",
"BoundingBox": [
2856,
1280,
3880,
4880
],
"Center": [
3368,
3080
],
"Confidence": 0,
"LowerBodyColor": [
0,
0,
0,
0
],
"MainColor": [
0,
0,
0,
0
],
"ObjectID": 863,
"ObjectType": "Human",
"RelativeID": 0,
"Speed": 0
},
"PTS": 43380319830.0,
"RuleID": 2,
"Track": [],
"UTC": 1711446999,
"UTCMS": 701
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.151",
"version": "0.0.162",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.151",
"version": "0.0.162",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.151",
"version": "0.0.162",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -27,6 +27,8 @@
"name": "Amcrest Plugin",
"type": "DeviceProvider",
"interfaces": [
"ScryptedSystemDevice",
"ScryptedDeviceCreator",
"DeviceProvider",
"DeviceCreator"
],

View File

@@ -4,103 +4,10 @@ import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
import contentType from 'content-type';
import { IncomingMessage } from 'http';
import { EventEmitter, Readable } from 'stream';
import { Destroyable } from '../../rtsp/src/rtsp';
import { createRtspMediaStreamOptions, Destroyable, UrlMediaStreamOptions } from '../../rtsp/src/rtsp';
import { getDeviceInfo } from './probe';
import { Point } from '@scrypted/sdk';
import { MediaStreamConfiguration, MediaStreamOptions, Point } from '@scrypted/sdk';
// Human
// {
// "Action" : "Cross",
// "Class" : "Normal",
// "CountInGroup" : 1,
// "DetectRegion" : [
// [ 455, 260 ],
// [ 3586, 260 ],
// [ 3768, 7580 ],
// [ 382, 7451 ]
// ],
// "Direction" : "Enter",
// "EventID" : 10181,
// "GroupID" : 0,
// "Name" : "Rule1",
// "Object" : {
// "Action" : "Appear",
// "BoundingBox" : [ 2856, 1280, 3880, 4880 ],
// "Center" : [ 3368, 3080 ],
// "Confidence" : 0,
// "LowerBodyColor" : [ 0, 0, 0, 0 ],
// "MainColor" : [ 0, 0, 0, 0 ],
// "ObjectID" : 863,
// "ObjectType" : "Human",
// "RelativeID" : 0,
// "Speed" : 0
// },
// "PTS" : 43380319830.0,
// "RuleID" : 2,
// "Track" : [],
// "UTC" : 1711446999,
// "UTCMS" : 701
// }
// Face
// {
// "CfgRuleId" : 1,
// "Class" : "FaceDetection",
// "CountInGroup" : 2,
// "DetectRegion" : null,
// "EventID" : 10360,
// "EventSeq" : 6,
// "Faces" : [
// {
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0
// }
// ],
// "FrameSequence" : 8251212,
// "GroupID" : 6,
// "Mark" : 0,
// "Name" : "FaceDetection",
// "Object" : {
// "Action" : "Appear",
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "Confidence" : 19,
// "FrameSequence" : 8251212,
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0,
// "SerialUUID" : "",
// "Source" : 0.0,
// "Speed" : 0,
// "SpeedTypeInternal" : 0
// },
// "Objects" : [
// {
// "Action" : "Appear",
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "Confidence" : 19,
// "FrameSequence" : 8251212,
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0,
// "SerialUUID" : "",
// "Source" : 0.0,
// "Speed" : 0,
// "SpeedTypeInternal" : 0
// }
// ],
// "PTS" : 43774941350.0,
// "Priority" : 0,
// "RuleID" : 1,
// "RuleId" : 1,
// "Source" : -1280470024.0,
// "UTC" : 947510337,
// "UTCMS" : 0
// }
export interface AmcrestObjectDetails {
Action: string;
BoundingBox: Point;
@@ -174,6 +81,65 @@ async function readAmcrestMessage(client: Readable): Promise<string[]> {
}
}
function findValue(blob: string, prefix: string, key: string) {
const lines = blob.split('\n');
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
if (!value)
return;
const parts = value.split('=');
return parts[1];
}
function fromAmcrestAudioCodec(audioCodec: string) {
audioCodec = audioCodec?.trim();
if (audioCodec === 'AAC')
return 'aac';
if (audioCodec === 'G.711A')
return 'pcm_alaw';
if (audioCodec === 'G.711Mu')
return 'pcm_mulaw';
}
function toAmcrestAudioCodec(audioCodec: string) {
if (audioCodec === 'aac')
return 'AAC';
if (audioCodec === 'pcm_alaw')
return 'G.711A';
if (audioCodec === 'pcm_mulaw')
return 'G.711Mu';
}
function fromAmcrestVideoCodec(videoCodec: string) {
videoCodec = videoCodec?.trim();
if (videoCodec === 'H.264')
videoCodec = 'h264';
else if (videoCodec === 'H.265')
videoCodec = 'h265';
return videoCodec;
}
const amcrestResolutions = {
"1080P": [1920, 1080],
"720P": [1280, 720],
"D1": [704, 480],
"HD1": [352, 480],
"BCIF": [704, 240],
"2CIF": [704, 240],
"CIF": [352, 240],
"QCIF": [176, 120],
"NHD": [640, 360],
"VGA": [640, 480],
"QVGA": [320, 240]
};
function fromAmcrestResolution(resolution: string) {
const named = amcrestResolutions[resolution];
if (named)
return named;
const parts = resolution.split('x');
return [parseInt(parts[0]), parseInt(parts[1])];
}
export class AmcrestCameraClient {
credential: AuthFetchCredentialState;
@@ -371,6 +337,7 @@ export class AmcrestCameraClient {
async unlock(): Promise<boolean> {
const response = await this.request({
// channel 1? this may fail through nvr.
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=openDoor&channel=1&UserID=101&Type=Remote`,
responseType: 'text',
});
@@ -379,9 +346,223 @@ export class AmcrestCameraClient {
async lock(): Promise<boolean> {
const response = await this.request({
// channel 1? this may fail through nvr.
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=closeDoor&channel=1&UserID=101&Type=Remote`,
responseType: 'text',
});
return response.body.includes('OK');
}
async resetMotionDetection(cameraNumber: number) {
const params = new URLSearchParams();
params.set(`MotionDetect[${cameraNumber - 1}].Enable`, 'true');
// from amcrest docs:
// basically a 22x18 binary grid.
// so a full cell block is 4194303.
// Currently, a region is divided into 18 lines and 22 blocks per line.
// A bit describes a block in the line.
// Bit = 1: motion in this block is monitored.
// Example:
// MotionDetect [0].Region [0] = 4194303 (0x3FFFFF): the 22 blocks in
// channel 0 line 0 is monitored.
// MotionDetect [0].Region [1] =0: the 22 blocks in channel 0 line 1 is
// not monitored.
// MotionDetect [0].Region [17] = 3: the left two blocks in the last line o
// channel 0 is monitored.
// there are 4 configurable motion windows, will use the first one, index 0.
// each window is 18 lines, 22 blocks per line.
// not sure what this first line is.
// table.MotionDetect[0].Level=3
// table.MotionDetect[0].MotionDetectWindow[0].Id=0
// table.MotionDetect[0].MotionDetectWindow[0].Name=Region1
// table.MotionDetect[0].MotionDetectWindow[0].Region[0]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[1]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[2]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[3]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[4]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[5]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[6]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[7]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[8]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[9]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[10]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[11]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[12]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[13]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[14]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[15]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[16]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Region[17]=4194303
// table.MotionDetect[0].MotionDetectWindow[0].Sensitive=60
// table.MotionDetect[0].MotionDetectWindow[0].Threshold=5
// doesn't seem to be able to be renamed.
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Name`, 'Scrypted');
for (let i = 0; i < 18; i++) {
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Region[${i}]`, '4194303');
}
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Sensitive`, '60');
params.set(`MotionDetect[${cameraNumber - 1}].MotionDetectWindow[0].Threshold`, '5');
const response = await this.request({
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reset motion result', response.body);
}
async configureCodecs(cameraNumber: number, options: MediaStreamConfiguration) {
if (!options.id?.startsWith('channel'))
throw new Error('invalid id');
const capsResponse = await this.request({
url: `http://${this.ip}/cgi-bin/encode.cgi?action=getConfigCaps&channel=${cameraNumber}`,
responseType: 'text',
});
this.console.log(capsResponse.body);
const formatNumber = Math.max(0, parseInt(options.id?.substring('channel'.length)) - 1);
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
const encode = `Encode[${cameraNumber - 1}].${format}[${formatNumber}]`;
const params = new URLSearchParams();
if (options.video?.bitrate) {
let bitrate = options?.video?.bitrate;
bitrate = Math.round(bitrate / 1000);
params.set(`${encode}.Video.BitRate`, bitrate.toString());
}
if (options.video?.codec === 'h264') {
params.set(`${encode}.Video.Compression`, 'H.264');
params.set(`${encode}.VideoEnable`, 'true');
}
if (options.video?.profile) {
let profile = 'Main';
if (options.video.profile === 'high')
profile = 'High';
else if (options.video.profile === 'baseline')
profile = 'Baseline';
params.set(`${encode}.Video.Profile`, profile);
}
if (options.video?.codec === 'h265') {
params.set(`${encode}.Video.Compression`, 'H.265');
}
if (options.video?.width && options.video?.height) {
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
}
if (options.video?.fps) {
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
}
if (options.video?.keyframeInterval) {
params.set(`${encode}.Video.GOP`, options.video?.keyframeInterval.toString());
}
if (options.video?.bitrateControl) {
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'constant' ? 'CBR' : 'VBR');
}
if (options.audio?.codec) {
params.set(`${encode}.Audio.Compression`, toAmcrestAudioCodec(options.audio.codec));
params.set(`${encode}.AudioEnable`, 'true');
}
// nothing else audio related seems configurable.
if ([...params.keys()].length) {
const response = await this.request({
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reconfigure result', response.body);
}
const caps = `caps[${cameraNumber - 1}].${format}[${formatNumber}]`;
const singleCaps = `caps.${format}[${formatNumber}]`;
const findCaps = (key: string) => {
const found = findValue(capsResponse.body, caps, key);
if (found)
return found;
// ad410 doesnt return a camera number if accessed directly
if (cameraNumber - 1 === 0)
return findValue(capsResponse.body, singleCaps, key);
}
const resolutions = findCaps('Video.ResolutionTypes').split(',').map(fromAmcrestResolution);
const bitrates = findCaps('Video.BitRateOptions').split(',').map(s => parseInt(s) * 1000);
const fpsMax = parseInt(findCaps('Video.FPSMax'));
const vso: MediaStreamConfiguration = {
id: options.id,
video: {},
};
vso.video.resolutions = resolutions;
vso.video.bitrateRange = [bitrates[0], bitrates[bitrates.length - 1]];
vso.video.fpsRange = [1, fpsMax];
return vso;
}
async getCodecs(cameraNumber: number): Promise<UrlMediaStreamOptions[]> {
const masResponse = await this.request({
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
responseType: 'text',
});
const mas = masResponse.body.split('=')[1].trim();
// amcrest reports more streams than are acually available in its responses,
// so checking the max extra streams prevents usage of invalid streams.
const maxExtraStreams = parseInt(mas) || 1;
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => createRtspMediaStreamOptions(undefined, subtype));
const encodeResponse = await this.request({
url: `http://${this.ip}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
responseType: 'text',
});
this.console.log(encodeResponse.body);
for (let i = 0; i < vsos.length; i++) {
const vso = vsos[i];
let encName: string;
if (i === 0) {
encName = `table.Encode[${cameraNumber - 1}].MainFormat[0]`;
}
else {
encName = `table.Encode[${cameraNumber - 1}].ExtraFormat[${i - 1}]`;
}
const videoCodec = fromAmcrestVideoCodec(findValue(encodeResponse.body, encName, 'Video.Compression'));
const audioCodec = fromAmcrestAudioCodec(findValue(encodeResponse.body, encName, 'Audio.Compression'));
if (vso.audio)
vso.audio.codec = audioCodec;
vso.video.codec = videoCodec;
const width = findValue(encodeResponse.body, encName, 'Video.Width');
const height = findValue(encodeResponse.body, encName, 'Video.Height');
if (width && height) {
vso.video.width = parseInt(width);
vso.video.height = parseInt(height);
}
const videoEnable = findValue(encodeResponse.body, encName, 'VideoEnable');
if (videoEnable?.trim() === 'false') {
this.console.warn('Video stream is disabled and should likely be enabled:', encName);
continue;
}
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
if (!encodeOptions)
continue;
vso.video.bitrate = parseInt(encodeOptions) * 1000;
}
return vsos;
}
}

View File

@@ -0,0 +1,24 @@
import { AudioStreamConfiguration, Setting } from '@scrypted/sdk';
import { autoconfigureCodecs as ac } from '../../../common/src/autoconfigure-codecs';
import { AmcrestCameraClient } from "./amcrest-api";
export const amcrestAutoConfigureSettings: Setting = {
key: 'amcrest-autoconfigure',
type: 'html',
value: 'Amcrest autoconfiguration will configure the camera codecs and the motion sensor.',
};
export async function autoconfigureSettings(client: AmcrestCameraClient, cameraNumber: number) {
const audioOptions: AudioStreamConfiguration = {
codec: 'aac',
sampleRate: 8000,
};
await client.resetMotionDetection(cameraNumber);
return ac(
() => client.getCodecs(cameraNumber),
options => client.configureCodecs(cameraNumber, options),
audioOptions,
);
}

View File

@@ -1,26 +1,27 @@
import { automaticallyConfigureSettings, checkPluginNeedsAutoConfigure } from "@scrypted/common/src/autoconfigure-codecs";
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
import { readLength } from "@scrypted/common/src/read-stream";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable, Stream } from "stream";
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { createRtspMediaStreamOptions, RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api";
import { amcrestAutoConfigureSettings, autoconfigureSettings } from "./amcrest-configure";
import { group } from "console";
const { mediaManager } = sdk;
const AMCREST_DOORBELL_TYPE = 'Amcrest Doorbell';
const DAHUA_DOORBELL_TYPE = 'Dahua Doorbell';
function findValue(blob: string, prefix: string, key: string) {
const lines = blob.split('\n');
const value = lines.find(line => line.startsWith(`${prefix}.${key}`));
if (!value)
return;
const parts = value.split('=');
return parts[1];
}
const rtspChannelSetting: Setting = {
subgroup: 'Advanced',
key: 'rtspChannel',
title: 'Channel Number Override',
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
placeholder: '1',
};
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, Lock, VideoRecorder, Reboot, ObjectDetector {
eventStream: Stream;
@@ -110,48 +111,10 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
this.info = deviceInfo;
}
async setVideoStreamOptions(options: MediaStreamOptions): Promise<void> {
if (!options.id?.startsWith('channel'))
throw new Error('invalid id');
async setVideoStreamOptions(options: MediaStreamOptions) {
const channel = parseInt(this.getRtspChannel()) || 1;
const formatNumber = parseInt(options.id?.substring('channel'.length)) - 1;
const format = options.id === 'channel0' ? 'MainFormat' : 'ExtraFormat';
const encode = `Encode[${channel - 1}].${format}[${formatNumber}]`;
const params = new URLSearchParams();
if (options.video?.bitrate) {
let bitrate = options?.video?.bitrate;
if (!bitrate)
return;
bitrate = Math.round(bitrate / 1000);
params.set(`${encode}.Video.BitRate`, bitrate.toString());
}
if (options.video?.codec === 'h264') {
params.set(`${encode}.Video.Compression`, 'H.264');
}
if (options.video?.codec === 'h265') {
params.set(`${encode}.Video.Compression`, 'H.265');
}
if (options.video?.width && options.video?.height) {
params.set(`${encode}.Video.resolution`, `${options.video.width}x${options.video.height}`);
}
if (options.video?.fps) {
params.set(`${encode}.Video.FPS`, options.video.fps.toString());
if (options.video?.idrIntervalMillis) {
params.set(`${encode}.Video.GOP`, (options.video.fps * options.video?.idrIntervalMillis / 1000).toString());
}
}
if (options.video?.bitrateControl) {
params.set(`${encode}.Video.BitRateControl`, options.video.bitrateControl === 'variable' ? 'VBR' : 'CBR');
}
if (![...params.keys()].length)
return;
const response = await this.getClient().request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=setConfig&${params}`,
responseType: 'text',
});
this.console.log('reconfigure result', response.body);
const client = this.getClient();
return client.configureCodecs(channel, options);
}
getClient() {
@@ -196,8 +159,9 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
else if (event === AmcrestEvent.MotionInfo) {
// this seems to be a motion pulse
if (this.motionDetected)
resetMotionTimeout();
if (!this.motionDetected)
this.motionDetected = true;
resetMotionTimeout();
}
else if (event === AmcrestEvent.MotionStop) {
// use resetMotionTimeout
@@ -283,6 +247,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const ret = await super.getOtherSettings();
ret.push(
{
subgroup: 'Advanced',
title: 'Doorbell Type',
choices: [
'Not a Doorbell',
@@ -353,6 +318,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
ret.push(
{
subgroup: 'Advanced',
title: 'Two Way Audio',
value: twoWayAudio,
key: 'twoWayAudio',
@@ -369,8 +335,18 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// },
);
return ret;
const ac = {
...automaticallyConfigureSettings,
subgroup: 'Advanced',
};
ac.type = 'button';
ret.push(ac);
ret.push({
...amcrestAutoConfigureSettings,
subgroup: 'Advanced',
});
return ret;
}
async takeSmartCameraPicture(options?: RequestPictureOptions): Promise<MediaObject> {
@@ -378,15 +354,12 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async getUrlSettings() {
const rtspChannel = {
...rtspChannelSetting,
value: this.storage.getItem('rtspChannel'),
};
return [
{
key: 'rtspChannel',
title: 'Channel Number Override',
subgroup: 'Advanced',
description: "The channel number to use for snapshots and video. E.g., 1, 2, etc.",
placeholder: '1',
value: this.storage.getItem('rtspChannel'),
},
rtspChannel,
...await super.getUrlSettings(),
]
}
@@ -396,7 +369,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
createRtspMediaStreamOptions(url: string, index: number) {
const ret = super.createRtspMediaStreamOptions(url, index);
const ret = createRtspMediaStreamOptions(url, index);
ret.tool = 'scrypted';
return ret;
}
@@ -404,98 +377,38 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
const client = this.getClient();
if (!this.videoStreamOptions) {
this.videoStreamOptions = (async () => {
let mas: string;
if (this.videoStreamOptions)
return this.videoStreamOptions;
this.videoStreamOptions = (async () => {
const cameraNumber = parseInt(this.getRtspChannel()) || 1;
try {
let vsos: UrlMediaStreamOptions[];
try {
const response = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/magicBox.cgi?action=getProductDefinition&name=MaxExtraStream`,
responseType: 'text',
})
mas = response.body.split('=')[1].trim();
this.storage.setItem('maxExtraStreams', mas.toString());
}
catch (e) {
this.console.error('error retrieving max extra streams', e);
mas = this.storage.getItem('maxExtraStreams');
}
const maxExtraStreams = parseInt(mas) || 1;
const channel = parseInt(this.getRtspChannel()) || 1;
const vsos = [...Array(maxExtraStreams + 1).keys()].map(subtype => this.createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${channel}&subtype=${subtype}`, subtype));
try {
const capResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/encode.cgi?action=getConfigCaps&channel=0`,
responseType: 'text',
});
this.console.log(capResponse.body);
const encodeResponse = await client.request({
url: `http://${this.getHttpAddress()}/cgi-bin/configManager.cgi?action=getConfig&name=Encode`,
responseType: 'text',
});
this.console.log(encodeResponse.body);
for (let i = 0; i < vsos.length; i++) {
const vso = vsos[i];
let capName: string;
let encName: string;
if (i === 0) {
capName = `caps[${channel - 1}].MainFormat[0]`;
encName = `table.Encode[${channel - 1}].MainFormat[0]`;
}
else {
capName = `caps[${channel - 1}].ExtraFormat[${i - 1}]`;
encName = `table.Encode[${channel - 1}].ExtraFormat[${i - 1}]`;
}
const videoCodec = findValue(encodeResponse.body, encName, 'Video.Compression')
?.replace('.', '')?.toLowerCase()?.trim();
let audioCodec = findValue(encodeResponse.body, encName, 'Audio.Compression')
?.replace('.', '')?.toLowerCase()?.trim();
if (audioCodec?.includes('aac'))
audioCodec = 'aac';
else if (audioCodec?.includes('g711a'))
audioCodec = 'pcm_alaw';
else if (audioCodec?.includes('g711u'))
audioCodec = 'pcm_mulaw';
else if (audioCodec?.includes('g711'))
audioCodec = 'pcm';
if (vso.audio)
vso.audio.codec = audioCodec;
vso.video.codec = videoCodec;
const width = findValue(encodeResponse.body, encName, 'Video.Width');
const height = findValue(encodeResponse.body, encName, 'Video.Height');
if (width && height) {
vso.video.width = parseInt(width);
vso.video.height = parseInt(height);
}
const bitrateOptions = findValue(capResponse.body, capName, 'Video.BitRateOptions');
if (!bitrateOptions)
continue;
const encodeOptions = findValue(encodeResponse.body, encName, 'Video.BitRate');
if (!encodeOptions)
continue;
const [min, max] = bitrateOptions.split(',');
if (!min || !max)
continue;
vso.video.bitrate = parseInt(encodeOptions) * 1000;
vso.video.maxBitrate = parseInt(max) * 1000;
vso.video.minBitrate = parseInt(min) * 1000;
}
vsos = await client.getCodecs(cameraNumber);
this.storage.setItem('vsosJSON', JSON.stringify(vsos));
}
catch (e) {
this.console.error('error retrieving stream configurations', e);
vsos = JSON.parse(this.storage.getItem('vsosJSON')) as UrlMediaStreamOptions[];
}
for (const [index, vso] of vsos.entries()) {
vso.tool = 'scrypted';
vso.url = `rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${index}`;
}
return vsos;
})();
}
}
catch (e) {
this.videoStreamOptions = undefined;
const vsos = [...Array(2).keys()].map(subtype => {
const ret = createRtspMediaStreamOptions(`rtsp://${this.getRtspAddress()}/cam/realmonitor?channel=${cameraNumber}&subtype=${subtype}`, subtype);
ret.tool = 'scrypted';
return ret;
});
return vsos;
}
})();
return this.videoStreamOptions;
}
@@ -534,6 +447,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async putSetting(key: string, value: string) {
if (key === automaticallyConfigureSettings.key) {
const client = this.getClient();
autoconfigureSettings(client, parseInt(this.getRtspChannel()) || 1)
.then(() => {
this.log.a('Successfully configured settings.');
})
.catch(e => {
this.log.a('There was an error automatically configuring settings. More information can be viewed in the console.');
this.console.error('error autoconfiguring', e);
});
return;
}
if (key === 'continuousRecording') {
if (value === 'true') {
try {
@@ -575,7 +501,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// not sure if this all works, since i don't actually have a doorbell.
// good luck!
const channel = this.getRtspChannel() || '1';
const channel = parseInt(this.getRtspChannel()) || 1;
const buffer = await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput);
const ffmpegInput = JSON.parse(buffer.toString()) as FFmpegInput;
@@ -693,6 +619,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
class AmcrestProvider extends RtspProvider {
constructor(nativeId?: ScryptedNativeId) {
super(nativeId);
checkPluginNeedsAutoConfigure(this);
}
getAdditionalInterfaces() {
return [
ScryptedInterface.Reboot,
@@ -703,6 +634,9 @@ class AmcrestProvider extends RtspProvider {
];
}
getScryptedDeviceCreator(): string {
return 'Amcrest Camera';
}
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
const httpAddress = `${settings.ip}:${settings.httpPort || 80}`;
@@ -712,8 +646,14 @@ class AmcrestProvider extends RtspProvider {
const password = settings.password?.toString();
const skipValidate = settings.skipValidate?.toString() === 'true';
let twoWayAudio: string;
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
if (settings.autoconfigure) {
const cameraNumber = parseInt(settings.rtspChannel as string) || 1;
await autoconfigureSettings(api, cameraNumber);
}
if (!skipValidate) {
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
try {
const deviceInfo = await api.getDeviceInfo();
@@ -744,8 +684,10 @@ class AmcrestProvider extends RtspProvider {
device.info = info;
device.putSetting('username', username);
device.putSetting('password', password);
device.setIPAddress(settings.ip?.toString());
if (settings.rtspChannel)
device.putSetting('rtspChannel', settings.rtspChannel as string);
device.setHttpPortOverride(settings.httpPort?.toString());
device.setIPAddress(settings.ip?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
device.updateDeviceInfo();
@@ -768,13 +710,18 @@ class AmcrestProvider extends RtspProvider {
title: 'IP Address',
placeholder: '192.168.2.222',
},
rtspChannelSetting,
{
subgroup: 'Advanced',
key: 'httpPort',
title: 'HTTP Port',
description: 'Optional: Override the HTTP Port from the default value of 80',
description: 'Optional: Override the HTTP Port from the default value of 80.',
placeholder: '80',
},
automaticallyConfigureSettings,
amcrestAutoConfigureSettings,
{
subgroup: 'Advanced',
key: 'skipValidate',
title: 'Skip Validation',
description: 'Add the device without verifying the credentials and network settings.',
@@ -786,6 +733,7 @@ class AmcrestProvider extends RtspProvider {
createCamera(nativeId: string) {
return new AmcrestCamera(nativeId, this);
}
}
export default AmcrestProvider;

View File

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

View File

@@ -1,4 +1,4 @@
# BTicino C300X Plugin for Scrypted
# BTicino Intercom Plugin for Scrypted
The C300X Plugin for Scrypted allows viewing your C300X intercom with incoming video/audio.

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/bticino",
"version": "0.0.16",
"version": "0.0.18",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -20,9 +20,11 @@
"sip"
],
"scrypted": {
"name": "BTicino SIP Plugin",
"name": "BTicino Intercom Plugin",
"type": "DeviceProvider",
"interfaces": [
"ScryptedSystemDevice",
"ScryptedDeviceCreator",
"DeviceProvider",
"DeviceCreator"
],
@@ -32,14 +34,14 @@
]
},
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@slyoldfox/sip": "^0.0.6-1",
"sdp": "^3.0.3",
"stun": "^2.1.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.9.6",
"@types/node": "^20.11.30",
"cross-env": "^7.0.3",
"ts-node": "^10.9.1"
}

View File

@@ -4,9 +4,12 @@ import { VoicemailHandler } from "./bticino-voicemailHandler";
export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, HttpRequestHandler {
private timeout : NodeJS.Timeout
private voicemailHandler : VoicemailHandler
constructor(private camera: BticinoSipCamera, private voicemailHandler : VoicemailHandler) {
constructor(private camera: BticinoSipCamera) {
super( camera.nativeId + "-aswm-switch")
this.voicemailHandler = new VoicemailHandler(camera)
camera.requestHandlers.add(this.voicemailHandler)
this.timeout = setTimeout( () => this.syncStatus() , 5000 )
}
@@ -29,6 +32,7 @@ export class BticinoAswmSwitch extends ScryptedDeviceBase implements OnOff, Http
if( this.timeout ) {
clearTimeout(this.timeout)
}
this.voicemailHandler?.cancelTimer()
}
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {

View File

@@ -2,13 +2,12 @@ import { createBindUdp, listenZeroSingleClient } from '@scrypted/common/src/list
import { sleep } from '@scrypted/common/src/sleep';
import { RtspServer } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, MotionSensor, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, MotionSensor, PictureOptions, Reboot, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
import { SipCallSession } from '../../sip/src/sip-call-session';
import { RtpDescription, getPayloadType, getSequenceNumber, isRtpMessagePayloadType, isStunMessage } from '../../sip/src/rtp-utils';
import { VoicemailHandler } from './bticino-voicemailHandler';
import { CompositeSipMessageHandler } from '../../sip/src/compositeSipMessageHandler';
import { SipHelper } from './sip-helper';
import child_process, { ChildProcess } from 'child_process';
import child_process from 'child_process';
import { BticinoStorageSettings } from './storage-settings';
import { BticinoSipPlugin } from './main';
import { BticinoSipLock } from './bticino-lock';
@@ -29,7 +28,6 @@ import { ControllerApi } from './c300x-controller-api';
import { BticinoAswmSwitch } from './bticino-aswm-switch';
import { BticinoMuteSwitch } from './bticino-mute-switch';
const STREAM_TIMEOUT = 65000;
const { mediaManager } = sdk;
const BTICINO_CLIPS = path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'bticino-clips');
@@ -38,16 +36,14 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
private session: SipCallSession
private remoteRtpDescription: Promise<RtpDescription>
private forwarder
private refreshTimeout: NodeJS.Timeout
public requestHandlers: CompositeSipMessageHandler = new CompositeSipMessageHandler()
public incomingCallRequest : SipRequest
private settingsStorage: BticinoStorageSettings = new BticinoStorageSettings( this )
private voicemailHandler : VoicemailHandler = new VoicemailHandler(this)
private inviteHandler : InviteHandler = new InviteHandler(this)
private controllerApi : ControllerApi = new ControllerApi(this)
private muteSwitch : BticinoMuteSwitch
private aswmSwitch : BticinoAswmSwitch
private deferredCleanup
private deferredCleanup: () => void
private currentMediaObject : Promise<MediaObject>
private lastImageRefresh : number
//TODO: randomize this
@@ -60,7 +56,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
constructor(nativeId: string, public provider: BticinoSipPlugin) {
super(nativeId)
this.requestHandlers.add( this.voicemailHandler ).add( this.inviteHandler )
this.requestHandlers.add( this.inviteHandler )
this.persistentSipManager = new PersistentSipManager( this );
(async() => {
this.doorbellWebhookUrl = await this.doorbellWebhookEndpoint()
@@ -149,7 +145,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
}).on('error', (error) => {
this.console.error(error)
reject(error)
} ).end(); ;
} ).end();
});
}
@@ -283,8 +279,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
// get a proxy object to make sure we pass prebuffer when already watching a stream
let cam : VideoCamera = sdk.systemManager.getDeviceById<VideoCamera>(this.id)
let vs : MediaObject = await cam.getVideoStream()
let buf : Buffer = await mediaManager.convertMediaObjectToBuffer(vs, 'image/jpeg');
this.cachedImage = buf
this.cachedImage = await mediaManager.convertMediaObjectToBuffer(vs, 'image/jpeg');
this.lastImageRefresh = new Date().getTime()
this.console.log(`Camera picture updated and cached: ${this.lastImageRefresh} + cache time: ${thumbnailCacheTime} < ${now}`)
@@ -349,19 +344,13 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
this.forwarder = undefined
}
resetStreamTimeout() {
this.log.d('starting/refreshing stream')
clearTimeout(this.refreshTimeout)
this.refreshTimeout = setTimeout(() => this.stopSession(), STREAM_TIMEOUT)
}
hasActiveCall() {
return this.session;
}
stopSession() {
if (this.session) {
this.log.d('ending sip session')
this.console.log('ending sip session')
this.session.stop()
this.session = undefined
}
@@ -406,7 +395,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
if (this.session === sip)
this.session = undefined
try {
this.log.d('cleanup(): stopping sip session.')
this.console.log('cleanup(): stopping sip session.')
sip?.stop()
this.currentMediaObject = undefined
}
@@ -479,7 +468,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
this.session = sip
videoSplitter.server.on('message', (message, rinfo) => {
videoSplitter.server.on('message', (message:Buffer) => {
if ( !isStunMessage(message)) {
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
if (!isRtpMessage)
@@ -498,7 +487,7 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
}
});
audioSplitter.server.on('message', (message, rinfo ) => {
audioSplitter.server.on('message', (message:Buffer) => {
if ( !isStunMessage(message)) {
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
if (!isRtpMessage)
@@ -617,13 +606,17 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
async getDevice(nativeId: string) : Promise<any> {
if( nativeId && nativeId.endsWith('-aswm-switch')) {
this.aswmSwitch = new BticinoAswmSwitch(this, this.voicemailHandler)
this.aswmSwitch = new BticinoAswmSwitch(this)
this.aswmSwitch.info = this.info
return this.aswmSwitch
} else if( nativeId && nativeId.endsWith('-mute-switch') ) {
this.muteSwitch = new BticinoMuteSwitch(this)
this.muteSwitch.info = this.info
return this.muteSwitch
}
return new BticinoSipLock(this)
const lock = new BticinoSipLock(this)
lock.info = this.info
return lock
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
@@ -633,7 +626,6 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
this.muteSwitch.cancelTimer()
} else {
this.stopIntercom()
this.voicemailHandler.cancelTimer()
this.persistentSipManager.cancelTimer()
this.controllerApi.cancelTimer()
}

View File

@@ -51,6 +51,9 @@ export class ControllerApi {
res.on("end", () => {
try {
let parsedBody = JSON.parse( body )
if( !parsedBody["model"] ) {
reject( new Error("Cannot determine model, update your c300x-controller.") )
}
if( parsedBody["errors"].length > 0 ) {
reject( new Error( parsedBody["errors"][0] ) )
} else {

View File

@@ -1,7 +1,8 @@
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, LockState, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, LockState, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'
import { randomBytes } from 'crypto'
import { BticinoSipCamera } from './bticino-camera'
import { ControllerApi } from './c300x-controller-api';
import { SipHelper } from './sip-helper';
const { systemManager, deviceManager } = sdk
@@ -9,6 +10,13 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
devices = new Map<string, BticinoSipCamera>()
constructor() {
super();
this.systemDevice = {
deviceCreator: 'Bticino Doorbell',
};
}
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
@@ -24,6 +32,18 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
]
}
deviceInfo(setupData) : DeviceInformation {
return {
model: setupData["model"].toLocaleUpperCase(),
manufacturer: `Bticino (c300x-controller v${setupData["version"]})`,
version: setupData["version"],
firmware: setupData["firmware"],
ip: setupData["ipAddress"],
mac: setupData["macAddress"],
managementUrl: 'http://' + setupData["ipAddress"] + ':8080'
}
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
if( !settings.ip ) {
throw new Error('IP address is required!')
@@ -34,16 +54,12 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
return validate.then( async (setupData) => {
const nativeId = randomBytes(4).toString('hex')
const name = settings.newCamera?.toString() === undefined ? "Doorbell" : settings.newCamera?.toString()
await this.updateDevice(nativeId, name)
const deviceInfo : DeviceInformation = this.deviceInfo(setupData)
await this.updateDevice(nativeId, name, deviceInfo)
const lockDevice: Device = {
providerNativeId: nativeId,
info: {
//model: `${camera.model} (${camera.data.kind})`,
manufacturer: 'BticinoPlugin',
//firmware: camera.data.firmware_version,
//serialNumber: camera.data.device_id
},
info: deviceInfo,
nativeId: nativeId + '-lock',
name: name + ' Lock',
type: ScryptedDeviceType.Lock,
@@ -52,12 +68,7 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
const aswmSwitchDevice: Device = {
providerNativeId: nativeId,
info: {
//model: `${camera.model} (${camera.data.kind})`,
manufacturer: 'BticinoPlugin',
//firmware: camera.data.firmware_version,
//serialNumber: camera.data.device_id
},
info: deviceInfo,
nativeId: nativeId + '-aswm-switch',
name: name + ' Voicemail',
type: ScryptedDeviceType.Switch,
@@ -66,27 +77,23 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
const muteSwitchDevice: Device = {
providerNativeId: nativeId,
info: {
//model: `${camera.model} (${camera.data.kind})`,
manufacturer: 'BticinoPlugin',
//firmware: camera.data.firmware_version,
//serialNumber: camera.data.device_id
},
info: deviceInfo,
nativeId: nativeId + '-mute-switch',
name: name + ' Muted',
type: ScryptedDeviceType.Switch,
interfaces: [ScryptedInterface.OnOff, ScryptedInterface.HttpRequestHandler],
}
}
const devices = setupData["model"] === 'c100x' ? [lockDevice, muteSwitchDevice] : [lockDevice, aswmSwitchDevice, muteSwitchDevice]
await deviceManager.onDevicesChanged({
providerNativeId: nativeId,
devices: [lockDevice, aswmSwitchDevice, muteSwitchDevice],
devices: devices
})
let sipCamera : BticinoSipCamera = await this.getDevice(nativeId)
sipCamera.putSetting("sipfrom", "scrypted-" + sipCamera.id + "@127.0.0.1")
sipCamera.putSetting("sipto", "c300x@" + setupData["ipAddress"] )
sipCamera.putSetting("sipto", setupData["model"] + "@" + setupData["ipAddress"] )
sipCamera.putSetting("sipdomain", setupData["domain"])
sipCamera.putSetting("sipdebug", true )
@@ -99,15 +106,10 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
})
}
updateDevice(nativeId: string, name: string) {
updateDevice(nativeId: string, name: string, deviceInfo) {
return deviceManager.onDeviceDiscovered({
nativeId,
info: {
//model: `${camera.model} (${camera.data.kind})`,
manufacturer: 'BticinoSipPlugin',
//firmware: camera.data.firmware_version,
//serialNumber: camera.data.device_id
},
info: deviceInfo,
name,
interfaces: [
ScryptedInterface.Camera,
@@ -128,6 +130,9 @@ export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvid
async getDevice(nativeId: string): Promise<any> {
if (!this.devices.has(nativeId)) {
const camera = new BticinoSipCamera(nativeId, this)
ControllerApi.validate(SipHelper.getIntercomIp(camera)).then( async (setupData) => {
camera.info = this.deviceInfo(setupData)
} )
this.devices.set(nativeId, camera)
}
return this.devices.get(nativeId)

View File

@@ -49,11 +49,23 @@ export class BticinoStorageSettings {
description: 'Enable SIP debugging',
placeholder: 'true or false',
},
DEVADDR: {
title: 'Device address (DEVADDR)',
type: 'string',
description: 'Only specify if this is different than 20. For c100x this is a UUID, see: tcpdump -i lo port 5060',
defaultValue: '20',
placeholder: '20',
},
notifyVoicemail: {
title: 'Notify on new voicemail messages',
type: 'boolean',
description: 'Enable voicemail alerts',
placeholder: 'true or false',
onGet: async () => {
return {
hide: this.storageSettings.values.sipto.indexOf('c100x') == 0,
}
}
},
doorbellWebhookUrl: {
title: 'Doorbell Sensor Webhook',

View File

@@ -4,6 +4,20 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Test Cloudflared",
"type": "node",
"request": "launch",
"args": [
"${workspaceFolder}/test/test-cloudflared.ts"
],
"runtimeArgs": [
"-r",
"ts-node/register"
],
"cwd": "${workspaceRoot}",
"internalConsoleOptions": "openOnSessionStart"
},
{
"name": "Scrypted Debugger",
"address": "${config:scrypted.debugHost}",

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -30,28 +30,28 @@
"realfs": true,
"interfaces": [
"SystemSettings",
"BufferConverter",
"MediaConverter",
"OauthClient",
"Settings",
"DeviceProvider",
"HttpRequestHandler"
]
},
"dependencies": {
"@eneris/push-receiver": "^3.1.5",
"@eneris/push-receiver": "^4.2.0",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"bpmux": "^8.2.1",
"cloudflared": "^0.5.2",
"cloudflared": "^0.5.3",
"exponential-backoff": "^3.1.1",
"http-proxy": "^1.18.1",
"nat-upnp": "file:./external/node-nat-upnp"
},
"devDependencies": {
"@types/http-proxy": "^1.17.14",
"@types/http-proxy": "^1.17.15",
"@types/ip": "^1.1.3",
"@types/nat-upnp": "^1.1.5",
"@types/node": "^20.14.6"
"@types/node": "^22.5.2",
"ts-node": "^10.9.2"
},
"version": "0.2.15"
"version": "0.2.37"
}

View File

@@ -0,0 +1,28 @@
import * as cloudflared from 'cloudflared';
import { once } from 'events';
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
import path from 'path';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
export async function installCloudflared() {
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
const version = 5;
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`, `${process.platform}-${process.arch}`);
const bin = path.join(cloudflareD, cloudflared.bin);
if (!fs.existsSync(bin)) {
for (let i = 0; i <= version; i++) {
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`);
rmSync(cloudflareD, {
force: true,
recursive: true,
});
}
await cloudflared.install(bin);
}
return {
bin,
cloudflareD,
};
}

View File

@@ -0,0 +1,128 @@
import * as cloudflared from 'cloudflared';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import path from 'path';
import child_process from 'child_process';
import { once } from 'events';
import { timeoutPromise } from '@scrypted/common/src/promise-utils';
function extractJsonFilePath(message: string): string | null {
const regex = /Tunnel credentials written to (.+?\.json)/;
const match = message.match(regex);
return match ? match[1] : null;
}
function runLog(bin: string, args: string[]) {
const cp = child_process.spawn(bin, args, {
stdio: 'pipe',
});
cp.stdio[1].on('data', (data) => {
console.log(data.toString());
});
cp.stdio[2].on('data', (data) => {
console.error(data.toString());
});
return cp;
}
async function runLogWait(bin: string, args: string[], timeout: number, signal?: AbortSignal, outputChanged?: (output: string) => void) {
const cp = runLog(bin, args);
signal?.addEventListener('abort', () => {
cp.kill();
});
let output: string = '';
cp.stdio[1].on('data', (data) => {
output += data.toString();
outputChanged?.(output);
});
cp.stdio[2].on('data', (data) => {
output += data.toString();
outputChanged?.(output);
});
await timeoutPromise(timeout, once(cp, 'exit'));
if (cp.exitCode !== 0)
throw new Error(`failed: cloudflared ${args.join(' ')}`);
return output;
}
async function login(bin: string, signal?: AbortSignal, urlCallback?: (url: string) => void) {
const userHome = process.env.HOME || process.env.USERPROFILE;
const certPem = path.join(userHome, '.cloudflared', 'cert.pem');
rmSync(certPem, { force: true, recursive: true });
await runLogWait(bin, ['tunnel', 'login'], 300000, signal, output => {
const match = output.match(/Please open the following URL and log in with your Cloudflare account:(?<url>.*?)Leave/s);
if (match) {
const url = match.groups.url.trim();
if (url)
urlCallback(url);
}
});
}
async function createTunnel(bin: string, domain: string) {
await runLogWait(bin, ['tunnel', 'cleanup', domain], 30000).catch(() => { });
await runLogWait(bin, ['tunnel', 'delete', domain], 30000).catch(() => { });
return runLogWait(bin, ['tunnel', 'create', domain], 30000);
}
async function routeDns(bin: string, tunnelId: string, domain: string) {
return runLogWait(bin, ['tunnel', 'route', "dns", "-f", tunnelId, domain], 30000);
}
export async function runLocallyManagedTunnel(jsonContents: any, url: string, workDir: string, bin?: string) {
bin = await ensureBin(bin);
const { TunnelID } = jsonContents;
const credentialsJson = path.join(workDir, `${TunnelID}.json`);
writeFileSync(credentialsJson, JSON.stringify(jsonContents));
const configYml =
`url: ${url}
tunnel: ${TunnelID}
credentials-file: ${workDir}/${TunnelID}.json
`;
const configYmlPath = path.join(workDir, `${TunnelID}.yml`);
writeFileSync(configYmlPath, configYml);
return runLog(bin, ['tunnel', '--config', configYmlPath, 'run', TunnelID]);
}
async function ensureBin(bin: string) {
if (bin)
return bin;
const dir = path.join(tmpdir(), 'cloudflared');
bin = path.join(dir, 'cloudflared');
if (!existsSync(bin)) {
try {
mkdirSync(dir, { recursive: true });
}
catch (e) {
}
const b = await cloudflared.install(bin);
console.warn(b);
}
return bin;
}
export async function createLocallyManagedTunnel(domain: string, bin?: string, signal?: AbortSignal, urlCallback?: (url: string) => void) {
bin = await ensureBin(bin);
await login(bin, signal, urlCallback);
const createOutput = await createTunnel(bin, domain);
const jsonFilePath = extractJsonFilePath(createOutput);
const jsonContents = JSON.parse(readFileSync(jsonFilePath).toString());
const { TunnelID } = jsonContents;
await routeDns(bin, TunnelID, domain);
return jsonContents;
}

View File

@@ -1,12 +1,12 @@
import { Deferred } from "@scrypted/common/src/deferred";
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MediaConverter, MediaObject, MediaObjectOptions, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import bpmux from 'bpmux';
import { ChildProcess } from "child_process";
import * as cloudflared from 'cloudflared';
import crypto from 'crypto';
import { once } from 'events';
import { backOff } from "exponential-backoff";
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
import http from 'http';
import HttpProxy from 'http-proxy';
import https from 'https';
@@ -17,13 +17,14 @@ import path from 'path';
import { Duplex } from 'stream';
import tls from 'tls';
import { readLine } from '../../../common/src/read-stream';
import { sleep } from '../../../common/src/sleep';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import { installCloudflared } from "./cloudflared-install";
import { createLocallyManagedTunnel, runLocallyManagedTunnel } from "./cloudflared-local-managed";
import { PushManager } from './push';
import { qsparse, qsstringify } from "./qs";
// import { registerDuckDns } from "./greenlock";
const { deviceManager, endpointManager, systemManager } = sdk;
export const DEFAULT_SENDER_ID = '827888101440';
@@ -31,32 +32,16 @@ const SCRYPTED_SERVER = localStorage.getItem('scrypted-server') || 'home.scrypte
const SCRYPTED_CLOUD_MESSAGE_PATH = '/_punch/cloudmessage';
class ScryptedPush extends ScryptedDeviceBase implements BufferConverter {
constructor(public cloud: ScryptedCloud) {
super('push');
this.fromMimeType = ScryptedMimeTypes.PushEndpoint;
this.toMimeType = ScryptedMimeTypes.Url;
}
async convert(data: Buffer | string, fromMimeType: string): Promise<Buffer> {
const validDomain = this.cloud.getSSLHostname();
if (validDomain)
return Buffer.from(`https://${validDomain}${await this.cloud.getCloudMessagePath()}/${data}`);
const url = `http://127.0.0.1/push/${data}`;
return this.cloud.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.cloud.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
}
}
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, BufferConverter, DeviceProvider, HttpRequestHandler {
class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings, MediaConverter, HttpRequestHandler {
cloudflareTunnel: string;
cloudflared: Awaited<ReturnType<typeof cloudflared.tunnel>>;
cloudflared: {
url: Promise<string>;
child: ChildProcess;
};
manager = new PushManager(DEFAULT_SENDER_ID);
server: http.Server;
secureServer: https.Server;
proxy: HttpProxy;
push: ScryptedPush;
whitelisted = new Map<string, string>();
reregisterTimer: NodeJS.Timeout;
storageSettings = new StorageSettings(this, {
@@ -79,15 +64,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
persistedDefaultValue: crypto.randomBytes(8).toString('hex'),
},
forwardingMode: {
title: "Port Forwarding Mode",
description: "The port forwarding mode used to expose the HTTPS port. If port forwarding is disabled or unavailable, Scrypted Cloud will fall back to push to initiate connections with this Scrypted server. Port Forwarding and UPNP are optional but will significantly speed up cloud connections.",
title: "Connection Mode",
description: "The connection mode that exposes this server to the internet.",
choices: [
"Default",
"UPNP",
"Router Forward",
"Custom Domain",
"Disabled",
],
defaultValue: 'UPNP',
defaultValue: 'Default',
onPut: () => this.scheduleRefreshPortForward(),
},
hostname: {
@@ -170,6 +156,39 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
onPut: () => {
this.cloudflared?.child.kill();
},
// this has been deprecated in favor of locally managed tunnels.
hide: true,
},
cloudflaredTunnelCredentials: {
group: 'Cloudflare',
json: true,
hide: true,
},
cloudflaredTunnelCustomDomain: {
group: 'Cloudflare',
title: 'Cloudflare Tunnel Custom Domain',
placeholder: 'scrypted.example.com',
description: 'Optional: Host a custom domain with Cloudflare. After setting the domain, complete the Cloudflare browser login link shown in Scrypted Cloud Plugin Console.',
mapPut: (ov, nv) => {
try {
const url = new URL(nv);
return url.hostname;
}
catch (e) {
return nv;
}
},
onPut: (_, nv) => {
if (!nv)
this.storageSettings.values.cloudflaredTunnelCredentials = undefined;
this.doCloudflaredLogin(nv);
},
},
cloudflaredTunnelLoginUrl: {
group: 'Cloudflare',
type: 'html',
title: 'Cloudflare Tunnel Login',
hide: true,
},
cloudflaredTunnelUrl: {
group: 'Cloudflare',
@@ -219,9 +238,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
upnpInterval: NodeJS.Timeout;
upnpClient = upnp.createClient();
upnpStatus = 'Starting';
securePort: number;
randomBytes = crypto.randomBytes(16).toString('base64');
reverseConnections = new Set<Duplex>();
cloudflaredLoginController?: AbortController;
get portForwardingDisabled() {
return this.storageSettings.values.forwardingMode === 'Disabled' || this.storageSettings.values.forwardingMode === 'Default';
}
get cloudflareTunnelHost() {
if (!this.cloudflareTunnel)
@@ -232,6 +255,17 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
constructor() {
super();
this.converters = [
[ScryptedMimeTypes.LocalUrl, ScryptedMimeTypes.Url],
[ScryptedMimeTypes.PushEndpoint, ScryptedMimeTypes.Url],
];
// legacy cleanup
this.fromMimeType = undefined;
this.toMimeType = undefined;
deviceManager.onDevicesChanged({
devices: [],
});
this.storageSettings.settings.register.onPut = async () => {
await this.sendRegistrationId(await this.manager.registrationId);
}
@@ -259,9 +293,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
};
this.storageSettings.settings.securePort.onGet = async () => {
const hide = this.portForwardingDisabled;
return {
group: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare' : undefined,
title: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare Port' : 'Forward Port',
hide,
}
};
@@ -271,42 +305,28 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
};
// this.storageSettings.settings.duckDnsToken.onGet = async () => {
// return {
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
// || this.storageSettings.values.forwardingMode === 'Disabled',
// }
// };
// this.storageSettings.settings.duckDnsHostname.onGet = async () => {
// return {
// hide: this.storageSettings.values.forwardingMode === 'Custom Domain'
// || this.storageSettings.values.forwardingMode === 'Disabled',
// }
// };
this.storageSettings.settings.cloudflaredTunnelToken.onGet =
this.storageSettings.settings.cloudflaredTunnelCustomDomain.onGet =
this.storageSettings.settings.cloudflaredTunnelUrl.onGet = async () => {
return {
hide: !this.storageSettings.values.cloudflareEnabled,
}
};
this.log.clearAlerts();
this.storageSettings.settings.securePort.onPut = (ov, nv) => {
if (ov && ov !== nv)
this.log.a('Reload the Scrypted Cloud Plugin to apply the port change.');
};
this.fromMimeType = ScryptedMimeTypes.LocalUrl;
this.toMimeType = ScryptedMimeTypes.Url;
if (!this.storageSettings.values.certificate)
this.storageSettings.values.certificate = createSelfSignedCertificate();
if (this.storageSettings.values.cloudflaredTunnelCustomDomain && !this.storageSettings.values.cloudflaredTunnelCredentials)
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
this.log.clearAlerts();
const proxy = this.setupProxyServer();
this.setupCloudPush();
this.updateCors();
const observeRegistrations = () => {
@@ -405,7 +425,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if (this.storageSettings.values.forwardingMode === 'Custom Domain' || this.cloudflareTunnelHost)
upnpPort = 443;
this.console.log(`Scrypted Cloud mapped https://${ip}:${upnpPort} to https://127.0.0.1:${this.securePort}`);
this.console.log(`Scrypted Cloud routing to https://${ip}:${upnpPort}`);
// the ip is not sent, but should be checked to see if it changed.
if (this.storageSettings.values.lastPersistedUpnpPort !== upnpPort || ip !== this.storageSettings.values.lastPersistedIp) {
@@ -424,7 +444,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
async testPortForward() {
try {
if (this.storageSettings.values.forwardingMode === 'Disabled')
if (this.portForwardingDisabled)
throw new Error('Port forwarding is disabled.');
const pluginPath = await endpointManager.getPath(undefined, {
@@ -459,7 +479,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if (!upnpPort)
upnpPort = Math.round(Math.random() * 20000 + 40000);
if (this.storageSettings.values.forwardingMode === 'Disabled') {
if (this.portForwardingDisabled) {
this.updatePortForward(upnpPort);
return;
}
@@ -491,7 +511,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
},
private: {
host: localAddress,
port: this.securePort,
port: this.storageSettings.values.securePort,
},
ttl: 1800,
}, async err => {
@@ -556,7 +576,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
try {
endpointManager.setAccessControlAllowOrigin({
origins: [
`http://${SCRYPTED_SERVER}`,
'https://manage.scrypted.app',
`https://${SCRYPTED_SERVER}`,
...this.storageSettings.values.additionalCorsOrigins,
],
@@ -581,10 +601,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
getAuthority() {
const { forwardingMode } = this.storageSettings.values;
if (forwardingMode === 'Disabled')
if (this.portForwardingDisabled)
return {};
const { forwardingMode } = this.storageSettings.values;
const upnp_port = forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
const hostname = forwardingMode === 'Custom Domain'
? this.storageSettings.values.hostname
@@ -666,18 +687,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
async setupCloudPush() {
await deviceManager.onDeviceDiscovered(
{
name: 'Cloud Push Endpoint',
type: ScryptedDeviceType.API,
nativeId: 'push',
interfaces: [ScryptedInterface.BufferConverter],
},
);
this.push = new ScryptedPush(this);
}
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
if (request.url.endsWith('/testPortForward')) {
response.send(this.randomBytes);
@@ -704,16 +713,13 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
async getDevice(nativeId: string) {
return this.push;
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
getSSLHostname() {
const validDomain = (this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
|| (this.storageSettings.values.cloudflaredTunnelToken && this.cloudflareTunnelHost)
|| (this.storageSettings.values.cloudflaredTunnelCredentials && this.cloudflareTunnelHost)
|| (this.storageSettings.values.duckDnsCertValid && this.storageSettings.values.duckDnsHostname && this.storageSettings.values.upnpPort && `${this.storageSettings.values.duckDnsHostname}:${this.storageSettings.values.upnpPort}`);
return validDomain;
}
@@ -722,19 +728,34 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
return this.getSSLHostname() || SCRYPTED_SERVER;
}
async convert(data: Buffer, fromMimeType: string, toMimeType: string): Promise<Buffer> {
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
// short lived urls.
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
if (params.get('short-lived') === 'true') {
const u = new URL(data.toString(), this.cloudflareTunnel);
u.host = this.cloudflareTunnelHost;
u.port = '';
return Buffer.from(u.toString());
async convertMedia(data: string | Buffer | any, fromMimeType: string, toMimeType: string, options?: MediaObjectOptions): Promise<MediaObject | Buffer | any> {
if (!toMimeType.startsWith(ScryptedMimeTypes.Url))
throw new Error('unsupported cloud url conversion');
if (fromMimeType.startsWith(ScryptedMimeTypes.LocalUrl)) {
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
// short lived urls.
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
if (params.get('short-lived') === 'true') {
const u = new URL(data.toString(), this.cloudflareTunnel);
u.host = this.cloudflareTunnelHost;
u.port = '';
return Buffer.from(u.toString());
}
}
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
}
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
else if (fromMimeType.startsWith(ScryptedMimeTypes.PushEndpoint)) {
const validDomain = this.getSSLHostname();
if (validDomain)
return Buffer.from(`https://${validDomain}${await this.getCloudMessagePath()}/${data}`);
const url = `http://127.0.0.1/push/${data}`;
return this.whitelist(url, 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}${SCRYPTED_CLOUD_MESSAGE_PATH}`);
}
throw new Error('unsupported cloud url conversion');
}
async getSettings(): Promise<Setting[]> {
@@ -867,16 +888,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.server.listen(0, '127.0.0.1');
await once(this.server, 'listening');
const port = (this.server.address() as any).port;
this.secureServer = https.createServer({
key: this.storageSettings.values.certificate.serviceKey,
cert: this.storageSettings.values.certificate.certificate,
}, handler);
this.secureServer.on('upgrade', wsHandler)
// this is the direct connection port
this.secureServer.listen(this.storageSettings.values.securePort, '0.0.0.0');
await once(this.secureServer, 'listening');
this.storageSettings.values.securePort = this.securePort = (this.secureServer.address() as any).port;
this.console.log('scrypted cloud server listening on', port);
const agent = new http.Agent({ maxSockets: Number.MAX_VALUE, keepAlive: true });
this.proxy = HttpProxy.createProxy({
@@ -888,7 +900,10 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.proxy.on('proxyRes', (res, req) => {
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
res.headers['X-Scrypted-Cloud-Address'] = this.cloudflareTunnel;
let domain = this.cloudflareTunnel;
if (!domain && this.storageSettings.values.forwardingMode === 'Custom Domain' && this.storageSettings.values.hostname)
domain = `https://${this.storageSettings.values.hostname}`;
res.headers['X-Scrypted-Cloud-Address'] = domain;
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address, X-Scrypted-Cloud-Address';
});
@@ -945,13 +960,36 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
socket.pipe(local).pipe(socket);
});
mux.on('error', () => {
client.destroy();
});
}
});
this.startCloudflared();
this.startCloudflared(port);
while (true) {
try {
this.secureServer = https.createServer({
key: this.storageSettings.values.certificate.serviceKey,
cert: this.storageSettings.values.certificate.certificate,
}, handler);
this.secureServer.on('upgrade', wsHandler)
// this is the direct connection port
this.secureServer.listen(this.storageSettings.values.securePort, '0.0.0.0');
await once(this.secureServer, 'listening');
this.storageSettings.values.securePort = (this.secureServer.address() as any).port;
break;
}
catch (e) {
this.console.log('error starting secure server. retrying.', e);
await sleep(60000);
}
}
}
async startCloudflared() {
async startCloudflared(quickTunnelPort: number) {
while (true) {
try {
if (!this.storageSettings.values.cloudflareEnabled) {
@@ -961,61 +999,50 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.console.log('starting cloudflared');
this.cloudflared = await backOff(async () => {
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME;
const version = 2;
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`, `${process.platform}-${process.arch}`);
const bin = path.join(cloudflareD, cloudflared.bin);
const { cloudflareD, bin } = await installCloudflared();
if (!fs.existsSync(bin)) {
for (let i = 0; i <= version; i++) {
const cloudflareD = path.join(pluginVolume, 'cloudflare.d', `v${version}`);
rmSync(cloudflareD, {
force: true,
recursive: true,
});
}
if (process.platform === 'darwin' && process.arch === 'arm64') {
const bin = path.join(cloudflareD, cloudflared.bin);
mkdirSync(path.dirname(bin), {
recursive: true,
});
const tmp = `${bin}.tmp`;
if (this.storageSettings.values.cloudflaredTunnelCredentials && this.storageSettings.values.cloudflaredTunnelCustomDomain) {
const tunnelUrl = `http://127.0.0.1:${quickTunnelPort}`;
const url = this.cloudflareTunnel = `https://${this.storageSettings.values.cloudflaredTunnelCustomDomain}`;
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
const stream = await httpFetch({
url: 'https://github.com/scryptedapp/cloudflared/releases/download/2023.8.2/cloudflared-darwin-arm64',
responseType: 'readable',
});
const write = stream.body.pipe(fs.createWriteStream(tmp));
await once(write, 'close');
renameSync(tmp, bin);
fs.chmodSync(bin, 0o0755)
}
else {
await cloudflared.install(bin);
const ret = await runLocallyManagedTunnel(this.storageSettings.values.cloudflaredTunnelCredentials, tunnelUrl, cloudflareD, bin);
return {
child: ret,
url: Promise.resolve(url),
}
}
// npm cloudflared package kinda sucks.
process.chdir(cloudflareD);
const secureUrl = `https://127.0.0.1:${this.securePort}`;
let tunnelUrl: string
const args: any = {};
if (this.storageSettings.values.cloudflaredTunnelToken) {
this.log.a('Cloudflare tunnel tokens are no longer supported. Please use the new Cloudflare Tunnel Custom Domain option.');
tunnelUrl = `https://127.0.0.1:${this.storageSettings.values.securePort}`;
args['run'] = null;
args['--token'] = this.storageSettings.values.cloudflaredTunnelToken;
}
else {
args['--no-tls-verify'] = null;
args['--url'] = secureUrl;
tunnelUrl = `http://127.0.0.1:${quickTunnelPort}`;
args['--url'] = tunnelUrl;
}
const deferred = new Deferred<string>();
const cloudflareTunnel = cloudflared.tunnel(args);
cloudflareTunnel.child.stdout.on('data', data => this.console.log(data.toString()));
cloudflareTunnel.child.stderr.on('data', data => {
const string: string = data.toString();
const processData = (string: string) => {
this.console.error(string);
const lines = string.split('\n');
for (const line of lines) {
if ((line.includes('Unregistered tunnel connection') || line.includes('Register tunnel error'))
&& deferred.finished) {
this.console.warn('Cloudflare registration failed after tunnel started. The old tunnel may be invalid. Terminating.');
cloudflareTunnel.child.kill();
}
if (line.includes('hostname'))
this.console.log(line);
const match = /config=(".*?}")/gm.exec(line)
@@ -1037,7 +1064,19 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
}
};
cloudflareTunnel.child.stdout.on('data', data => {
const d = data.toString();
this.console.log(d);
processData(d);
});
cloudflareTunnel.child.stderr.on('data', data => {
const d = data.toString();
this.console.error(d);
processData(d);
});
cloudflareTunnel.child.on('exit', () => deferred.resolve(undefined));
try {
this.cloudflareTunnel = await Promise.any([deferred.promise, cloudflareTunnel.url]);
@@ -1049,7 +1088,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.console.error('cloudflared error', e);
throw e;
}
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${secureUrl}`);
this.console.log(`cloudflare url mapped ${this.cloudflareTunnel} to ${tunnelUrl}`);
return cloudflareTunnel;
}, {
startingDelay: 60000,
@@ -1173,6 +1212,35 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
async doCloudflaredLogin(domain: string) {
if (!domain) {
this.cloudflared?.child.kill();
return;
}
// this.log.a('Visit the URL printed in the Scrypted Cloud plugin console to log into Cloudflare.');
const customDomain = this.storageSettings.values.cloudflaredTunnelCustomDomain;
try {
this.cloudflaredLoginController?.abort();
this.cloudflaredLoginController = new AbortController();
const { bin } = await installCloudflared();
const jsonContents = await createLocallyManagedTunnel(domain, bin, this.cloudflaredLoginController.signal, url => {
this.console.warn('Cloudflare login URL:', url);
this.storageSettings.values.cloudflaredTunnelLoginUrl = `<div style="padding-bottom: 16px"><a href="${url}" target="_blank" >Click here to log into Cloudflare</a></div>`;
this.storageSettings.settings.cloudflaredTunnelLoginUrl.hide = false;
this.onDeviceEvent(ScryptedInterface.Settings, undefined);
});
this.storageSettings.values.cloudflaredTunnelCredentials = jsonContents;
this.storageSettings.values.cloudflaredTunnelToken = undefined;
this.cloudflared?.child.kill();
}
catch (e) {
if (customDomain)
this.storageSettings.values.cloudflaredTunnelCustomDomain = undefined;
this.console.error('cloudflared login error', e);
this.log.a('Cloudflare login error. See console logs.');
}
}
}
export default ScryptedCloud;

View File

@@ -1,6 +1,7 @@
import { EventEmitter } from 'events';
import PushReceiver, { Types } from '@eneris/push-receiver';
import { PushReceiver } from '@eneris/push-receiver';
import { Deferred } from '@scrypted/common/src/deferred';
import type { Types } from '@eneris/push-receiver/dist/client';
export declare interface PushManager {
on(event: 'message', listener: (data: any) => void): this;
@@ -27,7 +28,15 @@ export class PushManager extends EventEmitter {
const instance = new PushReceiver({
...savedConfig,
senderId,
firebase: {
apiKey: "AIzaSyDI0bgFuVPIqKZoNpB-iTOU7ijIeepxOXE",
authDomain: "scrypted-app.firebaseapp.com",
databaseURL: "https://scrypted-app.firebaseio.com",
projectId: "scrypted-app",
storageBucket: "scrypted-app.appspot.com",
messagingSenderId: "827888101440",
appId: "1:827888101440:web:6ff9f8ada107e9cc0097a5"
},
heartbeatIntervalMs: 15 * 60 * 1000,
});
@@ -51,7 +60,12 @@ export class PushManager extends EventEmitter {
this.emit('message', message.data);
});
await instance.connect();
try {
await instance.connect();
}
catch (e) {
console.error('failed to connect to push server', e);
}
return savedConfig.credentials?.fcm?.token || deferred.promise;
})();

View File

@@ -0,0 +1,8 @@
import { createLocallyManagedTunnel, runLocallyManagedTunnel } from '../src/cloudflared-local-managed';
async function main() {
const jsonContents = await createLocallyManagedTunnel('test.scrypted.io')
await runLocallyManagedTunnel(jsonContents, 'http://127.0.0.1:49725', '/tmp/work');
}
main();

View File

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

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