Compare commits

..

293 Commits

Author SHA1 Message Date
rustdesk
9827c76514 fix ci 2024-05-11 16:17:55 +08:00
21pages
712bfbae92 fix, not unregister gpu texture when closeSession is false (#8018)
Signed-off-by: 21pages <pages21@163.com>
2024-05-11 11:11:12 +08:00
jxdv
0152e937ec update cs.rs (#8013) 2024-05-11 08:55:30 +08:00
jxdv
c066dc8c24 update sk.rs (#8012) 2024-05-11 08:55:16 +08:00
Kleofass
0b96b3f345 Update lv.rs (#8010) 2024-05-11 08:54:59 +08:00
Mr-Update
b5781933b6 Update de.rs (#8007) 2024-05-11 08:54:44 +08:00
fufesou
d851bf8b69 fix: move tab to new window, black screen (#8016)
* fix: move tab to new window, black screen

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: can remove

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-11 08:54:17 +08:00
fufesou
b6d6a4360f fix: linux resizable (#8015)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-11 08:53:19 +08:00
fufesou
69b11e8dc6 fix: linux, custom client, incoming only, resizable (#8005)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-10 16:40:29 +08:00
rustdesk
6e78037770 fix linux custom client tray 2024-05-10 15:12:07 +08:00
fufesou
bed0375f45 fix: android, stop capture, release surface (#8004)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-10 14:37:00 +08:00
rustdesk
42d723f82f less cpu usage of sciter version build 2024-05-10 14:19:06 +08:00
rustdesk
fb0ce8c974 for saving disk space 2024-05-10 14:03:40 +08:00
21pages
d55770f12b remove android resolution scale (#8002)
Signed-off-by: 21pages <pages21@163.com>
2024-05-10 10:18:20 +08:00
fufesou
125c275623 fix: android media project dialog every conn on Android 14 (#7997)
* fix: android media project dialog every conn on Android 14

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* comments

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* comments

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-09 23:33:34 +08:00
21pages
c640f7ed12 fix android crash caused by outdated video frame ptr (#7996)
Signed-off-by: 21pages <pages21@163.com>
2024-05-09 22:53:01 +08:00
fufesou
73662ed7d9 fix: linux, custom client, incoming, window size (#7995)
* fix: linux, custom client, incoming, window size

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* comments

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-09 22:51:53 +08:00
21pages
a595d83232 fix android possible waiting for image issue (#7994)
Signed-off-by: 21pages <pages21@163.com>
2024-05-09 18:59:51 +08:00
solokot
6743b4f290 Update ru.rs (#7993) 2024-05-09 18:09:44 +08:00
rustdesk
e8ecb738d0 remove msi beta 2024-05-09 18:09:17 +08:00
bovirus
9c20968e05 Update Italian language (#7992) 2024-05-09 16:31:04 +08:00
rustdesk
9f80690d8a fix flatpak 2024-05-09 16:09:19 +08:00
rustdesk
e58a95e7d9 fix flatpak 2024-05-09 16:03:12 +08:00
fufesou
d9b96d2f31 fix: build (#7989)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-09 11:13:35 +08:00
fufesou
bbe9017318 fix: wayland delete restore token (#7988)
* fix: wayland delete restore token

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Wayland close session when clearing restore token

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix build

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Refact Wayland option

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Wayland clear screen selection, fake token

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix build web

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: build

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* chore

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-09 11:03:59 +08:00
21pages
a31a68ba17 If there is hardware ram encoder as fallback, not require all adapters (#7987)
support the codec format

Signed-off-by: 21pages <pages21@163.com>
2024-05-09 09:02:25 +08:00
bovirus
9d56e29a09 Update Italian language (#7984) 2024-05-09 07:33:12 +08:00
Mr-Update
32fca6f9b3 Update de.rs (#7982) 2024-05-09 07:32:54 +08:00
Kleofass
72c2de575a Update lv.rs (#7979) 2024-05-09 07:32:38 +08:00
rustdesk
93d4987dcc remove package to image 2024-05-08 21:29:33 +08:00
rustdesk
2a25034039 fix flatpak 2024-05-08 20:45:09 +08:00
21pages
5ba1c2587d opt supported encoding update, consider multi connections (#7978)
Signed-off-by: 21pages <pages21@163.com>
2024-05-08 20:31:39 +08:00
rustdesk
1b0a3e610e refactor flatpak/appimage deb path 2024-05-08 20:07:57 +08:00
RustDesk
2820f3f798 Update flutter-build.yml 2024-05-08 18:09:29 +08:00
21pages
09f3850250 installed windows client save incoming recording to a specific directory (#7974)
Signed-off-by: 21pages <pages21@163.com>
2024-05-08 17:04:53 +08:00
RustDesk
35832f8f7f Revert "fix: bump winget releaser version & add dependabot (#4848)" (#7971)
This reverts commit 7466d8cb3a.
2024-05-08 14:19:09 +08:00
sitiom
7466d8cb3a fix: bump winget releaser version & add dependabot (#4848)
* fix: bump winget releaser version

* ci: add dependabot
2024-05-08 14:13:55 +08:00
21pages
ea3786457b cm window always on top (#7953)
Signed-off-by: 21pages <pages21@163.com>
2024-05-08 12:08:37 +08:00
fufesou
01322146c0 refact: window frame border (#7946)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-08 09:59:05 +08:00
bovirus
26adc557bf Update Italian language (#7943) 2024-05-08 09:52:04 +08:00
jxdv
36e52d8165 update sk.rs (#7949) 2024-05-08 09:51:50 +08:00
jxdv
4c0211fa23 update cs.rs (#7950) 2024-05-08 09:51:33 +08:00
fufesou
a3b4dcf762 fix: wayland, accept conn (#7951)
Show prompt window after the controlled side accepting the connection.

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-08 09:50:55 +08:00
fufesou
5537c525ca refact: comments, wayland, restore token (#7947)
We cannot store restore_token if the server is not running.

Because `org.freedesktop.portal.RemoteDesktop` does not support persist
mode for now.

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-08 09:49:16 +08:00
21pages
3746fd88b5 Show current codec in menu when auto codec is chosen (#7942)
* change negotiated codec name to negotiated codec format

Signed-off-by: 21pages <pages21@163.com>

* fallback to vp9 directly if failed to create encoder

Current fallback method is clear hwcodec config

Signed-off-by: 21pages <pages21@163.com>

* show current codec in menu when auto codec is chosen

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-05-07 20:34:23 +08:00
21pages
e373144350 android 13+ request notification permission when start service (#7941)
Signed-off-by: 21pages <pages21@163.com>
2024-05-07 18:45:05 +08:00
fufesou
3be4bfc821 fix: ci, ios (#7940)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-07 16:53:57 +08:00
fufesou
2c1595d0d5 fix: voice call, select audio input device (#7922)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-07 16:18:48 +08:00
fufesou
f08933f93c fix: translate, proxy (#7923)
* fix: translate, proxy

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: proxy server, change placeholder to tooltip

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-07 15:21:38 +08:00
21pages
51813b7d7e set vp9 as auto codec (#7928)
Signed-off-by: 21pages <pages21@163.com>
2024-05-07 15:20:55 +08:00
21pages
4e30418f79 disable av1 for all 32 bit platforms (#7921)
Signed-off-by: 21pages <pages21@163.com>
2024-05-06 23:09:09 +08:00
21pages
0af370d736 x86 sciter set not support av1 due to slow (#7920)
Signed-off-by: 21pages <pages21@163.com>
2024-05-06 22:03:49 +08:00
rustdesk
ac04a032ad more for custom client of linux 2024-05-06 22:02:13 +08:00
21pages
f6223a6f71 dropdown menu for tabs that cannot be displayed (#7918)
* The visibility threshold is 75%
* Used in remote, file transfer, port forward and cm

Signed-off-by: 21pages <pages21@163.com>
2024-05-06 17:10:02 +08:00
fufesou
937cea5a01 refact: remove virtual_display_driver (#7915)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-06 15:26:21 +08:00
fufesou
846522037f fix: mstsc switch, plug in virtual displays (#7913)
* fix: mstsc switch, plug in virtual displays

Signed-off-by: fufesou <shuanglongchen@yeah.ne>

* Update display_service.rs

---------

Signed-off-by: fufesou <shuanglongchen@yeah.ne>
Co-authored-by: fufesou <shuanglongchen@yeah.ne>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-05-06 14:01:59 +08:00
fufesou
b5a88d00af fix: headless, plug in multiple virtual displays (#7912)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-06 13:34:31 +08:00
bovirus
7391271107 Update Italian language (#7910) 2024-05-06 10:59:55 +08:00
21pages
991694aca5 update hwcodec, qsv support changing bitrate (#7911)
Signed-off-by: 21pages <pages21@163.com>
2024-05-06 10:59:25 +08:00
rustdesk
29b13d19d6 modify GIO_MODULE_DIR 2024-05-05 23:33:58 +08:00
rustdesk
b0ff74c799 fix appimage 2024-05-05 16:44:59 +08:00
rustdesk
75c60660e7 remove unused code 2024-05-05 15:47:17 +08:00
rustdesk
eacc840c54 to make server work on wayland, have to use host's gstreamer 2024-05-05 15:46:30 +08:00
rustdesk
6d09c0adcf fix ci 2024-05-04 19:34:10 +08:00
rustdesk
fe1cf1105a fix ci 2024-05-04 19:10:32 +08:00
rustdesk
9454334ddd try out APPRUN_LD_LIBRARY_PATH 2024-05-04 18:29:22 +08:00
rustdesk
cc482c589c XDG_SESSION_TYPE as final backup 2024-05-04 17:22:43 +08:00
fufesou
b387bc1038 fix: linux, check load dylib in main.cc (#7901)
* fix: linux, check load dylib in main.cc

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Better error message

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact error message

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Better error message

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* check if "libnsl.so.1" is required, mainly for AppImage and yum

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-04 14:07:29 +08:00
fufesou
890b735985 fix: test case, test_if_valid_server (#7903)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-04 14:06:41 +08:00
XLion
eb81e00d20 Add /r tip at id_input_tip (#7900) 2024-05-03 23:45:00 +08:00
fufesou
c619bfc05c fix: flutter, process hangup on loading dylib failed (#7898)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-05-03 22:58:49 +08:00
rustdesk
b5935eb4c5 fix ci 2024-05-03 16:12:36 +08:00
rustdesk
73b3023c2d do not run ci if update res/ 2024-05-03 14:37:01 +08:00
21pages
4cf709fc26 update hwcodec, build linux ffmpeg lib on ubuntu 18 (#7897)
* Build linux ffmpeg lib on ubuntu 18 container
* Fix ffmpeg ram nvnec not work if the display connected to nvidia gpu
  is not the primary display
* Fix windows mfx lib link error if vram feature is not enabled.

Signed-off-by: 21pages <pages21@163.com>
2024-05-03 13:05:21 +08:00
rustdesk
b471b934f2 fix service unit again 2024-05-03 04:51:01 +08:00
rustdesk
abbd4c8934 fix ci 2024-05-03 03:24:22 +08:00
rustdesk
ed8c852d35 fix ci 2024-05-03 03:12:47 +08:00
rustdesk
c3598b3513 fix ci 2024-05-03 02:57:24 +08:00
rustdesk
b5f6a9c91a fix one flatpak feature missing 2024-05-03 02:52:41 +08:00
rustdesk
d18810b612 fix appimage --cm 2024-05-03 02:13:44 +08:00
rustdesk
3c17d713b3 try out APPIMAGE and ARGV0 2024-05-03 01:50:58 +08:00
flusheDData
d46b2d52ce New terms (#7894)
* Update es.rs

New terms added

* Update es.rs

A slight correction
2024-05-03 01:40:02 +08:00
rustdesk
e07d0b3b25 refactor 2024-05-03 00:42:20 +08:00
rustdesk
2ff3b24b33 fix --server not killed after service stop or uninstallation 2024-05-03 00:36:29 +08:00
rustdesk
115fb05c85 SPA_PLUGIN_DIR works for finding support/libspa-support.so, I tested
manually, though crash because used spa0.2
2024-05-02 22:41:13 +08:00
rustdesk
1e7e72883d fix libspa-support.so 2024-05-02 20:50:06 +08:00
rustdesk
1edb7639f6 try libspa-support again 2024-05-02 20:35:09 +08:00
rustdesk
33c5ceecec pibpipewire-0.2-1 2024-05-02 19:47:56 +08:00
rustdesk
a8a7f306fe fix ci 2024-05-02 19:32:53 +08:00
rustdesk
9888e51b93 fix ci 2024-05-02 19:32:00 +08:00
rustdesk
c3124314ef try to fix libspa-support.so not found in arm64 appimage when connection
started
2024-05-02 18:47:39 +08:00
rustdesk
975cd183bc fix ci 2024-05-02 18:19:36 +08:00
rustdesk
aa9da0b0c8 fix ci 2024-05-02 18:14:42 +08:00
rustdesk
d83b106607 fix ci 2024-05-02 17:12:05 +08:00
rustdesk
1a0a6cedc7 fix ci 2024-05-02 17:06:50 +08:00
rustdesk
d4e1a83a5a fix ci 2024-05-02 17:06:01 +08:00
rustdesk
03e93d546a fix ci 2024-05-02 17:03:20 +08:00
rustdesk
a1b957b0f7 fix ci 2024-05-02 16:51:37 +08:00
rustdesk
eeffb11841 fix ci 2024-05-02 16:50:11 +08:00
rustdesk
860dc7dbc2 fix ci, we have no arm64 makepkg image 2024-05-02 16:02:22 +08:00
rustdesk
220f267976 no need to publish deb in unsigned any more since we removed
appimage/flatpak/headless feature
2024-05-02 15:28:04 +08:00
rustdesk
b5bd6e008b fix ci 2024-05-02 14:57:17 +08:00
rustdesk
9d20bc6441 fix ci 2024-05-02 14:23:59 +08:00
rustdesk
1c36734092 fix ci 2024-05-02 14:23:27 +08:00
rustdesk
9102b6e6b8 fix ci 2024-05-02 13:54:09 +08:00
rustdesk
cd78fa9762 fix ci 2024-05-02 13:46:01 +08:00
rustdesk
407e2d4b7b fix ci 2024-05-02 13:45:14 +08:00
rustdesk
62d6ee46c4 fix ci 2024-05-02 13:36:48 +08:00
21pages
f15bf8166f fix visibilty of address book menu divider (#7890)
* fix visibilty of address book menu divider

Signed-off-by: 21pages <pages21@163.com>

* update hwcodec, fix nvenc use d3d11 without judging windows or linux

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-05-02 13:24:33 +08:00
rustdesk
7ce0b225ef remove linux_headless/appimage/flatpak feature 2024-05-02 13:23:32 +08:00
21pages
f853b29fd9 fix list type peer view (#7887)
* Layout list type peer view with layout rather than calculated width
* Hide menu sync from recent and web console if no write permission
* Pull ab twice because it's force updated when refreshCurrentUser is called on startup and
  connection status changing
2024-05-01 23:38:39 +08:00
rustdesk
f9cc8de93e fix ci 2024-05-01 21:42:21 +08:00
rustdesk
a924a876ae fix app name refactor 2024-05-01 21:29:11 +08:00
rustdesk
1f15d2c66e also --disable-flutter-texture-render for mac arm 2024-05-01 20:49:10 +08:00
rustdesk
ffd8354208 optimize ci 2024-05-01 19:40:30 +08:00
21pages
b3b1f4664c update hwcodec, fix wrong linux-x86_64 ffmpeg lib (#7883)
Signed-off-by: 21pages <pages21@163.com>
2024-05-01 19:15:21 +08:00
rustdesk
03410ded9a fix ci 2024-05-01 18:53:50 +08:00
rustdesk
09c9671376 fix ci 2024-05-01 16:38:04 +08:00
rustdesk
736b3b93e9 r26b -> r26d 2024-05-01 14:52:02 +08:00
rustdesk
4a71ca6b81 fix ci 2024-05-01 14:23:08 +08:00
RustDesk
a828f9b1f9 Update build-macos-arm64.yml 2024-05-01 14:06:44 +08:00
rustdesk
7ce5f8e8b6 fix ci 2024-05-01 13:05:50 +08:00
rustdesk
08b7e036b4 test vcpkg new in android 2024-05-01 12:58:27 +08:00
rustdesk
b1722a085e fix ci 2024-05-01 12:35:58 +08:00
rustdesk
9d985585b9 test android ci 2024-05-01 12:25:53 +08:00
rustdesk
de057b7234 enable makepkg for aarch64 2024-05-01 10:35:19 +08:00
rustdesk
04ec49e18e change to jobs 3 for selfhost linux arm 2024-05-01 10:20:44 +08:00
21pages
34c7c25908 update hwcodec, add windows ffmpeg vram encoding (#7876)
* windows add ffmpeg vram encoding
* windows add missing nvenc and qsv ram encoding, linux add vaapi, current codec table:  https://github.com/21pages/hwcodec?tab=readme-ov-file#codec

Signed-off-by: 21pages <pages21@163.com>
2024-05-01 00:07:09 +08:00
rustdesk
f74374e759 refactor ci 2024-04-30 23:19:59 +08:00
rustdesk
cce74f5631 fix ci 2024-04-30 23:14:37 +08:00
fufesou
003b9e48e1 fix: macos, crash, key input. --server also has UI. (#7878)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-30 22:53:42 +08:00
rustdesk
28bf73cd5a disable flutter texture render of osx intel 2024-04-30 22:50:59 +08:00
rustdesk
511b982dce --jobs=1 for arm64 linux selfhost 2024-04-30 22:42:09 +08:00
fufesou
dcd176f95c fix: keyboard input method, for flutter input (#7875)
* fix: keyboard input method, for flutter input

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* comment

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-30 21:06:24 +08:00
fufesou
d394aa8a15 feat: msi reg QuietUninstallString (#7873)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-30 17:42:28 +08:00
rustdesk
c8b35d5ce1 fix ci 2024-04-30 16:27:44 +08:00
fufesou
6cd107c3e3 Fix/msi service (#7872)
* refact: msi service, shell execute control

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: msi, skip adding rules when uninstalling

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* line indentation

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-30 15:57:12 +08:00
rustdesk
a884e32816 fix ci 2024-04-30 15:50:02 +08:00
rustdesk
45d2d3cbb5 refactor unsigned to prepare for flathub 2024-04-30 15:47:36 +08:00
rustdesk
382f75d249 shorten github ci time 2024-04-30 12:55:08 +08:00
rustdesk
e90010f0c0 no flatpak build for no upload 2024-04-30 12:44:10 +08:00
rustdesk
d6aaed1d82 fix ci 2024-04-30 12:03:53 +08:00
rustdesk
5082ed3b9e test flatpak arm64 2024-04-30 12:02:20 +08:00
rustdesk
a858e07db0 try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" 2024-04-30 11:58:48 +08:00
rustdesk
df8f785731 rename some ci 2024-04-30 09:16:22 +08:00
rustdesk
332674a4a1 refactor linux build ci 2024-04-30 09:08:32 +08:00
fufesou
a686849bb6 fix: msi, delete service (#7867)
* fix: msi, delete service

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: msi, replace 1060 to ERROR_SERVICE_DOES_NOT_EXIST

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-30 08:21:34 +08:00
rustdesk
54d041701c print dlopen error 2024-04-29 18:00:34 +08:00
rustdesk
496eec17a1 add libc6:arm64 to same to x64 2024-04-29 15:43:57 +08:00
rustdesk
25eb67c582 try out ubuntu 20.04 for appimage arm64 2024-04-29 15:40:55 +08:00
XLion
a26fab3cce Update translation (#7861) 2024-04-29 14:42:26 +08:00
Mateusz Prais
7cb67cf8fb Add missing translations, improve wording and fix typos (#7860) 2024-04-29 11:26:46 +08:00
rustdesk
96ec1e937f libnsl.so.1 is included in libc6(already added), but it did not work, we
still need to install libnsl in fedora, however, installing libnsl on
x64 works, but on arm64 crashed (though no librustdesk.so any more)
2024-04-28 23:06:37 +08:00
rustdesk
b6616ed2cc change to libnsl 2024-04-28 21:12:04 +08:00
rustdesk
7e263af75f VideoConnCount for future use 2024-04-28 21:08:49 +08:00
rustdesk
b4c51f3d41 add libnsl2 for fedora 40 2024-04-28 20:24:39 +08:00
fufesou
1dfbaa1e02 fix: test if valid server, control if try test with proxy (#7858)
* fix: test if valid server, control if try test with proxy

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: build

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-28 14:22:21 +08:00
21pages
bd717349a7 update hwcodec, fix screen jitter (#7857)
Signed-off-by: 21pages <pages21@163.com>
2024-04-28 13:26:55 +08:00
rustdesk
99d7752e25 try libcanberra-gtk3-module 2024-04-28 12:58:15 +08:00
fufesou
22e58d7623 fix: test if valid server (#7856)
* fix: #7853, test if valid server

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact comments

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-28 12:17:39 +08:00
rustdesk
30ad142868 change to github m1 instead of selfhost m1 2024-04-28 12:14:00 +08:00
rustdesk
e084ff4f7b x64 mac 2024-04-28 11:43:53 +08:00
rustdesk
3bbec4081a add gtk modules 2024-04-28 10:44:14 +08:00
21pages
45137d5506 fix switch to setting page (#7849)
Signed-off-by: 21pages <pages21@163.com>
2024-04-27 23:24:07 +08:00
21pages
e9d9a656ab fix mouse wheel scroll up not work when controlling mac hidpi (#7848)
Signed-off-by: 21pages <pages21@163.com>
2024-04-27 17:31:35 +08:00
jxdv
ade458b820 update cs.rs (#7847) 2024-04-27 16:42:17 +08:00
jxdv
b022dcbb70 update sk.rs (#7846) 2024-04-27 16:40:24 +08:00
fufesou
b403a7a25d fix: scroll percent is auto reset after detecting displays change (#7845)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-27 15:01:21 +08:00
fufesou
a6632632fa fix: multi-window, click-move (#7844)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-27 13:45:44 +08:00
bovirus
b863ea51ad Update Italian language (#7838) 2024-04-27 13:13:21 +08:00
fufesou
796b66b057 fix: multi-window, init perms (#7839)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-26 22:34:11 +08:00
21pages
2626dcbc5f fix black screen issue when controlling the second screen on versions that lack multiple display support while using vram decoding (#7836)
* avoid create unnecessary video decoder

Signed-off-by: 21pages <pages21@163.com>

* controlled side uses the most frequent selected codec

Signed-off-by: 21pages <pages21@163.com>

* fix black screen when control old version's second screen

For versions that do not support multiple displays, the display parameter is always 0, need set type of current display

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-04-26 19:42:47 +08:00
rustdesk
105a758914 disable flutter build of ios, since ios sdk not installed yet 2024-04-26 19:16:52 +08:00
rustdesk
98cce186c7 fix ci 2024-04-26 19:07:16 +08:00
rustdesk
474e13f8b9 move ios ci to arm64 mac 2024-04-26 19:00:38 +08:00
rustdesk
09f87238dc vcpkg -> FLUTTER_ELINUX_COMMIT_ID, and remove FLUTTER_ELINUX_COMMIT 2024-04-26 18:15:16 +08:00
fufesou
2b3f87d6f2 fix: msi, custom client, license (#7834)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-26 13:49:53 +08:00
LelieL91
9d3c823603 Update it.rs (#7833)
Fix typo
2024-04-26 11:57:26 +08:00
Kleofass
7e00d70f4d Update lv.rs (#7832) 2024-04-26 11:57:15 +08:00
Mr-Update
0828f747e3 Update de.rs (#7831) 2024-04-26 11:57:05 +08:00
rustdesk
d7de0c2578 improe job 2024-04-26 11:54:31 +08:00
fufesou
4dfc82f684 fix: msi, app icon (#7830)
* fix: msi, app icon

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: check if icon.ico exists

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-25 22:35:51 +08:00
solokot
92748de7d4 Update ru.rs (#7827) 2024-04-25 20:17:25 +08:00
21pages
0e7e27f99d update hwcodec, fix gpu/cpu stuck caused by nv codec (#7826)
* Disable all nv codec encoding on windows except nv sdk encoding,
  because it doesn't use CUContext
* Keep nv codec on linux, because I didn't reproduce the stuck on it
* Add ffmpeg d3d11 vram decoding

Signed-off-by: 21pages <pages21@163.com>
2024-04-25 20:16:48 +08:00
FastAct
23147f2328 Update nl.rs (#7824) 2024-04-25 19:04:48 +08:00
Sahil Yeole
3811f41076 Feat: Follow remote cursor and window focus | Auto display switch (#7717)
* feat: auto switch display on follow remote cursor

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* feat: auto switch display on follow remote window focus

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build and remove unused imports

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix linux get_focused_window_id

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* lock show remote cursor when follow remote cursor is enabled

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix config

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* prevent auto display switch on show all display and displays as individual windows

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix options

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix options

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* remove unused function

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* remove unwraps and improve iterations

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* set updateCursorPos to false to avoid interrupting remote cursor

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* update lang

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix web build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* update checks for options and enable in view mode

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use focused display index for window focus service

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use window center for windows display focused

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* remove unused imports

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use libxdo instead of xdotool

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix multi monitor check

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* enable show cursor when follow cursor is default

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* remove show_all_displays,use runtime state instead

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix show cursor lock state on default

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* remove view mode with follow options

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use separate message for follow current display

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix options

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* sciter support for follow remote cursor and window

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* add check for ui session handlers count

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use cached displays and remove peer info write

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* No follow options when show all displays

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* No follow options when multi ui session

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* turn off follow options when not used|prevent msgs

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use window center for switch in linux

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* use subbed display count to prevent switch msgs

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix web build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* move subbed displays count

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* fix build

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* add noperms for window focus

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* add subscribe for window focus

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* remove window_focus message and unsub on multi ui

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

* add multi ui session field

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>

---------

Signed-off-by: Sahil Yeole <sahilyeole93@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-04-25 13:26:02 +08:00
yuluo
43a0a4f8e0 fix: http/https proxy (#7821)
* add http(s) proxy

* Add front-end translation

* fix ui description

* For linux platform, add rustls support

* fix: Fix the proxy address test function.

* add: Added default prompts for agency agreement and some multi-language translations

* add: Http proxy request client

* fix: add async http proxy func and format the code

* add: Preliminary support for flutter front-end calling rust back-end http request

* Optimize HTTP calls

* Optimize HTTP calls

* fix: Optimize HTTP requests, refine translations, and fix dependencies

* fix: Win and macOS compilation errors

* fix: web platforms

* fix: Optimize import

* fix: Fix web platform issues

* fix: Fix web platform issues

* fix: update ci

* fix: test ci

* test: test CI

* Revert "fix: update ci"

This reverts commit 2e5f247b2e.

* test: test CI

* test: test CI

* fix: fix lock file

* fix: Optimize imports
2024-04-25 11:46:21 +08:00
whosehang
66d1af63b0 chore: fix some typos in comments (#7814)
Signed-off-by: whosehang <whosehang@outlook.com>
2024-04-24 14:16:19 +08:00
Cong Zhang
bc68f400f6 fix: add support for headless in docker (#7811) 2024-04-24 09:47:45 +08:00
rustdesk
67ee3a5a67 refresh_process_specifics 2024-04-23 20:26:21 +08:00
fufesou
b407893db5 refact: win idd, x86 on x64 (#7796)
* refact: win idd, x86 on x64

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* comments

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* typo

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: win idd, check if x64 and deviceinstaller64 exits

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: win idd

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: win idd, add logs

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-23 17:00:41 +08:00
fufesou
1b4a41b522 fix: portable, clear old files (#7802)
* fix: portable, clear old files

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix build

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-23 16:54:43 +08:00
RustDesk
ac7f8a6447 Revert "http/https proxy (#7600)" (#7801)
This reverts commit da57fcb641.
2024-04-23 15:26:16 +08:00
yuluo
da57fcb641 http/https proxy (#7600)
* add http(s) proxy

* Add front-end translation

* fix ui description

* For linux platform, add rustls support

* fix: Fix the proxy address test function.

* add: Added default prompts for agency agreement and some multi-language translations

* add: Http proxy request client

* fix: add async http proxy func and format the code

* add: Preliminary support for flutter front-end calling rust back-end http request

* Optimize HTTP calls

* Optimize HTTP calls

* fix: Optimize HTTP requests, refine translations, and fix dependencies
2024-04-23 15:00:23 +08:00
rustdesk
f11c332cb4 continue to remove --service runtime 2024-04-23 13:52:36 +08:00
flusheDData
3b4006b821 New terms added (#7797)
* Update es.rs

tip translation added

* Update es.rs
2024-04-22 23:01:00 +08:00
rustdesk
4c62d8c1b2 optimize runtime performance of macOS --service 2024-04-22 21:31:53 +08:00
fufesou
4f47d4482b refact: win, idd control (#7789)
* refact: win, idd control

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: win device control, better addr of

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: simple refact

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-22 10:37:08 +08:00
fufesou
ad062486ff Fix/win query arch (#7786)
* fix: win, query arch with GetNativeSystemInfo

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: idd, ci

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-21 14:55:42 +08:00
Kleofass
33c8bdfabf Update lv.rs (#7784) 2024-04-21 14:53:22 +08:00
rustdesk
0365c94407 remove IsWow64Process2 2024-04-20 19:24:44 +08:00
rustdesk
c81c4f9114 visual studio 2019 -> 2022 2024-04-20 18:20:09 +08:00
fufesou
d920953df1 typo (#7783)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-20 15:28:33 +08:00
rustdesk
f02a2e44d8 remove temp support_windows_specific_session 2024-04-20 15:13:04 +08:00
fufesou
fb1aa9c028 Fix. Multi-display connection, resolutions (#7782)
* fix: multi-display, change resolution

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: multi-displays, resolutions of displays

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: build

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact: Function rename

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* refact. Function rename

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-20 14:54:23 +08:00
jxdv
d4a1d4cd7e update cs.rs (#7780) 2024-04-20 11:31:07 +08:00
jxdv
8a6d9a1496 update sk.rs (#7779) 2024-04-20 11:30:57 +08:00
Mr-Update
a8eefbc9f0 Update de.rs (#7778) 2024-04-20 11:30:48 +08:00
bovirus
75521fe363 Update Italian language (#7776) 2024-04-20 11:30:38 +08:00
fufesou
c75778943f refact: msi, version and build date, check (#7775)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-19 18:16:07 +08:00
solokot
0e6fa37ae4 Update ru.rs (#7771) 2024-04-19 17:58:06 +08:00
fufesou
819eea9456 refact: msi (#7774)
* refact: msi

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Remove unused coment

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-19 17:57:04 +08:00
rustdesk
25eb8dc9b0 typo 2024-04-19 14:35:50 +08:00
rustdesk
40067f5aa2 no countdown in scam warning of android for custom build 2024-04-19 13:49:45 +08:00
rustdesk
1ffc10e44f flutter 19.6 2024-04-19 13:25:02 +08:00
21pages
5d5547ffef put andriod custom client logo to settings section list (#7768)
Signed-off-by: 21pages <pages21@163.com>
2024-04-19 12:44:52 +08:00
fufesou
e83c28bf54 refact: win, virtual display (#7767)
* refact: win, virtual display

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Update flutter-build.yml

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
2024-04-19 11:31:52 +08:00
fufesou
a3c0911529 fix: msi, explicit wide char api (#7764)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-19 00:47:13 +08:00
rustdesk
ae23f0de03 Improve android threading, https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
todo: should we add some condition to assure imageReader not be closed
while callback is running?
2024-04-18 22:38:32 +08:00
rustdesk
edb5529df3 flutter_icons does not generate mipmap-ldpi, so I drop it also 2024-04-18 17:32:15 +08:00
21pages
4d3fb77786 remove hwcodec for sciter armv7 (#7753)
Signed-off-by: 21pages <pages21@163.com>
2024-04-18 16:19:29 +08:00
rustdesk
24ac7c1626 remove google-services.json 2024-04-18 15:23:12 +08:00
writegr
9b2ec62be9 chore: fix some typos in comments (#7752)
Signed-off-by: writegr <wellweek@outlook.com>
2024-04-18 14:39:38 +08:00
21pages
4252b5e273 enable ffmpeg native h26x software decoders for all platforms (#7750)
* enable ffmpeg native h26x software decoders for all platforms

* h26x software decoders depend on hwcodec feature, so all platforms
  enable it, software h26x decoders are always available like vpx, no available check and no option
* ffmpeg:
	- build: mac arm64 build ffmpeg with my m1, others build with ci
	- version: win/linux use ffmpeg release/5.1, becaues higher version require higher nvidia driver,  other platforms use release/7.0
* test:
	- ios not test.
	- android: sometimes the screen will appear blurry, but it will recover after a while.
	- arm64 linux: test a example of hwcodec repo

Signed-off-by: 21pages <pages21@163.com>

* check hwcodec only when enabled and immediately when clicked enabled

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-04-18 13:12:45 +08:00
rustdesk
4e8cbe3db1 buildPresetPasswordWarning for android 2024-04-17 22:01:09 +08:00
rustdesk
bdf8bbe26f custom android 2024-04-17 12:49:25 +08:00
Kleofass
990c05fc3d Update lv.rs (#7745) 2024-04-17 10:20:31 +08:00
rustdesk
7bb4e22a77 fix ci 2024-04-16 22:53:01 +08:00
rustdesk
736503df1b fix ci 2024-04-16 22:38:47 +08:00
rustdesk
414455a8fb fix ci 2024-04-16 22:25:10 +08:00
rustdesk
bc0ab88e74 do not load empty custom_client_config 2024-04-16 22:17:05 +08:00
rustdesk
01ec539065 load android custom client for jvm startServer 2024-04-16 21:46:54 +08:00
rustdesk
b9792fc17d bridge rust version 2024-04-16 20:18:17 +08:00
rustdesk
c656c3c087 typo 2024-04-16 18:46:46 +08:00
rustdesk
a6c1d2d486 customClientConfig in dart 2024-04-16 18:42:35 +08:00
rustdesk
44bce59777 more java_ffi_FFI 2024-04-16 18:22:36 +08:00
fufesou
9b5e5aa474 fix: Msi custom app, different component guids (#7738)
* fix: Msi custom app, different component guids

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* fix: msi, update readme

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-16 14:12:39 +08:00
fufesou
a5d02998ad Fix. Msi, remove RustDesk words (#7732)
* Fix. Msi, remove RustDesk words

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Fix. Replace RustDesk in langs

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-16 13:09:20 +08:00
flusheDData
1d4c129e9c Update es.rs (#7733)
tip translation added
2024-04-16 11:10:31 +08:00
rustdesk
e9a6ca8ebc typo 2024-04-15 21:19:46 +08:00
rustdesk
28340c80dd fix me 2024-04-15 15:58:07 +08:00
rustdesk
05f6fde467 ffi.RustDesk -> ffi.FFI 2024-04-15 15:49:37 +08:00
rustdesk
56d353cb64 fix me 2024-04-15 15:39:25 +08:00
rustdesk
260d0cdc67 fix me 2024-04-15 15:37:33 +08:00
rustdesk
cdd92303b8 refactor android ffi 2024-04-15 15:20:27 +08:00
alewicki95
0dba37f4f7 Update pl.rs (#7723) 2024-04-15 11:13:32 +08:00
jxdv
7f3775a061 update system2 (#7722) 2024-04-15 00:52:27 +08:00
fufesou
a7a10f4eaa Refact. Win, file copy paste, default true (#7719)
* Refact. Win, file copy paste, default true

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Fix. File copy and paste menu, compatible with 1.2.3

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-14 21:25:26 +08:00
fufesou
5166083406 Fix. Msi, reg add SoftwareSASGeneration (#7708)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-14 21:03:51 +08:00
21pages
5c4d95ac0f not use av1 as auto codec on x86 sciter (#7714)
Signed-off-by: 21pages <pages21@163.com>
2024-04-13 21:10:36 +08:00
rustdesk
71c4d74759 fix typo 2024-04-13 14:43:27 +08:00
Mr-Update
725e8221a5 Update de.rs (#7712) 2024-04-13 13:12:40 +08:00
jxdv
92ae41cc13 update cs.rs (#7711) 2024-04-13 13:12:31 +08:00
jxdv
333b9130fe update sk.rs (#7710) 2024-04-13 13:12:20 +08:00
fufesou
60ae903cf3 Fix. Msi, reg values in HKCR (#7709)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-13 13:11:50 +08:00
fufesou
24ea55b010 Fix. Msi, firewall outbound profile (#7704)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-12 18:44:18 +08:00
fufesou
8231d07706 Fix. Msi. Terminate brokers. (#7693)
* Fix. Msi. Terminate brokers.

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Fix. Msi, remove tray shortcut in startmenu

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Msi. format

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi, set property

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Fix. Mis, only do InstallValidate if is Install

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-12 17:42:26 +08:00
21pages
98df2b111e hwcodec uses one repository (#7701)
* update hwcodec, gpucodec repo is merged to hwcodec

Signed-off-by: 21pages <pages21@163.com>

* rename gpucodec.rs to vram.rs

Signed-off-by: 21pages <pages21@163.com>

* rename all gpucodec to vram, because vram is a feature of hwcodec

Signed-off-by: 21pages <pages21@163.com>

* use one check process and one config file

* set check encode image size to 720p

Signed-off-by: 21pages <pages21@163.com>

---------

Signed-off-by: 21pages <pages21@163.com>
2024-04-12 17:26:24 +08:00
rustdesk
d8875f381b protobuf 3.4 2024-04-12 11:38:19 +08:00
fufesou
6e3d16173a Fix. Mis, do not InstallValidate when uninstall (#7692)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-11 23:27:09 +08:00
FastAct
a88b189664 Update nl.rs (#7691) 2024-04-11 20:42:21 +08:00
fufesou
48da00eb66 Fix. Msi, error 5 on uninstall (#7690)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-11 20:41:23 +08:00
RustDesk
5322332c5d Update flutter-build.yml 2024-04-11 20:23:08 +08:00
fufesou
c92cfbb20a Fix. Github ci (#7689)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-11 19:26:29 +08:00
fufesou
f673647072 Feat/msi (#7688)
* Feat. Msi, check is self-installed

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi.

1. Check if is self-installation.
2. Add firewall rule by custom action.

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi, github ci

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi, github ci

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi, github ci

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi, refact preprocess.py

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Feat. Msi

Signed-off-by: fufesou <shuanglongchen@yeah.net>

* Trivial, renames

Signed-off-by: fufesou <shuanglongchen@yeah.net>

---------

Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-11 18:54:32 +08:00
rustdesk
7ea5a9bba3 devices.py 2024-04-11 18:39:46 +08:00
fufesou
aa002c5d60 Refact. Msi, remove unused code (#7685)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-11 12:06:10 +08:00
fufesou
64020758d9 Feat. Msi (#7684)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-11 11:51:35 +08:00
bovirus
a4f357fd80 Update Italian language (#7678) 2024-04-11 10:50:40 +08:00
XLion
4c6fdfd76a Update tw.rs (#7677) 2024-04-11 10:50:26 +08:00
Tom-Brian Garcia
c972452310 fix: do not call WakeLock on Web (mobile) (#7668)
* fix: do not call WakeLock on Web (mobile)

* fix: do not call WakeLock on Web (mobile) - replaced isMobile by !isWeb
2024-04-10 21:32:05 +08:00
Alen Bajo
6ba6a16836 Update hr.rs (#7675)
Some small things changed
2024-04-10 17:27:59 +08:00
solokot
c63f1dfc53 Update ru.rs (#7674) 2024-04-10 15:19:19 +08:00
21pages
ac79c45529 remove tooltip of main window tab, opt some tooltip by raising its hierarchy (#7672)
Signed-off-by: 21pages <pages21@163.com>
2024-04-10 14:04:12 +08:00
Integral
7f58737f1f Update cn.rs (#7669) 2024-04-10 11:00:14 +08:00
RustDesk
ead8a48436 Update lang.rs 2024-04-10 00:23:45 +08:00
rustdesk
178d33155f fix check_process for empty arg since on mac, p.cmd() get "/Applications/RustDesk.app/Contents/MacOS/RustDesk", "XPC_SERVICE_NAME=com.carriez.RustDesk_server" 2024-04-09 20:38:07 +08:00
rustdesk
07ab8e508c fix me 2024-04-09 19:48:58 +08:00
rustdesk
ea1d4adfa6 fix check_process on mac since normal user can not get system's
process's command line arguments
2024-04-09 19:33:39 +08:00
rustdesk
f34a8ef0e5 fix me 2024-04-09 19:02:17 +08:00
mehdi-song
0df4b39bcc Update fa.rs (#7662) 2024-04-09 18:37:56 +08:00
rustdesk
07eca00bd5 feat: allow-only-conn-window-open https://github.com/rustdesk/rustdesk/discussions/7033 2024-04-09 18:34:44 +08:00
rustdesk
cf8386aa50 remove useless code in is_installed windows 2024-04-09 16:33:03 +08:00
SOZEL
44de6a5549 docs: update README-VN.md (#7655) 2024-04-08 21:38:47 +08:00
RustDesk
a41a9bcbf7 Update build-macos-arm64.yml 2024-04-08 21:13:56 +08:00
RustDesk
a679fae9c0 Update build-macos-arm64.yml 2024-04-08 21:11:34 +08:00
RustDesk
0f906f3937 Update build-macos-arm64.yml 2024-04-08 21:08:40 +08:00
RustDesk
c907daa741 Update build-macos-arm64.yml 2024-04-08 21:05:19 +08:00
RustDesk
df579de147 Update build-macos-arm64.yml 2024-04-08 21:03:54 +08:00
rustdesk
e884bdbbc4 fix https://github.com/rustdesk/rustdesk/pull/7654 2024-04-08 16:41:46 +08:00
Alen Bajo
84fc70e9a9 Create hr.rs (#7654)
Croatian translation (Hrvatski)
2024-04-08 16:30:50 +08:00
BrazilianArmy
4fc5d3f03b Update ptbr.rs (#7649)
Atualização de algumas KEYS para português BR
2024-04-08 11:56:12 +08:00
fufesou
9402516acd Refact. Flutter pub upgrade web (#7648)
Signed-off-by: fufesou <shuanglongchen@yeah.net>
2024-04-07 22:03:50 +08:00
208 changed files with 10917 additions and 3836 deletions

View File

@@ -8,6 +8,7 @@ on:
env:
FLUTTER_VERSION: "3.16.9"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
jobs:
generate_bridge:
@@ -49,9 +50,9 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: ''
components: "rustfmt"
- uses: Swatinem/rust-cache@v2
with:

View File

@@ -7,55 +7,224 @@ on:
workflow_dispatch:
env:
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
CARGO_NDK_VERSION: "3.1.2"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.16.9"
FLUTTER_VERSION: "3.19.6"
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2023.10.19
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
# vcpkg version: 2024.03.25
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
VERSION: "1.2.4"
NDK_VERSION: "r26b"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
# To make a custom build with your own servers set the below secret values
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
API_SERVER: '${{ secrets.API_SERVER }}'
UPLOAD_ARTIFACT: true
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
API_SERVER: "${{ secrets.API_SERVER }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
jobs:
build-for-macOS-arm64:
name: build-for-macOS-arm64
runs-on: [self-hosted, macOS, ARM64]
build-appimage:
name: Build image ${{ matrix.job.target }}
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-unknown-linux-gnu,
arch: x86_64,
}
- {
target: aarch64-unknown-linux-gnu,
arch: aarch64,
}
steps:
#- name: Import the codesign cert
# if: env.MACOS_P12_BASE64 != null
# uses: apple-actions/import-codesign-certs@v1
# continue-on-error: true
# with:
# p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
# p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
# keychain: rustdesk
#- name: Check sign and import sign key
# if: env.MACOS_P12_BASE64 != null
# run: |
# security default-keychain -s rustdesk.keychain
# security find-identity -v
- name: Run
- name: Checkout source code
uses: actions/checkout@v3
- name: Rename Binary
run: |
sudo apt-get update -y
sudo apt-get install -y wget libarchive-tools
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
- name: Patch archlinux PKGBUILD
if: matrix.job.arch == 'x86_64'
run: |
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD
fi
bsdtar -zxvf rustdesk-${{ env.VERSION }}.deb
tar -xvf ./data.tar.xz
case ${{ matrix.job.arch }} in
aarch64)
mkdir -p flutter/build/linux/arm64/release/bundle
cp -rf usr/lib/rustdesk/* flutter/build/linux/arm64/release/bundle/
;;
x86_64)
mkdir -p flutter/build/linux/x64/release/bundle
cp -rf usr/lib/rustdesk/* flutter/build/linux/x64/release/bundle/
;;
esac
- name: Build archlinux package
if: matrix.job.arch == 'x86_64'
uses: rustdesk-org/arch-makepkg-action@master
with:
packages: >
llvm
clang
libva
libvdpau
rust
gstreamer
unzip
git
cmake
gcc
curl
wget
nasm
zip
make
pkg-config
clang
gtk3
xdotool
libxcb
libxfixes
alsa-lib
pipewire
python
ttf-arphic-uming
libappindicator-gtk3
pam
gst-plugins-base
gst-plugin-pipewire
scripts: |
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
- name: Publish archlinux package
if: matrix.job.arch == 'x86_64'
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
res/rustdesk-${{ env.VERSION }}*.zst
- name: Build appimage package
shell: bash
run: |
cd /opt/build
#./update_mac_template.sh
#security default-keychain -s rustdesk.keychain
#security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
./agent.sh
# set-up appimage-builder
pushd /tmp
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
popd
# run appimage-builder
pushd appimage
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
- name: Publish appimage package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
build-flatpak:
name: Build Flatpak ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-20.04,
arch: x86_64,
}
- {
target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04,
on: [self-hosted, Linux, ARM64],
arch: aarch64,
}
steps:
- name: Checkout source code
uses: actions/checkout@v3
- name: Rename Binary
run: |
sudo apt-get update -y
sudo apt-get install -y wget
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
id: rpm
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
dockerRunArgs: |
--volume "${PWD}:/workspace"
shell: /bin/bash
install: |
apt-get update -y
apt-get install -y \
curl \
git \
rpm \
wget
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /workspace
# install
apt-get update -y
apt-get install -y \
cmake \
curl \
flatpak \
flatpak-builder \
gcc \
git \
g++ \
libgtk-3-dev \
nasm \
wget
# flatpak deps
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
# package
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak

View File

@@ -21,6 +21,9 @@ on:
- ".github/**"
- "docs/**"
- "README.md"
- "res/**"
- "appimage/**"
- "flatpak/**"
jobs:
# ensure_cargo_fmt:
@@ -71,12 +74,12 @@ jobs:
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
# - { target: i686-pc-windows-msvc , os: windows-2019 }
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
# - { target: x86_64-apple-darwin , os: macos-10.15 }
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
# - { target: x86_64-pc-windows-msvc , os: windows-2019 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps:
@@ -103,6 +106,7 @@ jobs:
gcc \
git \
g++ \
libpam0g-dev \
libasound2-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ on:
- ".github/**"
- "docs/**"
- "README.md"
- "res/**"
- "appimage/**"
- "flatpak/**"
jobs:
run-ci:

View File

@@ -15,7 +15,7 @@ jobs:
secrets: inherit
with:
upload-artifact: true
upload-tag: ${{ env.GITHUB_REF_NAME }}
upload-tag: ${{ github.ref_name }}
update-fdroid-version-file:
name: Publish RustDesk version file for F-Droid updater

View File

@@ -18,7 +18,7 @@ jobs:
fail-fast: false
matrix:
job:
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
steps:
- name: Checkout source code
uses: actions/checkout@v4

View File

@@ -10,7 +10,7 @@ on:
description: 'Target'
required: true
type: string
default: 'windows-2019'
default: 'windows-2022'
configuration:
description: 'Configuration'
required: true

289
Cargo.lock generated
View File

@@ -115,17 +115,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "amf"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -311,9 +300,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.5"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
dependencies = [
"flate2",
"futures-core",
@@ -528,6 +517,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -1836,9 +1831,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encoding_rs"
version = "0.8.33"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if 1.0.0",
]
@@ -2649,36 +2644,6 @@ dependencies = [
"system-deps 6.1.2",
]
[[package]]
name = "gpu_common"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"log",
"serde 1.0.190",
"serde_derive",
"serde_json 1.0.107",
]
[[package]]
name = "gpucodec"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"amf",
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
"nv",
"serde 1.0.190",
"serde_derive",
"serde_json 1.0.107",
"vpl",
]
[[package]]
name = "gstreamer"
version = "0.16.7"
@@ -2866,9 +2831,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
@@ -2917,6 +2882,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"backtrace",
"base64 0.22.0",
"bytes",
"chrono",
"confy",
@@ -2928,6 +2894,7 @@ dependencies = [
"flexi_logger",
"futures",
"futures-util",
"httparse",
"lazy_static",
"libc",
"log",
@@ -2939,16 +2906,22 @@ dependencies = [
"quinn",
"rand 0.8.5",
"regex",
"rustls-pki-types",
"rustls-platform-verifier",
"serde 1.0.190",
"serde_derive",
"serde_json 1.0.107",
"socket2 0.3.19",
"sodiumoxide",
"sysinfo",
"thiserror",
"tokio",
"tokio-socks",
"tokio-native-tls",
"tokio-rustls 0.26.0",
"tokio-socks 0.5.1-2",
"tokio-util",
"toml 0.7.8",
"url",
"uuid",
"winapi 0.3.9",
"zstd 0.13.0",
@@ -3025,9 +2998,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.9"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
dependencies = [
"bytes",
"fnv",
@@ -3036,9 +3009,9 @@ dependencies = [
[[package]]
name = "http-body"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http",
@@ -3065,8 +3038,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.2.0"
source = "git+https://github.com/21pages/hwcodec?branch=stable#52e1da2aae86acec5f374bc065f5921945b55e7b"
version = "0.4.9"
source = "git+https://github.com/21pages/hwcodec#7a52282267cb6aadbcd74c132bd4ecd43ab6f505"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -3078,9 +3051,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.27"
version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
dependencies = [
"bytes",
"futures-channel",
@@ -3093,7 +3066,7 @@ dependencies = [
"httpdate",
"itoa 1.0.9",
"pin-project-lite",
"socket2 0.4.10",
"socket2 0.5.5",
"tokio",
"tower-service",
"tracing",
@@ -3111,7 +3084,7 @@ dependencies = [
"hyper",
"rustls 0.21.10",
"tokio",
"tokio-rustls",
"tokio-rustls 0.24.1",
]
[[package]]
@@ -4134,17 +4107,6 @@ dependencies = [
"libc",
]
[[package]]
name = "nv"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
]
[[package]]
name = "objc"
version = "0.2.7"
@@ -4616,7 +4578,7 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
dependencies = [
"base64",
"base64 0.21.5",
"indexmap 1.9.3",
"line-wrap",
"quick-xml",
@@ -4762,9 +4724,9 @@ dependencies = [
[[package]]
name = "protobuf"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190"
checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0"
dependencies = [
"bytes",
"once_cell",
@@ -4774,9 +4736,9 @@ dependencies = [
[[package]]
name = "protobuf-codegen"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78"
checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77"
dependencies = [
"anyhow",
"once_cell",
@@ -4789,9 +4751,9 @@ dependencies = [
[[package]]
name = "protobuf-parse"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba"
checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642"
dependencies = [
"anyhow",
"indexmap 1.9.3",
@@ -4805,9 +4767,9 @@ dependencies = [
[[package]]
name = "protobuf-support"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c"
checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7"
dependencies = [
"thiserror",
]
@@ -4889,7 +4851,7 @@ dependencies = [
"ring 0.16.20",
"rustc-hash",
"rustls 0.20.9",
"rustls-native-certs",
"rustls-native-certs 0.6.3",
"slab",
"thiserror",
"tinyvec",
@@ -5224,10 +5186,10 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.11.23"
source = "git+https://github.com/rustdesk-org/reqwest"
source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3"
dependencies = [
"async-compression",
"base64",
"base64 0.21.5",
"bytes",
"encoding_rs",
"futures-core",
@@ -5247,8 +5209,8 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"rustls 0.21.10",
"rustls-native-certs",
"rustls-pemfile",
"rustls-native-certs 0.6.3",
"rustls-pemfile 1.0.3",
"serde 1.0.190",
"serde_json 1.0.107",
"serde_urlencoded",
@@ -5256,14 +5218,15 @@ dependencies = [
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-rustls 0.24.1",
"tokio-socks 0.5.1",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"webpki-roots 0.25.4",
"winreg 0.50.0",
]
@@ -5409,7 +5372,6 @@ dependencies = [
"arboard",
"async-process",
"async-trait",
"base64",
"bytes",
"cc",
"cfg-if 1.0.0",
@@ -5570,10 +5532,25 @@ checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
dependencies = [
"log",
"ring 0.17.5",
"rustls-webpki",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1"
dependencies = [
"log",
"once_cell",
"ring 0.17.5",
"rustls-pki-types",
"rustls-webpki 0.102.2",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.3"
@@ -5581,7 +5558,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pemfile 1.0.3",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"schannel",
"security-framework",
]
@@ -5592,9 +5582,52 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [
"base64",
"base64 0.21.5",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
dependencies = [
"base64 0.22.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustls-platform-verifier"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d"
dependencies = [
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.8.4",
"jni 0.19.0",
"log",
"once_cell",
"rustls 0.23.4",
"rustls-native-certs 0.7.0",
"rustls-platform-verifier-android",
"rustls-webpki 0.102.2",
"security-framework",
"security-framework-sys",
"webpki-roots 0.26.1",
"winapi 0.3.9",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@@ -5605,6 +5638,17 @@ dependencies = [
"untrusted 0.9.0",
]
[[package]]
name = "rustls-webpki"
version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
"ring 0.17.5",
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]]
name = "rustversion"
version = "1.0.14"
@@ -5683,7 +5727,6 @@ dependencies = [
"cfg-if 1.0.0",
"dbus",
"docopt",
"gpucodec",
"gstreamer",
"gstreamer-app",
"gstreamer-video",
@@ -5718,22 +5761,23 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.9.2"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.8.4",
"libc",
"num-bigint",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
dependencies = [
"core-foundation-sys 0.8.4",
"libc",
@@ -6456,6 +6500,17 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.4",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.1-2"
@@ -6472,6 +6527,18 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "tokio-socks"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
dependencies = [
"either",
"futures-util",
"thiserror",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.10"
@@ -6689,9 +6756,9 @@ dependencies = [
[[package]]
name = "try-lock"
version = "0.2.4"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
@@ -6900,17 +6967,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "vpl"
version = "0.1.0"
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
dependencies = [
"bindgen 0.59.2",
"cc",
"gpu_common",
"log",
]
[[package]]
name = "waker-fn"
version = "1.1.1"
@@ -7144,9 +7200,18 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.25.3"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
@@ -7919,6 +7984,12 @@ dependencies = [
"syn 2.0.55",
]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]]
name = "zip"
version = "0.6.6"

View File

@@ -20,18 +20,14 @@ path = "src/naming.rs"
inline = []
cli = []
flutter_texture_render = []
appimage = []
flatpak = []
use_samplerate = ["samplerate"]
use_rubato = ["rubato"]
use_dasp = ["dasp"]
flutter = ["flutter_rust_bridge"]
default = ["use_dasp"]
hwcodec = ["scrap/hwcodec"]
gpucodec = ["scrap/gpucodec"]
vram = ["scrap/vram"]
mediacodec = ["scrap/mediacodec"]
linux_headless = ["pam" ]
virtual_display_driver = ["virtual_display"]
plugin_framework = []
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
unix-file-copy-paste = [
@@ -65,7 +61,6 @@ samplerate = { version = "0.2", optional = true }
uuid = { version = "1.3", features = ["v4"] }
clap = "4.2"
rpassword = "7.2"
base64 = "0.21"
num_cpus = "1.15"
bytes = { version = "1.4", features = ["serde"] }
default-net = "0.14"
@@ -100,10 +95,23 @@ system_shutdown = "4.0"
qrcode-generator = "4.1"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
winapi = { version = "0.3", features = [
"winuser",
"wincrypt",
"shellscalingapi",
"pdh",
"synchapi",
"memoryapi",
"shellapi",
"devguid",
"setupapi",
"cguid",
"cfgmgr32",
"ioapiset",
] }
winreg = "0.11"
windows-service = "0.6"
virtual_display = { path = "libs/virtual_display", optional = true }
virtual_display = { path = "libs/virtual_display" }
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
shared_memory = "0.12"
tauri-winrt-notification = "0.1.2"
@@ -132,10 +140,10 @@ wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false }
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.27" }
@@ -146,7 +154,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
pam = { git="https://github.com/fufesou/pam", optional = true }
pam = { git="https://github.com/fufesou/pam" }
users = { version = "0.11" }
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}

View File

@@ -2,7 +2,7 @@
version: 1
script:
- rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.4.deb
- bsdtar -zxvf rustdesk.deb
- tar -xvf ./data.tar.xz
- mkdir ./AppDir
- mv ./usr ./AppDir/usr
@@ -26,18 +26,18 @@ AppDir:
- arm64
allow_unauthenticated: true
sources:
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
universe multiverse
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
include:
- libc6
- libc6:arm64
- libgtk-3-0
- libxcb-randr0
- libxdo3
@@ -51,9 +51,15 @@ AppDir:
- libva-x11-2
- libvdpau1
- libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire
- libwayland-client0
- libwayland-cursor0
- libwayland-egl1
- libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude:
- humanity-icon-theme
- hicolor-icon-theme
@@ -69,8 +75,11 @@ AppDir:
- usr/share/doc/*/TODO.*
runtime:
env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
GDK_BACKEND: x11
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30

View File

@@ -2,7 +2,7 @@
version: 1
script:
- rm -rf ./AppDir || true
- bsdtar -zxvf ../rustdesk-1.2.4.deb
- bsdtar -zxvf rustdesk.deb
- tar -xvf ./data.tar.xz
- mkdir ./AppDir
- mv ./usr ./AppDir/usr
@@ -26,18 +26,16 @@ AppDir:
- amd64
allow_unauthenticated: true
sources:
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
universe multiverse
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
universe multiverse
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
bionic main
include:
- libc6:amd64
- libgtk-3-0
@@ -54,9 +52,14 @@ AppDir:
- libvdpau1
- libgstreamer-plugins-base1.0-0
- gstreamer1.0-pipewire
- libwayland-client0
- libwayland-cursor0
- libwayland-egl1
- libpulse0
- packagekit-gtk3-module
- libcanberra-gtk3-module
- libpam0g
- libdrm2
exclude:
- humanity-icon-theme
- hicolor-icon-theme
@@ -72,8 +75,11 @@ AppDir:
- usr/share/doc/*/TODO.*
runtime:
env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
GDK_BACKEND: x11
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30

View File

@@ -33,9 +33,9 @@ def get_arch() -> str:
def system2(cmd):
err = os.system(cmd)
if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.")
exit_code = os.system(cmd)
if exit_code != 0:
sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
sys.exit(-1)
@@ -111,6 +111,8 @@ def make_parser():
'Available: PrivacyMode. Special value is "ALL" and empty "". Default is empty.')
parser.add_argument('--flutter', action='store_true',
help='Build flutter package', default=False)
parser.add_argument('--disable-flutter-texture-render', action='store_true',
help='Build flutter package', default=False)
parser.add_argument(
'--hwcodec',
action='store_true',
@@ -118,9 +120,9 @@ def make_parser():
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
)
parser.add_argument(
'--gpucodec',
'--vram',
action='store_true',
help='Enable feature gpucodec, only available on windows now.'
help='Enable feature vram, only available on windows now.'
)
parser.add_argument(
'--portable',
@@ -132,16 +134,6 @@ def make_parser():
action='store_true',
help='Build with unix file copy paste feature'
)
parser.add_argument(
'--flatpak',
action='store_true',
help='Build rustdesk libs with the flatpak feature enabled'
)
parser.add_argument(
'--appimage',
action='store_true',
help='Build rustdesk libs with the appimage feature enabled'
)
parser.add_argument(
'--skip-cargo',
action='store_true',
@@ -282,15 +274,12 @@ def get_features(args):
features = ['inline'] if not args.flutter else []
if args.hwcodec:
features.append('hwcodec')
if args.gpucodec:
features.append('gpucodec')
if args.vram:
features.append('vram')
if args.flutter:
features.append('flutter')
features.append('flutter_texture_render')
if args.flatpak:
features.append('flatpak')
if args.appimage:
features.append('appimage')
if not args.disable_flutter_texture_render:
features.append('flutter_texture_render')
if args.unix_file_copy_paste:
features.append('unix-file-copy-paste')
print("features:", features)

View File

@@ -1,27 +1,29 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br>
<a href="#free-public-servers">Máy chủ</a> •
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#free-public-servers">Server</a> •
<a href="#raw-steps-to-build">Build</a> •
<a href="#how-to-build-with-docker">Docker</a> •
<a href="#file-structure">Cấu trúc tệp tin</a> •
<a href="#file-structure">Structure</a> •
<a href="#snapshot">Snapshot</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
</p>
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
**RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases)
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
@@ -29,28 +31,25 @@ Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để
## Dependencies
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bn cần tự tải về thư viện sciter.
Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bn Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính.
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Cách để build
## Các bước build cơ bản
- Chuẩn bị môi trường phát triển Rust và môi trường build C++
- Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng.
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
- Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `VCPKG_ROOT`.
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
- Linux/MacOS: `vcpkg install libvpx libyuv opus aom`
- Chạy lệnh `cargo run`
## [Build](https://rustdesk.com/docs/en/dev/build/)
## Cách để build cho Linux
## Cách build cho Linux
### Ubuntu 18 (Debian 10)
@@ -70,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Cách cài vcpkg
### Cách cài đặt `vcpkg`
```sh
git clone https://github.com/microsoft/vcpkg
@@ -82,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora)
### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -95,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Cách build
### Build
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -108,9 +107,9 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Cách để build sử dụng Docker
## Cách build bằng Docker
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:
Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -118,37 +117,37 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này:
Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, nhng lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh --release. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
```sh
target/debug/rustdesk
```
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh:
Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
```sh
target/release/rustdesk
```
Hãy đảm bảo bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn.
Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
## Cấu trúc tệp tin
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed.
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
## Snapshot

View File

@@ -8,17 +8,31 @@
"modules": [
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
"xdotool.json",
{
"name": "pam",
"buildsystem": "simple",
"build-commands": [
"./configure --disable-selinux --prefix=/app && make -j4 install"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
}
]
},
{
"name": "rustdesk",
"buildsystem": "simple",
"build-commands": [
"bsdtar -zxvf rustdesk-1.2.4.deb",
"bsdtar -zxvf rustdesk.deb",
"tar -xvf ./data.tar.xz",
"cp -r ./usr/* /app/",
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
"mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
],
@@ -26,7 +40,7 @@
"sources": [
{
"type": "file",
"path": "../rustdesk-1.2.4.deb"
"path": "./rustdesk.deb"
},
{
"type": "file",

View File

@@ -108,4 +108,3 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
}
apply plugin: 'com.google.gms.google-services'

View File

@@ -1,40 +0,0 @@
{
"project_info": {
"project_number": "768133699366",
"firebase_url": "https://rustdesk.firebaseio.com",
"project_id": "rustdesk",
"storage_bucket": "rustdesk.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
"android_client_info": {
"package_name": "com.carriez.flutter_hbb"
}
},
"oauth_client": [
{
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

View File

@@ -3,6 +3,7 @@
package="com.carriez.flutter_hbb">
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

View File

@@ -1,5 +1,7 @@
package com.carriez.flutter_hbb
import ffi.FFI
/**
* Capture screen,get video and audio,send to rust.
* Dispatch notifications
@@ -52,7 +54,6 @@ const val NOTIFY_ID_OFFSET = 100
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
// video const
const val MAX_SCREEN_SIZE = 1200
const val VIDEO_KEY_BIT_RATE = 1024_000
const val VIDEO_KEY_FRAME_RATE = 30
@@ -64,10 +65,6 @@ const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
class MainService : Service() {
init {
System.loadLibrary("rustdesk")
}
@Keep
@RequiresApi(Build.VERSION_CODES.N)
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
@@ -156,23 +153,9 @@ class MainService : Service() {
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
// jvm call rust
private external fun init(ctx: Context)
/// When app start on boot, app_dir will not be passed from flutter
/// so pass a app_dir here to rust server
private external fun startServer(app_dir: String)
private external fun startService()
private external fun onVideoFrameUpdate(buf: ByteBuffer)
private external fun onAudioFrameUpdate(buf: ByteBuffer)
private external fun translateLocale(localeName: String, input: String): String
private external fun refreshScreen()
private external fun setFrameRawEnable(name: String, value: Boolean)
// private external fun sendVp9(data: ByteArray)
private fun translate(input: String): String {
Log.d(logTag, "translate:$LOCAL_NAME")
return translateLocale(LOCAL_NAME, input)
return FFI.translateLocale(LOCAL_NAME, input)
}
companion object {
@@ -188,6 +171,7 @@ class MainService : Service() {
private val useVP9 = false
private val binder = LocalBinder()
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
// video
private var mediaProjection: MediaProjection? = null
@@ -210,8 +194,8 @@ class MainService : Service() {
override fun onCreate() {
super.onCreate()
Log.d(logTag,"MainService onCreate")
init(this)
Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
FFI.init(this)
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
@@ -223,7 +207,7 @@ class MainService : Service() {
// keep the config dir same with flutter
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
startServer(configPath)
FFI.startServer(configPath, "")
createForegroundNotification()
}
@@ -265,12 +249,6 @@ class MainService : Service() {
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
var scale = 1
if (w != 0 && h != 0) {
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
scale = 2
w /= scale
h /= scale
dpi /= scale
}
if (SCREEN_INFO.width != w) {
SCREEN_INFO.width = w
SCREEN_INFO.height = h
@@ -278,7 +256,7 @@ class MainService : Service() {
SCREEN_INFO.dpi = dpi
if (isStart) {
stopCapture()
refreshScreen()
FFI.refreshScreen()
startCapture()
}
}
@@ -306,7 +284,7 @@ class MainService : Service() {
createForegroundNotification()
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
startService()
FFI.startService()
}
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
val mediaProjectionManager =
@@ -354,12 +332,13 @@ class MainService : Service() {
).apply {
setOnImageAvailableListener({ imageReader: ImageReader ->
try {
// If not call acquireLatestImage, listener will not be called again
imageReader.acquireLatestImage().use { image ->
if (image == null) return@setOnImageAvailableListener
if (image == null || !isStart) return@setOnImageAvailableListener
val planes = image.planes
val buffer = planes[0].buffer
buffer.rewind()
onVideoFrameUpdate(buffer)
FFI.onVideoFrameUpdate(buffer)
}
} catch (ignored: java.lang.Exception) {
}
@@ -378,6 +357,7 @@ class MainService : Service() {
Log.w(logTag, "startCapture fail,mediaProjection is null")
return false
}
updateScreenInfo(resources.configuration.orientation)
Log.d(logTag, "Start Capture")
surface = createSurface()
@@ -393,34 +373,45 @@ class MainService : Service() {
}
checkMediaPermission()
_isStart = true
setFrameRawEnable("video",true)
setFrameRawEnable("audio",true)
FFI.setFrameRawEnable("video",true)
FFI.setFrameRawEnable("audio",true)
return true
}
@Synchronized
fun stopCapture() {
Log.d(logTag, "Stop Capture")
setFrameRawEnable("video",false)
setFrameRawEnable("audio",false)
FFI.setFrameRawEnable("video",false)
FFI.setFrameRawEnable("audio",false)
_isStart = false
// release video
virtualDisplay?.release()
surface?.release()
if (reuseVirtualDisplay) {
// The virtual display video projection can be paused by calling `setSurface(null)`.
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
virtualDisplay?.setSurface(null)
} else {
virtualDisplay?.release()
}
// suface needs to be release after `imageReader.close()` to imageReader access released surface
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
imageReader?.close()
imageReader = null
videoEncoder?.let {
it.signalEndOfInputStream()
it.stop()
it.release()
}
virtualDisplay = null
if (!reuseVirtualDisplay) {
virtualDisplay = null
}
videoEncoder = null
// suface needs to be release after `imageReader.close()` to imageReader access released surface
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
surface?.release()
// release audio
audioRecordStat = false
audioRecorder?.release()
audioRecorder = null
minBufferSize = 0
}
fun destroy() {
@@ -428,8 +419,11 @@ class MainService : Service() {
_isReady = false
stopCapture()
imageReader?.close()
imageReader = null
if (reuseVirtualDisplay) {
virtualDisplay?.release()
virtualDisplay = null
}
mediaProjection = null
checkMediaPermission()
@@ -459,11 +453,7 @@ class MainService : Service() {
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
return
}
virtualDisplay = mp.createVirtualDisplay(
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)
createOrSetVirtualDisplay(mp, surface!!)
}
private fun startVP9VideoRecorder(mp: MediaProjection) {
@@ -475,11 +465,28 @@ class MainService : Service() {
}
it.setCallback(cb)
it.start()
virtualDisplay = mp.createVirtualDisplay(
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)
createOrSetVirtualDisplay(mp, surface!!)
}
}
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
try {
virtualDisplay?.let {
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
it.setSurface(s)
} ?: let {
virtualDisplay = mp.createVirtualDisplay(
"RustDeskVD",
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
s, null, null
)
}
} catch (e: SecurityException) {
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
// This initiates a prompt dialog for the user to confirm screen projection.
requestMediaProjection()
}
}
@@ -537,9 +544,13 @@ class MainService : Service() {
thread {
while (audioRecordStat) {
audioReader!!.readSync(audioRecorder!!)?.let {
onAudioFrameUpdate(it)
FFI.onAudioFrameUpdate(it)
}
}
// let's release here rather than onDestroy to avoid threading issue
audioRecorder?.release()
audioRecorder = null
minBufferSize = 0
Log.d(logTag, "Exit audio thread")
}
} catch (e: Exception) {

View File

@@ -0,0 +1,21 @@
// ffi.kt
package ffi
import android.content.Context
import java.nio.ByteBuffer
object FFI {
init {
System.loadLibrary("rustdesk")
}
external fun init(ctx: Context)
external fun startServer(app_dir: String, custom_client_config: String)
external fun startService()
external fun onVideoFrameUpdate(buf: ByteBuffer)
external fun onAudioFrameUpdate(buf: ByteBuffer)
external fun translateLocale(localeName: String, input: String): String
external fun refreshScreen()
external fun setFrameRawEnable(name: String, value: Boolean)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -139,4 +139,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
COCOAPODS: 1.12.1
COCOAPODS: 1.15.2

View File

@@ -159,7 +159,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -55,6 +55,12 @@ var isMobile = isAndroid || isIOS;
var version = '';
int androidVersion = 0;
// Only used on Linux.
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
// https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
// So we need to use this flag to enable/disable resizable.
bool _linuxWindowResizable = true;
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@@ -1572,7 +1578,7 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
// `await windowManager.isMaximized()` will always return true
// if is not resizable. The reason is unknown.
//
// `windowManager.setResizable(!bind.isIncomingOnly());` in main.dart
// `setResizable(!bind.isIncomingOnly());` in main.dart
isMaximized =
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
position = await windowManager.getPosition();
@@ -2978,16 +2984,16 @@ Future<bool> setServerConfig(
}
// id
if (config.idServer.isNotEmpty && errMsgs != null) {
errMsgs[0].value =
translate(await bind.mainTestIfValidServer(server: config.idServer));
errMsgs[0].value = translate(await bind.mainTestIfValidServer(
server: config.idServer, testWithProxy: true));
if (errMsgs[0].isNotEmpty) {
return false;
}
}
// relay
if (config.relayServer.isNotEmpty && errMsgs != null) {
errMsgs[1].value =
translate(await bind.mainTestIfValidServer(server: config.relayServer));
errMsgs[1].value = translate(await bind.mainTestIfValidServer(
server: config.relayServer, testWithProxy: true));
if (errMsgs[1].isNotEmpty) {
return false;
}
@@ -3106,6 +3112,27 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
}
Widget loadPowered(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
launchUrl(Uri.parse('https://rustdesk.com'));
},
child: Opacity(
opacity: 0.5,
child: Text(
translate("powered_by_me"),
overflow: TextOverflow.clip,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
)),
),
).marginOnly(top: 6);
}
// max 300 x 60
Widget loadLogo() {
return FutureBuilder<ByteData>(
@@ -3155,3 +3182,102 @@ bool isInHomePage() {
final controller = Get.find<DesktopTabController>();
return controller.state.value.selected == 0;
}
Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>(
future: bind.isPresetPassword(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
} else if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) {
return Container(
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize: 20,
fontWeight: FontWeight.bold,
),
)).paddingOnly(bottom: 8),
Text(
translate("preset_password_warning"),
style: TextStyle(color: Colors.red),
)
],
).paddingAll(8),
); // Show a warning message if the Future completed with true
} else {
return SizedBox
.shrink(); // Show nothing if the Future completed with false or null
}
},
);
}
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
boxShadow() => isMainDesktopWindow
? <BoxShadow>[
if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
BoxShadow(
color: Colors.black.withOpacity(0.1),
offset: Offset(
0.0,
stateGlobal.isFocused.isTrue
? kFrameBoxShadowOffsetFocused
: kFrameBoxShadowOffsetUnfocused),
blurRadius: kFrameBoxShadowBlurRadius,
),
]
: null;
return Obx(
() => Container(
decoration: BoxDecoration(
color: isMainDesktopWindow
? Colors.transparent
: Theme.of(context).colorScheme.background,
border: Border.all(
color: Theme.of(context).dividerColor,
width: stateGlobal.windowBorderWidth.value,
),
borderRadius: BorderRadius.circular(
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
? 0
: kFrameBorderRadius,
),
boxShadow: boxShadow(),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
? 0
: kFrameClipRRectBorderRadius,
),
child: child,
),
),
);
}
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
// See _linuxWindowResizable for more details.
// So we use `setResizable()` instead of `windowManager.setResizable()`.
//
// We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
setResizable(bool resizable) {
if (isLinux) {
_linuxWindowResizable = resizable;
stateGlobal.refreshResizeEdgeSize();
} else {
windowManager.setResizable(resizable);
}
}

View File

@@ -168,6 +168,29 @@ class ShowRemoteCursorState {
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class ShowRemoteCursorLockState {
static String tag(String id) => 'show_remote_cursor_lock_$id';
static void init(String id) {
final key = tag(id);
if (!Get.isRegistered(tag: key)) {
final RxBool state = false.obs;
Get.put(state, tag: key);
} else {
Get.find<RxBool>(tag: key).value = false;
}
}
static void delete(String id) {
final key = tag(id);
if (Get.isRegistered(tag: key)) {
Get.delete(tag: key);
}
}
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
}
class KeyboardEnabledState {
static String tag(String id) => 'keyboard_enabled_$id';
@@ -315,6 +338,7 @@ initSharedStates(String id) {
CurrentDisplayState.init(id);
KeyboardEnabledState.init(id);
ShowRemoteCursorState.init(id);
ShowRemoteCursorLockState.init(id);
RemoteCursorMovedState.init(id);
FingerprintState.init(id);
PeerBoolOption.init(id, 'zoom-cursor', () => false);
@@ -327,6 +351,7 @@ removeSharedStates(String id) {
BlockInputState.delete(id);
CurrentDisplayState.delete(id);
ShowRemoteCursorState.delete(id);
ShowRemoteCursorLockState.delete(id);
KeyboardEnabledState.delete(id);
RemoteCursorMovedState.delete(id);
FingerprintState.delete(id);

View File

@@ -385,10 +385,11 @@ class _AddressBookState extends State<AddressBook> {
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
sortMenuItem(),
syncMenuItem(),
if (canWrite) syncMenuItem(),
filterMenuItem(),
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(),
if (!gFFI.abModel.legacyMode.value)
if (!gFFI.abModel.legacyMode.value && canWrite)
MenuEntryDivider<String>(),
if (!gFFI.abModel.legacyMode.value && canWrite)
getEntry(translate("ab_web_console_tip"), () async {
final url = await bind.mainGetApiServer();
if (await canLaunchUrlString(url)) {

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
typedef AudioINputSetDevice = void Function(String device);
typedef AudioInputBuilder = Widget Function(
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
class AudioInput extends StatelessWidget {
final AudioInputBuilder builder;
const AudioInput({Key? key, required this.builder}) : super(key: key);
static String getDefault() {
if (isWindows) return translate('System Sound');
return '';
}
static Future<String> getValue() async {
String device = await bind.mainGetOption(key: 'audio-input');
if (device.isNotEmpty) {
return device;
} else {
return getDefault();
}
}
static Future<void> setDevice(String device) async {
if (device == getDefault()) device = '';
await bind.mainSetOption(key: 'audio-input', value: device);
}
static Future<Map<String, Object>> getDevicesInfo() async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) {
devices.insert(0, translate('System Sound'));
}
String current = await getValue();
return {'devices': devices, 'current': current};
}
@override
Widget build(BuildContext context) {
return futureBuilder(
future: getDevicesInfo(),
hasData: (data) {
String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) {
return const Offstage();
}
return builder(devices, currentDevice, setDevice);
},
);
}
}

View File

@@ -86,17 +86,6 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
var _queryCount = 0;
var _exit = false;
late final mobileWidth = () {
const minWidth = 320.0;
final windowWidth = MediaQuery.of(context).size.width;
var width = windowWidth - 2 * space;
if (windowWidth > minWidth + 2 * space) {
final n = (windowWidth / (minWidth + 2 * space)).floor();
width = windowWidth / n - 2 * space;
}
return width;
}();
final _scrollController = ScrollController();
_PeersViewState() {
@@ -189,61 +178,60 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
onVisibilityChanged: onVisibilityChanged,
child: widget.peerCardBuilder(peer),
);
final windowWidth = MediaQuery.of(context).size.width;
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
//
// No need to listen the currentTab change event.
// Because the currentTab change event will trigger the peers change event,
// and the peers change event will trigger _buildPeersView().
final currentTab =
Provider.of<PeerTabModel>(context, listen: false).currentTab;
final hideAbTagsPanel =
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
return (isDesktop || isWebDesktop)
? Obx(
() => SizedBox(
width: peerCardUiType.value != PeerUiType.list
? 220
: currentTab == PeerTabIndex.group.index ||
(currentTab == PeerTabIndex.ab.index &&
!hideAbTagsPanel)
? windowWidth - 390
: windowWidth - 227,
height: peerCardUiType.value == PeerUiType.grid
? 140
: peerCardUiType.value != PeerUiType.list
? 42
: 45,
child: visibilityChild,
),
)
: SizedBox(width: mobileWidth, child: visibilityChild);
? Obx(() => peerCardUiType.value == PeerUiType.list
? Container(height: 45, child: visibilityChild)
: peerCardUiType.value == PeerUiType.grid
? SizedBox(
width: 220, height: 140, child: visibilityChild)
: SizedBox(
width: 220, height: 42, child: visibilityChild))
: Container(child: visibilityChild);
}
final Widget child;
if (isMobile) {
child = DynamicGridView.builder(
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
child = ListView.builder(
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
return buildOnePeer(peers[index]).marginOnly(
top: index == 0 ? 0 : space / 2, bottom: space / 2);
},
);
} else {
child = DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2, crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
);
child = Obx(() => peerCardUiType.value == PeerUiType.list
? DesktopScrollWrapper(
scrollController: _scrollController,
child: ListView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]).marginOnly(
right: space,
top: index == 0 ? 0 : space / 2,
bottom: space / 2);
}),
)
: DesktopScrollWrapper(
scrollController: _scrollController,
child: DynamicGridView.builder(
controller: _scrollController,
physics: DraggableNeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithWrapping(
mainAxisSpacing: space / 2,
crossAxisSpacing: space),
itemCount: peers.length,
itemBuilder: (BuildContext context, int index) {
return buildOnePeer(peers[index]);
}),
));
}
if (updateEvent == UpdateEvent.load) {

View File

@@ -203,7 +203,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
}
@@ -222,6 +222,9 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (handleTouch) {
if (isDesktop) {
ffi.cursorModel.trySetRemoteWindowCoords();
}
inputModel.sendMouse('down', MouseButtons.left);
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
@@ -241,13 +244,16 @@ class _RawTouchGestureDetectorRegionState
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
onOneFingerPanEnd(DragEndDetails d) {
if (lastDeviceKind != PointerDeviceKind.touch) {
return;
}
if (isDesktop) {
ffi.cursorModel.clearRemoteWindowCoords();
}
inputModel.sendMouse('up', MouseButtons.left);
}

View File

@@ -212,6 +212,8 @@ List<(String, String)> otherDefaultSettings() {
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
('Show remote cursor', 'show_remote_cursor'),
('Follow remote cursor', 'follow_remote_cursor'),
('Follow remote window focus', 'follow_remote_window'),
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
('Show quality monitor', 'show_quality_monitor'),
('Mute', 'disable_audio'),

View File

@@ -355,14 +355,18 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
TRadioMenu<String> radio(String label, String value, bool enabled) {
return TRadioMenu<String>(
child: Text(translate(label)),
child: Text(label),
value: value,
groupValue: groupValue,
onChanged: enabled ? onChanged : null);
}
var autoLabel = translate('Auto');
if (groupValue == 'auto') {
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
}
return [
radio('Auto', 'auto', true),
radio(autoLabel, 'auto', true),
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
radio('VP9', 'vp9', true),
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
@@ -371,7 +375,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
];
}
Future<List<TToggleMenu>> toolbarDisplayToggle(
Future<List<TToggleMenu>> toolbarCursor(
BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel;
@@ -384,12 +388,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland) {
final state = ShowRemoteCursorState.find(id);
final lockState = ShowRemoteCursorLockState.find(id);
final enabled = !ffiModel.viewOnly;
final option = 'show-remote-cursor';
if (pi.currentDisplay == kAllDisplayValue ||
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
lockState.value = false;
}
v.add(TToggleMenu(
child: Text(translate('Show remote cursor')),
value: state.value,
onChanged: enabled
onChanged: enabled && !lockState.value
? (value) async {
if (value == null) return;
await bind.sessionToggleOption(
@@ -399,6 +408,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
}
: null));
}
// follow remote cursor
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-cursor';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
final showCursorOption = 'show-remote-cursor';
final showCursorState = ShowRemoteCursorState.find(id);
final showCursorLockState = ShowRemoteCursorLockState.find(id);
final showCursorEnabled = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
showCursorLockState.value = value;
if (value && !showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
v.add(TToggleMenu(
child: Text(translate('Follow remote cursor')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
showCursorLockState.value = value;
if (!showCursorEnabled) {
await bind.sessionToggleOption(
sessionId: sessionId, value: showCursorOption);
showCursorState.value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: showCursorOption);
}
}));
}
// follow remote window focus
if (pi.platform != kPeerPlatformAndroid &&
!ffi.canvasModel.cursorEmbedded &&
!pi.isWayland &&
versionCmp(pi.version, "1.2.4") >= 0 &&
pi.displays.length > 1 &&
pi.currentDisplay != kAllDisplayValue &&
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
final option = 'follow-remote-window';
final value =
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
v.add(TToggleMenu(
child: Text(translate('Follow remote window focus')),
value: value,
onChanged: (value) async {
if (value == null) return;
await bind.sessionToggleOption(sessionId: sessionId, value: option);
value = bind.sessionGetToggleOptionSync(
sessionId: sessionId, arg: option);
}));
}
// zoom cursor
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
if (!isMobile &&
@@ -417,6 +487,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
},
));
}
return v;
}
Future<List<TToggleMenu>> toolbarDisplayToggle(
BuildContext context, String id, FFI ffi) async {
List<TToggleMenu> v = [];
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
// show quality monitor
final option = 'show-quality-monitor';
v.add(TToggleMenu(
@@ -441,10 +522,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Mute'))));
}
// file copy and paste
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
isWindows &&
pi.platform == kPeerPlatformWindows;
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
if (ffiModel.keyboard &&
perms['file'] != false &&
bind.mainHasFileClipboard() &&
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
final enabled = !ffiModel.viewOnly;
final option = 'enable-file-transfer';
final value =

View File

@@ -19,7 +19,11 @@ const kKeyTranslateMode = 'translate';
const String kPlatformAdditionsIsWayland = "is_wayland";
const String kPlatformAdditionsHeadless = "headless";
const String kPlatformAdditionsIsInstalled = "is_installed";
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
const String kPlatformAdditionsIddImpl = "idd_impl";
const String kPlatformAdditionsRustDeskVirtualDisplays =
"rustdesk_virtual_displays";
const String kPlatformAdditionsAmyuniVirtualDisplays =
"amyuni_virtual_displays";
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
const String kPlatformAdditionsSupportedPrivacyModeImpl =
"supported_privacy_mode_impl";
@@ -58,6 +62,7 @@ const String kWindowEventActiveSession = "active_session";
const String kWindowEventActiveDisplaySession = "active_display_session";
const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
@@ -121,12 +126,11 @@ double kNewWindowOffset = isWindows
? 30.0
: 50.0;
EdgeInsets get kDragToResizeAreaPadding =>
!kUseCompatibleUiMode && isLinux
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
? EdgeInsets.zero
: EdgeInsets.all(5.0)
: EdgeInsets.zero;
// https://en.wikipedia.org/wiki/Non-breaking_space
const int $nbsp = 0x00A0;
@@ -150,9 +154,15 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
const kMaximizeEdgeSize = 0.0;
var kWindowEdgeSize = isWindows ? 1.0 : 5.0;
const kWindowBorderWidth = 1.0;
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
final kWindowBorderWidth = isLinux ? 1.0 : 0.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
const kFrameBorderRadius = 12.0;
const kFrameClipRRectBorderRadius = 12.0;
const kFrameBoxShadowBlurRadius = 32.0;
const kFrameBoxShadowOffsetFocused = 4.0;
const kFrameBoxShadowOffsetUnfocused = 2.0;
const kInvalidValueStr = 'InvalidValueStr';
@@ -222,6 +232,7 @@ const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
const kRequestIgnoreBatteryOptimizations =
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
/// Android channel invoke type key
class AndroidChannel {

View File

@@ -262,7 +262,7 @@ class _ConnectionPageState extends State<ConnectionPage>
void onWindowLeaveFullScreen() {
// Restore edge border to default edge size.
stateGlobal.resizeEdgeSize.value =
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
}
@override

View File

@@ -69,44 +69,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
);
}
Widget buildPresetPasswordWarning() {
return FutureBuilder<bool>(
future: bind.isPresetPassword(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
} else if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
} else if (snapshot.hasData && snapshot.data == true) {
return Container(
color: Colors.yellow,
child: Column(
children: [
Align(
child: Text(
translate("Security Alert"),
style: TextStyle(
color: Colors.red,
fontSize: 20,
fontWeight: FontWeight.bold,
),
)).paddingOnly(bottom: 8),
Text(
translate("preset_password_warning"),
style: TextStyle(color: Colors.red),
)
],
).paddingAll(8),
); // Show a warning message if the Future completed with true
} else {
return SizedBox
.shrink(); // Show nothing if the Future completed with false or null
}
},
);
}
Widget buildLeftPane(BuildContext context) {
final isIncomingOnly = bind.isIncomingOnly();
final isOutgoingOnly = bind.isOutgoingOnly();
@@ -115,22 +77,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
if (bind.isCustomClient())
Align(
alignment: Alignment.center,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
launchUrl(Uri.parse('https://rustdesk.com'));
},
child: Opacity(
opacity: 0.5,
child: Text(
translate("powered_by_me"),
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 9, decoration: TextDecoration.underline),
)),
),
).marginOnly(top: 6),
child: loadPowered(context),
),
Align(
alignment: Alignment.center,
@@ -206,7 +153,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
size: 22,
),
),
onTap: () => DesktopSettingPage.switch2page(0),
onTap: () => {
if (DesktopSettingPage.tabKeys.isNotEmpty)
{
DesktopSettingPage.switch2page(
DesktopSettingPage.tabKeys[0])
}
},
onHover: (value) => _editHover.value = value,
),
),
@@ -298,19 +251,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
RxBool hover = false.obs;
return InkWell(
onTap: DesktopTabPage.onAddSetting,
child: Obx(
() => CircleAvatar(
radius: 15,
backgroundColor: hover.value
? Theme.of(context).scaffoldBackgroundColor
: Theme.of(context).colorScheme.background,
child: Tooltip(
message: translate('Settings'),
child: Icon(
Icons.more_vert_outlined,
size: 20,
color: hover.value ? textColor : textColor?.withOpacity(0.5),
)),
child: Tooltip(
message: translate('Settings'),
child: Obx(
() => CircleAvatar(
radius: 15,
backgroundColor: hover.value
? Theme.of(context).scaffoldBackgroundColor
: Theme.of(context).colorScheme.background,
child: Icon(
Icons.more_vert_outlined,
size: 20,
color: hover.value ? textColor : textColor?.withOpacity(0.5),
),
),
),
),
onHover: (value) => hover.value = value,
@@ -371,33 +325,36 @@ class _DesktopHomePageState extends State<DesktopHomePage>
),
AnimatedRotationWidget(
onPressed: () => bind.mainUpdateTemporaryPassword(),
child: Obx(() => RotatedBox(
quarterTurns: 2,
child: Tooltip(
message: translate('Refresh Password'),
child: Icon(
Icons.refresh,
color: refreshHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
)))),
child: Tooltip(
message: translate('Refresh Password'),
child: Obx(() => RotatedBox(
quarterTurns: 2,
child: Icon(
Icons.refresh,
color: refreshHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
))),
),
onHover: (value) => refreshHover.value = value,
).marginOnly(right: 8, top: 4),
if (!bind.isDisableSettings())
InkWell(
child: Obx(
() => Tooltip(
message: translate('Change Password'),
child: Icon(
Icons.edit,
color: editHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
)).marginOnly(right: 8, top: 4),
child: Tooltip(
message: translate('Change Password'),
child: Obx(
() => Icon(
Icons.edit,
color: editHover.value
? textColor
: Color(0xFFDDDDDD),
size: 22,
).marginOnly(right: 8, top: 4),
),
),
onTap: () => DesktopSettingPage.switch2page(0),
onTap: () => DesktopSettingPage.switch2page(
SettingsTabKey.safety),
onHover: (value) => editHover.value = value,
),
],
@@ -824,6 +781,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final screenRect = parseParamScreenRect(args);
await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount, screenRect);
} else if (call.method == kWindowEventRemoteWindowCoords) {
final windowId = int.tryParse(call.arguments);
if (windowId != null) {
return jsonEncode(
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
}
}
});
_uniLinksSubscription = listenUniLinks();

View File

@@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
@@ -36,34 +37,57 @@ const double _kTitleFontSize = 20;
const double _kContentFontSize = 15;
const Color _accentColor = MyTheme.accent;
const String _kSettingPageControllerTag = 'settingPageController';
const String _kSettingPageIndexTag = 'settingPageIndex';
const int _kPageCount = 6;
const String _kSettingPageTabKeyTag = 'settingPageTabKey';
class _TabInfo {
late final SettingsTabKey key;
late final String label;
late final IconData unselected;
late final IconData selected;
_TabInfo(this.label, this.unselected, this.selected);
_TabInfo(this.key, this.label, this.unselected, this.selected);
}
enum SettingsTabKey {
general,
safety,
network,
display,
plugin,
account,
about,
}
class DesktopSettingPage extends StatefulWidget {
final int initialPage;
final SettingsTabKey initialTabkey;
static final List<SettingsTabKey> tabKeys = [
SettingsTabKey.general,
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
SettingsTabKey.safety,
if (!bind.isDisableSettings()) SettingsTabKey.network,
if (!bind.isIncomingOnly()) SettingsTabKey.display,
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
SettingsTabKey.plugin,
if (!bind.isDisableAccount()) SettingsTabKey.account,
SettingsTabKey.about,
];
const DesktopSettingPage({Key? key, required this.initialPage})
: super(key: key);
DesktopSettingPage({Key? key, required this.initialTabkey}) : super(key: key);
@override
State<DesktopSettingPage> createState() => _DesktopSettingPageState();
static void switch2page(int page) {
if (page >= _kPageCount) return;
static void switch2page(SettingsTabKey page) {
try {
int index = tabKeys.indexOf(page);
if (index == -1) {
return;
}
if (Get.isRegistered<PageController>(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
PageController controller = Get.find(tag: _kSettingPageControllerTag);
RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag);
selectedIndex.value = page;
controller.jumpToPage(page);
Rx<SettingsTabKey> selected = Get.find(tag: _kSettingPageTabKeyTag);
selected.value = page;
controller.jumpToPage(index);
} else {
DesktopTabPage.onAddSetting(initialPage: page);
}
@@ -76,7 +100,7 @@ class DesktopSettingPage extends StatefulWidget {
class _DesktopSettingPageState extends State<DesktopSettingPage>
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
late PageController controller;
late RxInt selectedIndex;
late Rx<SettingsTabKey> selectedTab;
@override
bool get wantKeepAlive => true;
@@ -84,14 +108,20 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
@override
void initState() {
super.initState();
selectedIndex =
(widget.initialPage < _kPageCount ? widget.initialPage : 0).obs;
Get.put<RxInt>(selectedIndex, tag: _kSettingPageIndexTag);
controller = PageController(initialPage: widget.initialPage);
var initialIndex = DesktopSettingPage.tabKeys.indexOf(widget.initialTabkey);
if (initialIndex == -1) {
initialIndex = 0;
}
selectedTab = DesktopSettingPage.tabKeys[initialIndex].obs;
Get.put<Rx<SettingsTabKey>>(selectedTab, tag: _kSettingPageTabKeyTag);
controller = PageController(initialPage: initialIndex);
Get.put<PageController>(controller, tag: _kSettingPageControllerTag);
controller.addListener(() {
if (controller.page != null) {
selectedIndex.value = controller.page!.toInt();
int page = controller.page!.toInt();
if (page < DesktopSettingPage.tabKeys.length) {
selectedTab.value = DesktopSettingPage.tabKeys[page];
}
}
});
}
@@ -100,26 +130,43 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
void dispose() {
super.dispose();
Get.delete<PageController>(tag: _kSettingPageControllerTag);
Get.delete<RxInt>(tag: _kSettingPageIndexTag);
Get.delete<RxInt>(tag: _kSettingPageTabKeyTag);
}
List<_TabInfo> _settingTabs() {
final List<_TabInfo> settingTabs = <_TabInfo>[
_TabInfo('General', Icons.settings_outlined, Icons.settings),
if (!bind.isOutgoingOnly() && !bind.isDisableSettings())
_TabInfo('Security', Icons.enhanced_encryption_outlined,
Icons.enhanced_encryption),
if (!bind.isDisableSettings())
_TabInfo('Network', Icons.link_outlined, Icons.link),
if (!bind.isIncomingOnly())
_TabInfo(
'Display', Icons.desktop_windows_outlined, Icons.desktop_windows),
if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled())
_TabInfo('Plugin', Icons.extension_outlined, Icons.extension),
if (!bind.isDisableAccount())
_TabInfo('Account', Icons.person_outline, Icons.person),
_TabInfo('About', Icons.info_outline, Icons.info)
];
final List<_TabInfo> settingTabs = <_TabInfo>[];
for (final tab in DesktopSettingPage.tabKeys) {
switch (tab) {
case SettingsTabKey.general:
settingTabs.add(_TabInfo(
tab, 'General', Icons.settings_outlined, Icons.settings));
break;
case SettingsTabKey.safety:
settingTabs.add(_TabInfo(tab, 'Security',
Icons.enhanced_encryption_outlined, Icons.enhanced_encryption));
break;
case SettingsTabKey.network:
settingTabs
.add(_TabInfo(tab, 'Network', Icons.link_outlined, Icons.link));
break;
case SettingsTabKey.display:
settingTabs.add(_TabInfo(tab, 'Display',
Icons.desktop_windows_outlined, Icons.desktop_windows));
break;
case SettingsTabKey.plugin:
settingTabs.add(_TabInfo(
tab, 'Plugin', Icons.extension_outlined, Icons.extension));
break;
case SettingsTabKey.account:
settingTabs.add(
_TabInfo(tab, 'Account', Icons.person_outline, Icons.person));
break;
case SettingsTabKey.about:
settingTabs
.add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info));
break;
}
}
return settingTabs;
}
@@ -198,26 +245,26 @@ class _DesktopSettingPageState extends State<DesktopSettingPage>
child: ListView(
physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: tabs
.asMap()
.entries
.map((tab) => _listItem(tab: tab.value, index: tab.key))
.toList(),
children: tabs.map((tab) => _listItem(tab: tab)).toList(),
));
}
Widget _listItem({required _TabInfo tab, required int index}) {
Widget _listItem({required _TabInfo tab}) {
return Obx(() {
bool selected = index == selectedIndex.value;
bool selected = tab.key == selectedTab.value;
return SizedBox(
width: _kTabWidth,
height: _kTabHeight,
child: InkWell(
onTap: () {
if (selectedIndex.value != index) {
if (selectedTab.value != tab.key) {
int index = DesktopSettingPage.tabKeys.indexOf(tab.key);
if (index == -1) {
return;
}
controller.jumpToPage(index);
}
selectedIndex.value = index;
selectedTab.value = tab.key;
},
child: Row(children: [
Container(
@@ -271,6 +318,7 @@ class _GeneralState extends State<_General> {
hwcodec(),
audio(context),
record(context),
WaylandCard(),
_Card(title: 'Language', children: [language()]),
other()
],
@@ -400,11 +448,20 @@ class _GeneralState extends State<_General> {
Widget hwcodec() {
final hwcodec = bind.mainHasHwcodec();
final gpucodec = bind.mainHasGpucodec();
final vram = bind.mainHasVram();
return Offstage(
offstage: !(hwcodec || gpucodec),
offstage: !(hwcodec || vram),
child: _Card(title: 'Hardware Codec', children: [
_OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec')
_OptionCheckBox(
context,
'Enable hardware codec',
'enable-hwcodec',
update: () {
if (mainGetBoolOptionSync('enable-hwcodec')) {
bind.mainCheckHwcodec();
}
},
)
]),
);
}
@@ -414,38 +471,7 @@ class _GeneralState extends State<_General> {
return const Offstage();
}
String getDefault() {
if (isWindows) return translate('System Sound');
return '';
}
Future<String> getValue() async {
String device = await bind.mainGetOption(key: 'audio-input');
if (device.isNotEmpty) {
return device;
} else {
return getDefault();
}
}
setDevice(String device) {
if (device == getDefault()) device = '';
bind.mainSetOption(key: 'audio-input', value: device);
}
return futureBuilder(future: () async {
List<String> devices = (await bind.mainGetSoundInputs()).toList();
if (isWindows) {
devices.insert(0, translate('System Sound'));
}
String current = await getValue();
return {'devices': devices, 'current': current};
}(), hasData: (data) {
String currentDevice = data['current'];
List<String> devices = data['devices'] as List<String>;
if (devices.isEmpty) {
return const Offstage();
}
return AudioInput(builder: (devices, currentDevice, setDevice) {
return _Card(title: 'Audio Input Device', children: [
...devices.map((device) => _Radio<String>(context,
value: device,
@@ -460,42 +486,72 @@ class _GeneralState extends State<_General> {
}
Widget record(BuildContext context) {
final showRootDir = isWindows && bind.mainIsInstalled();
return futureBuilder(future: () async {
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
String user_dir = await bind.mainVideoSaveDirectory(root: false);
String root_dir =
showRootDir ? await bind.mainVideoSaveDirectory(root: true) : '';
bool user_dir_exists = await Directory(user_dir).exists();
bool root_dir_exists =
showRootDir ? await Directory(root_dir).exists() : false;
// canLaunchUrl blocked on windows portable, user SYSTEM
return {'dir': defaultDirectory, 'canlaunch': true};
return {
'user_dir': user_dir,
'root_dir': root_dir,
'user_dir_exists': user_dir_exists,
'root_dir_exists': root_dir_exists,
};
}(), hasData: (data) {
Map<String, dynamic> map = data as Map<String, dynamic>;
String dir = map['dir']!;
String customDirectory =
bind.mainGetOptionSync(key: 'video-save-directory');
if (customDirectory.isNotEmpty) {
dir = customDirectory;
}
bool canlaunch = map['canlaunch']! as bool;
String user_dir = map['user_dir']!;
String root_dir = map['root_dir']!;
bool root_dir_exists = map['root_dir_exists']!;
bool user_dir_exists = map['user_dir_exists']!;
return _Card(title: 'Recording', children: [
_OptionCheckBox(context, 'Automatically record incoming sessions',
'allow-auto-record-incoming'),
if (showRootDir)
Row(
children: [
Text('${translate("Incoming")}:'),
Expanded(
child: GestureDetector(
onTap: root_dir_exists
? () => launchUrl(Uri.file(root_dir))
: null,
child: Text(
root_dir,
softWrap: true,
style: root_dir_exists
? const TextStyle(
decoration: TextDecoration.underline)
: null,
)).marginOnly(left: 10),
),
],
).marginOnly(left: _kContentHMargin),
Row(
children: [
Text('${translate("Directory")}:'),
Text('${translate(showRootDir ? "Outgoing" : "Directory")}:'),
Expanded(
child: GestureDetector(
onTap: canlaunch ? () => launchUrl(Uri.file(dir)) : null,
onTap: user_dir_exists
? () => launchUrl(Uri.file(user_dir))
: null,
child: Text(
dir,
user_dir,
softWrap: true,
style:
const TextStyle(decoration: TextDecoration.underline),
style: user_dir_exists
? const TextStyle(decoration: TextDecoration.underline)
: null,
)).marginOnly(left: 10),
),
ElevatedButton(
onPressed: () async {
String? initialDirectory;
if (await Directory.fromUri(Uri.directory(dir))
if (await Directory.fromUri(Uri.directory(user_dir))
.exists()) {
initialDirectory = dir;
initialDirectory = user_dir;
}
String? selectedDirectory = await FilePicker.platform
.getDirectoryPath(initialDirectory: initialDirectory);
@@ -835,6 +891,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
...directIp(context),
whitelist(),
...autoDisconnect(context),
if (bind.mainIsInstalled())
_OptionCheckBox(context, 'allow-only-conn-window-open-tip',
'allow-only-conn-window-open',
reverse: false, enabled: enabled),
]);
}
@@ -1104,7 +1164,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
child: Column(children: [
server(enabled),
_Card(title: 'Proxy', children: [
_Button('Socks5 Proxy', changeSocks5Proxy,
_Button('Socks5/Http(s) Proxy', changeSocks5Proxy,
enabled: enabled),
]),
]),
@@ -1796,14 +1856,80 @@ Widget _Radio<T>(BuildContext context,
);
}
class WaylandCard extends StatefulWidget {
const WaylandCard({Key? key}) : super(key: key);
@override
State<WaylandCard> createState() => _WaylandCardState();
}
class _WaylandCardState extends State<WaylandCard> {
final restoreTokenKey = 'wayland-restore-token';
@override
Widget build(BuildContext context) {
return futureBuilder(
future: bind.mainHandleWaylandScreencastRestoreToken(
key: restoreTokenKey, value: "get"),
hasData: (restoreToken) {
final children = [
if (restoreToken.isNotEmpty)
_buildClearScreenSelection(context, restoreToken),
];
return Offstage(
offstage: children.isEmpty,
child: _Card(title: 'Wayland', children: children),
);
},
);
}
Widget _buildClearScreenSelection(BuildContext context, String restoreToken) {
onConfirm() async {
final msg = await bind.mainHandleWaylandScreencastRestoreToken(
key: restoreTokenKey, value: "clear");
gFFI.dialogManager.dismissAll();
if (msg.isNotEmpty) {
msgBox(gFFI.sessionId, 'custom-nocancel', 'Error', msg, '',
gFFI.dialogManager);
} else {
setState(() {});
}
}
showConfirmMsgBox() => msgBoxCommon(
gFFI.dialogManager,
'Confirmation',
Text(
translate('confirm_clear_Wayland_screen_selection_tip'),
),
[
dialogButton('OK', onPressed: onConfirm),
dialogButton('Cancel',
onPressed: () => gFFI.dialogManager.dismissAll())
]);
return _Button(
'Clear Wayland screen selection',
showConfirmMsgBox,
tip: 'clear_Wayland_screen_selection_tip',
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Theme.of(context).colorScheme.error.withOpacity(0.75)),
),
);
}
}
// ignore: non_constant_identifier_names
Widget _Button(String label, Function() onPressed,
{bool enabled = true, String? tip}) {
{bool enabled = true, String? tip, ButtonStyle? style}) {
var button = ElevatedButton(
onPressed: enabled ? onPressed : null,
child: Text(
translate(label),
).marginSymmetric(horizontal: 15),
style: style,
);
StatefulWidget child;
if (tip == null) {
@@ -2020,7 +2146,12 @@ void changeSocks5Proxy() async {
password = pwdController.text.trim();
if (proxy.isNotEmpty) {
proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy));
String domainPort = proxy;
if (domainPort.contains('://')) {
domainPort = domainPort.split('://')[1];
}
proxyMsg = translate(await bind.mainTestIfValidServer(
server: domainPort, testWithProxy: false));
if (proxyMsg.isEmpty) {
// ignore
} else {
@@ -2034,7 +2165,7 @@ void changeSocks5Proxy() async {
}
return CustomAlertDialog(
title: Text(translate('Socks5 Proxy')),
title: Text(translate('Socks5/Http(s) Proxy')),
content: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 500),
child: Column(
@@ -2043,15 +2174,35 @@ void changeSocks5Proxy() async {
Row(
children: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 140),
child: Text(
'${translate("Server")}:',
textAlign: TextAlign.right,
).marginOnly(right: 10)),
constraints: const BoxConstraints(minWidth: 140),
child: Align(
alignment: Alignment.centerRight,
child: Row(
children: [
Text(
translate('Server'),
).marginOnly(right: 4),
Tooltip(
waitDuration: Duration(milliseconds: 0),
message: translate("default_proxy_tip"),
child: Icon(
Icons.help_outline_outlined,
size: 16,
color: Theme.of(context)
.textTheme
.titleLarge
?.color
?.withOpacity(0.5),
),
),
],
)).marginOnly(right: 10),
),
Expanded(
child: TextField(
decoration: InputDecoration(
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null,
),
controller: proxyController,
autofocus: true,
),

View File

@@ -17,7 +17,8 @@ class DesktopTabPage extends StatefulWidget {
@override
State<DesktopTabPage> createState() => _DesktopTabPageState();
static void onAddSetting({int initialPage = 0}) {
static void onAddSetting(
{SettingsTabKey initialPage = SettingsTabKey.general}) {
try {
DesktopTabController tabController = Get.find();
tabController.add(TabInfo(
@@ -27,7 +28,7 @@ class DesktopTabPage extends StatefulWidget {
unselectedIcon: Icons.build_outlined,
page: DesktopSettingPage(
key: const ValueKey(kTabLabelSettingPage),
initialPage: initialPage,
initialTabkey: initialPage,
)));
} catch (e) {
debugPrintStack(label: '$e');
@@ -56,10 +57,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
tabController.onSelected = (key) {
if (key == kTabLabelHomePage) {
windowManager.setSize(getIncomingOnlyHomeSize());
windowManager.setResizable(false);
setResizable(false);
} else {
windowManager.setSize(getIncomingOnlySettingsSize());
windowManager.setResizable(true);
setResizable(true);
}
};
}

View File

@@ -91,18 +91,21 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
@override
Widget build(BuildContext context) {
final tabWidget = Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: Theme.of(context).cardColor,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.tablabelGetter,
)),
);
final child = Scaffold(
backgroundColor: Theme.of(context).cardColor,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
labelGetter: DesktopTab.tablabelGetter,
));
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
return isMacOS || kUseCompatibleUiMode
? tabWidget
: SubWindowDragToResizeArea(

View File

@@ -97,21 +97,30 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
@override
Widget build(BuildContext context) {
final tabWidget = Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: () async {
tabController.clear();
return true;
},
tail: AddButton().paddingOnly(left: 10),
labelGetter: DesktopTab.tablabelGetter,
)),
final child = Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: () async {
tabController.clear();
return true;
},
tail: AddButton(),
labelGetter: DesktopTab.tablabelGetter,
),
);
final tabWidget = isLinux
? buildVirtualWindowFrame(
context,
Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: child),
)
: Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: child,
);
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(

View File

@@ -7,6 +7,7 @@ import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
import 'package:flutter_hbb/models/state_model.dart';
import '../../consts.dart';
import '../../common/widgets/overlay.dart';
@@ -165,6 +166,7 @@ class _RemotePageState extends State<RemotePage>
// and let OS to handle events instead.
_rawKeyFocusNode.unfocus();
}
stateGlobal.isFocused.value = false;
}
@override
@@ -174,6 +176,7 @@ class _RemotePageState extends State<RemotePage>
if (isWindows) {
_isWindowBlur = false;
}
stateGlobal.isFocused.value = true;
}
@override

View File

@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
@@ -107,107 +108,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController.onRemoved = (_, id) => onRemoveId(id);
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
// Show the hidden window.
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
stateGlobal.closeOnFullscreen = null;
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
switchUuid: switchUuid,
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
}
_update_remote_count();
return returnValue;
});
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
if (!_isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
@@ -230,103 +131,103 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
@override
Widget build(BuildContext context) {
final tabWidget = Obx(
() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton().paddingOnly(left: 10),
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
bool secure =
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {
fingerprint = 'N/A';
}
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
msgFingerprint += '$first\n$second';
} else {
msgFingerprint += fingerprint;
}
final child = Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
tail: const AddButton(),
pageViewBuilder: (pageView) => pageView,
labelGetter: DesktopTab.tablabelGetter,
tabBuilder: (key, icon, label, themeConf) => Obx(() {
final connectionType = ConnectionTypeState.find(key);
if (!connectionType.isValid()) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
label,
],
);
} else {
bool secure =
connectionType.secure.value == ConnectionType.strSecure;
bool direct =
connectionType.direct.value == ConnectionType.strDirect;
String msgConn;
if (secure && direct) {
msgConn = translate("Direct and encrypted connection");
} else if (secure && !direct) {
msgConn = translate("Relayed and encrypted connection");
} else if (!secure && direct) {
msgConn = translate("Direct and unencrypted connection");
} else {
msgConn = translate("Relayed and unencrypted connection");
}
var msgFingerprint = '${translate('Fingerprint')}:\n';
var fingerprint = FingerprintState.find(key).value;
if (fingerprint.isEmpty) {
fingerprint = 'N/A';
}
if (fingerprint.length > 5 * 8) {
var first = fingerprint.substring(0, 39);
var second = fingerprint.substring(40);
msgFingerprint += '$first\n$second';
} else {
msgFingerprint += fingerprint;
}
final tab = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgConn\n$msgFingerprint',
child: SvgPicture.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
unreadMessageCountBuilder(UnreadChatCountState.find(key))
.marginOnly(left: 4),
],
);
final tab = Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
Tooltip(
message: '$msgConn\n$msgFingerprint',
child: SvgPicture.asset(
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
width: themeConf.iconSize,
height: themeConf.iconSize,
).paddingOnly(right: 5),
),
label,
unreadMessageCountBuilder(UnreadChatCountState.find(key))
.marginOnly(left: 4),
],
);
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as RemotePage;
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
},
target: e.position,
);
}
},
child: tab,
);
}
}),
),
),
return Listener(
onPointerDown: (e) {
if (e.kind != ui.PointerDeviceKind.mouse) {
return;
}
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == key)
.page as RemotePage;
if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) {
showRightMenu(
(CancelFunc cancelFunc) {
return _tabMenuBuilder(key, cancelFunc);
},
target: e.position,
);
}
},
child: tab,
);
}
}),
),
);
final tabWidget = isLinux
? buildVirtualWindowFrame(context, child)
: Obx(() => Container(
decoration: BoxDecoration(
border: Border.all(
color: MyTheme.color(context).border!,
width: stateGlobal.windowBorderWidth.value),
),
child: child,
));
return isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(() => SubWindowDragToResizeArea(
@@ -499,4 +400,130 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_update_remote_count() =>
RemoteCountState.find().value = tabController.length;
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
print(
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
dynamic returnValue;
// for simplify, just replace connectionId
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
final sessionId = args['session_id'];
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseParamScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
// Show the hidden window.
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
stateGlobal.setFullscreen(true);
}
// Reset the state
stateGlobal.closeOnFullscreen = null;
}
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
tabController.add(TabInfo(
key: id,
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
tabWindowId: tabWindowId,
display: display,
displays: displays?.cast<int>(),
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
switchUuid: switchUuid,
forceRelay: args['forceRelay'],
isSharedPassword: args['isSharedPassword'],
),
));
} else if (call.method == kWindowDisableGrabKeyboard) {
// ???
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventActiveDisplaySession) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final display = args['display'];
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventGetCachedSessionData) {
// Ready to show new window and close old tab.
final args = jsonDecode(call.arguments);
final id = args['id'];
final close = args['close'];
try {
final remotePage = tabController.state.value.tabs
.firstWhere((tab) => tab.key == id)
.page as RemotePage;
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
} catch (e) {
debugPrint('Failed to get cached session data: $e');
}
if (close && returnValue != null) {
closeSessionOnDispose[id] = false;
tabController.closeBy(id);
}
} else if (call.method == kWindowEventRemoteWindowCoords) {
final remotePage =
tabController.state.value.selectedTabInfo.page as RemotePage;
final ffi = remotePage.ffi;
final displayRect = ffi.ffiModel.displaysRect();
if (displayRect != null) {
final wc = WindowController.fromWindowId(windowId());
Rect? frame;
try {
frame = await wc.getFrame();
} catch (e) {
debugPrint(
"Failed to get frame of window $windowId, it may be hidden");
}
if (frame != null) {
ffi.cursorModel.moveLocal(0, 0);
final coords = RemoteWindowCoords(
frame,
CanvasCoords.fromCanvasModel(ffi.canvasModel),
CursorCoords.fromCursorModel(ffi.cursorModel),
displayRect);
returnValue = jsonEncode(coords.toJson());
}
}
}
_update_remote_count();
return returnValue;
}
}

View File

@@ -4,6 +4,7 @@ import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
@@ -76,14 +77,20 @@ class _DesktopServerPageState extends State<DesktopServerPage>
ChangeNotifierProvider.value(value: gFFI.chatModel),
],
child: Consumer<ServerModel>(
builder: (context, serverModel, child) => Container(
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
builder: (context, serverModel, child) {
final body = Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: ConnectionManager(),
),
),
);
return isLinux
? buildVirtualWindowFrame(context, body)
: Container(
decoration: BoxDecoration(
border:
Border.all(color: MyTheme.color(context).border!)),
child: body,
);
},
),
);
}
@@ -157,7 +164,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
controller: serverModel.tabController,
selectedBorderColor: MyTheme.accent,
maxLabelWidth: 100,
tail: buildScrollJumper(),
tail: null, //buildScrollJumper(),
selectedTabBackgroundColor:
Theme.of(context).hintColor.withOpacity(0),
tabBuilder: (key, icon, label, themeConf) {
@@ -701,17 +708,86 @@ class _CmControlPanel extends StatelessWidget {
children: [
Offstage(
offstage: !client.inVoiceCall,
child: buildButton(
context,
color: Colors.red,
onClick: () => closeVoiceCall(),
icon: Icon(
Icons.call_end_rounded,
color: Colors.white,
size: 14,
),
text: "Stop voice call",
textColor: Colors.white,
child: Row(
children: [
Expanded(
child: buildButton(context,
color: MyTheme.accent,
onClick: null, onTapDown: (details) async {
final devicesInfo = await AudioInput.getDevicesInfo();
List<String> devices = devicesInfo['devices'] as List<String>;
if (devices.isEmpty) {
msgBox(
gFFI.sessionId,
'custom-nocancel-info',
'Prompt',
'no_audio_input_device_tip',
'',
gFFI.dialogManager,
);
return;
}
String currentDevice = devicesInfo['current'] as String;
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
final position = RelativeRect.fromLTRB(x, y, x, y);
showMenu(
context: context,
position: position,
items: devices
.map((d) => PopupMenuItem<String>(
value: d,
height: 18,
padding: EdgeInsets.zero,
onTap: () => AudioInput.setDevice(d),
child: IgnorePointer(
child: RadioMenuButton(
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) AudioInput.setDevice(v);
},
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
constraints: BoxConstraints(
maxWidth:
kConnectionManagerWindowSizeClosedChat
.width -
80),
),
)),
))
.toList(),
);
},
icon: Icon(
Icons.call_rounded,
color: Colors.white,
size: 14,
),
text: "Audio input",
textColor: Colors.white),
),
Expanded(
child: buildButton(
context,
color: Colors.red,
onClick: () => closeVoiceCall(),
icon: Icon(
Icons.call_end_rounded,
color: Colors.white,
size: 14,
),
text: "Stop voice call",
textColor: Colors.white,
),
)
],
),
),
Offstage(
@@ -872,12 +948,14 @@ class _CmControlPanel extends StatelessWidget {
Widget buildButton(BuildContext context,
{required Color? color,
required Function() onClick,
Icon? icon,
GestureTapCallback? onClick,
Widget? icon,
BoxBorder? border,
required String text,
required Color? textColor,
String? tooltip}) {
String? tooltip,
GestureTapDownCallback? onTapDown}) {
assert(!(onClick == null && onTapDown == null));
Widget textWidget;
if (icon != null) {
textWidget = Text(
@@ -901,7 +979,16 @@ class _CmControlPanel extends StatelessWidget {
color: color, borderRadius: borderRadius, border: border),
child: InkWell(
borderRadius: borderRadius,
onTap: () => checkClickTime(client.id, onClick),
onTap: () {
if (onClick == null) return;
checkClickTime(client.id, onClick);
},
onTapDown: (details) {
if (onTapDown == null) return;
checkClickTime(client.id, () {
onTapDown.call(details);
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [

View File

@@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
child: Scaffold(
backgroundColor: isLinux ? Colors.transparent : null,
body: FileManagerTabPage(
params: params,
),

View File

@@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.ffiModel),
],
child: Scaffold(
backgroundColor: isLinux ? Colors.transparent : null,
body: PortForwardTabPage(
params: params,
),

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/audio_input.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -1045,7 +1046,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
@override
Widget build(BuildContext context) {
_screenAdjustor.updateScreen();
menuChildrenGetter() {
final menuChildren = <Widget>[
_screenAdjustor.adjustWindow(context),
@@ -1058,11 +1058,18 @@ class _DisplayMenuState extends State<_DisplayMenu> {
ffi: widget.ffi,
screenAdjustor: _screenAdjustor,
),
// We may add this feature if it is needed and we have an EV certificate.
// _VirtualDisplayMenu(
// id: widget.id,
// ffi: widget.ffi,
// ),
if (pi.isRustDeskIdd)
_RustDeskVirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
if (pi.isAmyuniIdd)
_AmyuniVirtualDisplayMenu(
id: widget.id,
ffi: widget.ffi,
),
Divider(),
cursorToggles(),
Divider(),
toggles(),
];
@@ -1207,6 +1214,23 @@ class _DisplayMenuState extends State<_DisplayMenu> {
});
}
cursorToggles() {
return futureBuilder(
future: toolbarCursor(context, id, ffi),
hasData: (data) {
final v = data as List<TToggleMenu>;
if (v.isEmpty) return Offstage();
return Column(
children: v
.map((e) => CkbMenuButton(
value: e.value,
onChanged: e.onChanged,
child: e.child,
ffi: ffi))
.toList());
});
}
toggles() {
return futureBuilder(
future: toolbarDisplayToggle(context, id, ffi),
@@ -1540,21 +1564,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
}
}
class _VirtualDisplayMenu extends StatefulWidget {
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_VirtualDisplayMenu({
_RustDeskVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
State<_RustDeskVirtualDisplayMenu> createState() =>
_RustDeskVirtualDisplayMenuState();
}
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
class _RustDeskVirtualDisplayMenuState
extends State<_RustDeskVirtualDisplayMenu> {
@override
void initState() {
super.initState();
@@ -1569,7 +1595,7 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
return Offstage();
}
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[];
@@ -1611,6 +1637,82 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
}
}
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
final String id;
final FFI ffi;
_AmyuniVirtualDisplayMenu({
Key? key,
required this.id,
required this.ffi,
}) : super(key: key);
@override
State<_AmyuniVirtualDisplayMenu> createState() =>
_AmiyuniVirtualDisplayMenuState();
}
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
return Offstage();
}
if (!widget.ffi.ffiModel.pi.isInstalled) {
return Offstage();
}
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
final privacyModeState = PrivacyModeState.find(widget.id);
final children = <Widget>[
Obx(() => Row(
children: [
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: false),
child: Icon(Icons.remove),
),
Text(count.toString()),
TextButton(
onPressed: privacyModeState.isNotEmpty || count == 4
? null
: () => bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId, index: 0, on: true),
child: Icon(Icons.add),
),
],
)),
Divider(),
Obx(() => MenuButton(
onPressed: privacyModeState.isNotEmpty || count == 0
? null
: () {
bind.sessionToggleVirtualDisplay(
sessionId: widget.ffi.sessionId,
index: kAllVirtualDisplay,
on: false);
},
ffi: widget.ffi,
child: Text(translate('Plug out all')),
)),
];
return _SubmenuButton(
ffi: widget.ffi,
menuChildren: children,
child: Text(translate("Virtual display")),
);
}
}
class _KeyboardMenu extends StatelessWidget {
final String id;
final FFI ffi;
@@ -1852,34 +1954,71 @@ class _VoiceCallMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
menuChildrenGetter() {
final audioInput =
AudioInput(builder: (devices, currentDevice, setDevice) {
return Column(
children: devices
.map((d) => RdoMenuButton<String>(
child: Container(
child: Text(
d,
overflow: TextOverflow.ellipsis,
),
constraints: BoxConstraints(maxWidth: 250),
),
value: d,
groupValue: currentDevice,
onChanged: (v) {
if (v != null) setDevice(v);
},
ffi: ffi,
))
.toList(),
);
});
return [
audioInput,
Divider(),
MenuButton(
child: Text(translate('End call')),
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
ffi: ffi,
),
];
}
return Obx(
() {
final String tooltip;
final String icon;
switch (ffi.chatModel.voiceCallStatus.value) {
case VoiceCallStatus.waitingForResponse:
tooltip = "Waiting";
icon = "assets/call_wait.svg";
break;
return buildCallWaiting(context);
case VoiceCallStatus.connected:
tooltip = "Disconnect";
icon = "assets/call_end.svg";
break;
return _IconSubmenuButton(
tooltip: 'Voice call',
svg: 'assets/voice_call.svg',
color: _ToolbarTheme.blueColor,
hoverColor: _ToolbarTheme.hoverBlueColor,
menuChildrenGetter: menuChildrenGetter,
ffi: ffi,
);
default:
return Offstage();
}
return _IconMenuButton(
assetName: icon,
tooltip: tooltip,
onPressed: () =>
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor);
},
);
}
}
Widget buildCallWaiting(BuildContext context) {
return _IconMenuButton(
assetName: "assets/call_wait.svg",
tooltip: "Waiting",
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
color: _ToolbarTheme.redColor,
hoverColor: _ToolbarTheme.hoverRedColor,
);
}
}
class _RecordMenu extends StatelessWidget {
const _RecordMenu({Key? key}) : super(key: key);
@@ -2014,7 +2153,7 @@ class _IconSubmenuButton extends StatefulWidget {
final Color hoverColor;
final List<Widget> Function() menuChildrenGetter;
final MenuStyle? menuStyle;
final FFI ffi;
final FFI? ffi;
final double? width;
_IconSubmenuButton({
@@ -2025,7 +2164,7 @@ class _IconSubmenuButton extends StatefulWidget {
required this.color,
required this.hoverColor,
required this.menuChildrenGetter,
required this.ffi,
this.ffi,
this.menuStyle,
this.width,
}) : super(key: key);
@@ -2107,13 +2246,13 @@ class MenuButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget? trailingIcon;
final Widget? child;
final FFI ffi;
final FFI? ffi;
MenuButton(
{Key? key,
this.onPressed,
this.trailingIcon,
required this.child,
required this.ffi})
this.ffi})
: super(key: key);
@override
@@ -2122,7 +2261,9 @@ class MenuButton extends StatelessWidget {
key: key,
onPressed: onPressed != null
? () {
_menuDismissCallback(ffi);
if (ffi != null) {
_menuDismissCallback(ffi!);
}
onPressed?.call();
}
: null,
@@ -2135,13 +2276,13 @@ class CkbMenuButton extends StatelessWidget {
final bool? value;
final ValueChanged<bool?>? onChanged;
final Widget? child;
final FFI ffi;
final FFI? ffi;
const CkbMenuButton(
{Key? key,
required this.value,
required this.onChanged,
required this.child,
required this.ffi})
this.ffi})
: super(key: key);
@override
@@ -2152,7 +2293,9 @@ class CkbMenuButton extends StatelessWidget {
child: child,
onChanged: onChanged != null
? (bool? value) {
_menuDismissCallback(ffi);
if (ffi != null) {
_menuDismissCallback(ffi!);
}
onChanged?.call(value);
}
: null,
@@ -2165,13 +2308,13 @@ class RdoMenuButton<T> extends StatelessWidget {
final T? groupValue;
final ValueChanged<T?>? onChanged;
final Widget? child;
final FFI ffi;
final FFI? ffi;
const RdoMenuButton({
Key? key,
required this.value,
required this.groupValue,
required this.child,
required this.ffi,
this.ffi,
this.onChanged,
}) : super(key: key);
@@ -2183,7 +2326,9 @@ class RdoMenuButton<T> extends StatelessWidget {
child: child,
onChanged: onChanged != null
? (T? value) {
_menuDismissCallback(ffi);
if (ffi != null) {
_menuDismissCallback(ffi!);
}
onChanged?.call(value);
}
: null,
@@ -2370,10 +2515,11 @@ class InputModeMenu {
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
return Listener(
onPointerHover: (PointerHoverEvent e) =>
ffi.inputModel.lastMousePos = e.position,
onPointerHover: (PointerHoverEvent e) => {
if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
},
child: MouseRegion(
child: child,
),

View File

@@ -16,6 +16,7 @@ import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:scroll_pos/scroll_pos.dart';
import 'package:window_manager/window_manager.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../../utils/multi_window_manager.dart';
@@ -256,6 +257,8 @@ class DesktopTab extends StatelessWidget {
late final DesktopTabType tabType;
late final bool isMainWindow;
final RxList<String> invisibleTabKeys = RxList.empty();
DesktopTab({
Key? key,
required this.controller,
@@ -430,6 +433,7 @@ class DesktopTab extends StatelessWidget {
},
child: _ListView(
controller: controller,
invisibleTabKeys: invisibleTabKeys,
tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
labelGetter: labelGetter,
@@ -448,12 +452,14 @@ class DesktopTab extends StatelessWidget {
tabType: tabType,
state: state,
tabController: controller,
invisibleTabKeys: invisibleTabKeys,
tail: tail,
showMinimize: showMinimize,
showMaximize: showMaximize,
showClose: showClose,
onClose: onWindowCloseButton,
)
labelGetter: labelGetter,
).paddingOnly(left: 10)
],
);
}
@@ -471,17 +477,22 @@ class WindowActionPanel extends StatefulWidget {
final Widget? tail;
final Future<bool> Function()? onClose;
final RxList<String> invisibleTabKeys;
final LabelGetter? labelGetter;
const WindowActionPanel(
{Key? key,
required this.isMainWindow,
required this.tabType,
required this.state,
required this.tabController,
required this.invisibleTabKeys,
this.tail,
this.showMinimize = true,
this.showMaximize = true,
this.showClose = true,
this.onClose})
this.onClose,
this.labelGetter})
: super(key: key);
@override
@@ -537,6 +548,16 @@ class WindowActionPanelState extends State<WindowActionPanel>
setState(() {});
}
@override
void onWindowFocus() {
stateGlobal.isFocused.value = true;
}
@override
void onWindowBlur() {
stateGlobal.isFocused.value = false;
}
@override
void onWindowMinimize() {
stateGlobal.setMinimized(true);
@@ -658,11 +679,34 @@ class WindowActionPanelState extends State<WindowActionPanel>
super.onWindowClose();
}
bool showTabDowndown() {
return widget.tabController.state.value.tabs.length > 1 &&
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
widget.tabController.tabType == DesktopTabType.fileTransfer ||
widget.tabController.tabType == DesktopTabType.portForward ||
widget.tabController.tabType == DesktopTabType.cm);
}
List<String> existingInvisibleTab() {
return widget.invisibleTabKeys
.where((key) =>
widget.tabController.state.value.tabs.any((tab) => tab.key == key))
.toList();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(() => Offstage(
offstage:
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
child: _TabDropDownButton(
controller: widget.tabController,
labelGetter: widget.labelGetter,
tabkeys: existingInvisibleTab()),
)),
Offstage(offstage: widget.tail == null, child: widget.tail),
Offstage(
offstage: kUseCompatibleUiMode,
@@ -814,6 +858,7 @@ Future<bool> closeConfirmDialog() async {
class _ListView extends StatelessWidget {
final DesktopTabController controller;
final RxList<String> invisibleTabKeys;
final TabBuilder? tabBuilder;
final TabMenuBuilder? tabMenuBuilder;
@@ -825,8 +870,9 @@ class _ListView extends StatelessWidget {
Rx<DesktopTabState> get state => controller.state;
const _ListView({
_ListView({
required this.controller,
required this.invisibleTabKeys,
this.tabBuilder,
this.tabMenuBuilder,
this.labelGetter,
@@ -846,6 +892,19 @@ class _ListView extends StatelessWidget {
controller.tabType == DesktopTabType.install;
}
onVisibilityChanged(VisibilityInfo info) {
final key = (info.key as ValueKey).value;
if (info.visibleFraction < 0.75) {
if (!invisibleTabKeys.contains(key)) {
invisibleTabKeys.add(key);
}
invisibleTabKeys.removeWhere((key) =>
controller.state.value.tabs.where((e) => e.key == key).isEmpty);
} else {
invisibleTabKeys.remove(key);
}
}
@override
Widget build(BuildContext context) {
return Obx(() => ListView(
@@ -858,35 +917,41 @@ class _ListView extends StatelessWidget {
: state.value.tabs.asMap().entries.map((e) {
final index = e.key;
final tab = e.value;
return _Tab(
final label = labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label);
return VisibilityDetector(
key: ValueKey(tab.key),
index: index,
tabInfoKey: tab.key,
label: labelGetter == null
? Rx<String>(tab.label)
: labelGetter!(tab.label),
selectedIcon: tab.selectedIcon,
unselectedIcon: tab.unselectedIcon,
closable: tab.closable,
selected: state.value.selected,
onClose: () {
if (tab.onTabCloseButton != null) {
tab.onTabCloseButton!();
} else {
controller.remove(index);
}
},
onTap: () {
controller.jumpTo(index);
tab.onTap?.call();
},
tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
maxLabelWidth: maxLabelWidth,
selectedTabBackgroundColor: selectedTabBackgroundColor ??
MyTheme.tabbar(context).selectedTabBackgroundColor,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
selectedBorderColor: selectedBorderColor,
onVisibilityChanged: onVisibilityChanged,
child: _Tab(
key: ValueKey(tab.key),
index: index,
tabInfoKey: tab.key,
label: label,
tabType: controller.tabType,
selectedIcon: tab.selectedIcon,
unselectedIcon: tab.unselectedIcon,
closable: tab.closable,
selected: state.value.selected,
onClose: () {
if (tab.onTabCloseButton != null) {
tab.onTabCloseButton!();
} else {
controller.remove(index);
}
},
onTap: () {
controller.jumpTo(index);
tab.onTap?.call();
},
tabBuilder: tabBuilder,
tabMenuBuilder: tabMenuBuilder,
maxLabelWidth: maxLabelWidth,
selectedTabBackgroundColor: selectedTabBackgroundColor ??
MyTheme.tabbar(context).selectedTabBackgroundColor,
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
selectedBorderColor: selectedBorderColor,
),
);
}).toList()));
}
@@ -896,6 +961,7 @@ class _Tab extends StatefulWidget {
final int index;
final String tabInfoKey;
final Rx<String> label;
final DesktopTabType tabType;
final IconData? selectedIcon;
final IconData? unselectedIcon;
final bool closable;
@@ -914,6 +980,7 @@ class _Tab extends StatefulWidget {
required this.index,
required this.tabInfoKey,
required this.label,
required this.tabType,
this.selectedIcon,
this.unselectedIcon,
this.tabBuilder,
@@ -953,7 +1020,9 @@ class _TabState extends State<_Tab> with RestorationMixin {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
child: Tooltip(
message: translate(widget.label.value),
message: widget.tabType == DesktopTabType.main
? ''
: translate(widget.label.value),
child: Text(
translate(widget.label.value),
textAlign: TextAlign.center,
@@ -1110,6 +1179,7 @@ class ActionIcon extends StatefulWidget {
final String? message;
final IconData icon;
final GestureTapCallback? onTap;
final GestureTapDownCallback? onTapDown;
final bool isClose;
final double iconSize;
final double boxSize;
@@ -1119,6 +1189,7 @@ class ActionIcon extends StatefulWidget {
this.message,
required this.icon,
this.onTap,
this.onTapDown,
this.isClose = false,
this.iconSize = _kActionIconSize,
this.boxSize = _kTabBarHeight - 1})
@@ -1148,6 +1219,7 @@ class _ActionIconState extends State<ActionIcon> {
: MyTheme.tabbar(context).hoverColor,
onHover: (value) => hover.value = value,
onTap: widget.onTap,
onTapDown: widget.onTapDown,
child: SizedBox(
height: widget.boxSize,
width: widget.boxSize,
@@ -1188,6 +1260,103 @@ class AddButton extends StatelessWidget {
}
}
class _TabDropDownButton extends StatefulWidget {
final DesktopTabController controller;
final List<String> tabkeys;
final LabelGetter? labelGetter;
const _TabDropDownButton(
{required this.controller, required this.tabkeys, this.labelGetter});
@override
State<_TabDropDownButton> createState() => _TabDropDownButtonState();
}
class _TabDropDownButtonState extends State<_TabDropDownButton> {
var position = RelativeRect.fromLTRB(0, 0, 0, 0);
@override
Widget build(BuildContext context) {
List<String> sortedKeys = widget.controller.state.value.tabs
.where((e) => widget.tabkeys.contains(e.key))
.map((e) => e.key)
.toList();
return ActionIcon(
onTapDown: (details) {
final x = details.globalPosition.dx;
final y = details.globalPosition.dy;
position = RelativeRect.fromLTRB(x, y, x, y);
},
icon: Icons.arrow_drop_down,
onTap: () {
showMenu(
context: context,
position: position,
items: sortedKeys.map((e) {
var label = e;
final tabInfo = widget.controller.state.value.tabs
.firstWhereOrNull((element) => element.key == e);
if (tabInfo != null) {
label = tabInfo.label;
}
if (widget.labelGetter != null) {
label = widget.labelGetter!(e).value;
}
var index = widget.controller.state.value.tabs
.indexWhere((t) => t.key == e);
label = '${index + 1}. $label';
final menuHover = false.obs;
final btnHover = false.obs;
return PopupMenuItem<String>(
value: e,
height: 32,
onTap: () {
widget.controller.jumpToByKey(e);
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
onHover: (event) => setState(() => menuHover.value = true),
onExit: (event) => setState(() => menuHover.value = false),
child: Row(
children: [
Expanded(
child: InkWell(child: Text(label)),
),
Obx(
() => Offstage(
offstage: !(tabInfo?.onTabCloseButton != null &&
menuHover.value),
child: InkWell(
onTap: () {
tabInfo?.onTabCloseButton?.call();
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
onHover: (event) =>
setState(() => btnHover.value = true),
onExit: (event) =>
setState(() => btnHover.value = false),
child: Icon(Icons.close,
color:
btnHover.value ? Colors.red : null))),
),
)
],
),
),
);
}).toList(),
);
},
);
}
}
class TabbarTheme extends ThemeExtension<TabbarTheme> {
final Color? selectedTabIconColor;
final Color? unSelectedTabIconColor;

View File

@@ -144,7 +144,8 @@ void runMainApp(bool startService) async {
}
windowManager.setOpacity(1);
windowManager.setTitle(getWindowName());
windowManager.setResizable(!bind.isIncomingOnly());
// Do not use `windowManager.setResizable()` here.
setResizable(!bind.isIncomingOnly());
});
}
@@ -238,7 +239,7 @@ void runConnectionManagerScreen() async {
} else {
await showCmWindow(isStartup: true);
}
windowManager.setResizable(false);
setResizable(false);
// Start the uni links handler and redirect links to Native, not for Flutter.
listenUniLinks(handleByFlutter: false);
}
@@ -248,7 +249,7 @@ bool _isCmReadyToShow = false;
showCmWindow({bool isStartup = false}) async {
if (isStartup) {
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
size: kConnectionManagerWindowSizeClosedChat);
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
await windowManager.waitUntilReadyToShow(windowOptions, null);
bind.mainHideDocker();
await Future.wait([
@@ -342,7 +343,7 @@ void runInstallPage() async {
}
WindowOptions getHiddenTitleBarWindowOptions(
{Size? size, bool center = false}) {
{Size? size, bool center = false, bool? alwaysOnTop}) {
var defaultTitleBarStyle = TitleBarStyle.hidden;
// we do not hide titlebar on win7 because of the frame overflow.
if (kUseCompatibleUiMode) {
@@ -354,6 +355,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: defaultTitleBarStyle,
alwaysOnTop: alwaysOnTop,
);
}
@@ -440,6 +442,9 @@ class _AppState extends State<App> {
if (isDesktop && desktopType == DesktopType.main) {
child = keyListenerBuilder(context, child);
}
if (isLinux) {
child = buildVirtualWindowFrame(context, child);
}
return child;
},
),

View File

@@ -84,7 +84,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
slivers: [
SliverList(
delegate: SliverChildListDelegate([
_buildUpdateUI(),
if (!bind.isCustomClient()) _buildUpdateUI(),
_buildRemoteIDTextField(),
])),
SliverFillRemaining(

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../common/widgets/chat_page.dart';
import '../../models/platform_model.dart';
import 'connection_page.dart';
abstract class PageShape extends Widget {
@@ -25,8 +26,9 @@ class HomePageState extends State<HomePage> {
var _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
final List<PageShape> _pages = [];
int _chatPageTabIndex = -1;
bool get isChatPageCurrentTab => isAndroid
? _selectedIndex == 1
? _selectedIndex == _chatPageTabIndex
: false; // change this when ios have chat page
void refreshPages() {
@@ -43,8 +45,9 @@ class HomePageState extends State<HomePage> {
void initPages() {
_pages.clear();
_pages.add(ConnectionPage());
if (isAndroid) {
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
if (isAndroid && !bind.isOutgoingOnly()) {
_chatPageTabIndex = _pages.length;
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
}
_pages.add(SettingsPage());
@@ -141,7 +144,7 @@ class HomePageState extends State<HomePage> {
],
);
}
return Text("RustDesk");
return Text(bind.mainGetAppNameSync());
}
}
@@ -154,7 +157,7 @@ class WebHomePage extends StatelessWidget {
// backgroundColor: MyTheme.grayBg,
appBar: AppBar(
centerTitle: true,
title: Text("RustDesk${isWeb ? " (Beta) " : ""}"),
title: Text(bind.mainGetAppNameSync()),
actions: connectionPage.appBarActions,
),
body: connectionPage,

View File

@@ -68,7 +68,9 @@ class _RemotePageState extends State<RemotePage> {
gFFI.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
});
WakelockPlus.enable();
if (!isWeb) {
WakelockPlus.enable();
}
_physicalFocusNode.requestFocus();
gFFI.inputModel.listenToMouse(true);
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
@@ -95,7 +97,9 @@ class _RemotePageState extends State<RemotePage> {
gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
await WakelockPlus.disable();
if (!isWeb) {
await WakelockPlus.disable();
}
await keyboardSubscription.cancel();
removeSharedStates(widget.id);
}
@@ -219,7 +223,7 @@ class _RemotePageState extends State<RemotePage> {
_timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboard, () {
// show now, and sleep a while to requestFocus to
// make sure edit ready, so that keyboard wont show/hide/show/hide happen
// make sure edit ready, so that keyboard won't show/hide/show/hide happen
setState(() => _showEdit = true);
_timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
@@ -832,6 +836,7 @@ void showOptions(
List<TRadioMenu<String>> imageQualityRadios =
await toolbarImageQuality(context, id, gFFI);
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
List<TToggleMenu> displayToggles =
await toolbarDisplayToggle(context, id, gFFI);
@@ -872,8 +877,23 @@ void showOptions(
})),
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
];
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
final cursorTogglesList = cursorToggles
.asMap()
.entries
.map((e) => Obx(() => CheckboxListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
value: rxCursorToggleValues[e.key].value,
onChanged: (v) {
e.value.onChanged?.call(v);
if (v != null) rxCursorToggleValues[e.key].value = v;
},
title: e.value.child)))
.toList();
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
final toggles = displayToggles
final displayTogglesList = displayToggles
.asMap()
.entries
.map((e) => Obx(() => CheckboxListTile(
@@ -886,6 +906,11 @@ void showOptions(
},
title: e.value.child)))
.toList();
final toggles = [
...cursorTogglesList,
if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
...displayTogglesList,
];
Widget privacyModeWidget = Offstage();
if (privacyModeList.length > 1) {

View File

@@ -158,6 +158,7 @@ class _ServerPageState extends State<ServerPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
buildPresetPasswordWarning(),
gFFI.serverModel.isStart
? ServerInfo()
: ServiceNotRunningNotification(),
@@ -226,7 +227,7 @@ class ScamWarningDialog extends StatefulWidget {
}
class ScamWarningDialogState extends State<ScamWarningDialog> {
int _countdown = 12;
int _countdown = bind.isCustomClient() ? 0 : 12;
bool show_warning = false;
late Timer _timer;
late ServerModel _serverModel;

View File

@@ -27,7 +27,7 @@ class SettingsPage extends StatefulWidget implements PageShape {
final icon = Icon(Icons.settings);
@override
final appBarActions = [ScanButton()];
final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
@override
State<SettingsPage> createState() => _SettingsState();
@@ -218,6 +218,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
Provider.of<FfiModel>(context);
final outgoingOnly = bind.isOutgoingOnly();
final customClientSection = CustomSettingsSection(
child: Column(
children: [
if (bind.isCustomClient())
Align(
alignment: Alignment.center,
child: loadPowered(context),
),
Align(
alignment: Alignment.center,
child: loadLogo(),
)
],
));
final List<AbstractSettingsTile> enhancementsTiles = [];
final List<AbstractSettingsTile> shareScreenTiles = [
SettingsTile.switchTile(
@@ -448,33 +463,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
}));
return SettingsList(
final disabledSettings = bind.isDisableSettings();
final settings = SettingsList(
sections: [
SettingsSection(
title: Text(translate('Account')),
tiles: [
SettingsTile(
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
leading: Icon(Icons.person),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
} else {
logOutConfirmDialog();
}
},
),
],
),
customClientSection,
if (!bind.isDisableAccount())
SettingsSection(
title: Text(translate('Account')),
tiles: [
SettingsTile(
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
leading: Icon(Icons.person),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {
loginDialog();
} else {
logOutConfirmDialog();
}
},
),
],
),
SettingsSection(title: Text(translate("Settings")), tiles: [
SettingsTile(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings(gFFI.dialogManager);
}),
if (!disabledSettings)
SettingsTile(
title: Text(translate('ID/Relay Server')),
leading: Icon(Icons.cloud),
onPressed: (context) {
showServerSettings(gFFI.dialogManager);
}),
SettingsTile(
title: Text(translate('Language')),
leading: Icon(Icons.translate),
@@ -494,7 +513,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
},
)
]),
if (isAndroid)
if (isAndroid && !outgoingOnly)
SettingsSection(
title: Text(translate("Recording")),
tiles: [
@@ -506,7 +525,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
builder: (ctx, data) => Offstage(
offstage: !data.hasData,
child: Text("${translate("Directory")}: ${data.data}")),
future: bind.mainDefaultVideoSaveDirectory()),
future: bind.mainVideoSaveDirectory(root: false)),
initialValue: _autoRecordIncomingSession,
onToggle: (v) async {
await bind.mainSetOption(
@@ -523,13 +542,13 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
),
],
),
if (isAndroid)
if (isAndroid && !disabledSettings && !outgoingOnly)
SettingsSection(
title: Text(translate("Share Screen")),
tiles: shareScreenTiles,
),
defaultDisplaySection(),
if (isAndroid)
if (!bind.isIncomingOnly()) defaultDisplaySection(),
if (isAndroid && !disabledSettings && !outgoingOnly)
SettingsSection(
title: Text(translate("Enhancements")),
tiles: enhancementsTiles,
@@ -578,6 +597,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
),
],
);
return settings;
}
Future<bool> canStartOnBoot() async {

View File

@@ -283,12 +283,3 @@ void setPrivacyModeDialog(
);
}, backDismiss: true, clickMaskDismiss: true);
}
Future<String?> validateAsync(String value) async {
value = value.trim();
if (value.isEmpty) {
return null;
}
final res = await bind.mainTestIfValidServer(server: value);
return res.isEmpty ? null : res;
}

View File

@@ -10,8 +10,8 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:http/http.dart' as http;
import '../utils/http_service.dart' as http;
import '../common.dart';
final syncAbOption = 'sync-ab-with-recent-sessions';
@@ -84,7 +84,7 @@ class AbModel {
reset() async {
print("reset ab model");
addressbooks.clear();
setCurrentName('');
_currentName.value = '';
await bind.mainClearAb();
listInitialized = false;
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:get/get.dart';
@@ -10,7 +11,7 @@ import './platform_model.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
// Feature flutter_texture_render need to be enabled if feature gpucodec is enabled.
// Feature flutter_texture_render need to be enabled if feature vram is enabled.
final useTextureRender = !isWeb &&
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
@@ -39,7 +40,7 @@ class _PixelbufferTexture {
final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
debugPrint(
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id");
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
}
});
}
@@ -101,13 +102,15 @@ class _GpuTexture {
}
}
destroy(FFI ffi) async {
destroy(bool unregisterTexture, FFI ffi) async {
// must stop texture render, render unregistered texture cause crash
if (!_destroying && support && _sessionId != null && _textureId != -1) {
_destroying = true;
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
// sleep for a while to avoid the texture is used after it's unregistered.
await Future.delayed(Duration(milliseconds: 100));
if (unregisterTexture) {
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
// sleep for a while to avoid the texture is used after it's unregistered.
await Future.delayed(Duration(milliseconds: 100));
}
await gpuTextureRenderer.unregisterTexture(_textureId);
_textureId = -1;
_destroying = false;
@@ -152,40 +155,36 @@ class TextureModel {
TextureModel(this.parent);
setTextureType({required int display, required bool gpuTexture}) {
debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture");
var texture = _control[display];
if (texture == null) {
texture = _Control();
_control[display] = texture;
debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture");
ensureControl(display);
_control[display]?.setTextureType(gpuTexture: gpuTexture);
// For versions that do not support multiple displays, the display parameter is always 0, need set type of current display
final ffi = parent.target;
if (ffi == null) return;
if (!ffi.ffiModel.pi.isSupportMultiDisplay) {
final currentDisplay = CurrentDisplayState.find(ffi.id).value;
if (currentDisplay != display) {
debugPrint(
"setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture");
ensureControl(currentDisplay);
_control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture);
}
}
texture.setTextureType(gpuTexture: gpuTexture);
}
setRgbaTextureId({required int display, required int id}) {
var ctl = _control[display];
if (ctl == null) {
ctl = _Control();
_control[display] = ctl;
}
ctl.setRgbaTextureId(id);
ensureControl(display);
_control[display]?.setRgbaTextureId(id);
}
setGpuTextureId({required int display, required int id}) {
var ctl = _control[display];
if (ctl == null) {
ctl = _Control();
_control[display] = ctl;
}
ctl.setGpuTextureId(id);
ensureControl(display);
_control[display]?.setGpuTextureId(id);
}
RxInt getTextureId(int display) {
var ctl = _control[display];
if (ctl == null) {
ctl = _Control();
_control[display] = ctl;
}
return ctl.textureID;
ensureControl(display);
return _control[display]!.textureID;
}
updateCurrentDisplay(int curDisplay) {
@@ -211,7 +210,7 @@ class TextureModel {
_pixelbufferRenderTextures.remove(idx);
}
if (_gpuRenderTextures.containsKey(idx)) {
_gpuRenderTextures[idx]!.destroy(ffi);
_gpuRenderTextures[idx]!.destroy(true, ffi);
_gpuRenderTextures.remove(idx);
}
}
@@ -238,7 +237,15 @@ class TextureModel {
await texture.destroy(closeSession, ffi);
}
for (final texture in _gpuRenderTextures.values) {
await texture.destroy(ffi);
await texture.destroy(closeSession, ffi);
}
}
ensureControl(int display) {
var ctl = _control[display];
if (ctl == null) {
ctl = _Control();
_control[display] = ctl;
}
}
}

View File

@@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../utils/http_service.dart' as http;
class GroupModel {
final RxBool groupLoading = false.obs;

View File

@@ -4,9 +4,12 @@ import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import '../../models/model.dart';
@@ -21,6 +24,128 @@ const _kMouseEventDown = 'mousedown';
const _kMouseEventUp = 'mouseup';
const _kMouseEventMove = 'mousemove';
class CanvasCoords {
double x = 0;
double y = 0;
double scale = 1.0;
double scrollX = 0;
double scrollY = 0;
ScrollStyle scrollStyle = ScrollStyle.scrollauto;
Size size = Size.zero;
CanvasCoords();
Map<String, dynamic> toJson() {
return {
'x': x,
'y': y,
'scale': scale,
'scrollX': scrollX,
'scrollY': scrollY,
'scrollStyle':
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
'size': {
'w': size.width,
'h': size.height,
}
};
}
static CanvasCoords fromJson(Map<String, dynamic> json) {
final model = CanvasCoords();
model.x = json['x'];
model.y = json['y'];
model.scale = json['scale'];
model.scrollX = json['scrollX'];
model.scrollY = json['scrollY'];
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
? ScrollStyle.scrollauto
: ScrollStyle.scrollbar;
model.size = Size(json['size']['w'], json['size']['h']);
return model;
}
static CanvasCoords fromCanvasModel(CanvasModel model) {
final coords = CanvasCoords();
coords.x = model.x;
coords.y = model.y;
coords.scale = model.scale;
coords.scrollX = model.scrollX;
coords.scrollY = model.scrollY;
coords.scrollStyle = model.scrollStyle;
coords.size = model.size;
return coords;
}
}
class CursorCoords {
Offset offset = Offset.zero;
CursorCoords();
Map<String, dynamic> toJson() {
return {
'offset_x': offset.dx,
'offset_y': offset.dy,
};
}
static CursorCoords fromJson(Map<String, dynamic> json) {
final model = CursorCoords();
model.offset = Offset(json['offset_x'], json['offset_y']);
return model;
}
static CursorCoords fromCursorModel(CursorModel model) {
final coords = CursorCoords();
coords.offset = model.offset;
return coords;
}
}
class RemoteWindowCoords {
RemoteWindowCoords(
this.windowRect, this.canvas, this.cursor, this.remoteRect);
Rect windowRect;
CanvasCoords canvas;
CursorCoords cursor;
Rect remoteRect;
Offset relativeOffset = Offset.zero;
Map<String, dynamic> toJson() {
return {
'canvas': canvas.toJson(),
'cursor': cursor.toJson(),
'windowRect': rectToJson(windowRect),
'remoteRect': rectToJson(remoteRect),
};
}
static Map<String, dynamic> rectToJson(Rect r) {
return {
'l': r.left,
't': r.top,
'w': r.width,
'h': r.height,
};
}
static Rect rectFromJson(Map<String, dynamic> json) {
return Rect.fromLTWH(
json['l'],
json['t'],
json['w'],
json['h'],
);
}
RemoteWindowCoords.fromJson(Map<String, dynamic> json)
: windowRect = rectFromJson(json['windowRect']),
canvas = CanvasCoords.fromJson(json['canvas']),
cursor = CursorCoords.fromJson(json['cursor']),
remoteRect = rectFromJson(json['remoteRect']);
}
extension ToString on MouseButtons {
String get value {
switch (this) {
@@ -188,32 +313,29 @@ class InputModel {
int _lastButtons = 0;
Offset lastMousePos = Offset.zero;
bool _queryOtherWindowCoords = false;
Rect? _windowRect;
List<RemoteWindowCoords> _remoteWindowCoords = [];
late final SessionID sessionId;
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
InputModel(this.parent) {
sessionId = parent.target!.sessionId;
// It is ok to call updateKeyboardMode() directly.
// Because `bind` is initialized in `PlatformFFI.init()` which is called very early.
// But we still wrap it in a Future.delayed() to make it more clear.
Future.delayed(Duration(milliseconds: 100), () {
updateKeyboardMode();
});
}
// This function must be called after the peer info is received.
// Because `sessionGetKeyboardMode` relies on the peer version.
updateKeyboardMode() async {
// * Currently mobile does not enable map mode
if (isDesktop || isWebDesktop) {
if (keyboardMode.isEmpty) {
keyboardMode =
await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
kKeyLegacyMode;
}
keyboardMode = await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
kKeyLegacyMode;
}
}
@@ -616,6 +738,9 @@ class InputModel {
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
if (isDesktop) _queryOtherWindowCoords = true;
_remoteWindowCoords = [];
_windowRect = null;
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) {
if (isPhysicalMouse.value) {
@@ -628,6 +753,7 @@ class InputModel {
}
void onPointUpImage(PointerUpEvent e) {
if (isDesktop) _queryOtherWindowCoords = false;
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (isPhysicalMouse.value) {
@@ -638,11 +764,37 @@ class InputModel {
void onPointMoveImage(PointerMoveEvent e) {
if (isViewOnly) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
if (_queryOtherWindowCoords) {
Future.delayed(Duration.zero, () async {
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
});
_queryOtherWindowCoords = false;
}
if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
}
}
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
List<RemoteWindowCoords> remoteWindowCoords) async {
final coords =
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
final wc = WindowController.fromWindowId(kWindowId!);
try {
final frame = await wc.getFrame();
for (final c in coords) {
c.relativeOffset = Offset(
c.windowRect.left - frame.left, c.windowRect.top - frame.top);
remoteWindowCoords.add(c);
}
return frame;
} catch (e) {
// Unreachable code
debugPrint("Failed to get frame of window $kWindowId, it may be hidden");
}
return null;
}
void onPointerSignalImage(PointerSignalEvent e) {
if (isViewOnly) return;
if (e is PointerScrollEvent) {
@@ -843,43 +995,107 @@ class InputModel {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
final canvasModel = parent.target!.canvasModel;
final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
Rect? rect = ffiModel.rect;
if (isMove) {
canvasModel.moveDesktopMouse(x, y);
if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
!_isInCurrentWindow(x, y)) {
final coords =
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
if (coords != null) {
isMove = false;
canvas = coords.canvas;
rect = coords.remoteRect;
x -= coords.relativeOffset.dx / devicePixelRatio;
y -= coords.relativeOffset.dy / devicePixelRatio;
}
}
}
final nearThr = 3;
var nearRight = (canvasModel.size.width - x) < nearThr;
var nearBottom = (canvasModel.size.height - y) < nearThr;
final rect = ffiModel.rect;
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
if (isMove) {
parent.target!.canvasModel.moveDesktopMouse(x, y);
}
return _handlePointerDevicePos(
kind,
x,
y,
isMove,
canvas,
rect,
evtType,
onExit: onExit,
buttons: buttons,
);
}
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
return x >= 0 && y >= 0 && x <= w && y <= h;
}
static RemoteWindowCoords? findRemoteCoords(double x, double y,
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
x *= devicePixelRatio;
y *= devicePixelRatio;
for (final c in remoteWindowCoords) {
if (x >= c.relativeOffset.dx &&
y >= c.relativeOffset.dy &&
x <= c.relativeOffset.dx + c.windowRect.width &&
y <= c.relativeOffset.dy + c.windowRect.height) {
return c;
}
}
return null;
}
Point? _handlePointerDevicePos(
String kind,
double x,
double y,
bool moveInCanvas,
CanvasCoords canvas,
Rect? rect,
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
}) {
if (rect == null) {
return null;
}
final imageWidth = rect.width * canvasModel.scale;
final imageHeight = rect.height * canvasModel.scale;
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
x += imageWidth * canvasModel.scrollX;
y += imageHeight * canvasModel.scrollY;
final nearThr = 3;
var nearRight = (canvas.size.width - x) < nearThr;
var nearBottom = (canvas.size.height - y) < nearThr;
final imageWidth = rect.width * canvas.scale;
final imageHeight = rect.height * canvas.scale;
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
x += imageWidth * canvas.scrollX;
y += imageHeight * canvas.scrollY;
// boxed size is a center widget
if (canvasModel.size.width > imageWidth) {
x -= ((canvasModel.size.width - imageWidth) / 2);
if (canvas.size.width > imageWidth) {
x -= ((canvas.size.width - imageWidth) / 2);
}
if (canvasModel.size.height > imageHeight) {
y -= ((canvasModel.size.height - imageHeight) / 2);
if (canvas.size.height > imageHeight) {
y -= ((canvas.size.height - imageHeight) / 2);
}
} else {
x -= canvasModel.x;
y -= canvasModel.y;
x -= canvas.x;
y -= canvas.y;
}
x /= canvasModel.scale;
y /= canvasModel.scale;
if (canvasModel.scale > 0 && canvasModel.scale < 1) {
final step = 1.0 / canvasModel.scale - 1;
x /= canvas.scale;
y /= canvas.scale;
if (canvas.scale > 0 && canvas.scale < 1) {
final step = 1.0 / canvas.scale - 1;
if (nearRight) {
x += step;
}
@@ -902,8 +1118,7 @@ class InputModel {
evtX = x.round();
evtY = y.round();
} catch (e) {
debugPrintStack(
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
return null;
}

View File

@@ -50,6 +50,8 @@ class CachedPeerData {
Map<String, dynamic> peerInfo = {};
List<Map<String, dynamic>> cursorDataList = [];
Map<String, dynamic> lastCursorId = {};
Map<String, bool> permissions = {};
bool secure = false;
bool direct = false;
@@ -62,6 +64,7 @@ class CachedPeerData {
'peerInfo': peerInfo,
'cursorDataList': cursorDataList,
'lastCursorId': lastCursorId,
'permissions': permissions,
'secure': secure,
'direct': direct,
});
@@ -77,6 +80,9 @@ class CachedPeerData {
data.cursorDataList.add(cursorData);
}
data.lastCursorId = map['lastCursorId'];
map['permissions'].forEach((key, value) {
data.permissions[key] = value;
});
data.secure = map['secure'];
data.direct = map['direct'];
return data;
@@ -116,6 +122,10 @@ class FfiModel with ChangeNotifier {
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
Map<String, bool> get permissions => _permissions;
setPermissions(Map<String, bool> permissions) {
_permissions.clear();
_permissions.addAll(permissions);
}
bool? get secure => _secure;
@@ -138,6 +148,7 @@ class FfiModel with ChangeNotifier {
FfiModel(this.parent) {
clear();
sessionId = parent.target!.sessionId;
cachedPeerData.permissions = _permissions;
}
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
@@ -367,6 +378,8 @@ class FfiModel with ChangeNotifier {
}
} else if (name == 'sync_peer_option') {
_handleSyncPeerOption(evt, peerId);
} else if (name == 'follow_current_display') {
handleFollowCurrentDisplay(evt, sessionId, peerId);
} else {
debugPrint('Unknown event name: $name');
}
@@ -440,7 +453,7 @@ class FfiModel with ChangeNotifier {
}
}
updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) {
updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
final newRect = displaysRect();
if (newRect == null) {
return;
@@ -561,8 +574,12 @@ class FfiModel with ChangeNotifier {
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
} else if (text == 'Connected, waiting for image...') {
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
} else if (title == 'Privacy mode') {
final hasRetry = evt['hasRetry'] == 'true';
showPrivacyFailedDialog(
sessionId, type, title, text, link, hasRetry, dialogManager);
} else {
var hasRetry = evt['hasRetry'] == 'true';
final hasRetry = evt['hasRetry'] == 'true';
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
}
}
@@ -657,6 +674,27 @@ class FfiModel with ChangeNotifier {
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
}
void showPrivacyFailedDialog(
SessionID sessionId,
String type,
String title,
String text,
String link,
bool hasRetry,
OverlayDialogManager dialogManager) {
if (text == 'no_need_privacy_mode_no_physical_displays_tip' ||
text == 'Enter privacy mode') {
// There are display changes on the remote side,
// which will cause some messages to refresh the canvas and dismiss dialogs.
// So we add a delay here to ensure the dialog is displayed.
Future.delayed(Duration(milliseconds: 3000), () {
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
});
} else {
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
}
}
_updateSessionWidthHeight(SessionID sessionId) {
if (_rect == null) return;
if (_rect!.width <= 0 || _rect!.height <= 0) {
@@ -687,9 +725,14 @@ class FfiModel with ChangeNotifier {
/// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
// This call is to ensuer the keyboard mode is updated depending on the peer version.
parent.target?.inputModel.updateKeyboardMode();
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
// Because this function is asynchronous, there's an "await" in this function.
cachedPeerData.peerInfo = {...evt};
// Do not cache resolutions, because a new display connection have different resolutions.
cachedPeerData.peerInfo.remove('resolutions');
// Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
bind.mainLoadRecentPeers();
@@ -745,7 +788,9 @@ class FfiModel with ChangeNotifier {
}
Map<String, dynamic> features = json.decode(evt['features']);
_pi.features.privacyMode = features['privacy_mode'] == 1;
handleResolutions(peerId, evt["resolutions"]);
if (!isCache) {
handleResolutions(peerId, evt["resolutions"]);
}
parent.target?.elevationModel.onPeerInfo(_pi);
}
if (connType == ConnType.defaultConn) {
@@ -975,6 +1020,8 @@ class FfiModel with ChangeNotifier {
}
}
}
parent.target!.canvasModel
.tryUpdateScrollStyle(Duration(milliseconds: 300), null);
notifyListeners();
}
@@ -986,15 +1033,21 @@ class FfiModel with ChangeNotifier {
}
if (updateData.isEmpty) {
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
_pi.platformAdditions.remove(kPlatformAdditionsRustDeskVirtualDisplays);
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
} else {
try {
final updateJson = json.decode(updateData) as Map<String, dynamic>;
for (final key in updateJson.keys) {
_pi.platformAdditions[key] = updateJson[key];
}
if (!updateJson.containsKey(kPlatformAdditionsVirtualDisplays)) {
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
if (!updateJson
.containsKey(kPlatformAdditionsRustDeskVirtualDisplays)) {
_pi.platformAdditions
.remove(kPlatformAdditionsRustDeskVirtualDisplays);
}
if (!updateJson.containsKey(kPlatformAdditionsAmyuniVirtualDisplays)) {
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
}
} catch (e) {
debugPrint('Failed to decode platformAdditions $e');
@@ -1005,9 +1058,30 @@ class FfiModel with ChangeNotifier {
json.encode(_pi.platformAdditions);
}
handleFollowCurrentDisplay(
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
if (evt['display_idx'] != null) {
if (pi.currentDisplay == kAllDisplayValue) {
return;
}
_pi.currentDisplay = int.parse(evt['display_idx']);
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([_pi.currentDisplay]),
);
}
notifyListeners();
}
// Directly switch to the new display without waiting for the response.
switchToNewDisplay(int display, SessionID sessionId, String peerId,
{bool updateCursorPos = true}) {
{bool updateCursorPos = false}) {
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
parent.target?.recordingModel.onClose();
// no need to wait for the response
@@ -1343,10 +1417,20 @@ class CanvasModel with ChangeNotifier {
if (refreshMousePos) {
parent.target?.inputModel.refreshMousePos();
}
if (style == kRemoteViewStyleOriginal &&
_scrollStyle == ScrollStyle.scrollbar) {
updateScrollPercent();
tryUpdateScrollStyle(Duration.zero, style);
}
tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
if (style != kRemoteViewStyleOriginal) {
return;
}
_resetScroll();
Future.delayed(duration, () async {
updateScrollPercent();
});
}
updateScrollStyle() async {
@@ -1660,6 +1744,8 @@ class CursorModel with ChangeNotifier {
double _displayOriginX = 0;
double _displayOriginY = 0;
DateTime? _firstUpdateMouseTime;
Rect? _windowRect;
List<RemoteWindowCoords> _remoteWindowCoords = [];
bool gotMouseControl = true;
DateTime _lastPeerMouse = DateTime.now()
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
@@ -1672,6 +1758,8 @@ class CursorModel with ChangeNotifier {
double get x => _x - _displayOriginX;
double get y => _y - _displayOriginY;
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
Offset get offset => Offset(_x, _y);
double get hotx => _hotx;
@@ -1741,15 +1829,13 @@ class CursorModel with ChangeNotifier {
notifyListeners();
}
updatePan(double dx, double dy, bool touchMode) {
updatePan(Offset delta, Offset localPosition, bool touchMode) {
if (touchMode) {
final scale = parent.target?.canvasModel.scale ?? 1.0;
_x += dx / scale;
_y += dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
notifyListeners();
_handleTouchMode(delta, localPosition);
return;
}
double dx = delta.dx;
double dy = delta.dy;
if (parent.target?.imageModel.image == null) return;
final scale = parent.target?.canvasModel.scale ?? 1.0;
dx /= scale;
@@ -1816,6 +1902,41 @@ class CursorModel with ChangeNotifier {
notifyListeners();
}
bool _isInCurrentWindow(double x, double y) {
final w = _windowRect!.width / devicePixelRatio;
final h = _windowRect!.width / devicePixelRatio;
return x >= 0 && y >= 0 && x <= w && y <= h;
}
_handleTouchMode(Offset delta, Offset localPosition) {
bool isMoved = false;
if (_remoteWindowCoords.isNotEmpty &&
_windowRect != null &&
!_isInCurrentWindow(localPosition.dx, localPosition.dy)) {
final coords = InputModel.findRemoteCoords(localPosition.dx,
localPosition.dy, _remoteWindowCoords, devicePixelRatio);
if (coords != null) {
double x2 =
(localPosition.dx - coords.relativeOffset.dx / devicePixelRatio) /
coords.canvas.scale;
double y2 =
(localPosition.dy - coords.relativeOffset.dy / devicePixelRatio) /
coords.canvas.scale;
x2 += coords.cursor.offset.dx;
y2 += coords.cursor.offset.dy;
parent.target?.inputModel.moveMouse(x2, y2);
isMoved = true;
}
}
if (!isMoved) {
final scale = parent.target?.canvasModel.scale ?? 1.0;
_x += delta.dx / scale;
_y += delta.dy / scale;
parent.target?.inputModel.moveMouse(_x, _y);
}
notifyListeners();
}
updateCursorData(Map<String, dynamic> evt) async {
final id = int.parse(evt['id']);
final hotx = double.parse(evt['hotx']);
@@ -1955,6 +2076,18 @@ class CursorModel with ChangeNotifier {
deleteCustomCursor(k);
}
}
trySetRemoteWindowCoords() {
Future.delayed(Duration.zero, () async {
_windowRect =
await InputModel.fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
});
}
clearRemoteWindowCoords() {
_windowRect = null;
_remoteWindowCoords.clear();
}
}
class QualityMonitorData {
@@ -2284,8 +2417,11 @@ class FFI {
debugPrint('Unreachable, the cached data cannot be decoded.');
return;
}
ffiModel.setPermissions(data.permissions);
await ffiModel.handleCachedPeerData(data, id);
await sessionRefreshVideo(sessionId, ffiModel.pi);
await bind.sessionRequestNewDisplayInitMsgs(
sessionId: sessionId, display: ffiModel.pi.currentDisplay);
});
isToNewWindowNotified.value = true;
}
@@ -2490,14 +2626,21 @@ class PeerInfo with ChangeNotifier {
bool get isInstalled =>
platform != kPeerPlatformWindows ||
platformAdditions[kPlatformAdditionsIsInstalled] == true;
List<int> get virtualDisplays => List<int>.from(
platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []);
List<int> get RustDeskVirtualDisplays => List<int>.from(
platformAdditions[kPlatformAdditionsRustDeskVirtualDisplays] ?? []);
int get amyuniVirtualDisplayCount =>
platformAdditions[kPlatformAdditionsAmyuniVirtualDisplays] ?? 0;
bool get isSupportMultiDisplay =>
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
bool get isRustDeskIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'rustdesk_idd';
bool get isAmyuniIdd =>
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
Display? tryGetDisplay() {
if (displays.isEmpty) {
return null;

View File

@@ -198,7 +198,10 @@ class PlatformFFI {
await _ffiBind.mainDeviceId(id: id);
await _ffiBind.mainDeviceName(name: name);
await _ffiBind.mainSetHomeDir(home: _homeDir);
await _ffiBind.mainInit(appDir: _dir);
await _ffiBind.mainInit(
appDir: _dir,
customClientConfig: '',
);
} catch (e) {
debugPrintStack(label: 'initialize failed: $e');
}

View File

@@ -322,6 +322,20 @@ class ServerModel with ChangeNotifier {
}
}
Future<bool> checkRequestNotificationPermission() async {
debugPrint("androidVersion $androidVersion");
if (androidVersion < 33) {
return true;
}
if (await AndroidPermissionManager.check(kAndroid13Notification)) {
debugPrint("notification permission already granted");
return true;
}
var res = await AndroidPermissionManager.request(kAndroid13Notification);
debugPrint("notification permission request result: $res");
return res;
}
/// Toggle the screen sharing service.
toggleService() async {
if (_isStart) {
@@ -348,6 +362,7 @@ class ServerModel with ChangeNotifier {
stopService();
}
} else {
await checkRequestNotificationPermission();
final res = await parent.target?.dialogManager
.show<bool>((setState, close, context) {
submit() => close(true);

View File

@@ -14,12 +14,13 @@ class StateGlobal {
bool _isMinimized = false;
final RxBool isMaximized = false.obs;
final RxBool _showTabBar = true.obs;
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
final RxBool showRemoteToolBar = false.obs;
final svcStatus = SvcStatus.notReady.obs;
// Only used for macOS
bool? closeOnFullscreen;
final RxBool isFocused = false.obs;
String _inputSource = '';
@@ -55,8 +56,7 @@ class StateGlobal {
if (!_fullscreen.isTrue) {
if (isMaximized.value != v) {
isMaximized.value = v;
_resizeEdgeSize.value =
isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
refreshResizeEdgeSize();
}
if (!isMacOS) {
_windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
@@ -70,11 +70,7 @@ class StateGlobal {
if (_fullscreen.value != v) {
_fullscreen.value = v;
_showTabBar.value = !_fullscreen.value;
_resizeEdgeSize.value = fullscreen.isTrue
? kFullScreenEdgeSize
: isMaximized.isTrue
? kMaximizeEdgeSize
: kWindowEdgeSize;
refreshResizeEdgeSize();
print(
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
@@ -95,6 +91,12 @@ class StateGlobal {
}
}
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
? kFullScreenEdgeSize
: isMaximized.isTrue
? kMaximizeEdgeSize
: windowEdgeSize;
String getInputSource({bool force = false}) {
if (force || _inputSource.isEmpty) {
_inputSource = bind.mainGetInputSource();
@@ -112,4 +114,5 @@ class StateGlobal {
static final StateGlobal instance = StateGlobal._();
}
// This final variable is initialized when the first time it is accessed.
final stateGlobal = StateGlobal.instance;

View File

@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/ab_model.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import '../common.dart';
import '../utils/http_service.dart' as http;
import 'model.dart';
import 'platform_model.dart';
@@ -136,7 +136,6 @@ class UserModel {
Future<LoginResponse> login(LoginRequest loginRequest) async {
final url = await bind.mainGetApiServer();
final resp = await http.post(Uri.parse('$url/api/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(loginRequest.toJson()));
final Map<String, dynamic> body;

View File

@@ -0,0 +1,115 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../models/platform_model.dart';
export 'package:http/http.dart' show Response;
enum HttpMethod { get, post, put, delete }
class HttpService {
Future<http.Response> sendRequest(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
headers ??= {'Content-Type': 'application/json'};
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
final isProxy = await bind.mainGetProxyStatus();
if (!isProxy) {
return await _pollFultterHttp(url, method, headers: headers, body: body);
}
String headersJson = jsonEncode(headers);
String methodName = method.toString().split('.').last;
await bind.mainHttpRequest(
url: url.toString(),
method: methodName.toLowerCase(),
body: body,
header: headersJson);
var resJson = await _pollForResponse(url.toString());
return _parseHttpResponse(resJson);
}
Future<http.Response> _pollFultterHttp(
Uri url,
HttpMethod method, {
Map<String, String>? headers,
dynamic body,
}) async {
var response = http.Response('', 400);
switch (method) {
case HttpMethod.get:
response = await http.get(url, headers: headers);
break;
case HttpMethod.post:
response = await http.post(url, headers: headers, body: body);
break;
case HttpMethod.put:
response = await http.put(url, headers: headers, body: body);
break;
case HttpMethod.delete:
response = await http.delete(url, headers: headers, body: body);
break;
default:
throw Exception('Unsupported HTTP method');
}
return response;
}
Future<String> _pollForResponse(String url) async {
String? responseJson = " ";
while (responseJson == " ") {
responseJson = await bind.mainGetHttpStatus(url: url);
if (responseJson == null) {
throw Exception('The HTTP request failed');
}
if (responseJson == " ") {
await Future.delayed(const Duration(milliseconds: 100));
}
}
return responseJson!;
}
http.Response _parseHttpResponse(String responseJson) {
try {
var parsedJson = jsonDecode(responseJson);
String body = parsedJson['body'];
Map<String, String> headers = {};
for (var key in parsedJson['headers'].keys) {
headers[key] = parsedJson['headers'][key];
}
int statusCode = parsedJson['status_code'];
return http.Response(body, statusCode, headers: headers);
} catch (e) {
throw Exception('Failed to parse response: $e');
}
}
}
Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
return await HttpService().sendRequest(url, HttpMethod.get, headers: headers);
}
Future<http.Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.post, body: body, headers: headers);
}
Future<http.Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.put, body: body, headers: headers);
}
Future<http.Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
return await HttpService()
.sendRequest(url, HttpMethod.delete, body: body, headers: headers);
}

View File

@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/input_model.dart';
/// must keep the order
// ignore: constant_identifier_names
@@ -431,6 +433,39 @@ class RustDeskMultiWindowManager {
void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback);
}
// This function is called from the main window.
// It will query the active remote windows to get their coords.
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
List<String> coords = [];
for (final windowId in _remoteDesktopWindows) {
if (windowId != wId) {
if (_activeWindows.contains(windowId)) {
final res = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventRemoteWindowCoords, '');
if (res != null) {
coords.add(res);
}
}
}
}
return coords;
}
// This function is called from one remote window.
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
// So we need to call the main window to get the other remote windows' coords.
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
List<RemoteWindowCoords> coords = [];
// Call the main window to get the coords of other remote windows.
String res = await DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
List<dynamic> list = jsonDecode(res);
for (var item in list) {
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
}
return coords;
}
}
final rustDeskWinManager = RustDeskMultiWindowManager.instance;

View File

@@ -346,6 +346,10 @@ class RustdeskImpl {
return mode == kKeyLegacyMode;
}
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
return false;
}
Future<void> sessionSetCustomImageQuality(
{required UuidValue sessionId, required int value, dynamic hint}) {
return Future(() => js.context.callMethod('setByName', [
@@ -676,7 +680,8 @@ class RustdeskImpl {
return Future(() => js.context.callMethod('setByName', ['options', json]));
}
Future<String> mainTestIfValidServer({required String server, dynamic hint}) {
Future<String> mainTestIfValidServer(
{required String server, required bool testWithProxy, dynamic hint}) {
// TODO: implement
return Future.value('');
}
@@ -770,6 +775,24 @@ class RustdeskImpl {
throw UnimplementedError();
}
Future<bool> mainGetProxyStatus({dynamic hint}) {
return Future(() => false);
}
Future<void> mainHttpRequest({
required String url,
required String method,
String? body,
required String header,
dynamic hint,
}) {
throw UnimplementedError();
}
Future<String?> mainGetHttpStatus({required String url, dynamic hint}) {
throw UnimplementedError();
}
String mainGetLocalOption({required String key, dynamic hint}) {
return js.context.callMethod('getByName', ['option:local', key]);
}
@@ -920,7 +943,7 @@ class RustdeskImpl {
throw UnimplementedError();
}
Future<String> mainDefaultVideoSaveDirectory({dynamic hint}) {
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
throw UnimplementedError();
}
@@ -1052,7 +1075,7 @@ class RustdeskImpl {
throw UnimplementedError();
}
bool mainHasGpucodec({dynamic hint}) {
bool mainHasVram({dynamic hint}) {
throw UnimplementedError();
}
@@ -1573,5 +1596,19 @@ class RustdeskImpl {
throw UnimplementedError();
}
Future<void> mainCheckHwcodec({dynamic hint}) {
throw UnimplementedError();
}
Future<void> sessionRequestNewDisplayInitMsgs(
{required UuidValue sessionId, required int display, dynamic hint}) {
throw UnimplementedError();
}
Future<String> mainHandleWaylandScreencastRestoreToken(
{required String key, required String value, dynamic hint}) {
throw UnimplementedError();
}
void dispose() {}
}

View File

@@ -2,21 +2,33 @@
#include "my_application.h"
#define RUSTDESK_LIB_PATH "librustdesk.so"
// #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so"
typedef bool (*RustDeskCoreMain)();
bool gIsConnectionManager = false;
void print_help_install_pkg(const char* so);
bool flutter_rustdesk_core_main() {
void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY);
if (!librustdesk) {
fprintf(stderr,"load librustdesk.so failed\n");
return true;
fprintf(stderr,"Failed to load \"librustdesk.so\"\n");
char* error;
if ((error = dlerror()) != nullptr) {
fprintf(stderr, "%s\n", error);
char* libmissed = strstr(error, ": cannot open shared object file: No such file or directory");
if (libmissed != nullptr) {
*libmissed = '\0';
char* so = strdup(error);
print_help_install_pkg(so);
free(so);
}
}
return false;
}
auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main");
char* error;
if ((error = dlerror()) != nullptr) {
fprintf(stderr, "error finding rustdesk_core_main: %s", error);
return true;
fprintf(stderr, "Program entry \"rustdesk_core_main\" is not found: %s\n", error);
return false;
}
return core_main();
}
@@ -33,3 +45,80 @@ int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
typedef struct {
const char* mgr;
const char* search;
} PkgMgrSearch;
const PkgMgrSearch g_mgrs[] = {
{
"apt",
"apt-file search",
},
{
"dnf",
"dnf provides",
},
{
"yum",
"yum provides",
},
{
"zypper",
"zypper wp",
},
{
"pacman",
"pacman -Qo",
},
{
NULL,
NULL,
},
};
int is_command_exists(const char* command) {
char* path = getenv("PATH");
char* path_copy = strdup(path);
char* dir = strtok(path_copy, ":");
while (dir != NULL) {
char command_path[256];
snprintf(command_path, sizeof(command_path), "%s/%s", dir, command);
if (access(command_path, X_OK) == 0) {
free(path_copy);
return 1;
}
dir = strtok(NULL, ":");
}
free(path_copy);
return 0;
}
// We do not automatically search pkg
// as the search process can be time consuming and update may be required.
void print_help_install_pkg(const char* so)
{
if (strcmp(so, "libnsl.so.1") == 0) {
const char* mgr[] = {"yum", "dnf", NULL};
const char** m = mgr;
while (*m != NULL) {
if (is_command_exists(*m)) {
fprintf(stderr, "Please run \"%s install libnsl\" to install the required package.\n", *m);
return;
}
m++;
}
}
const PkgMgrSearch *mgr_search = g_mgrs;
while (mgr_search->mgr != NULL) {
if (is_command_exists(mgr_search->mgr) == 1) {
fprintf(stderr, "Please run \"%s %s\" to search and install the pkg.\n", mgr_search->search, so);
break;
}
mgr_search++;
}
}

View File

@@ -21,7 +21,6 @@ static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// we have custom window frame
gtk_window_set_decorated(window, FALSE);
// try setting icon for rustdesk, which uses the system cache
GtkIconTheme* theme = gtk_icon_theme_get_default();
@@ -75,12 +74,7 @@ static void my_application_activate(GApplication* application) {
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
auto border_frame = gtk_frame_new(nullptr);
gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN);
gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view));
gtk_widget_show(GTK_WIDGET(border_frame));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));

View File

@@ -210,7 +210,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter,hwcodec

View File

@@ -849,18 +849,18 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.11.0"
mime:
dependency: transitive
description:
@@ -921,10 +921,10 @@ packages:
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_parsing:
dependency: transitive
description:
@@ -1526,10 +1526,10 @@ packages:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "0.4.2"
web_socket_channel:
dependency: transitive
description:

View File

@@ -8,7 +8,7 @@ edition = "2018"
[dependencies]
flexi_logger = { version = "0.27", features = ["async"] }
protobuf = { version = "3.3", features = ["with-bytes"] }
protobuf = { version = "3.4", features = ["with-bytes"] }
tokio = { version = "1.36", features = ["full"] }
tokio-util = { version = "0.7", features = ["full"] }
futures = "0.3"
@@ -40,17 +40,26 @@ toml = "0.7"
uuid = { version = "1.3", features = ["v4"] }
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
thiserror = "1.0"
httparse = "1.5"
base64 = "0.22"
url = "2.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
mac_address = "1.1"
machine-uid = { git = "https://github.com/21pages/machine-uid" }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
rustls-platform-verifier = "0.3.1"
rustls-pki-types = "1.4"
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
tokio-native-tls ="0.3"
[features]
quic = []
flatpak = []
[build-dependencies]
protobuf-codegen = { version = "3.3" }
protobuf-codegen = { version = "3.4" }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi"] }

View File

@@ -504,6 +504,11 @@ message Resolution {
int32 height = 2;
}
message DisplayResolution {
int32 display = 1;
Resolution resolution = 2;
}
message SupportedResolutions { repeated Resolution resolutions = 1; }
message SwitchDisplay {
@@ -596,7 +601,10 @@ message OptionMessage {
BoolOption disable_keyboard = 12;
// Position 13 is used for Resolution. Remove later.
// Resolution custom_resolution = 13;
BoolOption support_windows_specific_session = 14;
// BoolOption support_windows_specific_session = 14;
// starting from 15 please, do not use removed fields
BoolOption follow_remote_cursor = 15;
BoolOption follow_remote_window = 16;
}
message TestDelay {
@@ -716,6 +724,13 @@ message WindowsSessions {
uint32 current_sid = 2;
}
// Query messages from peer.
message MessageQuery {
// The SwitchDisplay message of the target display.
// If the target display is not found, the message will be ignored.
int32 switch_display = 1;
}
message Misc {
oneof union {
ChatMessage chat_message = 4;
@@ -736,6 +751,8 @@ message Misc {
bool portable_service_running = 20;
SwitchSidesRequest switch_sides_request = 21;
SwitchBack switch_back = 22;
// Deprecated since 1.2.4, use `change_display_resolution` (36) instead.
// But we must keep it for compatibility when peer version < 1.2.4.
Resolution change_resolution = 24;
PluginRequest plugin_request = 25;
PluginFailure plugin_failure = 26;
@@ -748,6 +765,9 @@ message Misc {
TogglePrivacyMode toggle_privacy_mode = 33;
SupportedEncoding supported_encoding = 34;
uint32 selected_sid = 35;
DisplayResolution change_display_resolution = 36;
MessageQuery message_query = 37;
int32 follow_current_display = 38;
}
}

View File

@@ -281,6 +281,10 @@ pub struct PeerConfig {
pub enable_file_transfer: EnableFileTransfer,
#[serde(flatten)]
pub show_quality_monitor: ShowQualityMonitor,
#[serde(flatten)]
pub follow_remote_cursor: FollowRemoteCursor,
#[serde(flatten)]
pub follow_remote_window: FollowRemoteWindow,
#[serde(
default,
deserialize_with = "deserialize_string",
@@ -353,6 +357,8 @@ impl Default for PeerConfig {
disable_clipboard: Default::default(),
enable_file_transfer: Default::default(),
show_quality_monitor: Default::default(),
follow_remote_cursor: Default::default(),
follow_remote_window: Default::default(),
keyboard_mode: Default::default(),
view_only: Default::default(),
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
@@ -409,9 +415,7 @@ fn patch(path: PathBuf) -> PathBuf {
if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") {
if user != "root" {
let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user);
if let Ok(output) =
crate::platform::linux::run_cmds_trim_newline(&cmd)
{
if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) {
return output.into();
}
return format!("/home/{user}").into();
@@ -505,7 +509,7 @@ impl Config {
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
let file = Self::file_(suffix);
if let Err(err) = store_path(file, config) {
log::error!("Failed to store config: {}", err);
log::error!("Failed to store {suffix} config: {err}");
}
}
@@ -1260,6 +1264,19 @@ serde_field_bool!(
default_show_remote_cursor,
"ShowRemoteCursor::default_show_remote_cursor"
);
serde_field_bool!(
FollowRemoteCursor,
"follow_remote_cursor",
default_follow_remote_cursor,
"FollowRemoteCursor::default_follow_remote_cursor"
);
serde_field_bool!(
FollowRemoteWindow,
"follow_remote_window",
default_follow_remote_window,
"FollowRemoteWindow::default_follow_remote_window"
);
serde_field_bool!(
ShowQualityMonitor,
"show_quality_monitor",
@@ -1495,8 +1512,10 @@ impl LanPeers {
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct HwCodecConfig {
#[serde(default, deserialize_with = "deserialize_hashmap_string_string")]
pub options: HashMap<String, String>,
#[serde(default, deserialize_with = "deserialize_string")]
pub ram: String,
#[serde(default, deserialize_with = "deserialize_string")]
pub vram: String,
}
impl HwCodecConfig {
@@ -1511,25 +1530,17 @@ impl HwCodecConfig {
pub fn clear() {
HwCodecConfig::default().store();
}
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct GpucodecConfig {
#[serde(default, deserialize_with = "deserialize_string")]
pub available: String,
}
impl GpucodecConfig {
pub fn load() -> GpucodecConfig {
Config::load_::<GpucodecConfig>("_gpucodec")
pub fn clear_ram() {
let mut c = Self::load();
c.ram = Default::default();
c.store();
}
pub fn store(&self) {
Config::store_(self, "_gpucodec");
}
pub fn clear() {
GpucodecConfig::default().store();
pub fn clear_vram() {
let mut c = Self::load();
c.vram = Default::default();
c.store();
}
}
@@ -1569,6 +1580,7 @@ impl UserDefaultConfig {
}
"custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64),
"custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
"enable_file_transfer" => self.get_string(key, "Y", vec![""]),
_ => self
.get_after(key)
.map(|v| v.to_string())

View File

@@ -16,6 +16,7 @@ use std::{
};
pub use tokio;
pub use tokio_util;
pub mod proxy;
pub mod socket_client;
pub mod tcp;
pub mod udp;
@@ -46,9 +47,13 @@ pub mod keyboard;
pub use dlopen;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use machine_uid;
pub use serde_derive;
pub use serde_json;
pub use sysinfo;
pub use toml;
pub use uuid;
pub use base64;
pub use thiserror;
#[cfg(feature = "quic")]
pub type Stream = quic::Connection;

View File

@@ -49,6 +49,16 @@ pub fn is_x11_or_headless() -> bool {
const INVALID_SESSION: &str = "4294967295";
pub fn get_display_server() -> String {
// Check for forced display server environment variable first
if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") {
return forced_display;
}
// Check if `loginctl` can be called successfully
if run_loginctl(None).is_err() {
return DISPLAY_SERVER_X11.to_owned();
}
let mut session = get_values_of_seat0(&[0])[0].clone();
if session.is_empty() {
// loginctl has not given the expected output. try something else.
@@ -64,7 +74,7 @@ pub fn get_display_server() -> String {
}
}
if session.is_empty() {
"".to_owned()
std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned())
} else {
get_display_server_of_session(&session)
}
@@ -213,8 +223,19 @@ pub fn run_cmds_trim_newline(cmds: &str) -> ResultType<String> {
})
}
#[cfg(not(feature = "flatpak"))]
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
if std::env::var("FLATPAK_ID").is_ok() {
let mut l_args = String::from("loginctl");
if let Some(a) = args.as_ref() {
l_args = format!("{} {}", l_args, a.join(" "));
}
let res = std::process::Command::new("flatpak-spawn")
.args(vec![String::from("--host"), l_args])
.output();
if res.is_ok() {
return res;
}
}
let mut cmd = std::process::Command::new("loginctl");
if let Some(a) = args {
return cmd.args(a).output();
@@ -222,17 +243,6 @@ fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output
cmd.output()
}
#[cfg(feature = "flatpak")]
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
let mut l_args = String::from("loginctl");
if let Some(a) = args {
l_args = format!("{} {}", l_args, a.join(" "));
}
std::process::Command::new("flatpak-spawn")
.args(vec![String::from("--host"), l_args])
.output()
}
/// forever: may not work
#[cfg(target_os = "linux")]
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
@@ -280,6 +290,9 @@ mod tests {
fn test_run_cmds_trim_newline() {
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
assert_eq!(run_cmds_trim_newline("whoami").unwrap() + "\n", run_cmds("whoami").unwrap());
assert_eq!(
run_cmds_trim_newline("whoami").unwrap() + "\n",
run_cmds("whoami").unwrap()
);
}
}

View File

@@ -0,0 +1,561 @@
use std::{
io::Error as IoError,
net::{SocketAddr, ToSocketAddrs},
};
use base64::{engine::general_purpose, Engine};
use httparse::{Error as HttpParseError, Response, EMPTY_HEADER};
use log::info;
use thiserror::Error as ThisError;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
#[cfg(any(target_os = "windows", target_os = "macos"))]
use tokio_native_tls::{native_tls, TlsConnector, TlsStream};
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
use tokio_rustls::{client::TlsStream, TlsConnector};
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr};
use tokio_util::codec::Framed;
use url::Url;
use crate::{
bytes_codec::BytesCodec,
config::Socks5Server,
tcp::{DynTcpStream, FramedStream},
ResultType,
};
#[derive(Debug, ThisError)]
pub enum ProxyError {
#[error("IO Error: {0}")]
IoError(#[from] IoError),
#[error("Target parse error: {0}")]
TargetParseError(String),
#[error("HTTP parse error: {0}")]
HttpParseError(#[from] HttpParseError),
#[error("The maximum response header length is exceeded: {0}")]
MaximumResponseHeaderLengthExceeded(usize),
#[error("The end of file is reached")]
EndOfFile,
#[error("The url is error: {0}")]
UrlBadScheme(String),
#[error("The url parse error: {0}")]
UrlParseScheme(#[from] url::ParseError),
#[error("No HTTP code was found in the response")]
NoHttpCode,
#[error("The HTTP code is not equal 200: {0}")]
HttpCode200(u16),
#[error("The proxy address resolution failed: {0}")]
AddressResolutionFailed(String),
#[cfg(any(target_os = "windows", target_os = "macos"))]
#[error("The native tls error: {0}")]
NativeTlsError(#[from] tokio_native_tls::native_tls::Error),
}
const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096;
/// The maximum HTTP Headers, which can be parsed.
const MAXIMUM_RESPONSE_HEADERS: usize = 16;
const DEFINE_TIME_OUT: u64 = 600;
pub trait IntoUrl {
// Besides parsing as a valid `Url`, the `Url` must be a valid
// `http::Uri`, in that it makes sense to use in a network request.
fn into_url(self) -> Result<Url, ProxyError>;
fn as_str(&self) -> &str;
}
impl IntoUrl for Url {
fn into_url(self) -> Result<Url, ProxyError> {
if self.has_host() {
Ok(self)
} else {
Err(ProxyError::UrlBadScheme(self.to_string()))
}
}
fn as_str(&self) -> &str {
self.as_ref()
}
}
impl<'a> IntoUrl for &'a str {
fn into_url(self) -> Result<Url, ProxyError> {
Url::parse(self)
.map_err(ProxyError::UrlParseScheme)?
.into_url()
}
fn as_str(&self) -> &str {
self
}
}
impl<'a> IntoUrl for &'a String {
fn into_url(self) -> Result<Url, ProxyError> {
(&**self).into_url()
}
fn as_str(&self) -> &str {
self.as_ref()
}
}
impl<'a> IntoUrl for String {
fn into_url(self) -> Result<Url, ProxyError> {
(&*self).into_url()
}
fn as_str(&self) -> &str {
self.as_ref()
}
}
#[derive(Clone)]
pub struct Auth {
user_name: String,
password: String,
}
impl Auth {
fn get_proxy_authorization(&self) -> String {
format!(
"Proxy-Authorization: Basic {}\r\n",
self.get_basic_authorization()
)
}
pub fn get_basic_authorization(&self) -> String {
let authorization = format!("{}:{}", &self.user_name, &self.password);
general_purpose::STANDARD.encode(authorization.as_bytes())
}
}
#[derive(Clone)]
pub enum ProxyScheme {
Http {
auth: Option<Auth>,
host: String,
},
Https {
auth: Option<Auth>,
host: String,
},
Socks5 {
addr: SocketAddr,
auth: Option<Auth>,
remote_dns: bool,
},
}
impl ProxyScheme {
pub fn maybe_auth(&self) -> Option<&Auth> {
match self {
ProxyScheme::Http { auth, .. }
| ProxyScheme::Https { auth, .. }
| ProxyScheme::Socks5 { auth, .. } => auth.as_ref(),
}
}
fn socks5(addr: SocketAddr) -> Result<Self, ProxyError> {
Ok(ProxyScheme::Socks5 {
addr,
auth: None,
remote_dns: false,
})
}
fn http(host: &str) -> Result<Self, ProxyError> {
Ok(ProxyScheme::Http {
auth: None,
host: host.to_string(),
})
}
fn https(host: &str) -> Result<Self, ProxyError> {
Ok(ProxyScheme::Https {
auth: None,
host: host.to_string(),
})
}
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
let auth = Auth {
user_name: username.into(),
password: password.into(),
};
match self {
ProxyScheme::Http { auth: a, .. } => *a = Some(auth),
ProxyScheme::Https { auth: a, .. } => *a = Some(auth),
ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth),
}
}
fn parse(url: Url) -> Result<Self, ProxyError> {
use url::Position;
// Resolve URL to a host and port
let to_addr = || {
let addrs = url.socket_addrs(|| match url.scheme() {
"socks5" => Some(1080),
_ => None,
})?;
addrs
.into_iter()
.next()
.ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost))
};
let mut scheme: Self = match url.scheme() {
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
"socks5" => Self::socks5(to_addr()?)?,
e => return Err(ProxyError::UrlBadScheme(e.to_string())),
};
if let Some(pwd) = url.password() {
let username = url.username();
scheme.set_basic_auth(username, pwd);
}
Ok(scheme)
}
pub async fn socket_addrs(&self) -> Result<SocketAddr, ProxyError> {
info!("Resolving socket address");
match self {
ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await,
ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await,
ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()),
}
}
async fn resolve_host(&self, host: &str, default_port: u16) -> Result<SocketAddr, ProxyError> {
let (host_str, port) = match host.split_once(':') {
Some((h, p)) => (h, p.parse::<u16>().ok()),
None => (host, None),
};
let addr = (host_str, port.unwrap_or(default_port))
.to_socket_addrs()?
.next()
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?;
Ok(addr)
}
pub fn get_domain(&self) -> Result<String, ProxyError> {
match self {
ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => {
let domain = host
.split(':')
.next()
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?;
Ok(domain.to_string())
}
ProxyScheme::Socks5 { addr, .. } => match addr {
SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()),
SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()),
},
}
}
pub fn get_host_and_port(&self) -> Result<String, ProxyError> {
match self {
ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)),
ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)),
ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)),
}
}
fn append_default_port(&self, host: &str, default_port: u16) -> String {
if host.contains(':') {
host.to_string()
} else {
format!("{}:{}", host, default_port)
}
}
}
pub trait IntoProxyScheme {
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError>;
}
impl<S: IntoUrl> IntoProxyScheme for S {
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
// validate the URL
let url = match self.as_str().into_url() {
Ok(ok) => ok,
Err(e) => {
match e {
// If the string does not contain protocol headers, try to parse it using the socks5 protocol
ProxyError::UrlParseScheme(_source) => {
let try_this = format!("socks5://{}", self.as_str());
try_this.into_url()?
}
_ => {
return Err(e);
}
}
}
};
ProxyScheme::parse(url)
}
}
impl IntoProxyScheme for ProxyScheme {
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
Ok(self)
}
}
#[derive(Clone)]
pub struct Proxy {
pub intercept: ProxyScheme,
ms_timeout: u64,
}
impl Proxy {
pub fn new<U: IntoProxyScheme>(proxy_scheme: U, ms_timeout: u64) -> Result<Self, ProxyError> {
Ok(Self {
intercept: proxy_scheme.into_proxy_scheme()?,
ms_timeout,
})
}
pub fn is_http_or_https(&self) -> bool {
return match self.intercept {
ProxyScheme::Socks5 { .. } => false,
_ => true,
};
}
pub fn from_conf(conf: &Socks5Server, ms_timeout: Option<u64>) -> Result<Self, ProxyError> {
let mut proxy;
match ms_timeout {
None => {
proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?;
}
Some(time_out) => {
proxy = Self::new(&conf.proxy, time_out)?;
}
}
if !conf.password.is_empty() && !conf.username.is_empty() {
proxy = proxy.basic_auth(&conf.username, &conf.password);
}
Ok(proxy)
}
pub async fn proxy_addrs(&self) -> Result<SocketAddr, ProxyError> {
self.intercept.socket_addrs().await
}
fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
self.intercept.set_basic_auth(username, password);
self
}
pub async fn connect<'t, T>(
self,
target: T,
local_addr: Option<SocketAddr>,
) -> ResultType<FramedStream>
where
T: IntoTargetAddr<'t>,
{
info!("Connect to proxy server");
let proxy = self.proxy_addrs().await?;
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
};
let stream = super::timeout(
self.ms_timeout,
crate::tcp::new_socket(local, true)?.connect(proxy),
)
.await??;
stream.set_nodelay(true).ok();
let addr = stream.local_addr()?;
return match self.intercept {
ProxyScheme::Http { .. } => {
info!("Connect to remote http proxy server: {}", proxy);
let stream =
super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??;
Ok(FramedStream(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
))
}
ProxyScheme::Https { .. } => {
info!("Connect to remote https proxy server: {}", proxy);
let stream =
super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??;
Ok(FramedStream(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
))
}
ProxyScheme::Socks5 { .. } => {
info!("Connect to remote socket5 proxy server: {}", proxy);
let stream = if let Some(auth) = self.intercept.maybe_auth() {
super::timeout(
self.ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream,
target,
&auth.user_name,
&auth.password,
),
)
.await??
} else {
super::timeout(
self.ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
};
Ok(FramedStream(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
))
}
};
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub async fn https_connect<'a, Input, T>(
self,
io: Input,
target: T,
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
where
Input: AsyncRead + AsyncWrite + Unpin,
T: IntoTargetAddr<'a>,
{
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
let stream = tls_connector
.connect(&self.intercept.get_domain()?, io)
.await?;
self.http_connect(stream, target).await
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
pub async fn https_connect<'a, Input, T>(
self,
io: Input,
target: T,
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
where
Input: AsyncRead + AsyncWrite + Unpin,
T: IntoTargetAddr<'a>,
{
use std::convert::TryFrom;
let verifier = rustls_platform_verifier::tls_config();
let url_domain = self.intercept.get_domain()?;
let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str())
.map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))?
.to_owned();
let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier));
let stream = tls_connector.connect(domain, io).await?;
self.http_connect(stream, target).await
}
pub async fn http_connect<'a, Input, T>(
self,
io: Input,
target: T,
) -> Result<BufStream<Input>, ProxyError>
where
Input: AsyncRead + AsyncWrite + Unpin,
T: IntoTargetAddr<'a>,
{
let mut stream = BufStream::new(io);
let (domain, port) = get_domain_and_port(target)?;
let request = self.make_request(&domain, port);
stream.write_all(request.as_bytes()).await?;
stream.flush().await?;
recv_and_check_response(&mut stream).await?;
Ok(stream)
}
fn make_request(&self, host: &str, port: u16) -> String {
let mut request = format!(
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n",
host = host,
port = port
);
if let Some(auth) = self.intercept.maybe_auth() {
request = format!("{}{}", request, auth.get_proxy_authorization());
}
request.push_str("\r\n");
request
}
}
fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> {
let target_addr = target
.into_target_addr()
.map_err(|e| ProxyError::TargetParseError(e.to_string()))?;
match target_addr {
tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())),
tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)),
}
}
async fn get_response<IO>(stream: &mut BufStream<IO>) -> Result<String, ProxyError>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
use tokio::io::AsyncBufReadExt;
let mut response = String::new();
loop {
if stream.read_line(&mut response).await? == 0 {
return Err(ProxyError::EndOfFile);
}
if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() {
return Err(ProxyError::MaximumResponseHeaderLengthExceeded(
response.len(),
));
}
if response.ends_with("\r\n\r\n") {
return Ok(response);
}
}
}
async fn recv_and_check_response<IO>(stream: &mut BufStream<IO>) -> Result<(), ProxyError>
where
IO: AsyncRead + AsyncWrite + Unpin,
{
let response_string = get_response(stream).await?;
let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS];
let mut response = Response::new(&mut response_headers);
let response_bytes = response_string.into_bytes();
response.parse(&response_bytes)?;
return match response.code {
Some(code) => {
if code == 200 {
Ok(())
} else {
Err(ProxyError::HttpCode200(code))
}
}
None => Err(ProxyError::NoHttpCode),
};
}

View File

@@ -49,19 +49,27 @@ pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
host
}
pub fn test_if_valid_server(host: &str) -> String {
pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String {
let host = check_port(host, 0);
use std::net::ToSocketAddrs;
match Config::get_network_type() {
NetworkType::Direct => match host.to_socket_addrs() {
if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() {
test_if_valid_server_for_proxy_(&host)
} else {
match host.to_socket_addrs() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
},
NetworkType::ProxySocks => match &host.into_target_addr() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
},
}
}
}
#[inline]
pub fn test_if_valid_server_for_proxy_(host: &str) -> String {
// `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing,
// it can be used for both `socks` and `http` proxy.
match &host.into_target_addr() {
Err(err) => err.to_string(),
Ok(_) => "".to_owned(),
}
}
@@ -107,15 +115,7 @@ pub async fn connect_tcp_local<
ms_timeout: u64,
) -> ResultType<FramedStream> {
if let Some(conf) = Config::get_socks() {
return FramedStream::connect(
conf.proxy.as_str(),
target,
local,
conf.username.as_str(),
conf.password.as_str(),
ms_timeout,
)
.await;
return FramedStream::connect(target, local, &conf, ms_timeout).await;
}
if let Some(target) = target.resolve() {
if let Some(local) = local {
@@ -255,10 +255,20 @@ mod tests {
#[test]
fn test_test_if_valid_server() {
assert!(!test_if_valid_server("a").is_empty());
assert!(!test_if_valid_server("a", false).is_empty());
// on Linux, "1" is resolved to "0.0.0.1"
assert!(test_if_valid_server("1.1.1.1").is_empty());
assert!(test_if_valid_server("1.1.1.1:1").is_empty());
assert!(test_if_valid_server("1.1.1.1", false).is_empty());
assert!(test_if_valid_server("1.1.1.1:1", false).is_empty());
assert!(test_if_valid_server("microsoft.com", false).is_empty());
assert!(test_if_valid_server("microsoft.com:1", false).is_empty());
// with proxy
// `:0` indicates `let host = check_port(host, 0);` is called.
assert!(test_if_valid_server_for_proxy_("a:0").is_empty());
assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty());
assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty());
assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty());
assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty());
}
#[test]

View File

@@ -1,4 +1,4 @@
use crate::{bail, bytes_codec::BytesCodec, ResultType};
use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy};
use anyhow::Context as AnyhowCtx;
use bytes::{BufMut, Bytes, BytesMut};
use futures::{SinkExt, StreamExt};
@@ -18,20 +18,20 @@ use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
};
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs};
use tokio_socks::IntoTargetAddr;
use tokio_util::codec::Framed;
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send + Sync>);
pub struct DynTcpStream(pub(crate) Box<dyn TcpStreamTrait + Send + Sync>);
#[derive(Clone)]
pub struct Encrypt(Key, u64, u64);
pub struct FramedStream(
Framed<DynTcpStream, BytesCodec>,
SocketAddr,
Option<Encrypt>,
u64,
pub(crate) Framed<DynTcpStream, BytesCodec>,
pub(crate) SocketAddr,
pub(crate) Option<Encrypt>,
pub(crate) u64,
);
impl Deref for FramedStream {
@@ -62,7 +62,7 @@ impl DerefMut for DynTcpStream {
}
}
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
let socket = match addr {
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
@@ -109,51 +109,17 @@ impl FramedStream {
bail!(format!("Failed to connect to {remote_addr}"));
}
pub async fn connect<'a, 't, P, T>(
proxy: P,
pub async fn connect<'t, T>(
target: T,
local_addr: Option<SocketAddr>,
username: &'a str,
password: &'a str,
proxy_conf: &Socks5Server,
ms_timeout: u64,
) -> ResultType<Self>
where
P: ToProxyAddrs,
T: IntoTargetAddr<'t>,
{
if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
let local = if let Some(addr) = local_addr {
addr
} else {
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
};
let stream =
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
stream.set_nodelay(true).ok();
let stream = if username.trim().is_empty() {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_socket(stream, target),
)
.await??
} else {
super::timeout(
ms_timeout,
Socks5Stream::connect_with_password_and_socket(
stream, target, username, password,
),
)
.await??
};
let addr = stream.local_addr()?;
return Ok(Self(
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
addr,
None,
0,
));
}
bail!("could not resolve to any address");
let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?;
proxy.connect::<T>(target, local_addr).await
}
pub fn local_addr(&self) -> SocketAddr {

View File

@@ -4,6 +4,7 @@ import os
import optparse
from hashlib import md5
import brotli
import datetime
# 4GB maximum
length_count = 4
@@ -22,7 +23,7 @@ def generate_md5_table(folder: str, level) -> dict:
for f in files:
md5_generator = md5()
full_path = os.path.join(root, f)
print(f"processing {full_path}...")
print(f"Processing {full_path}...")
f = open(full_path, "rb")
content = f.read()
content_compressed = brotli.compress(
@@ -34,7 +35,7 @@ def generate_md5_table(folder: str, level) -> dict:
return res
def write_metadata(md5_table: dict, output_folder: str, exe: str):
def write_package_metadata(md5_table: dict, output_folder: str, exe: str):
output_path = os.path.join(output_folder, "data.bin")
with open(output_path, "wb") as f:
f.write("rustdesk".encode(encoding=encoding))
@@ -55,8 +56,13 @@ def write_metadata(md5_table: dict, output_folder: str, exe: str):
f.write("rustdesk".encode(encoding=encoding))
# executable
f.write(exe.encode(encoding='utf-8'))
print(f"metadata had written to {output_path}")
print(f"Metadata has been written to {output_path}")
def write_app_metadata(output_folder: str):
output_path = os.path.join(output_folder, "app_metadata.toml")
with open(output_path, "w") as f:
f.write(f"timestamp = {int(datetime.datetime.now().timestamp() * 1000)}\n")
print(f"App metadata has been written to {output_path}")
def build_portable(output_folder: str, target: str):
os.chdir(output_folder)
@@ -91,11 +97,12 @@ if __name__ == '__main__':
options.executable = folder + '/' + options.executable
exe: str = os.path.abspath(options.executable)
if not exe.startswith(os.path.abspath(folder)):
print("the executable must locate in source folder")
print("The executable must locate in source folder")
exit(-1)
exe = '.' + exe[len(os.path.abspath(folder)):]
print("executable path: " + exe)
print("compression level: " + str(options.level))
print("Executable path: " + exe)
print("Compression level: " + str(options.level))
md5_table = generate_md5_table(folder, options.level)
write_metadata(md5_table, output_folder, exe)
write_package_metadata(md5_table, output_folder, exe)
write_app_metadata(output_folder)
build_portable(output_folder, options.target)

View File

@@ -9,9 +9,52 @@ use bin_reader::BinaryReader;
pub mod bin_reader;
#[cfg(windows)]
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
#[cfg(not(windows))]
const APP_METADATA: &[u8] = &[];
const APP_METADATA_CONFIG: &str = "meta.toml";
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
const APP_PREFIX: &str = "rustdesk";
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
return true;
};
for line in app_metadata.lines() {
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
*ts = stored_ts;
break;
}
}
}
if *ts == 0 {
return true;
}
if let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) {
for line in content.lines() {
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
return *ts == stored_ts;
}
}
}
}
false
}
fn write_meta(dir: &PathBuf, ts: u64) {
let meta_file = dir.join(APP_METADATA_CONFIG);
if ts != 0 {
let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts);
// Ignore is ok here
let _ = std::fs::write(meta_file, content);
}
}
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> {
let dir = if let Some(dir) = dir {
dir
@@ -24,12 +67,15 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
return None;
}
};
if clear {
let mut ts = 0;
if clear || !is_timestamp_matches(&dir, &mut ts) {
std::fs::remove_dir_all(&dir).ok();
}
for file in reader.files.iter() {
file.write_to_file(&dir);
}
write_meta(&dir, ts);
#[cfg(windows)]
windows::copy_runtime_broker(&dir);
#[cfg(linux)]

View File

@@ -13,6 +13,8 @@ edition = "2018"
wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing"]
mediacodec = ["ndk"]
linux-pkg-config = ["dep:pkg-config"]
hwcodec = ["dep:hwcodec"]
vram = ["hwcodec/vram"]
[dependencies]
cfg-if = "1.0"
@@ -20,6 +22,7 @@ num_cpus = "1.15"
lazy_static = "1.4"
hbb_common = { path = "../hbb_common" }
webm = { git = "https://github.com/21pages/rust-webm" }
serde = {version="1.0", features=["derive"]}
[dependencies.winapi]
version = "0.3"
@@ -41,7 +44,6 @@ ndk-context = "0.1"
[target.'cfg(not(target_os = "android"))'.dev-dependencies]
repng = "0.2"
docopt = "1.1"
serde = {version="1.0", features=["derive"]}
quest = "0.3"
[build-dependencies]
@@ -56,8 +58,8 @@ gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true }
gstreamer-video = { version = "0.16", optional = true }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
hwcodec = { git = "https://github.com/21pages/hwcodec", branch = "stable", optional = true }
[dependencies.hwcodec]
git = "https://github.com/21pages/hwcodec"
optional = true
[target.'cfg(target_os = "windows")'.dependencies]
gpucodec = { git = "https://github.com/21pages/gpucodec", optional = true }

View File

@@ -239,22 +239,21 @@ fn test_av1(
#[cfg(feature = "hwcodec")]
mod hw {
use hwcodec::ffmpeg::CodecInfo;
use hwcodec::ffmpeg_ram::CodecInfo;
use scrap::{
hwcodec::{HwDecoder, HwEncoder, HwEncoderConfig},
hwcodec::{HwRamDecoder, HwRamEncoder, HwRamEncoderConfig},
CodecFormat,
};
use super::*;
pub fn test(c: &mut Capturer, width: usize, height: usize, quality: Q, yuv_count: usize) {
let best = HwEncoder::best();
let mut h264s = Vec::new();
let mut h265s = Vec::new();
if let Some(info) = best.h264 {
if let Some(info) = HwRamEncoder::try_get(CodecFormat::H264) {
test_encoder(width, height, quality, info, c, yuv_count, &mut h264s);
}
if let Some(info) = best.h265 {
if let Some(info) = HwRamEncoder::try_get(CodecFormat::H265) {
test_encoder(width, height, quality, info, c, yuv_count, &mut h265s);
}
test_decoder(CodecFormat::H264, &h264s);
@@ -270,8 +269,8 @@ mod hw {
yuv_count: usize,
h26xs: &mut Vec<Vec<u8>>,
) {
let mut encoder = HwEncoder::new(
EncoderCfg::HW(HwEncoderConfig {
let mut encoder = HwRamEncoder::new(
EncoderCfg::HWRAM(HwRamEncoderConfig {
name: info.name.clone(),
width,
height,
@@ -321,7 +320,7 @@ mod hw {
}
fn test_decoder(format: CodecFormat, h26xs: &Vec<Vec<u8>>) {
let mut decoder = HwDecoder::new(format).unwrap();
let mut decoder = HwRamDecoder::new(format).unwrap();
let start = Instant::now();
let mut cnt = 0;
for h26x in h26xs {

View File

@@ -48,6 +48,8 @@ impl FrameRaw {
fn set_enable(&mut self, value: bool) {
self.enable = value;
self.ptr.store(std::ptr::null_mut(), SeqCst);
self.len = 0;
}
fn update(&mut self, data: *mut u8, len: usize) {
@@ -94,7 +96,7 @@ pub fn get_audio_raw<'a>() -> Option<&'a [u8]> {
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpdate(
pub extern "system" fn Java_ffi_FFI_onVideoFrameUpdate(
env: JNIEnv,
_class: JClass,
buffer: JObject,
@@ -108,7 +110,7 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpd
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpdate(
pub extern "system" fn Java_ffi_FFI_onAudioFrameUpdate(
env: JNIEnv,
_class: JClass,
buffer: JObject,
@@ -122,7 +124,7 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpd
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_setFrameRawEnable(
pub extern "system" fn Java_ffi_FFI_setFrameRawEnable(
env: JNIEnv,
_class: JClass,
name: JString,
@@ -141,11 +143,7 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_setFrameRawEnab
}
#[no_mangle]
pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init(
env: JNIEnv,
_class: JClass,
ctx: JObject,
) {
pub extern "system" fn Java_ffi_FFI_init(env: JNIEnv, _class: JClass, ctx: JObject) {
log::debug!("MainService init from java");
if let Ok(jvm) = env.get_java_vm() {
*JVM.write().unwrap() = Some(jvm);

View File

@@ -268,7 +268,7 @@ impl EncoderApi for AomEncoder {
self.yuvfmt.clone()
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}

View File

@@ -5,17 +5,17 @@ use std::{
sync::{Arc, Mutex},
};
#[cfg(feature = "gpucodec")]
use crate::gpucodec::*;
#[cfg(feature = "hwcodec")]
use crate::hwcodec::*;
#[cfg(feature = "mediacodec")]
use crate::mediacodec::{MediaCodecDecoder, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT};
#[cfg(feature = "vram")]
use crate::vram::*;
use crate::{
aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
common::GoogleImage,
vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
CodecFormat, CodecName, EncodeInput, EncodeYuvFormat, ImageRgb,
CodecFormat, EncodeInput, EncodeYuvFormat, ImageRgb,
};
use hbb_common::{
@@ -31,13 +31,14 @@ use hbb_common::{
tokio::time::Instant,
ResultType,
};
#[cfg(any(feature = "hwcodec", feature = "mediacodec", feature = "gpucodec"))]
#[cfg(any(feature = "hwcodec", feature = "mediacodec", feature = "vram"))]
use hbb_common::{config::Config2, lazy_static};
lazy_static::lazy_static! {
static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
static ref ENCODE_CODEC_NAME: Arc<Mutex<CodecName>> = Arc::new(Mutex::new(CodecName::VP9));
static ref ENCODE_CODEC_FORMAT: Arc<Mutex<CodecFormat>> = Arc::new(Mutex::new(CodecFormat::VP9));
static ref THREAD_LOG_TIME: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
static ref USABLE_ENCODING: Arc<Mutex<Option<SupportedEncoding>>> = Arc::new(Mutex::new(None));
}
pub const ENCODE_NEED_SWITCH: &'static str = "ENCODE_NEED_SWITCH";
@@ -47,9 +48,9 @@ pub enum EncoderCfg {
VPX(VpxEncoderConfig),
AOM(AomEncoderConfig),
#[cfg(feature = "hwcodec")]
HW(HwEncoderConfig),
#[cfg(feature = "gpucodec")]
GPU(GpuEncoderConfig),
HWRAM(HwRamEncoderConfig),
#[cfg(feature = "vram")]
VRAM(VRamEncoderConfig),
}
pub trait EncoderApi {
@@ -61,7 +62,7 @@ pub trait EncoderApi {
fn yuvfmt(&self) -> EncodeYuvFormat;
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool;
fn set_quality(&mut self, quality: Quality) -> ResultType<()>;
@@ -94,13 +95,13 @@ pub struct Decoder {
vp9: Option<VpxDecoder>,
av1: Option<AomDecoder>,
#[cfg(feature = "hwcodec")]
h264_ram: Option<HwDecoder>,
h264_ram: Option<HwRamDecoder>,
#[cfg(feature = "hwcodec")]
h265_ram: Option<HwDecoder>,
#[cfg(feature = "gpucodec")]
h264_vram: Option<GpuDecoder>,
#[cfg(feature = "gpucodec")]
h265_vram: Option<GpuDecoder>,
h265_ram: Option<HwRamDecoder>,
#[cfg(feature = "vram")]
h264_vram: Option<VRamDecoder>,
#[cfg(feature = "vram")]
h265_vram: Option<VRamDecoder>,
#[cfg(feature = "mediacodec")]
h264_media_codec: MediaCodecDecoder,
#[cfg(feature = "mediacodec")]
@@ -131,26 +132,28 @@ impl Encoder {
}),
#[cfg(feature = "hwcodec")]
EncoderCfg::HW(_) => match HwEncoder::new(config, i444) {
EncoderCfg::HWRAM(_) => match HwRamEncoder::new(config, i444) {
Ok(hw) => Ok(Encoder {
codec: Box::new(hw),
}),
Err(e) => {
log::error!("new hw encoder failed: {e:?}, clear config");
hbb_common::config::HwCodecConfig::clear();
*ENCODE_CODEC_NAME.lock().unwrap() = CodecName::VP9;
hbb_common::config::HwCodecConfig::clear_ram();
Self::update(EncodingUpdate::Check);
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
Err(e)
}
},
#[cfg(feature = "gpucodec")]
EncoderCfg::GPU(_) => match GpuEncoder::new(config, i444) {
#[cfg(feature = "vram")]
EncoderCfg::VRAM(_) => match VRamEncoder::new(config, i444) {
Ok(tex) => Ok(Encoder {
codec: Box::new(tex),
}),
Err(e) => {
log::error!("new gpu encoder failed: {e:?}, clear config");
hbb_common::config::GpucodecConfig::clear();
*ENCODE_CODEC_NAME.lock().unwrap() = CodecName::VP9;
log::error!("new vram encoder failed: {e:?}, clear config");
hbb_common::config::HwCodecConfig::clear_vram();
Self::update(EncodingUpdate::Check);
*ENCODE_CODEC_FORMAT.lock().unwrap() = CodecFormat::VP9;
Err(e)
}
},
@@ -180,48 +183,50 @@ impl Encoder {
}
let vp8_useable = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_vp8 > 0);
let av1_useable = decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_av1 > 0);
let av1_useable = decodings.len() > 0
&& decodings.iter().all(|(_, s)| s.ability_av1 > 0)
&& !disable_av1();
let _all_support_h264_decoding =
decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h264 > 0);
let _all_support_h265_decoding =
decodings.len() > 0 && decodings.iter().all(|(_, s)| s.ability_h265 > 0);
#[allow(unused_mut)]
let mut h264gpu_encoding = false;
let mut h264vram_encoding = false;
#[allow(unused_mut)]
let mut h265gpu_encoding = false;
#[cfg(feature = "gpucodec")]
if enable_gpucodec_option() {
let mut h265vram_encoding = false;
#[cfg(feature = "vram")]
if enable_vram_option() {
if _all_support_h264_decoding {
if GpuEncoder::available(CodecName::H264GPU).len() > 0 {
h264gpu_encoding = true;
if VRamEncoder::available(CodecFormat::H264).len() > 0 {
h264vram_encoding = true;
}
}
if _all_support_h265_decoding {
if GpuEncoder::available(CodecName::H265GPU).len() > 0 {
h265gpu_encoding = true;
if VRamEncoder::available(CodecFormat::H265).len() > 0 {
h265vram_encoding = true;
}
}
}
#[allow(unused_mut)]
let mut h264hw_encoding = None;
let mut h264hw_encoding: Option<String> = None;
#[allow(unused_mut)]
let mut h265hw_encoding = None;
let mut h265hw_encoding: Option<String> = None;
#[cfg(feature = "hwcodec")]
if enable_hwcodec_option() {
let best = HwEncoder::best();
if _all_support_h264_decoding {
h264hw_encoding = best.h264.map_or(None, |c| Some(c.name));
h264hw_encoding =
HwRamEncoder::try_get(CodecFormat::H264).map_or(None, |c| Some(c.name));
}
if _all_support_h265_decoding {
h265hw_encoding = best.h265.map_or(None, |c| Some(c.name));
h265hw_encoding =
HwRamEncoder::try_get(CodecFormat::H265).map_or(None, |c| Some(c.name));
}
}
let h264_useable =
_all_support_h264_decoding && (h264gpu_encoding || h264hw_encoding.is_some());
_all_support_h264_decoding && (h264vram_encoding || h264hw_encoding.is_some());
let h265_useable =
_all_support_h265_decoding && (h265gpu_encoding || h265hw_encoding.is_some());
let mut name = ENCODE_CODEC_NAME.lock().unwrap();
let mut preference = PreferCodec::Auto;
_all_support_h265_decoding && (h265vram_encoding || h265hw_encoding.is_some());
let mut format = ENCODE_CODEC_FORMAT.lock().unwrap();
let preferences: Vec<_> = decodings
.iter()
.filter(|(_, s)| {
@@ -233,40 +238,51 @@ impl Encoder {
})
.map(|(_, s)| s.prefer)
.collect();
if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) {
preference = preferences[0].enum_value_or(PreferCodec::Auto);
*USABLE_ENCODING.lock().unwrap() = Some(SupportedEncoding {
vp8: vp8_useable,
av1: av1_useable,
h264: h264_useable,
h265: h265_useable,
..Default::default()
});
// find the most frequent preference
let mut counts = Vec::new();
for pref in &preferences {
match counts.iter_mut().find(|(p, _)| p == pref) {
Some((_, count)) => *count += 1,
None => counts.push((pref.clone(), 1)),
}
}
let max_count = counts.iter().map(|(_, count)| *count).max().unwrap_or(0);
let (most_frequent, _) = counts
.into_iter()
.find(|(_, count)| *count == max_count)
.unwrap_or((PreferCodec::Auto.into(), 0));
let preference = most_frequent.enum_value_or(PreferCodec::Auto);
#[allow(unused_mut)]
let mut auto_codec = CodecName::VP9;
if av1_useable {
auto_codec = CodecName::AV1;
}
let mut auto_codec = CodecFormat::VP9;
let mut system = System::new();
system.refresh_memory();
if vp8_useable && system.total_memory() <= 4 * 1024 * 1024 * 1024 {
// 4 Gb
auto_codec = CodecName::VP8
auto_codec = CodecFormat::VP8
}
*name = match preference {
PreferCodec::VP8 => CodecName::VP8,
PreferCodec::VP9 => CodecName::VP9,
PreferCodec::AV1 => CodecName::AV1,
*format = match preference {
PreferCodec::VP8 => CodecFormat::VP8,
PreferCodec::VP9 => CodecFormat::VP9,
PreferCodec::AV1 => CodecFormat::AV1,
PreferCodec::H264 => {
if h264gpu_encoding {
CodecName::H264GPU
} else if let Some(v) = h264hw_encoding {
CodecName::H264HW(v)
if h264vram_encoding || h264hw_encoding.is_some() {
CodecFormat::H264
} else {
auto_codec
}
}
PreferCodec::H265 => {
if h265gpu_encoding {
CodecName::H265GPU
} else if let Some(v) = h265hw_encoding {
CodecName::H265HW(v)
if h265vram_encoding || h265hw_encoding.is_some() {
CodecFormat::H265
} else {
auto_codec
}
@@ -281,21 +297,21 @@ impl Encoder {
"connection count: {}, used preference: {:?}, encoder: {:?}",
decodings.len(),
preference,
*name
*format
)
}
}
#[inline]
pub fn negotiated_codec() -> CodecName {
ENCODE_CODEC_NAME.lock().unwrap().clone()
pub fn negotiated_codec() -> CodecFormat {
ENCODE_CODEC_FORMAT.lock().unwrap().clone()
}
pub fn supported_encoding() -> SupportedEncoding {
#[allow(unused_mut)]
let mut encoding = SupportedEncoding {
vp8: true,
av1: true,
av1: !disable_av1(),
i444: Some(CodecAbility {
vp9: true,
av1: true,
@@ -306,50 +322,53 @@ impl Encoder {
};
#[cfg(feature = "hwcodec")]
if enable_hwcodec_option() {
let best = HwEncoder::best();
encoding.h264 |= best.h264.is_some();
encoding.h265 |= best.h265.is_some();
encoding.h264 |= HwRamEncoder::try_get(CodecFormat::H264).is_some();
encoding.h265 |= HwRamEncoder::try_get(CodecFormat::H265).is_some();
}
#[cfg(feature = "gpucodec")]
if enable_gpucodec_option() {
encoding.h264 |= GpuEncoder::available(CodecName::H264GPU).len() > 0;
encoding.h265 |= GpuEncoder::available(CodecName::H265GPU).len() > 0;
#[cfg(feature = "vram")]
if enable_vram_option() {
encoding.h264 |= VRamEncoder::available(CodecFormat::H264).len() > 0;
encoding.h265 |= VRamEncoder::available(CodecFormat::H265).len() > 0;
}
encoding
}
pub fn usable_encoding() -> Option<SupportedEncoding> {
USABLE_ENCODING.lock().unwrap().clone()
}
pub fn set_fallback(config: &EncoderCfg) {
let name = match config {
let format = match config {
EncoderCfg::VPX(vpx) => match vpx.codec {
VpxVideoCodecId::VP8 => CodecName::VP8,
VpxVideoCodecId::VP9 => CodecName::VP9,
VpxVideoCodecId::VP8 => CodecFormat::VP8,
VpxVideoCodecId::VP9 => CodecFormat::VP9,
},
EncoderCfg::AOM(_) => CodecName::AV1,
EncoderCfg::AOM(_) => CodecFormat::AV1,
#[cfg(feature = "hwcodec")]
EncoderCfg::HW(hw) => {
EncoderCfg::HWRAM(hw) => {
if hw.name.to_lowercase().contains("h264") {
CodecName::H264HW(hw.name.clone())
CodecFormat::H264
} else {
CodecName::H265HW(hw.name.clone())
CodecFormat::H265
}
}
#[cfg(feature = "gpucodec")]
EncoderCfg::GPU(gpu) => match gpu.feature.data_format {
gpucodec::gpu_common::DataFormat::H264 => CodecName::H264GPU,
gpucodec::gpu_common::DataFormat::H265 => CodecName::H265GPU,
#[cfg(feature = "vram")]
EncoderCfg::VRAM(vram) => match vram.feature.data_format {
hwcodec::common::DataFormat::H264 => CodecFormat::H264,
hwcodec::common::DataFormat::H265 => CodecFormat::H265,
_ => {
log::error!(
"should not reach here, gpucodec not support {:?}",
gpu.feature.data_format
"should not reach here, vram not support {:?}",
vram.feature.data_format
);
return;
}
},
};
let current = ENCODE_CODEC_NAME.lock().unwrap().clone();
if current != name {
log::info!("codec fallback: {:?} -> {:?}", current, name);
*ENCODE_CODEC_NAME.lock().unwrap() = name;
let current = ENCODE_CODEC_FORMAT.lock().unwrap().clone();
if current != format {
log::info!("codec fallback: {:?} -> {:?}", current, format);
*ENCODE_CODEC_FORMAT.lock().unwrap() = format;
}
}
@@ -365,9 +384,9 @@ impl Encoder {
},
EncoderCfg::AOM(_) => decodings.iter().all(|d| d.1.i444.av1),
#[cfg(feature = "hwcodec")]
EncoderCfg::HW(_) => false,
#[cfg(feature = "gpucodec")]
EncoderCfg::GPU(_) => false,
EncoderCfg::HWRAM(_) => false,
#[cfg(feature = "vram")]
EncoderCfg::VRAM(_) => false,
};
prefer_i444 && i444_useable && !decodings.is_empty()
}
@@ -386,7 +405,7 @@ impl Decoder {
let mut decoding = SupportedDecoding {
ability_vp8: 1,
ability_vp9: 1,
ability_av1: 1,
ability_av1: if disable_av1() { 0 } else { 1 },
i444: Some(CodecAbility {
vp9: true,
av1: true,
@@ -398,19 +417,26 @@ impl Decoder {
..Default::default()
};
#[cfg(feature = "hwcodec")]
if enable_hwcodec_option() {
let best = HwDecoder::best();
decoding.ability_h264 |= if best.h264.is_some() { 1 } else { 0 };
decoding.ability_h265 |= if best.h265.is_some() { 1 } else { 0 };
}
#[cfg(feature = "gpucodec")]
if enable_gpucodec_option() && _flutter {
decoding.ability_h264 |= if GpuDecoder::available(CodecFormat::H264, _luid).len() > 0 {
{
decoding.ability_h264 |= if HwRamDecoder::try_get(CodecFormat::H264).is_some() {
1
} else {
0
};
decoding.ability_h265 |= if GpuDecoder::available(CodecFormat::H265, _luid).len() > 0 {
decoding.ability_h265 |= if HwRamDecoder::try_get(CodecFormat::H265).is_some() {
1
} else {
0
};
}
#[cfg(feature = "vram")]
if enable_vram_option() && _flutter {
decoding.ability_h264 |= if VRamDecoder::available(CodecFormat::H264, _luid).len() > 0 {
1
} else {
0
};
decoding.ability_h265 |= if VRamDecoder::available(CodecFormat::H265, _luid).len() > 0 {
1
} else {
0
@@ -449,7 +475,7 @@ impl Decoder {
let (mut vp8, mut vp9, mut av1) = (None, None, None);
#[cfg(feature = "hwcodec")]
let (mut h264_ram, mut h265_ram) = (None, None);
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
let (mut h264_vram, mut h265_vram) = (None, None);
#[cfg(feature = "mediacodec")]
let (mut h264_media_codec, mut h265_media_codec) = (None, None);
@@ -482,17 +508,17 @@ impl Decoder {
valid = av1.is_some();
}
CodecFormat::H264 => {
#[cfg(feature = "gpucodec")]
if !valid && enable_gpucodec_option() && _luid.clone().unwrap_or_default() != 0 {
match GpuDecoder::new(format, _luid) {
#[cfg(feature = "vram")]
if !valid && enable_vram_option() && _luid.clone().unwrap_or_default() != 0 {
match VRamDecoder::new(format, _luid) {
Ok(v) => h264_vram = Some(v),
Err(e) => log::error!("create H264 vram decoder failed: {}", e),
}
valid = h264_vram.is_some();
}
#[cfg(feature = "hwcodec")]
if !valid && enable_hwcodec_option() {
match HwDecoder::new(format) {
if !valid {
match HwRamDecoder::new(format) {
Ok(v) => h264_ram = Some(v),
Err(e) => log::error!("create H264 ram decoder failed: {}", e),
}
@@ -508,17 +534,17 @@ impl Decoder {
}
}
CodecFormat::H265 => {
#[cfg(feature = "gpucodec")]
if !valid && enable_gpucodec_option() && _luid.clone().unwrap_or_default() != 0 {
match GpuDecoder::new(format, _luid) {
#[cfg(feature = "vram")]
if !valid && enable_vram_option() && _luid.clone().unwrap_or_default() != 0 {
match VRamDecoder::new(format, _luid) {
Ok(v) => h265_vram = Some(v),
Err(e) => log::error!("create H265 vram decoder failed: {}", e),
}
valid = h265_vram.is_some();
}
#[cfg(feature = "hwcodec")]
if !valid && enable_hwcodec_option() {
match HwDecoder::new(format) {
if !valid {
match HwRamDecoder::new(format) {
Ok(v) => h265_ram = Some(v),
Err(e) => log::error!("create H265 ram decoder failed: {}", e),
}
@@ -550,9 +576,9 @@ impl Decoder {
h264_ram,
#[cfg(feature = "hwcodec")]
h265_ram,
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
h264_vram,
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
h265_vram,
#[cfg(feature = "mediacodec")]
h264_media_codec,
@@ -604,31 +630,31 @@ impl Decoder {
bail!("av1 decoder not available");
}
}
#[cfg(any(feature = "hwcodec", feature = "gpucodec"))]
#[cfg(any(feature = "hwcodec", feature = "vram"))]
video_frame::Union::H264s(h264s) => {
*chroma = Some(Chroma::I420);
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
if let Some(decoder) = &mut self.h264_vram {
*_pixelbuffer = false;
return Decoder::handle_gpu_video_frame(decoder, h264s, _texture);
return Decoder::handle_vram_video_frame(decoder, h264s, _texture);
}
#[cfg(feature = "hwcodec")]
if let Some(decoder) = &mut self.h264_ram {
return Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420);
return Decoder::handle_hwram_video_frame(decoder, h264s, rgb, &mut self.i420);
}
Err(anyhow!("don't support h264!"))
}
#[cfg(any(feature = "hwcodec", feature = "gpucodec"))]
#[cfg(any(feature = "hwcodec", feature = "vram"))]
video_frame::Union::H265s(h265s) => {
*chroma = Some(Chroma::I420);
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
if let Some(decoder) = &mut self.h265_vram {
*_pixelbuffer = false;
return Decoder::handle_gpu_video_frame(decoder, h265s, _texture);
return Decoder::handle_vram_video_frame(decoder, h265s, _texture);
}
#[cfg(feature = "hwcodec")]
if let Some(decoder) = &mut self.h265_ram {
return Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420);
return Decoder::handle_hwram_video_frame(decoder, h265s, rgb, &mut self.i420);
}
Err(anyhow!("don't support h265!"))
}
@@ -710,8 +736,8 @@ impl Decoder {
// rgb [in/out] fmt and stride must be set in ImageRgb
#[cfg(feature = "hwcodec")]
fn handle_hw_video_frame(
decoder: &mut HwDecoder,
fn handle_hwram_video_frame(
decoder: &mut HwRamDecoder,
frames: &EncodedVideoFrames,
rgb: &mut ImageRgb,
i420: &mut Vec<u8>,
@@ -728,9 +754,9 @@ impl Decoder {
return Ok(ret);
}
#[cfg(feature = "gpucodec")]
fn handle_gpu_video_frame(
decoder: &mut GpuDecoder,
#[cfg(feature = "vram")]
fn handle_vram_video_frame(
decoder: &mut VRamDecoder,
frames: &EncodedVideoFrames,
texture: &mut *mut c_void,
) -> ResultType<bool> {
@@ -791,13 +817,16 @@ impl Decoder {
#[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
pub fn enable_hwcodec_option() -> bool {
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
return v != "N";
if cfg!(windows) || cfg!(target_os = "linux") || cfg!(feature = "mediacodec") {
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
return v != "N";
}
return true; // default is true
}
return true; // default is true
false
}
#[cfg(feature = "gpucodec")]
pub fn enable_gpucodec_option() -> bool {
#[cfg(feature = "vram")]
pub fn enable_vram_option() -> bool {
if let Some(v) = Config2::get().options.get("enable-hwcodec") {
return v != "N";
}
@@ -893,3 +922,9 @@ pub fn codec_thread_num(limit: usize) -> usize {
}
res
}
fn disable_av1() -> bool {
// aom is very slow for x86 sciter version on windows x64
// disable it for all 32 bit platforms
std::mem::size_of::<usize>() == 4
}

View File

@@ -18,7 +18,7 @@ pub mod hw {
use super::*;
use crate::ImageFormat;
#[cfg(target_os = "windows")]
use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat};
use hwcodec::{ffmpeg::AVPixelFormat, ffmpeg_ram::ffmpeg_linesize_offset_length};
#[cfg(target_os = "windows")]
pub fn hw_nv12_to(
@@ -222,9 +222,7 @@ pub fn convert_to_yuv(
);
}
}
let align = |x:usize| {
(x + 63) / 64 * 64
};
let align = |x: usize| (x + 63) / 64 * 64;
match (src_pixfmt, dst_fmt.pixfmt) {
(crate::Pixfmt::BGRA, crate::Pixfmt::I420) | (crate::Pixfmt::RGBA, crate::Pixfmt::I420) => {
@@ -282,7 +280,8 @@ pub fn convert_to_yuv(
let dst_stride_u = dst_fmt.stride[1];
let dst_stride_v = dst_fmt.stride[2];
dst.resize(
align(dst_fmt.h) * (align(dst_stride_y) + align(dst_stride_u) + align(dst_stride_v)),
align(dst_fmt.h)
* (align(dst_stride_y) + align(dst_stride_u) + align(dst_stride_v)),
0,
);
let dst_y = dst.as_mut_ptr();

View File

@@ -1,4 +1,4 @@
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
use crate::AdapterDevice;
use crate::{common::TraitCapturer, dxgi, Frame, Pixfmt};
use std::{
@@ -57,12 +57,12 @@ impl TraitCapturer for Capturer {
self.inner.set_gdi()
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn device(&self) -> AdapterDevice {
self.inner.device()
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn set_output_texture(&mut self, texture: bool) {
self.inner.set_output_texture(texture);
}
@@ -197,7 +197,7 @@ impl Display {
self.origin() == (0, 0)
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
pub fn adapter_luid(&self) -> Option<i64> {
self.0.adapter_luid()
}
@@ -247,11 +247,11 @@ impl TraitCapturer for CapturerMag {
false
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn device(&self) -> AdapterDevice {
AdapterDevice::default()
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn set_output_texture(&mut self, _texture: bool) {}
}

View File

@@ -1,28 +1,30 @@
use crate::{
codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg, Quality as Q},
codec::{
base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q,
},
hw, CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN,
};
use hbb_common::{
allow_err,
anyhow::{anyhow, bail, Context},
bytes::Bytes,
config::HwCodecConfig,
log,
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
ResultType,
serde_derive::{Deserialize, Serialize},
serde_json, ResultType,
};
use hwcodec::{
decode::{DecodeContext, DecodeFrame, Decoder},
encode::{EncodeContext, EncodeFrame, Encoder},
ffmpeg::{CodecInfo, CodecInfos, DataFormat},
AVPixelFormat,
Quality::{self, *},
RateControl::{self, *},
common::DataFormat,
ffmpeg::AVPixelFormat,
ffmpeg_ram::{
decode::{DecodeContext, DecodeFrame, Decoder},
encode::{EncodeContext, EncodeFrame, Encoder},
CodecInfo,
Quality::{self, *},
RateControl::{self, *},
},
};
const CFG_KEY_ENCODER: &str = "bestHwEncoders";
const CFG_KEY_DECODER: &str = "bestHwDecoders";
const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_NV12;
pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30];
const DEFAULT_GOP: i32 = i32::MAX;
@@ -30,7 +32,7 @@ const DEFAULT_HW_QUALITY: Quality = Quality_Default;
const DEFAULT_RC: RateControl = RC_DEFAULT;
#[derive(Debug, Clone)]
pub struct HwEncoderConfig {
pub struct HwRamEncoderConfig {
pub name: String,
pub width: usize,
pub height: usize,
@@ -38,7 +40,7 @@ pub struct HwEncoderConfig {
pub keyframe_interval: Option<usize>,
}
pub struct HwEncoder {
pub struct HwRamEncoder {
encoder: Encoder,
name: String,
pub format: DataFormat,
@@ -48,13 +50,13 @@ pub struct HwEncoder {
bitrate: u32, //kbs
}
impl EncoderApi for HwEncoder {
impl EncoderApi for HwRamEncoder {
fn new(cfg: EncoderCfg, _i444: bool) -> ResultType<Self>
where
Self: Sized,
{
match cfg {
EncoderCfg::HW(config) => {
EncoderCfg::HWRAM(config) => {
let b = Self::convert_quality(config.quality);
let base_bitrate = base_bitrate(config.width as _, config.height as _);
let mut bitrate = base_bitrate * b / 100;
@@ -85,7 +87,7 @@ impl EncoderApi for HwEncoder {
}
};
match Encoder::new(ctx.clone()) {
Ok(encoder) => Ok(HwEncoder {
Ok(encoder) => Ok(HwRamEncoder {
encoder,
name: config.name,
format,
@@ -94,10 +96,7 @@ impl EncoderApi for HwEncoder {
height: ctx.height as _,
bitrate,
}),
Err(_) => {
HwCodecConfig::clear();
Err(anyhow!(format!("Failed to create encoder")))
}
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
}
}
_ => Err(anyhow!("encoder type mismatch")),
@@ -126,6 +125,7 @@ impl EncoderApi for HwEncoder {
match self.format {
DataFormat::H264 => vf.set_h264s(frames),
DataFormat::H265 => vf.set_h265s(frames),
_ => bail!("unsupported format: {:?}", self.format),
}
Ok(vf)
} else {
@@ -160,7 +160,7 @@ impl EncoderApi for HwEncoder {
}
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}
@@ -180,16 +180,30 @@ impl EncoderApi for HwEncoder {
}
fn support_abr(&self) -> bool {
!self.name.contains("qsv")
["qsv", "vaapi"].iter().all(|&x| !self.name.contains(x))
}
}
impl HwEncoder {
pub fn best() -> CodecInfos {
get_config(CFG_KEY_ENCODER).unwrap_or(CodecInfos {
h264: None,
h265: None,
})
impl HwRamEncoder {
pub fn try_get(format: CodecFormat) -> Option<CodecInfo> {
let mut info = None;
if let Ok(hw) = get_config().map(|c| c.e) {
let best = CodecInfo::prioritized(hw);
match format {
CodecFormat::H264 => {
if let Some(v) = best.h264 {
info = Some(v);
}
}
CodecFormat::H265 => {
if let Some(v) = best.h265 {
info = Some(v);
}
}
_ => {}
}
}
info
}
pub fn encode(&mut self, yuv: &[u8]) -> ResultType<Vec<EncodeFrame>> {
@@ -214,44 +228,54 @@ impl HwEncoder {
}
}
pub struct HwDecoder {
pub struct HwRamDecoder {
decoder: Decoder,
pub info: CodecInfo,
}
#[derive(Default)]
pub struct HwDecoders {
pub h264: Option<HwDecoder>,
pub h265: Option<HwDecoder>,
}
impl HwDecoder {
pub fn best() -> CodecInfos {
get_config(CFG_KEY_DECODER).unwrap_or(CodecInfos {
h264: None,
h265: None,
})
}
pub fn new(format: CodecFormat) -> ResultType<Self> {
log::info!("try create {format:?} ram decoder");
let best = HwDecoder::best();
let info = match format {
impl HwRamDecoder {
pub fn try_get(format: CodecFormat) -> Option<CodecInfo> {
let mut info = None;
let soft = CodecInfo::soft();
match format {
CodecFormat::H264 => {
if let Some(info) = best.h264 {
info
} else {
bail!("no h264 decoder, should not be here");
if let Some(v) = soft.h264 {
info = Some(v);
}
}
CodecFormat::H265 => {
if let Some(info) = best.h265 {
info
} else {
bail!("no h265 decoder, should not be here");
if let Some(v) = soft.h265 {
info = Some(v);
}
}
_ => bail!("unsupported format: {:?}", format),
_ => {}
}
if enable_hwcodec_option() {
if let Ok(hw) = get_config().map(|c| c.d) {
let best = CodecInfo::prioritized(hw);
match format {
CodecFormat::H264 => {
if let Some(v) = best.h264 {
info = Some(v);
}
}
CodecFormat::H265 => {
if let Some(v) = best.h265 {
info = Some(v);
}
}
_ => {}
}
}
}
info
}
pub fn new(format: CodecFormat) -> ResultType<Self> {
let info = HwRamDecoder::try_get(format);
log::info!("try create {info:?} ram decoder");
let Some(info) = info else {
bail!("unsupported format: {:?}", format);
};
let ctx = DecodeContext {
name: info.name.clone(),
@@ -259,26 +283,26 @@ impl HwDecoder {
thread_count: codec_thread_num(16) as _,
};
match Decoder::new(ctx) {
Ok(decoder) => Ok(HwDecoder { decoder, info }),
Ok(decoder) => Ok(HwRamDecoder { decoder, info }),
Err(_) => {
HwCodecConfig::clear();
HwCodecConfig::clear_ram();
Err(anyhow!(format!("Failed to create decoder")))
}
}
}
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<HwDecoderImage>> {
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<HwRamDecoderImage>> {
match self.decoder.decode(data) {
Ok(v) => Ok(v.iter().map(|f| HwDecoderImage { frame: f }).collect()),
Ok(v) => Ok(v.iter().map(|f| HwRamDecoderImage { frame: f }).collect()),
Err(e) => Err(anyhow!(e)),
}
}
}
pub struct HwDecoderImage<'a> {
pub struct HwRamDecoderImage<'a> {
frame: &'a DecodeFrame,
}
impl HwDecoderImage<'_> {
impl HwRamDecoderImage<'_> {
// rgb [in/out] fmt and stride must be set in ImageRgb
pub fn to_fmt(&self, rgb: &mut ImageRgb, i420: &mut Vec<u8>) -> ResultType<()> {
let frame = self.frame;
@@ -332,23 +356,24 @@ impl HwDecoderImage<'_> {
}
}
fn get_config(k: &str) -> ResultType<CodecInfos> {
let v = HwCodecConfig::load()
.options
.get(k)
.unwrap_or(&"".to_owned())
.to_owned();
match CodecInfos::deserialize(&v) {
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
struct Available {
e: Vec<CodecInfo>,
d: Vec<CodecInfo>,
}
fn get_config() -> ResultType<Available> {
match serde_json::from_str(&HwCodecConfig::load().ram) {
Ok(v) => Ok(v),
Err(_) => Err(anyhow!("Failed to get config:{}", k)),
Err(e) => Err(anyhow!("Failed to get config:{e:?}")),
}
}
pub fn check_available_hwcodec() {
let ctx = EncodeContext {
name: String::from(""),
width: 1920,
height: 1080,
width: 1280,
height: 720,
pixfmt: DEFAULT_PIXFMT,
align: HW_STRIDE_ALIGN as _,
bitrate: 0,
@@ -358,30 +383,25 @@ pub fn check_available_hwcodec() {
rc: DEFAULT_RC,
thread_count: 4,
};
let encoders = CodecInfo::score(Encoder::available_encoders(ctx));
let decoders = CodecInfo::score(Decoder::available_decoders());
if let Ok(old_encoders) = get_config(CFG_KEY_ENCODER) {
if let Ok(old_decoders) = get_config(CFG_KEY_DECODER) {
if encoders == old_encoders && decoders == old_decoders {
return;
}
}
#[cfg(feature = "vram")]
let vram = crate::vram::check_available_vram();
#[cfg(not(feature = "vram"))]
let vram = "".to_owned();
let ram = Available {
e: Encoder::available_encoders(ctx, Some(vram.clone())),
d: Decoder::available_decoders(Some(vram.clone())),
};
if let Ok(ram) = serde_json::to_string_pretty(&ram) {
HwCodecConfig { ram, vram }.store();
}
if let Ok(encoders) = encoders.serialize() {
if let Ok(decoders) = decoders.serialize() {
let mut config = HwCodecConfig::load();
config.options.insert(CFG_KEY_ENCODER.to_owned(), encoders);
config.options.insert(CFG_KEY_DECODER.to_owned(), decoders);
config.store();
return;
}
}
log::error!("Failed to serialize codec info");
}
pub fn hwcodec_new_check_process() {
#[cfg(any(target_os = "windows", target_os = "linux"))]
pub fn start_check_process(force: bool) {
if !force && !enable_hwcodec_option() {
return;
}
use hbb_common::allow_err;
use std::sync::Once;
let f = || {
// Clear to avoid checking process errors
@@ -421,7 +441,11 @@ pub fn hwcodec_new_check_process() {
};
};
static ONCE: Once = Once::new();
ONCE.call_once(|| {
if force && ONCE.is_completed() {
std::thread::spawn(f);
});
} else {
ONCE.call_once(|| {
std::thread::spawn(f);
});
}
}

View File

@@ -37,13 +37,13 @@ cfg_if! {
pub mod codec;
pub mod convert;
#[cfg(feature = "gpucodec")]
pub mod gpucodec;
#[cfg(feature = "hwcodec")]
pub mod hwcodec;
#[cfg(feature = "mediacodec")]
pub mod mediacodec;
pub mod vpxcodec;
#[cfg(feature = "vram")]
pub mod vram;
pub use self::convert::*;
pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc caller
pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer
@@ -111,10 +111,10 @@ pub trait TraitCapturer {
#[cfg(windows)]
fn set_gdi(&mut self) -> bool;
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn device(&self) -> AdapterDevice;
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn set_output_texture(&mut self, texture: bool);
}
@@ -245,10 +245,10 @@ pub enum CodecName {
VP8,
VP9,
AV1,
H264HW(String),
H265HW(String),
H264GPU,
H265GPU,
H264RAM(String),
H265RAM(String),
H264VRAM,
H265VRAM,
}
#[derive(PartialEq, Debug, Clone, Copy)]
@@ -280,8 +280,8 @@ impl From<&CodecName> for CodecFormat {
CodecName::VP8 => Self::VP8,
CodecName::VP9 => Self::VP9,
CodecName::AV1 => Self::AV1,
CodecName::H264HW(_) | CodecName::H264GPU => Self::H264,
CodecName::H265HW(_) | CodecName::H265GPU => Self::H265,
CodecName::H264RAM(_) | CodecName::H264VRAM => Self::H264,
CodecName::H265RAM(_) | CodecName::H265VRAM => Self::H265,
}
}
}

View File

@@ -2,9 +2,7 @@ use crate::CodecFormat;
#[cfg(feature = "hwcodec")]
use hbb_common::anyhow::anyhow;
use hbb_common::{
bail, chrono,
config::Config,
log,
bail, chrono, log,
message_proto::{message, video_frame, EncodedVideoFrame, Message},
ResultType,
};
@@ -26,7 +24,7 @@ const MIN_SECS: u64 = 1;
pub struct RecorderContext {
pub server: bool,
pub id: String,
pub default_dir: String,
pub dir: String,
pub filename: String,
pub width: usize,
pub height: usize,
@@ -36,18 +34,11 @@ pub struct RecorderContext {
impl RecorderContext {
pub fn set_filename(&mut self) -> ResultType<()> {
let mut dir = Config::get_option("video-save-directory");
if !dir.is_empty() {
if !PathBuf::from(&dir).exists() {
std::fs::create_dir_all(&dir)?;
}
} else {
dir = self.default_dir.clone();
if !dir.is_empty() && !PathBuf::from(&dir).exists() {
std::fs::create_dir_all(&dir)?;
}
if !PathBuf::from(&self.dir).exists() {
std::fs::create_dir_all(&self.dir)?;
}
let file = if self.server { "s" } else { "c" }.to_string()
let file = if self.server { "incoming" } else { "outgoing" }.to_string()
+ "_"
+ &self.id.clone()
+ &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string()
+ &self.format.to_string().to_lowercase()
@@ -59,7 +50,10 @@ impl RecorderContext {
} else {
".mp4"
};
self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string();
self.filename = PathBuf::from(&self.dir)
.join(file)
.to_string_lossy()
.to_string();
log::info!("video will save to {}", self.filename);
Ok(())
}

View File

@@ -207,7 +207,7 @@ impl EncoderApi for VpxEncoder {
self.yuvfmt.clone()
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
false
}

View File

@@ -5,26 +5,24 @@ use std::{
};
use crate::{
codec::{base_bitrate, enable_gpucodec_option, EncoderApi, EncoderCfg, Quality},
AdapterDevice, CodecFormat, CodecName, EncodeInput, EncodeYuvFormat, Pixfmt,
};
use gpucodec::gpu_common::{
self, Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext, MAX_GOP,
};
use gpucodec::{
decode::{self, DecodeFrame, Decoder},
encode::{self, EncodeFrame, Encoder},
codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg, Quality},
AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt,
};
use hbb_common::{
allow_err,
anyhow::{anyhow, bail, Context},
bytes::Bytes,
log,
message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame},
ResultType,
};
const OUTPUT_SHARED_HANDLE: bool = false;
use hwcodec::{
common::{AdapterVendor::*, DataFormat, Driver, MAX_GOP},
vram::{
decode::{self, DecodeFrame, Decoder},
encode::{self, EncodeFrame, Encoder},
Available, DecodeContext, DynamicContext, EncodeContext, FeatureContext,
},
};
// https://www.reddit.com/r/buildapc/comments/d2m4ny/two_graphics_cards_two_monitors/
// https://www.reddit.com/r/techsupport/comments/t2v9u6/dual_monitor_setup_with_dual_gpu/
@@ -35,31 +33,32 @@ lazy_static::lazy_static! {
}
#[derive(Debug, Clone)]
pub struct GpuEncoderConfig {
pub struct VRamEncoderConfig {
pub device: AdapterDevice,
pub width: usize,
pub height: usize,
pub quality: Quality,
pub feature: gpucodec::gpu_common::FeatureContext,
pub feature: FeatureContext,
pub keyframe_interval: Option<usize>,
}
pub struct GpuEncoder {
pub struct VRamEncoder {
encoder: Encoder,
pub format: gpu_common::DataFormat,
pub format: DataFormat,
ctx: EncodeContext,
bitrate: u32,
last_frame_len: usize,
same_bad_len_counter: usize,
config: VRamEncoderConfig,
}
impl EncoderApi for GpuEncoder {
impl EncoderApi for VRamEncoder {
fn new(cfg: EncoderCfg, _i444: bool) -> ResultType<Self>
where
Self: Sized,
{
match cfg {
EncoderCfg::GPU(config) => {
EncoderCfg::VRAM(config) => {
let b = Self::convert_quality(config.quality, &config.feature);
let base_bitrate = base_bitrate(config.width as _, config.height as _);
let mut bitrate = base_bitrate * b / 100;
@@ -79,18 +78,16 @@ impl EncoderApi for GpuEncoder {
},
};
match Encoder::new(ctx.clone()) {
Ok(encoder) => Ok(GpuEncoder {
Ok(encoder) => Ok(VRamEncoder {
encoder,
ctx,
format: config.feature.data_format,
bitrate,
last_frame_len: 0,
same_bad_len_counter: 0,
config,
}),
Err(_) => {
hbb_common::config::GpucodecConfig::clear();
Err(anyhow!(format!("Failed to create encoder")))
}
Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
}
}
_ => Err(anyhow!("encoder type mismatch")),
@@ -138,8 +135,8 @@ impl EncoderApi for GpuEncoder {
..Default::default()
};
match self.format {
gpu_common::DataFormat::H264 => vf.set_h264s(frames),
gpu_common::DataFormat::H265 => vf.set_h265s(frames),
DataFormat::H264 => vf.set_h264s(frames),
DataFormat::H265 => vf.set_h265s(frames),
_ => bail!("{:?} not supported", self.format),
}
Ok(vf)
@@ -160,7 +157,7 @@ impl EncoderApi for GpuEncoder {
}
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
fn input_texture(&self) -> bool {
true
}
@@ -181,60 +178,69 @@ impl EncoderApi for GpuEncoder {
}
fn support_abr(&self) -> bool {
self.ctx.f.driver != gpu_common::EncodeDriver::VPL
self.config.device.vendor_id != ADAPTER_VENDOR_INTEL as u32
}
}
impl GpuEncoder {
pub fn try_get(device: &AdapterDevice, name: CodecName) -> Option<FeatureContext> {
let v: Vec<_> = Self::available(name)
impl VRamEncoder {
pub fn try_get(device: &AdapterDevice, format: CodecFormat) -> Option<FeatureContext> {
let v: Vec<_> = Self::available(format)
.drain(..)
.filter(|e| e.luid == device.luid)
.collect();
if v.len() > 0 {
// prefer ffmpeg
if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) {
return Some(ctx.clone());
}
Some(v[0].clone())
} else {
None
}
}
pub fn available(name: CodecName) -> Vec<FeatureContext> {
pub fn available(format: CodecFormat) -> Vec<FeatureContext> {
let not_use = ENOCDE_NOT_USE.lock().unwrap().clone();
if not_use.values().any(|not_use| *not_use) {
log::info!("currently not use gpucodec encoders: {not_use:?}");
log::info!("currently not use vram encoders: {not_use:?}");
return vec![];
}
let data_format = match name {
CodecName::H264GPU => gpu_common::DataFormat::H264,
CodecName::H265GPU => gpu_common::DataFormat::H265,
let data_format = match format {
CodecFormat::H264 => DataFormat::H264,
CodecFormat::H265 => DataFormat::H265,
_ => return vec![],
};
let Ok(displays) = crate::Display::all() else {
log::error!("failed to get displays");
return vec![];
};
if displays.is_empty() {
log::error!("no display found");
return vec![];
}
let luids = displays
.iter()
.map(|d| d.adapter_luid())
.collect::<Vec<_>>();
let v: Vec<_> = get_available_config()
.map(|c| c.e)
.unwrap_or_default()
.drain(..)
.filter(|c| c.data_format == data_format)
.collect();
if luids
.iter()
.all(|luid| v.iter().any(|f| Some(f.luid) == *luid))
{
if crate::hwcodec::HwRamEncoder::try_get(format).is_some() {
// has fallback, no need to require all adapters support
v
} else {
log::info!("not all adapters support {data_format:?}, luids = {luids:?}");
vec![]
let Ok(displays) = crate::Display::all() else {
log::error!("failed to get displays");
return vec![];
};
if displays.is_empty() {
log::error!("no display found");
return vec![];
}
let luids = displays
.iter()
.map(|d| d.adapter_luid())
.collect::<Vec<_>>();
if luids
.iter()
.all(|luid| v.iter().any(|f| Some(f.luid) == *luid))
{
v
} else {
log::info!("not all adapters support {data_format:?}, luids = {luids:?}");
vec![]
}
}
}
@@ -252,27 +258,21 @@ impl GpuEncoder {
pub fn convert_quality(quality: Quality, f: &FeatureContext) -> u32 {
match quality {
Quality::Best => {
if f.driver == gpu_common::EncodeDriver::VPL
&& f.data_format == gpu_common::DataFormat::H264
{
if f.driver == Driver::MFX && f.data_format == DataFormat::H264 {
200
} else {
150
}
}
Quality::Balanced => {
if f.driver == gpu_common::EncodeDriver::VPL
&& f.data_format == gpu_common::DataFormat::H264
{
if f.driver == Driver::MFX && f.data_format == DataFormat::H264 {
150
} else {
100
}
}
Quality::Low => {
if f.driver == gpu_common::EncodeDriver::VPL
&& f.data_format == gpu_common::DataFormat::H264
{
if f.driver == Driver::MFX && f.data_format == DataFormat::H264 {
75
} else {
50
@@ -283,29 +283,23 @@ impl GpuEncoder {
}
pub fn set_not_use(display: usize, not_use: bool) {
log::info!("set display#{display} not use gpucodec encode to {not_use}");
log::info!("set display#{display} not use vram encode to {not_use}");
ENOCDE_NOT_USE.lock().unwrap().insert(display, not_use);
}
pub fn not_use() -> bool {
ENOCDE_NOT_USE.lock().unwrap().iter().any(|v| *v.1)
}
}
pub struct GpuDecoder {
pub struct VRamDecoder {
decoder: Decoder,
}
#[derive(Default)]
pub struct GpuDecoders {
pub h264: Option<GpuDecoder>,
pub h265: Option<GpuDecoder>,
}
impl GpuDecoder {
impl VRamDecoder {
pub fn try_get(format: CodecFormat, luid: Option<i64>) -> Option<DecodeContext> {
let v: Vec<_> = Self::available(format, luid);
if v.len() > 0 {
// prefer ffmpeg
if let Some(ctx) = v.iter().find(|c| c.driver == Driver::FFMPEG) {
return Some(ctx.clone());
}
Some(v[0].clone())
} else {
None
@@ -315,8 +309,8 @@ impl GpuDecoder {
pub fn available(format: CodecFormat, luid: Option<i64>) -> Vec<DecodeContext> {
let luid = luid.unwrap_or_default();
let data_format = match format {
CodecFormat::H264 => gpu_common::DataFormat::H264,
CodecFormat::H265 => gpu_common::DataFormat::H265,
CodecFormat::H264 => DataFormat::H264,
CodecFormat::H265 => DataFormat::H265,
_ => return vec![],
};
get_available_config()
@@ -328,25 +322,23 @@ impl GpuDecoder {
}
pub fn possible_available_without_check() -> (bool, bool) {
if !enable_gpucodec_option() {
if !enable_vram_option() {
return (false, false);
}
let v = get_available_config().map(|c| c.d).unwrap_or_default();
(
v.iter()
.any(|d| d.data_format == gpu_common::DataFormat::H264),
v.iter()
.any(|d| d.data_format == gpu_common::DataFormat::H265),
v.iter().any(|d| d.data_format == DataFormat::H264),
v.iter().any(|d| d.data_format == DataFormat::H265),
)
}
pub fn new(format: CodecFormat, luid: Option<i64>) -> ResultType<Self> {
log::info!("try create {format:?} vram decoder, luid: {luid:?}");
let ctx = Self::try_get(format, luid).ok_or(anyhow!("Failed to get decode context"))?;
log::info!("try create vram decoder: {ctx:?}");
match Decoder::new(ctx) {
Ok(decoder) => Ok(Self { decoder }),
Err(_) => {
hbb_common::config::GpucodecConfig::clear();
hbb_common::config::HwCodecConfig::clear_vram();
Err(anyhow!(format!(
"Failed to create decoder, format: {:?}",
format
@@ -354,91 +346,42 @@ impl GpuDecoder {
}
}
}
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<GpuDecoderImage>> {
pub fn decode(&mut self, data: &[u8]) -> ResultType<Vec<VRamDecoderImage>> {
match self.decoder.decode(data) {
Ok(v) => Ok(v.iter().map(|f| GpuDecoderImage { frame: f }).collect()),
Ok(v) => Ok(v.iter().map(|f| VRamDecoderImage { frame: f }).collect()),
Err(e) => Err(anyhow!(e)),
}
}
}
pub struct GpuDecoderImage<'a> {
pub struct VRamDecoderImage<'a> {
pub frame: &'a DecodeFrame,
}
impl GpuDecoderImage<'_> {}
impl VRamDecoderImage<'_> {}
fn get_available_config() -> ResultType<Available> {
let available = hbb_common::config::GpucodecConfig::load().available;
let available = hbb_common::config::HwCodecConfig::load().vram;
match Available::deserialize(&available) {
Ok(v) => Ok(v),
Err(_) => Err(anyhow!("Failed to deserialize:{}", available)),
}
}
pub fn check_available_gpucodec() {
pub(crate) fn check_available_vram() -> String {
let d = DynamicContext {
device: None,
width: 1920,
height: 1080,
width: 1280,
height: 720,
kbitrate: 5000,
framerate: 60,
gop: MAX_GOP as _,
};
let encoders = encode::available(d);
let decoders = decode::available(OUTPUT_SHARED_HANDLE);
let decoders = decode::available();
let available = Available {
e: encoders,
d: decoders,
};
if let Ok(available) = available.serialize() {
let mut config = hbb_common::config::GpucodecConfig::load();
config.available = available;
config.store();
return;
}
log::error!("Failed to serialize gpucodec");
}
pub fn gpucodec_new_check_process() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
std::thread::spawn(move || {
// Remove to avoid checking process errors
// But when the program is just started, the configuration file has not been updated, and the new connection will read an empty configuration
hbb_common::config::GpucodecConfig::clear();
if let Ok(exe) = std::env::current_exe() {
let arg = "--check-gpucodec-config";
if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() {
// wait up to 30 seconds
for _ in 0..30 {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Ok(Some(_)) = child.try_wait() {
break;
}
}
allow_err!(child.kill());
std::thread::sleep(std::time::Duration::from_millis(30));
match child.try_wait() {
Ok(Some(status)) => {
log::info!("Check gpucodec config, exit with: {status}")
}
Ok(None) => {
log::info!(
"Check gpucodec config, status not ready yet, let's really wait"
);
let res = child.wait();
log::info!("Check gpucodec config, wait result: {res:?}");
}
Err(e) => {
log::error!("Check gpucodec config, error attempting to wait: {e}")
}
}
}
};
});
});
available.serialize().unwrap_or_default()
}

View File

@@ -185,7 +185,7 @@ impl Capturer {
self.gdi_capturer.take();
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
pub fn set_output_texture(&mut self, texture: bool) {
self.output_texture = texture;
}
@@ -620,7 +620,7 @@ impl Display {
)
}
#[cfg(feature = "gpucodec")]
#[cfg(feature = "vram")]
pub fn adapter_luid(&self) -> Option<i64> {
unsafe {
if !self.adapter.is_null() {

View File

@@ -30,11 +30,36 @@ lazy_static! {
pub static ref RDP_RESPONSE: Mutex<Option<RdpResponse>> = Mutex::new(None);
}
#[inline]
pub fn close_session() {
let _ = RDP_RESPONSE.lock().unwrap().take();
}
#[inline]
pub fn is_rdp_session_hold() -> bool {
RDP_RESPONSE.lock().unwrap().is_some()
}
pub fn try_close_session() {
let mut rdp_res = RDP_RESPONSE.lock().unwrap();
let mut close = false;
if let Some(rdp_res) = &*rdp_res {
// If is server running and restore token is supported, there's no need to keep the session.
if is_server_running() && rdp_res.is_support_restore_token {
close = true;
}
}
if close {
*rdp_res = None;
}
}
pub struct RdpResponse {
pub conn: Arc<SyncConnection>,
pub streams: Vec<PwStreamInfo>,
pub fd: OwnedFd,
pub session: dbus::Path<'static>,
pub is_support_restore_token: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct PwStreamInfo {
@@ -476,6 +501,7 @@ pub fn request_remote_desktop() -> Result<
OwnedFd,
Vec<PwStreamInfo>,
dbus::Path<'static>,
bool,
),
Box<dyn Error>,
> {
@@ -504,6 +530,14 @@ pub fn request_remote_desktop() -> Result<
"handle_token".to_string(),
Variant(Box::new("u1".to_string())),
);
let mut is_support_restore_token = false;
if let Ok(version) = screencast_portal::version(&portal) {
if version >= 4 {
is_support_restore_token = true;
}
}
// The following code may be improved.
// https://flatpak.github.io/xdg-desktop-portal/#:~:text=To%20avoid%20a%20race%20condition
// To avoid a race condition
@@ -524,6 +558,7 @@ pub fn request_remote_desktop() -> Result<
streams.clone(),
session.clone(),
failure.clone(),
is_support_restore_token,
),
failure_res.clone(),
)?;
@@ -547,7 +582,13 @@ pub fn request_remote_desktop() -> Result<
if let Some(fd_res) = fd_res.clone() {
if let Some(session) = session_res.clone() {
if !streams_res.is_empty() {
return Ok((conn, fd_res, streams_res.clone(), session));
return Ok((
conn,
fd_res,
streams_res.clone(),
session,
is_support_restore_token,
));
}
}
}
@@ -561,6 +602,7 @@ fn on_create_session_response(
streams: Arc<Mutex<Vec<PwStreamInfo>>>,
session: Arc<Mutex<Option<dbus::Path<'static>>>>,
failure: Arc<AtomicBool>,
is_support_restore_token: bool,
) -> impl Fn(
OrgFreedesktopPortalRequestResponse,
&SyncConnection,
@@ -589,16 +631,15 @@ fn on_create_session_response(
let portal = get_portal(c);
let mut args: PropMap = HashMap::new();
// See `is_server_running()` to understand the following code.
if is_server_running() {
if let Ok(version) = screencast_portal::version(&portal) {
if version >= 4 {
let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY);
if !restore_token.is_empty() {
args.insert(RESTORE_TOKEN.to_string(), Variant(Box::new(restore_token)));
}
// persist_mode may be configured by the user.
args.insert("persist_mode".to_string(), Variant(Box::new(2u32)));
if is_support_restore_token {
let restore_token = config::LocalConfig::get_option(RESTORE_TOKEN_CONF_KEY);
if !restore_token.is_empty() {
args.insert(RESTORE_TOKEN.to_string(), Variant(Box::new(restore_token)));
}
// persist_mode may be configured by the user.
args.insert("persist_mode".to_string(), Variant(Box::new(2u32)));
}
args.insert(
"handle_token".to_string(),
@@ -612,7 +653,13 @@ fn on_create_session_response(
handle_response(
c,
path,
on_select_sources_response(fd.clone(), streams.clone(), failure.clone(), ses),
on_select_sources_response(
fd.clone(),
streams.clone(),
failure.clone(),
ses,
is_support_restore_token,
),
failure.clone(),
)?;
} else {
@@ -626,7 +673,13 @@ fn on_create_session_response(
handle_response(
c,
path,
on_select_devices_response(fd.clone(), streams.clone(), failure.clone(), ses),
on_select_devices_response(
fd.clone(),
streams.clone(),
failure.clone(),
ses,
is_support_restore_token,
),
failure.clone(),
)?;
}
@@ -640,6 +693,7 @@ fn on_select_devices_response(
streams: Arc<Mutex<Vec<PwStreamInfo>>>,
failure: Arc<AtomicBool>,
session: dbus::Path<'static>,
is_support_restore_token: bool,
) -> impl Fn(
OrgFreedesktopPortalRequestResponse,
&SyncConnection,
@@ -661,7 +715,13 @@ fn on_select_devices_response(
handle_response(
c,
path,
on_select_sources_response(fd.clone(), streams.clone(), failure.clone(), session),
on_select_sources_response(
fd.clone(),
streams.clone(),
failure.clone(),
session,
is_support_restore_token,
),
failure.clone(),
)?;
@@ -674,6 +734,7 @@ fn on_select_sources_response(
streams: Arc<Mutex<Vec<PwStreamInfo>>>,
failure: Arc<AtomicBool>,
session: dbus::Path<'static>,
is_support_restore_token: bool,
) -> impl Fn(
OrgFreedesktopPortalRequestResponse,
&SyncConnection,
@@ -695,7 +756,12 @@ fn on_select_sources_response(
handle_response(
c,
path,
on_start_response(fd.clone(), streams.clone(), session.clone()),
on_start_response(
fd.clone(),
streams.clone(),
session.clone(),
is_support_restore_token,
),
failure.clone(),
)?;
@@ -707,6 +773,7 @@ fn on_start_response(
fd: Arc<Mutex<Option<OwnedFd>>>,
streams: Arc<Mutex<Vec<PwStreamInfo>>>,
session: dbus::Path<'static>,
is_support_restore_token: bool,
) -> impl Fn(
OrgFreedesktopPortalRequestResponse,
&SyncConnection,
@@ -714,16 +781,15 @@ fn on_start_response(
) -> Result<(), Box<dyn Error>> {
move |r: OrgFreedesktopPortalRequestResponse, c, _| {
let portal = get_portal(c);
// See `is_server_running()` to understand the following code.
if is_server_running() {
if let Ok(version) = screencast_portal::version(&portal) {
if version >= 4 {
if let Some(restore_token) = r.results.get(RESTORE_TOKEN) {
if let Some(restore_token) = restore_token.as_str() {
config::LocalConfig::set_option(
RESTORE_TOKEN_CONF_KEY.to_owned(),
restore_token.to_owned(),
);
}
if is_support_restore_token {
if let Some(restore_token) = r.results.get(RESTORE_TOKEN) {
if let Some(restore_token) = restore_token.as_str() {
config::LocalConfig::set_option(
RESTORE_TOKEN_CONF_KEY.to_owned(),
restore_token.to_owned(),
);
}
}
}
@@ -750,7 +816,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
};
if rdp_connection.is_none() {
let (conn, fd, streams, session) = request_remote_desktop()?;
let (conn, fd, streams, session, is_support_restore_token) = request_remote_desktop()?;
let conn = Arc::new(conn);
let rdp_res = RdpResponse {
@@ -758,6 +824,7 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
streams,
fd,
session,
is_support_restore_token,
};
*rdp_connection = Some(rdp_res);
}
@@ -777,10 +844,20 @@ pub fn get_capturables() -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
.collect())
}
// If `is_server_running()` is true, then `screencast_portal::start` is called.
// Otherwise, `remote_desktop_portal::start` is called.
//
// If `is_server_running()` is true, `--service` process is running,
// then we can use uinput as the input method.
// Otherwise, we have to use remote_desktop_portal's input method.
//
// `screencast_portal` supports restore_token and persist_mode if the version is greater than or equal to 4.
// `remote_desktop_portal` does not support restore_token and persist_mode.
fn is_server_running() -> bool {
let app_name = config::APP_NAME.read().unwrap().clone().to_lowercase();
let output = match Command::new("sh")
.arg("-c")
.arg("ps aux | grep rustdesk")
.arg(&format!("ps aux | grep {}", app_name))
.output()
{
Ok(output) => output,
@@ -790,6 +867,6 @@ fn is_server_running() -> bool {
};
let output_str = String::from_utf8_lossy(&output.stdout);
let is_running = output_str.contains("rustdesk --server");
let is_running = output_str.contains(&format!("{} --server", app_name));
is_running
}

143
res/devices.py Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
import requests
import argparse
from datetime import datetime, timedelta
def view(
url,
token,
id=None,
device_name=None,
user_name=None,
group_name=None,
offline_days=None,
):
headers = {"Authorization": f"Bearer {token}"}
pageSize = 30
params = {
"id": id,
"device_name": device_name,
"user_name": user_name,
"group_name": group_name,
}
params = {
k: "%" + v + "%" if (v != "-" and "%" not in v) else v
for k, v in params.items()
if v is not None
}
params["pageSize"] = pageSize
devices = []
current = 1
while True:
params["current"] = current
response = requests.get(f"{url}/api/devices", headers=headers, params=params)
response_json = response.json()
data = response_json.get("data", [])
for device in data:
if offline_days is None:
devices.append(device)
continue
last_online = datetime.strptime(
device["last_online"], "%Y-%m-%dT%H:%M:%S"
) # assuming date is in this format
if (datetime.utcnow() - last_online).days >= offline_days:
devices.append(device)
total = response_json.get("total", 0)
current += pageSize
if len(data) < pageSize or current > total:
break
return devices
def check(response):
if response.status_code == 200:
try:
response_json = response.json()
return response_json
except ValueError:
return response.text or "Success"
else:
return "Failed", response.status_code, response.text
def disable(url, token, guid, id):
print("Disable", id)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{url}/api/devices/{guid}/disable", headers=headers)
return check(response)
def enable(url, token, guid, id):
print("Enable", id)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{url}/api/devices/{guid}/enable", headers=headers)
return check(response)
def delete(url, token, guid, id):
print("Delete", id)
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/devices/{guid}", headers=headers)
return check(response)
def main():
parser = argparse.ArgumentParser(description="Device manager")
parser.add_argument(
"command",
choices=["view", "disable", "enable", "delete"],
help="Command to execute",
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument(
"--token", required=True, help="Bearer token for authentication"
)
parser.add_argument("--id", help="Device ID")
parser.add_argument("--device_name", help="Device name")
parser.add_argument("--user_name", help="User name")
parser.add_argument("--group_name", help="Group name")
parser.add_argument(
"--offline_days", type=int, help="Offline duration in days, e.g., 7"
)
args = parser.parse_args()
devices = view(
args.url,
args.token,
args.id,
args.device_name,
args.user_name,
args.group_name,
args.offline_days,
)
if args.command == "view":
for device in devices:
print(device)
elif args.command == "disable":
for device in devices:
response = disable(args.url, args.token, device["guid"], device["id"])
print(response)
elif args.command == "enable":
for device in devices:
response = enable(args.url, args.token, device["guid"], device["id"])
print(response)
elif args.command == "delete":
for device in devices:
response = delete(args.url, args.token, device["guid"], device["id"])
print(response)
if __name__ == "__main__":
main()

View File

@@ -23,17 +23,21 @@ SECRET_KEY = os.getenv("SECRET_KEY") or "worldpeace2024"
# The headers for API requests
HEADERS = {"Authorization": f"Bearer {SECRET_KEY}"}
TIMEOUT = int(os.getenv("TIMEOUT") or "30")
SIGN_TIMEOUT = int(os.getenv("SIGN_TIMEOUT") or "30")
TIMEOUT = float(os.getenv("TIMEOUT") or "900")
def create(task_name, file_path=None):
if file_path is None:
response = requests.post(f"{BASE_URL}/tasks/{task_name}", headers=HEADERS)
response = requests.post(
f"{BASE_URL}/tasks/{task_name}", timeout=TIMEOUT, headers=HEADERS
)
else:
with open(file_path, "rb") as f:
files = {"file": f}
response = requests.post(
f"{BASE_URL}/tasks/{task_name}",
timeout=TIMEOUT,
headers=HEADERS,
files=files,
)
@@ -44,19 +48,27 @@ def upload_file(task_id, file_path):
with open(file_path, "rb") as f:
files = {"file": f}
response = requests.post(
f"{BASE_URL}/tasks/{task_id}/files", headers=HEADERS, files=files
f"{BASE_URL}/tasks/{task_id}/files",
timeout=TIMEOUT,
headers=HEADERS,
files=files,
)
return get_json(response)
def get_status(task_id):
response = requests.get(f"{BASE_URL}/tasks/{task_id}/status", headers=HEADERS)
response = requests.get(
f"{BASE_URL}/tasks/{task_id}/status", timeout=TIMEOUT, headers=HEADERS
)
return get_json(response)
def download_files(task_id, output_dir, fn=None):
response = requests.get(
f"{BASE_URL}/tasks/{task_id}/files", headers=HEADERS, stream=True
f"{BASE_URL}/tasks/{task_id}/files",
timeout=TIMEOUT,
headers=HEADERS,
stream=True,
)
# Check if the request was successful
@@ -73,7 +85,10 @@ def download_files(task_id, output_dir, fn=None):
def download_one_file(task_id, file_id, output_dir):
response = requests.get(
f"{BASE_URL}/tasks/{task_id}/files/{file_id}", headers=HEADERS, stream=True
f"{BASE_URL}/tasks/{task_id}/files/{file_id}",
timeout=TIMEOUT,
headers=HEADERS,
stream=True,
)
# Check if the request was successful
@@ -86,14 +101,19 @@ def download_one_file(task_id, file_id, output_dir):
return response.ok
def fetch():
response = requests.get(f"{BASE_URL}/tasks/fetch_task", headers=HEADERS)
def fetch(tag=None):
response = requests.get(
f"{BASE_URL}/tasks/fetch_task" + ("?tag=%s" % tag if tag else ""),
timeout=TIMEOUT,
headers=HEADERS,
)
return get_json(response)
def update_status(task_id, status):
response = requests.patch(
f"{BASE_URL}/tasks/{task_id}/status",
timeout=TIMEOUT,
headers=HEADERS,
json=status,
)
@@ -103,6 +123,7 @@ def update_status(task_id, status):
def delete_task(task_id):
response = requests.delete(
f"{BASE_URL}/tasks/{task_id}",
timeout=TIMEOUT,
headers=HEADERS,
)
return get_json(response)
@@ -135,7 +156,7 @@ def sign_one_file(file_path):
task_id = res["id"]
n = 0
while True:
if n >= TIMEOUT:
if n >= SIGN_TIMEOUT:
delete_task(task_id)
logging.error(f"Failed to sign {file_path}")
break

2
res/msi/.gitignore vendored
View File

@@ -9,3 +9,5 @@ packages
CustomActions/x64
CustomActions/*.user
CustomActions/*.filters
Package/Resources

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